Esempio n. 1
0
class Feature(SavedObject):
    syntactic_object = True

    editable = {}
    addable = {}

    def __init__(self, ftype=None, value=None):
        super().__init__()
        self.ftype = ftype
        self.value = value

    def __repr__(self):
        if self.ftype == 'cat':
            return self.value
        elif self.ftype == 'sel':
            return '=' + self.value
        elif self.ftype == 'neg':
            return '-' + self.value
        elif self.ftype == 'pos':
            return '+' + self.value

    def __eq__(self, other):
        return self.ftype == other.ftype and self.value == other.value

    def __hash__(self):
        return hash(str(self))

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    name = SavedField("name")
    value = SavedField("value")
Esempio n. 2
0
class DerivationStep(SavedObject):
    """ Packed state of syntactic objects for stepwise animation of trees growth.
     """

    def __init__(self, synobjs=None, numeration=None, other=None, msg=None, gloss=None,
                 transferred=None, mover=None, uid=None):
        super().__init__(uid=uid)
        self.synobjs = synobjs or []
        self.numeration = numeration
        self.other = other
        self.msg = msg
        self.gloss = gloss
        self.transferred = transferred
        self.mover = mover

    def __str__(self):
        return "DS(" + str(self.synobjs) + ", " + str(self.numeration) + ", " + str(self.other) \
               + ", '" + str(self.msg) + "')"


    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    synobjs = SavedField("synobjs")
    numeration = SavedField("numeration")
    other = SavedField("other")
    msg = SavedField("msg")
    gloss = SavedField("gloss")
    transferred = SavedField("transferred")
    mover = SavedField("mover")
Esempio n. 3
0
class HiConstituent(BaseConstituent):
    """ HiConstituent is a slight modification from BaseConstituent.
    Everything that is not explicitly defined here is inherited from parent class."""
    def __init__(self, *args, **kwargs):
        """ Constructor for new HiConstituents """
        super().__init__(*args, **kwargs)
        if 'hi' in kwargs:
            self.hi = kwargs['hi']
        else:
            self.hi = 'hello'

    def __repr__(self):
        """ Readable representation for debugging. (E.g. how item will look as a list member,
        when the list is printed out.) You can also define __str__ for even more
        readable output, otherwise __str__ will use __repr__.
        :return:
        """
        if self.is_leaf():
            return 'HiConstituent(id=%s)' % self.label
        else:
            return "[ %s ]" % (' '.join((x.__repr__() for x in self.parts)))

    def compose_html_for_viewing(self, node):
        """ This method builds the html to display in label. For convenience, syntactic objects
        can override this (going against the containment logic) by having their own
        'compose_html_for_viewing' -method. This is so that it is easier to create custom
        implementations for constituents without requiring custom constituentnodes.

        Note that synobj's compose_html_for_viewing receives the node object as parameter,
        so you can call the parent to do its part and then add your own to it.
        :return:
        """

        html, lower_html = node.compose_html_for_viewing(
            peek_into_synobj=False)
        html += ', hi: ' + self.hi
        return html, lower_html

    def copy(self):
        """ Make a deep copy of constituent. Useful for picking constituents from Lexicon.
        :return: HiConstituent
        """
        nc = super().copy()
        nc.hi = self.hi
        return nc

    #  Announce save support for given fields (+ those that are inherited from parent class):
    hi = SavedField("hi")
Esempio n. 4
0
class HiConstituent(BaseConstituent):
    """ HiConstituent is a slight modification from BaseConstituent.
    Everything that is not explicitly defined here is inherited from parent class."""
    def __init__(self, **kw):
        """
         """
        super().__init__(**kw)
        self.hi = 'hi'

    def __repr__(self):
        if self.is_leaf():
            return 'HiConstituent(id=%s)' % self.label
        else:
            return "[ %s ]" % (' '.join((x.__repr__() for x in self.parts)))

    def compose_html_for_viewing(self, node):
        """ This method builds the html to display in label. For convenience, syntactic objects
        can override this (going against the containment logic) by having their own
        'compose_html_for_viewing' -method. This is so that it is easier to create custom
        implementations for constituents without requiring custom constituentnodes.

        Note that synobj's compose_html_for_viewing receives the node object as parameter,
        so you can call the parent to do its part and then add your own to it.
        :return:
        """

        html, lower_html = node.compose_html_for_viewing(
            peek_into_synobj=False)
        html += ', hi: ' + self.hi
        return html, lower_html

    def copy(self):
        """ Make a deep copy of constituent. Useful for picking constituents from Lexicon.
        :return: BaseConstituent
        """
        nc = super().copy()
        nc.hi = self.hi
        return nc

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    hi = SavedField("hi")
Esempio n. 5
0
class Feature(MyBaseClass):
    role = "Feature"

    def __init__(self,
                 namestring='',
                 counter=0,
                 name='',
                 value='',
                 unvalued=False,
                 ifeature=False,
                 valued_by=None,
                 inactive=False):
        if in_kataja:
            super().__init__(name=name, value=value)
        if namestring:
            if namestring.startswith('u'):
                namestring = namestring[1:]
                self.unvalued = True
                self.ifeature = False
            elif namestring.startswith('i'):
                namestring = namestring[1:]
                self.unvalued = False
                self.ifeature = True
            else:
                self.unvalued = False
                self.ifeature = False
            digits = ''
            digit_count = 0
            for char in reversed(namestring):
                if char.isdigit():
                    digits = char + digits
                    digit_count += 1
                else:
                    break
            if digit_count:
                self.counter = int(digits)
                namestring = namestring[:-digit_count]
            else:
                self.counter = 0
            self.name, part, self.value = namestring.partition(':')
            self.valued_by = None
            self.inactive = inactive
        else:
            self.name = name
            self.counter = counter
            self.value = value
            self.unvalued = unvalued
            self.ifeature = ifeature
            self.valued_by = None
            self.inactive = inactive

    def __str__(self):
        return repr(self)

    def __repr__(self):
        parts = []
        if self.ifeature:
            parts.append('i')
        elif self.unvalued:
            parts.append('u')
        parts.append(self.name)
        if self.value:
            parts.append(':' + self.value)
        if self.counter:
            parts.append(str(self.counter))
        if self.valued_by:
            parts.append('(%s)' % self.valued_by)
        #return "'"+''.join(parts)+"'"
        return ''.join(parts)

    def get_parts(self):
        """ This is what Kataja uses to find if there is inner structure/links to other nodes to
        display. Use it here for valued_by -relation.

        :return:
        """
        return self.valued_by or []

    def __eq__(self, o):
        if isinstance(o, Feature):
            return self.counter == o.counter and self.name == o.name and \
                   self.value == o.value and self.unvalued == o.unvalued and \
                   self.ifeature == o.ifeature and self.valued_by == o.valued_by
        elif isinstance(o, str):
            return str(self) == o
        else:
            return False

    def __lt__(self, other):
        return repr(self) < repr(other)

    def __gt__(self, other):
        return repr(self) > repr(other)

    def __hash__(self):
        return hash(str(self))

    def __contains__(self, item):
        """ This doesn't look into linked features, you should be looking into them first
        (use expanded_features -function in Constituent to get all of them)
        :param item:
        :return:
        """
        if isinstance(item, Feature):
            if item.name != self.name:
                return False
            if item.ifeature and not self.ifeature:
                return False
            if item.value and self.value != item.value:
                return False
            if item.unvalued and not self.unvalued:
                return False
            return True
        else:
            return item in str(self)

    def unvalued_and_alone(self):
        return self.unvalued and not self.valued_by

    def value_with(self, other):
        assert other is not self
        if self.valued_by:
            self.valued_by.append(other)
        else:
            self.valued_by = [other]

    def expand_linked_features(self):
        if self.valued_by:
            flist = []
            for f in self.valued_by:
                flist += f.expand_linked_features()
            return flist
        else:
            return [self]

    def match(self, name=None, value=None, u=None, i=None, phi=None):
        if isinstance(name, list):
            if self.name not in name:
                return None
        elif name is not None and self.name != name:
            return None
        elif value is not None and self.value != value:
            return None
        elif u is not None and self.unvalued != u:
            return None
        elif i is not None and self.ifeature != i:
            return None
        elif phi and self.name not in ("Person", "Number", "Gender"):
            return None
        return True

    def copy(self):
        return Feature(counter=self.counter,
                       name=self.name,
                       value=self.value,
                       unvalued=self.unvalued,
                       ifeature=self.ifeature,
                       valued_by=valued_by)

    if in_kataja:
        unvalued = SavedField("unvalued")
        ifeature = SavedField("ifeature")
        counter = SavedField("counter")
        valued_by = SavedField("valued_by")
        inactive = SavedField("inactive")
Esempio n. 6
0
class BaseFeature(SavedObject):
    """ BaseFeatures are the simplest feature implementation.
    BaseFeatures have name, which is used to identify and discriminate between features.
    BaseFeatures can have simple comparable items as values, generally assumed to be booleans or
    strings.
    Distinguishing between assigned and unassigned features is such a common property in
    minimalist grammars that it is supported by BaseFeature. 'assigned' is by default True,
    'unassigned' features have False.
    Family property can be used e.g. to mark phi-features or LF features.
    """

    syntactic_object = True
    role = "Feature"

    editable = {}
    addable = {}

    def __init__(self, name='Feature', value=None, family=''):
        super().__init__()
        self.name = name
        self.value = value
        self.family = family
        self.checks = None  # this has no syntactic effect but storing which feature this
        # feature has checked helps presentation

    def has_value(self, prop):
        return self.value == prop

    @property
    def unassigned(self):
        return self.unvalued()

    @property
    def assigned(self):
        return not self.unvalued()

    def unvalued(self):
        return self.value == 'u' or self.value == '='

    def satisfies(self, feature):
        return feature.unvalued(
        ) and feature.name == self.name and not self.unvalued()

    def __eq__(self, other):
        if other:
            return self.value == other.value and self.name == other.name and self.family == \
                                                                             other.family
        return False

    def copy(self):
        return self.__class__(name=self.name,
                              value=self.value,
                              family=self.family)

    def checked(self):
        return self.value.startswith('✓')

    def __str__(self):
        s = []
        signs = ('+', '-', '=', 'u', '✓')
        if self.value and (len(self.value) == 1 and self.value in signs or \
           len(self.value) == 2 and self.value[1] in signs):
            s.append(self.value + str(self.name))
        elif self.value or self.family:
            s.append(str(self.name))
            s.append(str(self.value))
            if self.family:
                s.append(str(self.family))
        else:
            s.append(str(self.name))
        return ":".join(s)

    def __repr__(self):
        s = [str(self.name)]
        if self.value or self.family:
            s.append(str(self.value))
        if self.family:
            s.append(str(self.family))
        return ":".join(s)

    def __hash__(self):
        return id(self)

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    name = SavedField("name")
    value = SavedField("value")
    family = SavedField("family")
    checks = SavedField("checks")

    @staticmethod
    def from_string(s):
        if not s:
            return
        if s[0] in '-=+':
            value = s[0]
            name = s[1:]
        else:
            value = ''
            name = s
        name, foo, family = name.partition(
            ':')  # 'case:acc' -> name = 'case', subtype = 'acc'
        return BaseFeature(name, value, family)
Esempio n. 7
0
class Forest(SavedObject):
    """ Forest is a group of trees that together form one view.
    Often there needs to be more than one trees visible at same time,
     so that they can be compared or to show states of construction
      where some edges are not yet linked to the main root.
      Forest is the container for these.
      Forest also takes care of the operations manipulating, creating and
      removing trees. """

    def __init__(self, gloss_text='', comments=None, syntax=None):
        """ Create an empty forest. Gloss_text and comments are metadata
        about trees that doesn't belong to syntax implementation, so its kept here. Syntax
        implementations may still use it.

        By default, a new Forest doesn't create its nodes -- it doesn't do the derivation yet.
        This is to save speed and memory with large structures. If the is_parsed -flag is False
        when created, but once Forest is displayed, the derivation has to run and after that
        is_parsed is True.
        """
        super().__init__()
        self.nodes_from_synobs = {}
        self.main = ctrl.main
        self.main.forest = self  # assign self to be the active forest while
        # creating the managers.
        self.in_display = False
        self.visualization = None
        self.gloss = None
        self.is_parsed = False
        self.syntax = syntax or classes.get('SyntaxConnection')()
        self.parser = INodeToKatajaConstituent(self)
        self.undo_manager = UndoManager(self)
        self.chain_manager = ChainManager(self)
        self.tree_manager = TreeManager(self)
        self.free_drawing = FreeDrawing(self)
        self.projection_manager = ProjectionManager(self)
        self.derivation_steps = DerivationStepManager(self)
        self.old_label_mode = 0
        self.trees = []
        self.nodes = {}
        self.edges = {}
        self.groups = {}
        self.others = {}
        self.vis_data = {}
        self.width_map = {}
        self.traces_to_draw = {}
        self.comments = []
        self.gloss_text = ''
        self.ongoing_animations = set()
        self.halt_drawing = False
        self.gloss_text = gloss_text
        self.comments = comments

        # Update request flags
        self._do_edge_visibility_check = False
        #self.change_view_mode(ctrl.settings.get('syntactic_mode'))

    def after_model_update(self, updated_fields, transition_type):
        """ Compute derived effects of updated values in sensible order.
        :param updated_fields: field keys of updates
        :param transition_type: 0:edit, 1:CREATED, -1:DELETED
        :return: None
        """
        if 'nodes' in updated_fields:
            # rebuild from-syntactic_object-to-node -dict
            self.nodes_from_synobs = {}
            for node in self.nodes.values():
                if node.syntactic_object:
                    self.nodes_from_synobs[node.syntactic_object.uid] = node
            for tree in self.trees:
                tree.update_items()
        if 'vis_data' in updated_fields:
            self.restore_visualization()

    def after_init(self):
        """ After_init is called in 2nd step in process of creating objects:
            1st wave creates the objects and calls __init__, and then
            iterates through and sets the values.
            2nd wave calls after_inits for all created objects. Now they can
            properly refer to each other and know their
                values.
        :return: None
        """
        # print('created a forest %s , its traces should be visible: %s ' % (
        # self, self.traces_are_visible()))
        pass
        # for node in self.nodes.values():
        # if node.syntactic_object:
        # self.nodes_by_uid[node.syntactic_object.uid] = node

    @property
    def scene(self):
        """ Return the graphics scene where objects are stored and drawn.
        :return: GraphScene instance
        """
        return self.main.graph_scene

    def prepare_for_drawing(self):
        """ Prepares the forest instance to be displayed in graph scene --
         called when switching forests
        :return: None
        """
        self.in_display = True
        ctrl.disable_undo()
        if not self.is_parsed:
            self.syntax.create_derivation(self)
            self.after_model_update('nodes', 0)
            self.is_parsed = True
            self.forest_edited()
        ctrl.add_watcher(self, 'palette_changed')
        ctrl.main.update_colors()
        self.add_all_to_scene()
        self.update_visualization()
        self.scene.keep_updating_visible_area = True
        self.scene.manual_zoom = False
        self.draw()  # do draw once to avoid having the first draw in undo stack.
        ctrl.graph_scene.fit_to_window()
        ctrl.resume_undo()
        ctrl.graph_view.setFocus()

    def retire_from_drawing(self):
        """ Announce that this forest should not try to work with scene
        anymore --
         some other forest is occupying the scene now.
        :return:
        """
        for item in self.get_all_objects():
            self.remove_from_scene(item, fade_out=False)
        ctrl.remove_from_watch(self)
        self.in_display = False

    def clear(self):
        if self.in_display:
            for item in self.get_all_objects():
                self.remove_from_scene(item, fade_out=False)
        self.nodes_from_synobs = {}
        self.gloss = None
        self.parser = INodeToKatajaConstituent(self)
        self.undo_manager = UndoManager(self)
        self.chain_manager = ChainManager(self)
        self.tree_manager = TreeManager(self)
        self.free_drawing = FreeDrawing(self)
        self.projection_manager = ProjectionManager(self)
        self.derivation_steps = DerivationStepManager(self)
        self.trees = []
        self.nodes = {}
        self.edges = {}
        self.groups = {}
        self.others = {}
        self.width_map = {}
        self.traces_to_draw = {}
        self.comments = []
        self.gloss_text = ''

    @time_me
    def forest_edited(self):
        """ Called after forest editing/free drawing actions that have changed the node graph.
        Analyse the node graph and update/rebuild syntactic objects according to graph.
        :return:
        """
        self.chain_manager.update()
        self.tree_manager.update_trees()
        self.projection_manager.update_projections()
        if ctrl.free_drawing_mode:
            print('doing nodes to synobjs in forest_edited')
            self.syntax.nodes_to_synobjs(self, [x.top for x in self.trees])


    @staticmethod
    def list_nodes(first):
        """
        Do left-first iteration through all nodes. Can become quite large if
        there is lots of
         multidomination.
        :param first: Node, can be started from a certain point in structure
        :return: iterator through nodes
        """

        def _iterate(node):
            yield node
            for child in node.get_children(similar=False, visible=False):
                _iterate(child)

        return _iterate(first)

    @staticmethod
    def list_visible_nodes_once(first):
        """
        Do left-first iteration through all nodes and return an iterator
        where only first instance
         of each node is present.
        :param first: Node, can be started from a certain point in structure
        :return: iterator through nodes
        """
        result = []

        def _iterate(node):
            if node not in result:
                result.append(node)
                for child in node.get_children(visible=True, similar=True):
                    _iterate(child)

        _iterate(first)
        return result

    @staticmethod
    def list_nodes_once(first):
        """
        Do left-first iteration through all nodes and return a list where
        only first instance of
        each node is present.
        :param first: Node, start from a certain point in structure
        :return: iterator through nodes
        """
        result = []

        def _iterate(node):
            if node not in result:
                result.append(node)
                for child in node.get_children(similar=False, visible=False):
                    _iterate(child)

        _iterate(first)
        return result

    def visible_nodes(self):
        """ Any node that is visible. Ignore the type.
        :return:
        """
        return (x for x in self.nodes.values() if x.is_visible())

    def get_nodes_by_index(self, index) -> (Node, set):
        head = None
        traces = set()
        for node in self.nodes.values():
            if node.node_type == g.CONSTITUENT_NODE:
                if node.index == index:
                    if node.is_trace:
                        traces.add(node)
                    else:
                        head = node
        return head, traces


    def get_numeration(self):
        for tree in self.trees:
            if tree.numeration:
                return tree
        tree = Tree(numeration=True)
        self.add_to_scene(tree)
        self.trees.append(tree)
        #tree.show()
        return tree

    def set_visualization(self, name):
        """ Switches the active visualization to visualization with given key
        :param name: string
        """
        if self.visualization and self.visualization.say_my_name() == name:
            self.visualization.reselect()
        else:
            vs = self.main.visualizations
            self.visualization = vs.get(name, vs.get(ctrl.settings.get('visualization'), None))
            self.vis_data = {'name': self.visualization.say_my_name()}
            self.visualization.prepare(self)
            ctrl.settings.set('hide_edges_if_nodes_overlap',
                              self.visualization.hide_edges_if_nodes_overlap, level=g.FOREST)
            self.scene.keep_updating_visible_area = True
        self.main.graph_scene.manual_zoom = False

    def restore_visualization(self):
        name = self.vis_data.get('name', ctrl.settings.get('visualization'))
        if (not self.visualization) or name != self.visualization.say_my_name():
            v = self.main.visualizations.get(name, None)
            if v:
                self.visualization = v
                v.prepare(self, reset=False)
                self.main.graph_scene.manual_zoom = False

    def update_visualization(self):
        """ Verify that the active visualization is the same as defined in
        the vis_data (saved visualization state)
        :return: None
        """
        name = self.vis_data.get('name', ctrl.settings.get('visualization'))
        if (not self.visualization) or name != self.visualization.say_my_name():
            self.set_visualization(name)

    # ### Maintenance and support methods
    # ################################################

    def __iter__(self):
        return self.trees.__iter__()

    def textual_form(self, tree=None, node=None):
        """ return (unicode) version of linearizations of all trees with
        traces removed --
            as close to original sentences as possible. If trees or node is given,
            return linearization of only that.
        :param tree: Tree instance
        :param node: Node instance
        """

        def _tree_as_text(tree, node, gap):
            """ Cheapo linearization algorithm for Node structures."""
            l = []
            if node in tree.sorted_constituents:
                i = tree.sorted_constituents.index(node)
                for n in tree.sorted_constituents[i:]:
                    l.append(str(n.syntactic_object))
            return gap.join(l)

        if tree:
            return _tree_as_text(tree, tree.top, ' ')
        elif node:
            return _tree_as_text(node.tree[0], node, ' ')
        else:
            trees = []
            for tree in self.trees:
                new_line = _tree_as_text(tree, tree.top, ' ')
                if new_line:
                    trees.append(new_line)
            return '/ '.join(trees)

    def syntax_trees_as_string(self):
        """
        :return:
        """
        s = []
        for tree in self.trees:
            if tree.top and tree.top.is_constituent:
                s.append(tree.top.syntactic_object.print_tree())
        return '\n'.join(s)

    # Scene and storage ---------------------------------------------------------------

    def store(self, item):
        """ Confirm that item is stored in some dictionary or other storage
        in forest
        :param item:
        """
        # if isinstance(item, ConstituentNode):
        # self.nodes[item.key] = item
        # elif isinstance(item, FeatureNode):
        # self.features[item.key] = item

        if isinstance(item, Node):
            self.poke('nodes')
            self.nodes[item.uid] = item
            self.free_drawing.node_types.add(item.node_type)
            if item.syntactic_object:
                # remember to rebuild nodes_by_uid in undo/redo, as it is not
                #  stored in model
                self.nodes_from_synobs[item.syntactic_object.uid] = item
        elif isinstance(item, Edge):
            self.poke('edges')
            self.edges[item.uid] = item
            self.free_drawing.edge_types.add(item.edge_type)
        else:
            key = getattr(item, 'uid', '') or getattr(item, 'key', '')
            if key and key not in self.others:
                self.poke('others')
                self.others[key] = item
            else:
                print('F trying to store broken type:', item.__class__.__name__)

    def add_all_to_scene(self):
        """ Put items belonging to this forest to scene """
        if self.in_display:
            for item in self.get_all_objects():
                sc = item.scene()
                if not sc:
                    self.scene.addItem(item)
                # if not item.parentItem():
                #    print('adding to scene: ', item)
                #    self.scene.addItem(item)

    def add_to_scene(self, item):
        """ Put items belonging to this forest to scene
        :param item:
        """
        if self.in_display:
            if isinstance(item, QtWidgets.QGraphicsItem):
                sc = item.scene()
                if not sc:
                    # print('..adding to scene ', item.uid )
                    self.scene.addItem(item)
                elif sc != self.scene:
                    # print('..adding to scene ', item.uid )
                    self.scene.addItem(item)

    def remove_from_scene(self, item, fade_out=True):
        """ Remove item from this scene
        :param item:
        :param fade_out: fade instead of immediate disappear
        :return:
        """
        if fade_out and hasattr(item, 'fade_out_and_delete'):
            item.fade_out_and_delete()

        elif isinstance(item, QtWidgets.QGraphicsItem):
            sc = item.scene()
            if sc == self.scene:
                #print('..removing from scene ', item.uid)
                sc.removeItem(item)
            elif sc:
                print('unknown scene for item %s : %s ' % (item, sc))
                sc.removeItem(item)
                print(' - removing anyways')
        else:
            print(type(item))

    # Getting objects ------------------------------------------------------

    def get_all_objects(self):
        """ Just return all objects governed by Forest -- not all scene objects 
        :return: iterator through objects
        """
        for n in self.trees:
            yield n
        for n in self.nodes.values():
            yield n
        for n in self.edges.values():
            yield n
        for n in self.others.values():
            yield n
        for n in self.projection_manager.projections.values():
            if n.visual:
                yield n.visual
        for n in self.groups.values():
            yield n
        if self.gloss:
            yield self.gloss

    def get_node(self, constituent):
        """
        Returns a node corresponding to a constituent
        :rtype : kataja.BaseConstituentNode
        :param constituent: syntax.BaseConstituent
        :return: kataja.ConstituentNode
        """
        if not constituent:
            return None
        return self.nodes_from_synobs.get(constituent.uid, None)

    def get_constituent_edges(self):
        """ Return generator of constituent edges
        :return: generator
        """
        return (x for x in self.edges.values() if
                x.edge_type == g.CONSTITUENT_EDGE and x.is_visible())

    def get_constituent_nodes(self):
        """ Return generator of constituent nodes
        :return: generator
        """
        return (x for x in self.nodes.values() if
                isinstance(x, ConstituentNode) and x.is_visible())

    def get_feature_nodes(self):
        """ Return generator of feature nodes
        :return: generator
        """
        return (x for x in self.nodes.values() if isinstance(x, FeatureNode))

    def get_attribute_nodes(self):
        """ Return generator of attribute nodes
        :return: generator
        """
        return (x for x in self.nodes.values() if isinstance(x, AttributeNode))

    # Drawing and updating --------------------------------------------

    def animation_started(self, key):
        """ Announce animation that should be waited before redrawing
        :param key:
        :return:
        """
        self.ongoing_animations.add(key)

    def animation_finished(self, key):
        """ Check out animation that was waited for, when all are checked out, redraw forest
        :param key:
        :return:
        """
        if key in self.ongoing_animations:
            self.ongoing_animations.remove(key)
        # fixme: put this back on when triangle animations work again
        #if not self.ongoing_animations:
        #    self.draw()

    def flush_and_rebuild_temporary_items(self):
        """ Clean up temporary stuff that may be invalidated by changes made by undo/redo.
        Notice that draw() does some of this, don't have to do those here.
        :return:
        """
        # Selection and related UI
        legits = list(self.get_all_objects())
        ctrl.multiselection_start()
        for item in ctrl.selected:
            if item not in legits:
                ctrl.remove_from_selection(item)
        ctrl.multiselection_end()

    def draw(self):
        """ Update all trees in the forest according to current visualization
        """
        if self.halt_drawing:
            return
        if not self.in_display:
            print("Why are we drawing a forest which shouldn't be in scene")
        assert self.is_parsed
        sc = ctrl.graph_scene
        sc.stop_animations()
        #self.tree_manager.update_trees()
        #for tree in self.trees:
        #    if tree.top:
        #        tree.top.update_visibility()  # fixme, delete trees with no visible tops
        #self.projection_manager.update_projections()
        self.update_forest_gloss()
        if self.visualization:
            self.visualization.prepare_draw()
            x = 0
            first = True
            for tree in self.trees:
                if tree.top:
                    #self.visualization.prepare_to_normalise(tree)
                    self.visualization.draw_tree(tree)
                    self.visualization.normalise_to_origo(tree)
                    #self.visualization.normalise_movers_to_top(tree)
                    br = tree.boundingRect()
                    if not first:
                        x -= br.left()
                    tree.move_to(x, 0)
                    x += br.right()
                    tree.start_moving()
                    first = False
        #if not sc.manual_zoom:
        #    sc.fit_to_window()
        sc.start_animations()
        ctrl.graph_view.repaint()

    def redraw_edges(self, edge_type=None):
        if edge_type:
            for edge in self.edges.values():
                if edge.edge_type == edge_type:
                    edge.update_shape()
        else:
            for edge in self.edges.values():
                edge.update_shape()

    def simple_parse(self, text):
        return self.parser.simple_parse(text)

    def create_node_from_string(self, text):
        """
        :param text:
        """
        return self.parser.string_into_forest(text)

    def order_edge_visibility_check(self):
        """ Make sure that all edges are checked to update their visibility.
        This can be called multiple
        times, but the visibility check is done only once.
        """
        self._do_edge_visibility_check = True

    def edge_visibility_check(self):
        """ Perform check for each edge: hide them if their start/end is
        hidden, show them if necessary.
        """
        if not self._do_edge_visibility_check:
            return
        for edge in set(self.edges.values()):
            changed = edge.update_visibility()
            if changed:
                if edge.is_visible():
                    if ctrl.is_selected(edge):
                        ctrl.ui.add_control_points(edge)
                else:
                    ctrl.ui.remove_ui_for(edge)
        self._do_edge_visibility_check = False

    def update_label_shape(self):
        shape = ctrl.settings.get('label_shape')
        ctrl.release_editor_focus()
        for node in self.nodes.values():
            if node.node_type == g.CONSTITUENT_NODE:
                node.label_object.label_shape = shape
                node.update_label()
            if node.is_triangle_host():
                ctrl.free_drawing.add_or_update_triangle_for(node)
        parents = []
        for node in self.nodes.values():
            node.update_relations(parents)
        for parent in parents:
            parent.gather_children()
        self.prepare_width_map()

    def update_forest_gloss(self):
        """ Draw the gloss text on screen, if it exists. """
        strat = ctrl.settings.get('gloss_strategy')
        if strat:
            if strat == 'linearisation':
                gts = []
                for tree in self.trees:
                    gt = ctrl.syntax.linearize(tree.top)
                    if gt:
                        gts.append(gt)
                self.gloss_text = ' '.join(gts)
            elif strat == 'message':
                pass
            elif strat == 'manual':
                pass
            elif strat == 'no':
                self.gloss_text = ''
        else:
            self.gloss_text = ''

        if self.gloss_text and not ctrl.settings.get('syntactic_mode'):
            if not self.gloss:
                self.gloss = self.free_drawing.create_node(node_type=g.GLOSS_NODE)
                self.gloss.label = self.gloss_text
            elif self.gloss.text != self.gloss_text:
                self.gloss.label = self.gloss_text
            self.gloss.update_label()
            self.gloss.physics_x = False
            self.gloss.physics_y = False
            self.gloss.put_to_top_of_trees()
            #self.gloss.show()
        elif self.gloss:
            self.remove_from_scene(self.gloss)
            self.gloss = None

    def compute_traces_to_draw(self, rotator) -> int:
        """ This is complicated, but returns a dictionary that tells for each index key
        (used by chains) in which position at trees to draw the node. Positions are identified by
        key of their immediate parent: {'i': ConstituentNode394293, ...} """
        # highest row = index at trees
        # x = cannot be skipped, last instance of that trace
        # i/j/k = index key
        # rows = rotation
        # * = use this node

        # 2 3 7 9 13 15 16
        # i j i i k  j  k
        #       x    x  x
        # * *     *
        #   * *   *
        #     *   *  *
        #       * *  *
        #       *    *  *
        # make an index-keyless version of this.
        trace_dict = {}
        sorted_parents = []
        required_keys = set()
        for tree in self:
            sortable_parents = []
            ltree = tree.sorted_nodes
            for node in ltree:
                if not hasattr(node, 'index'):
                    continue
                parents = node.get_parents(visible=True, similar=True)
                if len(parents) > 1:
                    node_key = node.uid
                    required_keys.add(node_key)
                    my_parents = []
                    for parent in parents:
                        if parent in ltree:
                            i = ltree.index(parent)
                            my_parents.append((i, node_key, parent, True))
                    if my_parents:
                        my_parents.sort()
                        a, b, c, d = my_parents[-1]  # @UnusedVariable
                        my_parents[-1] = a, b, c, False
                        sortable_parents += my_parents
            sortable_parents.sort()
            sorted_parents += sortable_parents
        if rotator < 0:
            rotator = len(sorted_parents) - len(required_keys)
        skips = 0
        for i, node_key, parent, can_be_skipped in sorted_parents:
            if node_key in required_keys:
                if skips == rotator or not can_be_skipped:
                    trace_dict[node_key] = parent.uid
                    required_keys.remove(node_key)
                else:
                    skips += 1
        self.traces_to_draw = trace_dict
        return rotator

    def should_we_draw(self, node, parent) -> bool:
        """ With multidominated nodes the child will eventually be drawn under one of its parents.
        Under which one is stored in traces_to_draw -dict. This checks if the node should be
        drawn under given parent.

        :param node:
        :param parent:
        :return:
        """
        if not parent:
            return True
        elif not self.traces_to_draw:
            return True
        elif hasattr(node, 'index') and len(node.get_parents(similar=True, visible=True)) > 1:
            key = node.uid
            if key in self.traces_to_draw:
                if parent.uid != self.traces_to_draw[key]:
                    return False
        return True

    def prepare_width_map(self):
        """ A map of how much horizontal space each node would need -- it is better to do this
        once than recursively compute these when updating labels.
        :return:
        """
        def recursive_width(node):
            if node.is_leaf(only_similar=True, only_visible=True):
                if node.is_visible():
                    w = node.label_object.width
                else:
                    w = 0
            else:
                w = node.label_object.left_bracket_width() + node.label_object.right_bracket_width()
                for n in node.get_children(similar=True, visible=True):
                    if self.should_we_draw(n, node):
                        w += recursive_width(n)
            self.width_map[node.uid] = w
            node.update_label()
            return w

        self.width_map = {}
        for tree in self:
            recursive_width(tree.top)
        return self.width_map

    # ### Minor updates for forest elements
    # #######################################################################

    def reform_constituent_node_from_string(self, text, node):
        """

        :param text:
        :param node:
        """
        new_nodes = self.parser.string_into_forest(text)
        if new_nodes:
            self.free_drawing.replace_node(node, new_nodes[0])

    # View mode
    @time_me
    def change_view_mode(self, syntactic_mode):
        t = time.time()
        ctrl.settings.set('syntactic_mode', syntactic_mode, level=g.FOREST)
        label_text_mode = ctrl.settings.get('label_text_mode')
        if syntactic_mode:
            self.old_label_mode = label_text_mode
            if label_text_mode == g.NODE_LABELS:
                ctrl.settings.set('label_text_mode', g.SYN_LABELS, level=g.FOREST)
            elif label_text_mode == g.NODE_LABELS_FOR_LEAVES:
                ctrl.settings.set('label_text_mode', g.SYN_LABELS_FOR_LEAVES, level=g.FOREST)
        else:
            if self.old_label_mode == g.NODE_LABELS or \
                            self.old_label_mode == g.NODE_LABELS_FOR_LEAVES:
                ctrl.settings.set('label_text_mode', self.old_label_mode, level=g.FOREST)
        nodes = list(self.nodes.values())
        for node in nodes:
            node.update_label()
            node.update_visibility(skip_label=True)
        ctrl.call_watchers(self, 'view_mode_changed', value=syntactic_mode)
        if syntactic_mode:
            if ctrl.main.color_manager.paper().value() < 100:
                ctrl.settings.set('temp_color_theme', 'dk_gray', level=g.FOREST)
            else:
                ctrl.settings.set('temp_color_theme', 'gray', level=g.FOREST)
        else:
            ctrl.settings.set('temp_color_theme', '', level=g.FOREST)
        ctrl.main.update_colors()

    ### Watcher #########################

    def watch_alerted(self, obj, signal, field_name, value):
        """ Receives alerts from signals that this object has chosen to
        listen. These signals
         are declared in 'self.watchlist'.

         This method will try to sort out the received signals and act
         accordingly.

        :param obj: the object causing the alarm
        :param signal: identifier for type of the alarm
        :param field_name: name of the field of the object causing the alarm
        :param value: value given to the field
        :return:
        """
        if signal == 'palette_changed':
            for other in self.others.values():
                other.update_colors()

    # ######## Utility functions ###############################

    # def parse_features(self, string, node):
    #     """
    #
    #     :param string:
    #     :param node:
    #     :return:
    #     """
    #     return self.parser.parse_definition(string, node)

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    trees = SavedField("trees")  # the current line of trees
    nodes = SavedField("nodes")
    edges = SavedField("edges")  #, if_changed=reserve_update_for_trees)
    groups = SavedField("groups")
    others = SavedField("others")
    vis_data = SavedField("vis_data", watcher="visualization")
    derivation_steps = SavedField("derivation_steps")
    comments = SavedField("comments")
    gloss_text = SavedField("gloss_text")
    syntax = SavedField("syntax")
    is_parsed = SavedField("is_parsed")
    gloss = SavedField("gloss")
Esempio n. 8
0
class Constituent(MyBaseClass):  # collections.UserList):

    role = "Constituent"

    def __init__(self, label='', part1=None, part2=None, features=None):
        if in_kataja:
            if features is not None:
                features = list(features)
            else:
                features = []
            if part1 and part2:
                super().__init__(label=label, parts=[part1, part2], features=features)
            else:
                super().__init__(label=label, features=features)
        self.label = label
        self.part1 = part1
        self.part2 = part2
        self.features = []
        self.stacked = False
        self.transfered = False
        if features:
            for f in features:
                if isinstance(f, str):
                    self.features.append(Feature(f))
                elif isinstance(f, Feature):
                    self.features.append(f) #.copy())

    def __repr__(self):
        parts = [self.label]
        if self.part1:
            parts.append(self.part1)
        if self.part2:
            parts.append(self.part2)
        if self.features:
            parts.append(sorted(list(self.features)))
        return repr(parts)

    def featureless_str(self):
        """ Used to replicate Model10 output
        :return:
        """
        if self.part1 and self.part2:
            return '[%s %s %s]' % (self.label, self.part1.featureless_str(), self.part2.featureless_str())
        else:
            return '[%s ]' % self.label

    def model10_string(self):
        parts = [self.label]
        if self.part1:
            parts.append(self.part1.model10_string())
        if self.part2:
            parts.append(self.part2.model10_string())
        if self.features:
            fs = []
            for item in list(expanded_features(self.features)):
                fs.append(repr(item))
            parts.append(fs)
        return parts

    def __str__(self):
        """ Override BaseConstituent's __str__
        :return:
        """
        return repr(self)

    def __hash__(self):
        return id(self)

    def labelstring(self):
        if self.part1 and self.part2:
            return '[%s %s %s]' % (self.label, self.part1.labelstring(), self.part2.labelstring())
        else:
            return self.label

    def is_leaf(self):
        return not (self.part1 and self.part2)

    def shared_features(self, other):
        my_feats = self.get_head_features()
        other_feats = other.get_head_features()
        return find_shared_features(my_feats, other_feats)

    def add_feature(self, feat):
        self.poke('features')
        if not isinstance(feat, Feature):
            feat = Feature(feat)
        self.features.append(feat)

    def remove_feature(self, feat):
        self.poke('features')
        self.features.remove(feat)

    def replace_feature(self, old, new):
        if old in self.features:
            self.poke('features')
            self.features.remove(old)
            self.features.append(new)

    def has_feature(self, key):
        """ Check the existence of feature within this constituent
        :param key: string for identifying feature type or Feature instance
        :return: bool
        """
        if isinstance(key, Feature):
            return key in self.features
        return key in self.features

    def replace_feature_by_name(self, old_name, new):
        found = None
        for item in self.features:
            if item.name == old_name:
                found = item
                break
        if found is not None:
            self.features.remove(found)
            self.features.append(new)
        else:
            print('couldnt find ', old_name, self)

    def ordered_parts(self):
        if self.part1 and self.part2:
            return [self.part1, self.part2]
        elif self.part1:
            return [self.part1]
        elif self.part2:
            return [self.part2]
        else:
            return []

    def ordered_features(self):
        return list(self.features)

    def is_unlabeled(self):
        return self.label == '_'

    def is_labeled(self):
        return self.label != '_'

    def copy(self):
        if self.part1 and self.part2:
            return Constituent(label=self.label, part1=self.part1.copy(), part2=self.part2.copy())
        elif self.features:
            return Constituent(label=self.label, features=self.features)
        else:
            raise TypeError

    def demote_to_copy(self):
        """ Turn this into a SO with "Copy" feature and return a new fresh version without the
        copy-feature.
        :return:
        """
        feature_giver = self.get_feature_giver()
        copy_feature = Feature("Copy")
        if copy_feature in feature_giver.features:
            feature_giver.features.remove(copy_feature)
        fresh = self.copy()
        feature_giver.features.append(copy_feature)
        return fresh

    def __contains__(self, item):
        if isinstance(item, Constituent):
            if self.part1 == item:
                return True
            elif self.part2 == item:
                return True
            if self.part1:
                if item in self.part1:
                    return True
            if self.part2:
                if item in self.part2:
                    return True
        elif isinstance(item, Feature):
            return item in self.features
        return False

    def tex_tree(self, stack):
        stacked = ''
        if self.stacked:
            stacked = '(S?)'
            for i, item in enumerate(reversed(stack)):
                if item is self:
                    stacked = '(S%s)' % i
                    break
        label = self.label.replace('_', '-')
        if self.part1 and self.part2:
            return '[.%s%s %s %s ]' % (label, stacked, self.part1.tex_tree(stack),
                                       self.part2.tex_tree(stack))
        else:
            return label + stacked

    def get_head(self, no_sharing=True):
        if self.is_leaf():
            return self
        if self.is_unlabeled():
            return self.part1.get_head()
        elif self.part2.is_unlabeled():
            f = self.part1.get_head()
            if f:
                return f
        if self.part1.label == self.label:
            return self.part1.get_head()
        elif self.part2.label == self.label:
            return self.part2.get_head()
        elif self.label in SHARED_FEAT_LABELS:
            if no_sharing:
                return self.part1.get_head(no_sharing=True)
            else:
                head1 = self.part1.get_head()
                head2 = self.part2.get_head()
                result = []
                if isinstance(head1, list):
                    result += head1
                else:
                    result.append(head1)
                if isinstance(head2, list):
                    result += head2
                else:
                    result.append(head2)
                return result
        if "Phi" in self.label:
            return self.part1.get_head()
        assert False

    def get_head_features(self, no_sharing=False, expanded=False):
        if no_sharing:
            head = self.get_head(no_sharing=True)
            if expanded:
                return expanded_features(head.features)
            else:
                return head.features
        else:
            head = self.get_head(no_sharing=False)
            if isinstance(head, list):
                shared_feats = find_shared_features(head[0].features, head[1].features)
                if shared_feats:
                    feats_list = shared_feats
                else:
                    head = self.get_head(no_sharing=True)
                    feats_list = head.features
            else:
                feats_list = head.features
            if expanded:
                return expanded_features(feats_list)
            else:
                return feats_list

    def get_feature_giver(self):
        return self.get_head(no_sharing=True)

    def replace_within(self, old_chunk, new_chunk, label=False):
        if self == old_chunk:
            self.label = new_chunk.label
            self.features = new_chunk.features
            self.part1 = new_chunk.part1
            self.part2 = new_chunk.part2
            return
        if label:
            self.recursive_replace_label(old_chunk, new_chunk)
        elif isinstance(old_chunk, Constituent):
            self.recursive_replace_constituent(old_chunk, new_chunk)
        elif isinstance(old_chunk, list):
            found = self.recursive_replace_feature_set(old_chunk, new_chunk)
        elif isinstance(old_chunk, Feature):
            found = self.recursive_replace_feature(old_chunk, new_chunk)
        else:
            raise TypeError

    def recursive_replace_label(self, old_label, new_label):
        if self.part1:
            self.part1.recursive_replace_label(old_label, new_label)
        if self.part2:
            self.part2.recursive_replace_label(old_label, new_label)
        if self.label == old_label:
            self.label = new_label

    def recursive_replace_feature_set(self, old, new, found=0):
        # print(type(old),old, type(new), new)
        for item in new:
            if not isinstance(item, Feature):
                print(new)
                raise TypeError
        if self.part1:
            found = self.part1.recursive_replace_feature_set(old, new, found)
        if self.part2:
            found = self.part2.recursive_replace_feature_set(old, new, found)
        if old == self.features:
            self.features = list(new)
            found += 1
        return found

    def recursive_replace_feature(self, old, new, found=0):
        if new and not isinstance(new, Feature):
            print(new)
            raise TypeError
        if self.part1:
            found = self.part1.recursive_replace_feature(old, new, found)
        if self.part2:
            found = self.part2.recursive_replace_feature(old, new, found)
        if old in self.features:
            found += 1
            self.features.remove(old)
            if new:
                self.features.append(new)
        return found

    def recursive_replace_constituent(self, old, new):
        assert(self != old)
        if self.part1:
            if self.part1 == old:
                self.part1 = new
            else:
                self.part1.recursive_replace_constituent(old, new)
        if self.part2:
            if self.part2 == old:
                self.part2 = new
            else:
                self.part2.recursive_replace_constituent(old, new)

    if in_kataja:
        part1 = SavedField("part1")
        part2 = SavedField("part2")
Esempio n. 9
0
class Group(SavedObject, QtWidgets.QGraphicsObject):

    __qt_type_id__ = next_available_type_id()
    permanent_ui = False
    unique = False
    can_fade = False
    scene_item = True
    is_widget = False

    def __init__(self, selection=None, persistent=True):
        SavedObject.__init__(self)
        QtWidgets.QGraphicsObject.__init__(self)
        # -- Fake as UIItem to make selection groups part of UI:
        self.ui_key = next_available_ui_key()
        self.ui_type = self.__class__.__name__
        self.ui_manager = ctrl.ui
        self.role = None
        self.host = None
        self.watchlist = []
        self.is_fading_in = False
        self.is_fading_out = False
        # -- end faking as UIItem
        self.selection = []
        self.selection_with_children = []
        self.persistent = persistent
        self._skip_this = not persistent
        self._selected = False
        self.points = []
        self.center_point = None
        self.outline = False
        self.fill = True
        self.color_key = ''
        self.color = None
        self.color_tr_tr = None
        self.purpose = None
        self.path = None
        self.label_item = None
        self.label_data = {}
        self.include_children = False
        self.allow_overlap = True
        self._br = None
        #self.setFlag(QtWidgets.QGraphicsObject.ItemIsMovable)
        self.setFlag(QtWidgets.QGraphicsObject.ItemIsSelectable)
        if selection:
            self.update_selection(selection)
        self.update_shape()
        self.update_colors()

    def __contains__(self, item):
        return item in self.selection_with_children

    def type(self):
        """ Qt's type identifier, custom QGraphicsItems should have different type ids if events
        need to differentiate between them. These are set when the program starts.
        :return:
        """
        return self.__qt_type_id__

    def after_init(self):
        self.update_selection(self.selection)
        self.update_shape()
        self.update_colors()

    def after_model_update(self, updated_fields, transition_type):
        """ Compute derived effects of updated values in sensible order.
        :param updated_fields: field keys of updates
        :param transition_type: 0:edit, 1:CREATED, -1:DELETED
        :return: None
        """
        if transition_type == g.CREATED:
            ctrl.forest.store(self)
            ctrl.forest.add_to_scene(self)
        elif transition_type == g.DELETED:
            ctrl.forest.remove_from_scene(self, fade_out=False)
            return
        if updated_fields:
            self.update_selection(self.selection)
            self.update_shape()
            self.update_colors()

    def copy_from(self, source):
        """ Helper method to easily make a similar selection with different identity
        :param source:
        :return:
        """
        self.selection = source.selection
        self.selection_with_children = source.selection_with_children
        self.points = list(source.points)
        self.center_point = source.center_point
        self.outline = source.outline
        self.fill = source.fill
        self.color_key = source.color_key
        self.color = source.color
        self.color_tr_tr = source.color_tr_tr
        self.path = source.path
        text = source.get_label_text()
        if text:
            self.set_label_text(text)
        self.include_children = source.include_children
        self.allow_overlap = source.allow_overlap

    def get_label_text(self):
        """ Label text is actually stored in model.label_data, but this is a
        shortcut for it.
        :return:
        """
        return self.label_data.get('text', '')

    def set_label_text(self, value):
        if self.label_item:
            old = self.get_label_text()
            if old != value:
                self.poke('label_data')
                self.label_data['text'] = value
                self.label_item.update_text(value)
        else:
            self.label_item = GroupLabel(value, parent=self)
            self.poke('label_data')
            self.label_data['text'] = value
        if self.label_item.automatic_position:
            self.label_item.position_at_bottom()
        else:
            self.label_item.update_position()

    def if_changed_color_id(self, value):
        """ Set group color, uses palette id strings as values.
        :param value: string
        """
        if self.label_item:
            self.label_item.setDefaultTextColor(ctrl.cm.get(value))

    def remove_node(self, node):
        """ Manual removal of single node, should be called e.g. when node is deleted.
        :param node:
        :return:
        """
        if node in self.selection:
            self.poke('selection')
            self.selection.remove(node)
        if node in self.selection_with_children:
            self.selection_with_children.remove(node)

        if self.selection:
            self.update_shape()
        else:
            if self.persistent:
                ctrl.free_drawing.remove_group(self)
            else:
                ctrl.ui.remove_ui_for(self)

    def remove_nodes(self, nodes):
        """ Remove multiple nodes, just to avoid repeated calls to expensive updates
        :param nodes:
        :return:
        """
        self.poke('selection')
        for node in nodes:
            if node in self.selection:
                self.selection.remove(node)
            if node in self.selection_with_children:
                self.selection_with_children.remove(node)
        if self.selection:
            self.update_shape()
        else:
            if self.persistent:
                ctrl.free_drawing.remove_group(self)
            else:
                ctrl.ui.remove_ui_for(self)

    def clear(self, remove=True):
        self.selection = set()
        self.selection_with_children = set()
        self.update_shape()
        if remove:
            if self.persistent:
                ctrl.free_drawing.remove_group(self)
            else:
                ctrl.ui.remove_ui_for(self)

    def add_node(self, node):
        """ Manual addition of single node
        :param node:
        :return:
        """
        if node not in self.selection:
            self.poke('selection')
            self.selection.append(node)
            self.update_selection(self.selection)
            self.update_shape()

    def update_selection(self, selection):
        swc = []
        other_selections = set()
        if not self.allow_overlap:
            for group in ctrl.forest.groups.values():
                other_selections = other_selections | set(
                    group.selection_with_children)

        def recursive_add_children(i):
            if isinstance(i, Node) and i not in swc and \
                    (i in selection or i not in other_selections):
                swc.append(i)
                for child in i.get_children(similar=False, visible=False):
                    recursive_add_children(child)

        if selection:
            self.selection = [
                item for item in selection
                if isinstance(item, Node) and item.can_be_in_groups
            ]
            if self.include_children:
                for item in self.selection:
                    recursive_add_children(item)
                self.selection_with_children = swc
            else:
                self.selection_with_children = self.selection
        else:
            self.selection = []
            self.selection_with_children = []

    def update_shape(self):
        def embellished_corners(item):
            x1, y1, x2, y2 = item.sceneBoundingRect().getCoords()
            corners = [(x1 - 5, y1 - 5), (x2 + 5, y1 - 5), (x2 + 5, y2 + 5),
                       (x1 - 5, y2 + 5)]

            return x1, y1, x2, y2, corners

        sel = [x for x in self.selection_with_children if x.isVisible()]

        if len(sel) == 0:
            self._br = QtCore.QRectF()
            self.path = None
            self.center_point = 0, 0
            return
        elif len(sel) == 1:
            x1, y1, x2, y2, route = embellished_corners(sel[0])
            self._br = QtCore.QRectF(x1 - 5, y1 - 5, x2 - x1 + 10,
                                     y2 - y1 + 10)
            self.path = QtGui.QPainterPath(
                QtCore.QPointF(route[0][0], route[0][1]))
            for x, y in route[1:]:
                self.path.lineTo(x, y)
            self.path.closeSubpath()
            center = self._br.center()
            self.center_point = center.x(), center.y()
            cx, cy = self.center_point

        else:
            corners = []
            c = 0
            x_sum = 0
            y_sum = 0
            min_x = 50000
            max_x = -50000
            min_y = 50000
            max_y = -50000
            for item in sel:
                c += 2
                x1, y1, x2, y2, icorners = embellished_corners(item)
                x_sum += x1
                x_sum += x2
                y_sum += y1
                y_sum += y2
                if x1 < min_x:
                    min_x = x1
                if x2 > max_x:
                    max_x = x2
                if y1 < min_y:
                    min_y = y1
                if y2 > max_y:
                    max_y = y2
                corners += icorners
            self.prepareGeometryChange()
            self._br = QtCore.QRectF(min_x, min_y, max_x - min_x,
                                     max_y - min_y)
            cx = (min_x + max_x) / 2
            cy = (min_y + max_y) / 2
            self.center_point = cx, cy
            r = max(max_x - min_x, max_y - min_y) * 1.1
            dots = 32
            step = 2 * math.pi / dots
            deg = 0
            route = []
            for n in range(0, dots):
                cpx = math.cos(deg) * r + cx
                cpy = math.sin(deg) * r + cy
                deg += step
                closest = None
                closest_d = 2000000
                for px, py in corners:
                    d = (px - cpx)**2 + (py - cpy)**2
                    if d < closest_d:
                        closest = px, py
                        closest_d = d
                if closest:
                    if route:
                        last = route[-1]
                        if last == closest:
                            continue
                    route.append(closest)

        if self.label_item:
            if self.label_item.automatic_position:
                self.label_item.position_at_bottom()
            else:
                self.label_item.update_position()

        curved_path = Group.interpolate_point_with_bezier_curves(route)
        sx, sy = route[0]
        self.path = QtGui.QPainterPath(QtCore.QPointF(sx, sy))
        for fx, fy, sx, sy, ex, ey in curved_path:
            self.path.cubicTo(fx, fy, sx, sy, ex, ey)
        # This is costly
        if True:
            for item in self.collidingItems():
                if isinstance(item, Node) and item.node_type == g.CONSTITUENT_NODE and item not in \
                        self.selection_with_children:
                    x, y = item.current_scene_position
                    subshape = item.shape().translated(x, y)
                    subshape_points = []
                    for i in range(0, subshape.elementCount()):
                        element = subshape.elementAt(i)
                        subshape_points.append((element.x, element.y))
                    curved_path = Group.interpolate_point_with_bezier_curves(
                        subshape_points)
                    sx, sy = subshape_points[0]
                    subshape = QtGui.QPainterPath(QtCore.QPointF(sx, sy))
                    for fx, fy, sx, sy, ex, ey in curved_path:
                        subshape.cubicTo(fx, fy, sx, sy, ex, ey)

                    self.path = self.path.subtracted(subshape)

    def shape(self):
        if self.path:
            return self.path
        else:
            return QtGui.QPainterPath()

    def update_position(self):
        self.update_shape()

    def clockwise_path_points(self, margin=2):
        """ Return points along the path circling the group. A margin can be provided to make the
        points be n points away from the path. Points start from the topmost, rightmost point.
        :param margin:
        :return:
        """
        if not self.path:
            return QtCore.QPointF(
                0, 0)  # hope this group will be removed immediately
        max_x = -30000
        min_y = 30000
        start_i = 0
        ppoints = []
        cx, cy = self.center_point
        better_path = [
        ]  # lets have not only corners, but also points along the edges
        last_element = self.path.elementAt(self.path.elementCount() - 1)
        last_x = last_element.x
        last_y = last_element.y
        for i in range(0, self.path.elementCount()):
            element = self.path.elementAt(i)
            x = element.x
            y = element.y
            better_path.append(((last_x + x) / 2, (last_y + y) / 2))
            better_path.append((x, y))
            last_x = x
            last_y = y

        for i, (x, y) in enumerate(better_path):
            if margin != 0:
                dx = x - cx
                dy = y - cy
                d = math.hypot(dx, dy)
                if d == 0:
                    change = 0
                else:
                    change = (d + margin) / d  # should return values like 1.08
                x = cx + (dx * change)
                y = cy + (dy * change)
            ppoints.append((x, y))
            if y < min_y or (y == min_y and x > max_x):
                min_y = y
                max_x = x
                start_i = i
        return ppoints[start_i:] + ppoints[:start_i]

    def boundingRect(self):
        if not self._br:
            self.update_shape()
        return self._br

    def get_color_id(self):
        return self.color_key

    def update_colors(self, color_key=''):
        if not self.color_key:
            self.color_key = color_key or "accent1"
        elif color_key:
            self.color_key = color_key
        self.color = ctrl.cm.get(self.color_key)
        self.color_tr_tr = QtGui.QColor(self.color)
        self.color_tr_tr.setAlphaF(0.2)
        if self.label_item:
            self.label_item.update_color()

    def mousePressEvent(self, event):
        ctrl.press(self)
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if ctrl.pressed is self:
            ctrl.release(self)
            if ctrl.dragged_set:
                ctrl.graph_scene.kill_dragging()
                ctrl.ui.update_selections(
                )  # drag operation may have changed visible affordances
            else:  # This is regular click on 'pressed' object
                self.select(event)
                self.update()
            return None  # this mouseRelease is now consumed
        super().mouseReleaseEvent(event)

    def select(self, event=None, multi=False):
        """ Scene has decided that this node has been clicked
        :param event:
        :param multi: assume multiple selection (append, don't replace)
        """
        if not self.persistent:
            return
        ctrl.multiselection_start()
        if (event and event.modifiers() == QtCore.Qt.ShiftModifier) or multi:
            # multiple selection
            if ctrl.is_selected(self):
                ctrl.remove_from_selection(self)
            else:
                ctrl.add_to_selection(self)
                for item in self.selection:
                    ctrl.add_to_selection(item)
        elif ctrl.is_selected(self):
            ctrl.deselect_objects()
        else:
            ctrl.deselect_objects()
            ctrl.add_to_selection(self)
            for item in self.selection:
                ctrl.add_to_selection(item)
        ctrl.multiselection_end()

    def update_selection_status(self, value):
        """

        :param value:
        :return:
        """
        self._selected = value

    def paint(self, painter, style, QWidget_widget=None):
        if self.selection and self.path:
            if self.fill:
                painter.fillPath(self.path, self.color_tr_tr)
            if self._selected:
                painter.setPen(ctrl.cm.selection())
                painter.drawPath(self.path)
            elif self.outline:
                painter.setPen(self.color)
                painter.drawPath(self.path)

    @staticmethod
    def interpolate_point_with_bezier_curves(points):
        """ Curved path algorithm based on example by Raul Otaño Hurtado, from
        http://www.codeproject.com/Articles/769055/Interpolate-D-points-usign-Bezier-curves-in-WPF
        :param points:
        :return:
        """
        if len(points) < 3:
            return None
        res = []

        # if is close curve then add the first point at the end
        if points[-1] != points[0]:
            points.append(points[0])

        for i, (x1, y1) in enumerate(points[:-1]):
            if i == 0:
                x0, y0 = points[-2]
            else:
                x0, y0 = points[i - 1]
            x2, y2 = points[i + 1]
            if i == len(points) - 2:
                x3, y3 = points[1]
            else:
                x3, y3 = points[i + 2]

            xc1 = (x0 + x1) / 2.0
            yc1 = (y0 + y1) / 2.0
            xc2 = (x1 + x2) / 2.0
            yc2 = (y1 + y2) / 2.0
            xc3 = (x2 + x3) / 2.0
            yc3 = (y2 + y3) / 2.0
            len1 = math.hypot(x1 - x0, y1 - y0)
            len2 = math.hypot(x2 - x1, y2 - y1)
            len3 = math.hypot(x3 - x2, y3 - y2)

            k1 = len1 / (len1 + len2)
            k2 = len2 / (len2 + len3)

            xm1 = xc1 + (xc2 - xc1) * k1
            ym1 = yc1 + (yc2 - yc1) * k1

            xm2 = xc2 + (xc3 - xc2) * k2
            ym2 = yc2 + (yc3 - yc2) * k2

            smooth = 0.8
            ctrl1_x = xm1 + (xc2 - xm1) * smooth + x1 - xm1
            ctrl1_y = ym1 + (yc2 - ym1) * smooth + y1 - ym1
            ctrl2_x = xm2 + (xc2 - xm2) * smooth + x2 - xm2
            ctrl2_y = ym2 + (yc2 - ym2) * smooth + y2 - ym2
            res.append((ctrl1_x, ctrl1_y, ctrl2_x, ctrl2_y, x2, y2))
        return res

    selection = SavedField("selection")
    color_key = SavedField("color_key")
    label_data = SavedField("label_data")
    purpose = SavedField("purpose")
    include_children = SavedField("include_children")
    allow_overlap = SavedField("allow_overlap")
    fill = SavedField("fill")
    outline = SavedField("outline")
    persistent = SavedField("persistent")
    forest = SavedField("forest")
Esempio n. 10
0
class Movable(SavedObject, QtWidgets.QGraphicsObject):
    """
    Movable items
    -------------

    Movable items are items on canvas that can be affected by visualisation algorithms.
    There are three types of movement:

    set_position(p3):
    item is immediately put to given position.

    move_to(p3):
    item slides to given position

    use_physics(True|False)
    physics_x = True|False:
    physics_y = True|False:
    after move, the item can wander around according to physics, in set dimensions. use_physics sets
    all xy to True|False.
    Physics is handled by visualization algorithm, Movable only announces if it is responsive for
    physics.

    Movements using move_to -are affected by adjustment. Adjustment is a vector that displaces the
    item given amount from the move_to -command.

    Movement is triggered manually with move_to. Element may have wandered away from its target
    position: target position should not be used after the movement if node uses physics.
    Adjustment needs to be taken into account always when using target_position

    Nodes that use physics disable adjustment after the move_to has ended.

    When nodes that use physics are dragged around, they are locked into position. 'Locked' state
    overrides physics: the node stays in those coordinates.
    When nodes that don't use physics are dragged, the adjustment.

    """
    def __init__(self):
        SavedObject.__init__(self)
        QtWidgets.QGraphicsObject.__init__(self)
        # Common movement-related elements
        self._current_position = (random.random() *
                                  150) - 75, (random.random() * 150) - 75
        self._dragged = False
        self.trees = set(
        )  # each Movable belongs to some trees, either formed by Movable
        # alone or set of Movables. Tree has abstract position adjustment information.

        # MOVE_TO -elements
        self.target_position = 0, 0
        self.adjustment = 0, 0
        self._start_position = 0, 0
        self._move_frames = 0
        self._move_counter = 0
        self._use_easing = True
        self._distance = None
        self.unmoved = True  # flag to distinguish newly created nodes
        self.after_move_function = None
        self.use_adjustment = False
        self._high_priority_move = False
        self.locked_to_node = None
        # PHYSICS -elements
        self.locked = False
        self.physics_x = False
        self.physics_y = False
        self.repulsion = 0.2
        # Other
        self._visible_by_logic = True
        self._fade_anim = None
        self.is_fading_in = False
        self.is_fading_out = False
        self._hovering = False

    def type(self):
        """ Qt's type identifier, custom QGraphicsItems should have different type ids if events
        need to differentiate between them. These are set when the program starts.
        :return:
        """
        return self.__qt_type_id__

    def late_init(self):
        pass

    def after_model_update(self, updated_fields, transition_type):
        """ Compute derived effects of updated values in sensible order.
        :param updated_fields: field keys of updates
        :param transition_type: 0:edit, 1:CREATED, -1:DELETED
        :return: None
        """
        #print('movable after_model_update, ', transition_type, revert_transition)
        if transition_type == CREATED:
            ctrl.forest.store(self)
            ctrl.forest.add_to_scene(self)
        elif transition_type == DELETED:
            ctrl.forest.remove_from_scene(self, fade_out=False)
            return
        self.update_position()

    def use_physics(self):
        return (self.physics_x or self.physics_y) and not self.locked_to_node

    def reset(self):
        """ Remove mode information, eg. hovering
        :return: None
        """
        self._hovering = False

    @property
    def current_position(self):
        return self._current_position

    @current_position.setter
    def current_position(self, value):
        self._current_position = value
        self.setPos(*value)

    @property
    def current_scene_position(self):
        """ Return current position in scene coordinates and turned to xy -tuple.
        :return:
        """
        xy = self.scenePos()
        return int(xy.x()), int(xy.y())

    # ## Movement ##############################################################

    def move_to(self,
                x,
                y,
                after_move_function=None,
                valign=MIDDLE,
                align=NO_ALIGN,
                can_adjust=True):
        """ Start movement to given position
        :param x:
        :param y:
        :param after_move_function: Function to call when the movement is finished
        :param valign: What position inside the moved item should correspond to given coordinates.
        By default align is in center, but often you may want to move items
        so that e.g. their top rows are aligned.
        Values are TOP(0), TOP_ROW(1), MIDDLE(2), BOTTOM_ROW(3) and BOTTOM(4)_
        :param align: NO_ALIGN, LEFT_ALIGN, CENTER_ALIGN, RIGHT_ALIGN
        :param can_adjust: can use movable's adjustment to adjust the target position
        :return:
        """
        if self.use_adjustment and can_adjust:
            ax, ay = self.adjustment
            x += ax
            y += ay
        if valign == MIDDLE:
            pass
        elif valign == TOP:
            y -= self.boundingRect().top()
        elif valign == TOP_ROW:
            y -= self.get_top_y()
        elif valign == BOTTOM_ROW:
            y -= self.get_lower_part_y()
        elif valign == BOTTOM:
            y -= self.boundingRect().bottom()
        if align == NO_ALIGN:
            pass
        elif align == CENTER_ALIGN:
            br = self.boundingRect()
            x -= br.width() / 2 + br.x()
        elif align == LEFT_ALIGN:
            x -= self.boundingRect().x()
        if (x, y) == self.target_position and self._move_counter:
            # already moving there
            return
        self.target_position = x, y
        if after_move_function:
            self.after_move_function = after_move_function
        self.start_moving()

    def get_lower_part_y(self):
        """ Implement this if the movable has content where differentiating between bottom row
        and top row can potentially make sense.
        :return:
        """
        return 0

    def get_top_y(self):
        """ Implement this if the movable has content where differentiating between bottom row
        and top row can potentially make sense.
        :return:
        """
        return 0

    def move(self, md: dict) -> (bool, bool):
        """ Do one frame of movement: either move towards target position or
        take a step according to algorithm
        1. item folding towards position in part of animation to disappear etc.
        2. item is being dragged
        3. item is locked by user
        4. item is tightly attached to another node which is moving (then the move is handled by
        the other node, it is _not_ parent node, though.)
        5. visualisation algorithm setting it specifically
        (6) or (0) -- places where subclasses can add new movements.

        :param md: movement data dict, collects sum of all movement to help normalize it
        :return:
        """
        # _high_priority_move can be used together with _move_counter

        self.unmoved = False
        if not self._high_priority_move:
            # Dragging overrides (almost) everything, don't try to move this anywhere
            if self._dragged:
                return True, False
            # Locked nodes are immune to physics
            elif self.locked:
                return False, False
            #elif self.locked_to_node:
            #    return False, False
        # MOVE_TO -based movement has priority over physics. This way e.g. triangles work without
        # additional stipulation
        if self._move_counter:
            position = self.current_position
            # stop even despite the _move_counter, if we are close enough
            if about_there(position, self.target_position):
                self.stop_moving()
                return False, False
            self._move_counter -= 1
            # move a precalculated step
            if self._use_easing:
                if self._move_frames != self._move_counter:
                    time_f = 1 - (self._move_counter / self._move_frames)
                    f = qt_prefs.curve.valueForProgress(time_f)
                else:
                    f = 0
                movement = multiply_xy(self._distance, f)
                self.current_position = add_xy(self._start_position, movement)
            else:
                movement = div_xy(sub_xy(self.target_position, position),
                                  self._move_counter)
                self.current_position = add_xy(self.current_position, movement)
            # if move counter reaches zero, stop and do clean-up.
            if not self._move_counter:
                self.stop_moving()
            if self.locked_to_node:
                self.locked_to_node.update_bounding_rect()
            return True, False
        # Physics move node around only if other movement types have not overridden it
        elif self.use_physics() and self.is_visible():
            movement = ctrl.forest.visualization.calculate_movement(self)
            md['sum'] = add_xy(movement, md['sum'])
            md['nodes'].append(self)
            self.current_position = add_xy(self.current_position, movement)
            return abs(movement[0]) + abs(movement[1]) > 0.6, True
        return False, False

    def distance_to(self, movable):
        """ Return current x,y distance to another movable
        :param movable:
        :return: x, y
        """
        return sub_xy(self.current_position, movable.current_position)

    def set_original_position(self, pos):
        """ Sets both current position and computed position to same place,
        use when first adding items to scene to prevent them wandering from afar
        :param pos: tuple (x, y)
        """
        if isinstance(pos, (QtCore.QPoint, QtCore.QPointF)):
            pos = pos.x(), pos.y()
        self.target_position = tuple(pos)
        self.use_adjustment = False
        self.adjustment = (0, 0)
        self.current_position = tuple(pos)
        self._dragged = False
        self.locked = False

    def start_moving(self):
        """ Initiate moving animation for object.
        :return: None
        """
        self._use_easing = True
        dx, dy = sub_xy(self.target_position, self.current_position)
        d = math.sqrt(dx * dx + dy * dy)
        self._distance = dx, dy
        # this scales nicely:
        # d = 0 -> p = 0
        # d = 50 -> p = 0.5849
        # d = 100 -> p = 1
        # d = 200 -> p = 1.5849
        # d = 500 -> p = 2.5849
        # d = 1000 -> p = 3.4594
        p = math.log2(d * 0.01 + 1)
        self._move_frames = int(p * prefs.move_frames)
        if self._move_frames == 0:
            self._move_frames = 1
        #self._move_frames = prefs.move_frames
        self._move_counter = self._move_frames
        self._start_position = self.current_position
        # self.adjustment affects both elements in the previous subtraction, so it can be ignored
        ctrl.graph_scene.item_moved()

    def stop_moving(self):
        """ Kill moving animation for this object.
        :return: None
        """
        self._high_priority_move = False
        self.target_position = self.current_position
        if self.after_move_function:
            self.after_move_function()
            self.after_move_function = None
        self._move_counter = 0

    def _current_position_changed(self, value):
        self.setPos(value[0], value[1])

    def update_position(self):
        """ Compute new current_position and target_position
        :return: None
        """
        #if (not self.use_physics()) and (not self._move_counter):

        if hasattr(self, 'setPos'):
            self.setPos(*self.current_position)

    def release(self):
        """ Remove lock and adjustment"""
        if self.locked:
            self.locked = False
        elif self.use_adjustment:
            self.adjustment = (0, 0)
            self.use_adjustment = False
        self.update_position()

    def lock(self):
        """ Item cannot be moved by physics or it is set to use adjustment"""
        if self.use_physics():
            self.locked = True
        else:
            self.use_adjustment = True

    # ## Opacity ##############################################################

    def fade_in(self, s=300):
        """ Simple fade effect. The object exists already when fade starts.
        :return: None
        """
        if self.is_fading_in:
            return
        self.is_fading_in = True
        self.show()
        if self.is_fading_out:
            print('interrupting fade out ', self.uid)
            self._fade_anim.stop()
        self._fade_anim = QtCore.QPropertyAnimation(self, qbytes_opacity)
        self._fade_anim.setDuration(s)
        self._fade_anim.setStartValue(0.0)
        self._fade_anim.setEndValue(1.0)
        self._fade_anim.setEasingCurve(QtCore.QEasingCurve.InQuad)
        self._fade_anim.start(QtCore.QAbstractAnimation.KeepWhenStopped)
        self._fade_anim.finished.connect(self.fade_in_finished)

    def fade_in_finished(self):
        self.is_fading_in = False

    def fade_out(self, s=300):
        """ Start fade out. The object exists until fade end.
        :return: None
        """
        if self.is_fading_out:
            return
        if not self.isVisible():
            return
        self.is_fading_out = True
        if self.is_fading_in:
            self._fade_anim.stop()
        self._fade_anim = QtCore.QPropertyAnimation(self, qbytes_opacity)
        self._fade_anim.setDuration(s)
        self._fade_anim.setStartValue(1.0)
        self._fade_anim.setEndValue(0)
        self._fade_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
        self._fade_anim.start(QtCore.QAbstractAnimation.KeepWhenStopped)
        self._fade_anim.finished.connect(self.fade_out_finished)

    def fade_out_and_delete(self, s=300):
        """ Start fade out. The object exists until fade end.
        :return: None
        """
        if self.is_fading_out:
            self._fade_anim.finished.disconnect(
            )  # regular fade_out isn't enough
            self._fade_anim.finished.connect(self.fade_out_finished_delete)
            return
        if not self.isVisible():
            self.fade_out_finished_delete()
            return
        self.is_fading_out = True
        if self.is_fading_in:
            self._fade_anim.stop()
        self._fade_anim = QtCore.QPropertyAnimation(self, qbytes_opacity)
        self._fade_anim.setDuration(s)
        self._fade_anim.setStartValue(1.0)
        self._fade_anim.setEndValue(0)
        self._fade_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
        self._fade_anim.start(QtCore.QAbstractAnimation.KeepWhenStopped)
        self._fade_anim.finished.connect(self.fade_out_finished_delete)

    def fade_out_finished(self):
        self.is_fading_out = False
        if self.after_move_function:
            self.after_move_function()
            self.after_move_function = None
        self.hide()

    def fade_out_finished_delete(self):
        self.is_fading_out = False
        ctrl.forest.remove_from_scene(self, fade_out=False)

    def is_visible(self):
        """ Our own tracking of object visibility, not based on Qt's scene
        visibility.
        :return: bool
        """
        return self._visible_by_logic

    # ## Selection ############################################################

    def is_selected(self):
        """Return the selection status of this object.
        :return: boolean
        """
        return ctrl.is_selected(self)

    # ## Dragging ############################################################

    def dragged_to(self, scene_pos):
        """ Dragged focus is in scene_pos. Move there.
        :param scene_pos: current drag focus
        :return:
        """
        if self.parentItem():
            p = self.parentItem().mapFromScene(scene_pos[0], scene_pos[1])
            new_pos = p.x(), p.y()
        else:
            new_pos = scene_pos[0], scene_pos[1]

        if self.use_physics():
            self.locked = True
            self.current_position = new_pos
        else:
            self.use_adjustment = True
            diff = sub_xy(new_pos, self.current_position)
            self.adjustment = add_xy(self.adjustment, diff)
            self.target_position = new_pos
            self.current_position = new_pos

    def dragged_over_by(self, dragged):
        """ When dragging other items on top of this item, should this item react, e.g. show somehow that item can be dropped on this.

        :param dragged:
        """
        if ctrl.drag_hovering_on is self:
            return True
        elif self.accepts_drops(dragged):
            ctrl.set_drag_hovering(self)
            return True
        else:
            return False

    def drop_to(self, x, y, recipient=None):
        """
        This item is dropped to screen coordinates. Evaluate if there are
        sensitive objects (TouchAreas) there and if
        there are, call their 'drop'-method with self as argument.
        :param x: int or float
        :param y: int or float
        :param recipient: Movable?
        """
        print('movable drop to %s,%s , recipient=%s' % (x, y, recipient))
        # closest_ma = None
        # for ma in ctrl.main.ui_manager.touch_areas:  # @UndefinedVariable
        # if ma.sceneBoundingRect().contains(x, y):
        # closest_ma = ma
        # break
        # if closest_ma:
        # closest_ma.drop(self)
        # print('dropped to:', closest_ma)
        # # ctrl.scene.fit_to_window()

    def accepts_drops(self, dragged):
        """ Reimplement this to evaluate if this Movable should accept drops from dragged. Default returns False.

        :param dragged: Item that is being dragged. You may want to look into what kind of object
        this is and decide from that.
        :return:
        """
        return False

    # ## Existence ############################################################

    def show(self):
        if not self.isVisible():
            super().show()

    def update_visibility(self, fade_in=True, fade_out=True) -> bool:
        """ Subclasses should set _visible_by_logic based on their many factors. In this level
        the actual hide/show/fade -operations are made.

        This is called logical visibility and can be checked with is_visible().
        Qt's isVisible() checks for scene visibility. Items that are e.g. fading away
        have False for logical visibility but True for scene visibility and items that are part
        of graph in a forest that is not currently drawn may have True for logical visibility but
        false for scene visibility.


        :return: True if visibility has changed. Use this information to notify related parties
        """
        if self.scene():
            if self._visible_by_logic:
                if self.is_fading_out:
                    if fade_in:
                        self.fade_in()
                        return True
                    else:
                        self._fade_anim.stop()
                        self.is_fading_out = False
                        self.show()
                        return True
                elif not self.isVisible():
                    if fade_in:
                        self.fade_in()
                        return True
                    else:
                        self.show()
                        return True
            else:
                if self.isVisible():
                    if fade_out:
                        if not self.is_fading_out:
                            self.fade_out()
                            return True
                    else:
                        self.hide()
                        return True
        return False

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    #current_position = Saved("current_position", if_changed=_current_position_changed)
    target_position = SavedField("target_position")
    adjustment = SavedField("adjustment")
    use_adjustment = SavedField("use_adjustment")
    locked = SavedField("locked")
    physics_x = SavedField("physics_x")
    physics_y = SavedField("physics_y")
    trees = SavedField("trees")
    locked_to_node = SavedField("locked_to_node")
Esempio n. 11
0
class Constituent(BaseConstituent):
    """ The main difference between mgtdbp Constituents and Kataja's BaseConstituents is that
    features are stored as lists instead of dicts. See footnote 1, p.3 in Stabler 2012, 'Two models
    of minimalist, incremental syntactic analysis'. The order of features is important in parsing
    (though Constituents are actually used only for displaying results, not in parsing itself)
    and -- more importantly -- one Constituent can have two counts for the same feature,
    e.g. =D =D. and we have to be able to present such constituents. """
    role = "Constituent"

    def __init__(self, label='', features=None, parts=None, index_str=None):
        super().__init__(label=label, parts=parts, features=features)
        self.label = label or ''
        self.features = features or []
        self.parts = parts or []
        self.index_str = index_str
        self.secondary_label = ''
        self.touched = True  # flag to help remove nodes that don't belong to current parse

    def __repr__(self):
        return '[%r:%r, %r]' % (self.label, self.features, self.parts)

    def get_secondary_label(self):
        """ Visualisation can switch between showing labels and some other information in label
        space. If you want to support this, have "support_secondary_labels = True"
        in SyntaxConnection and provide something from this getter.
        :return:
        """
        #return self.frozen_features
        return self.secondary_label

    @staticmethod
    def build_from_dnodes(dnode,
                          dnodes,
                          terminals,
                          dtrees,
                          all_features=False):
        key = dnode.path
        c = dtrees.get(key, Constituent(index_str=key))
        if terminals and terminals[0].path == key:
            leaf = terminals.pop(0)
            c.label = ' '.join(leaf.label)
            c.features = list(reversed(leaf.features))
            c.secondary_label = ' '.join([str(f) for f in c.features])
            if dnode.features and dnode.features != leaf.features:
                print('dnode has features: ', dnode.features)
                print('leaf has features: ', leaf.features)
            c.parts = []
        elif dnodes and dnodes[0].path.startswith(key):
            parts = []
            child_dnode = dnodes.pop(0)
            child = Constituent.build_from_dnodes(child_dnode,
                                                  dnodes,
                                                  terminals,
                                                  dtrees,
                                                  all_features=all_features)
            parts.append(child)
            if dnodes and dnodes[0].path.startswith(key):
                child_dnode = dnodes.pop(0)
                child = Constituent.build_from_dnodes(
                    child_dnode,
                    dnodes,
                    terminals,
                    dtrees,
                    all_features=all_features)
                parts.append(child)

            if all_features:
                c.features = list(reversed(dnode.features))
            else:
                c.features = []
            c.secondary_label = ' '.join([str(f) for f in c.features])
            if len(parts) > 1:
                c.label = '•'
            elif len(parts) == 1:
                c.label = '◦'
            else:
                c.label = ''
            c.parts = parts
        c.touched = True
        dtrees[key] = c
        return c

    def as_list_tree(self):
        if len(self.parts) == 2:
            return [
                self.label, self.parts[0].as_list_tree(),
                self.parts[1].as_list_tree()
            ]
        elif len(self.parts) == 1:
            return [self.label, self.parts[0].as_list_tree()]
        elif self.features:
            if self.label:
                label = [self.label]
            else:
                label = []
            return label, [str(f) for f in self.features]

    @staticmethod
    def dnodes_to_dtree(dnodes, all_features=False, dtrees=None):
        if dtrees is None:
            dtrees = {}
        nonterms = []
        terms = []
        for dn in dnodes:
            if dn.terminal:
                terms.append(dn)
            else:
                nonterms.append(dn)
        terms.sort()
        nonterms.sort()
        root = nonterms.pop(0)
        for item in dtrees.values():
            item.touched = False
        dtree = Constituent.build_from_dnodes(root,
                                              nonterms,
                                              terms,
                                              dtrees,
                                              all_features=all_features)

        for key, item in list(dtrees.items()):
            if not item.touched:
                del dtrees[key]
        #dtree = Constituent()
        #dtree.build_from_dnodes(root.path, nonterms, terms, dtrees, all_features=all_features)
        if terms or nonterms:
            print('dnodes_to_dtree error: unused derivation steps')
            print('terms=' + str(terms))
            print('nonterms=' + str(nonterms))
        return dtree

    def __hash__(self):
        return hash(self.index_str)

    secondary_label = SavedField("secondary_label")
Esempio n. 12
0
class ConstituentNode(Node):
    """ ConstituentNode is enriched with few elements that have no syntactic meaning but help with
     reading the trees aliases, indices and glosses.
    """
    __qt_type_id__ = next_available_type_id()
    display_name = ('Constituent', 'Constituents')
    display = True
    width = 20
    height = 20
    is_constituent = True
    node_type = g.CONSTITUENT_NODE
    wraps = 'constituent'
    editable = {
    }  # Uses custom ConstituentNodeEmbed instead of template-based NodeEditEmbed

    default_style = {
        'plain': {
            'color_id': 'content1',
            'font_id': g.MAIN_FONT,
            'font-size': 10
        },
        'fancy': {
            'color_id': 'content1',
            'font_id': g.MAIN_FONT,
            'font-size': 10
        }
    }

    default_edge = g.CONSTITUENT_EDGE

    # Touch areas are UI elements that scale with the trees: they can be
    # temporary shapes suggesting to drag or click here to create the
    # suggested shape.

    # touch_areas_when_dragging and touch_areas_when_selected use the same
    # format.

    # 'condition': there are some general conditions implemented in UIManager,
    # but condition can refer to method defined for node instance. When used
    # for when-dragging checks, the method will be called with two parameters
    # 'dragged_type' and 'dragged_host'.
    # 'place': there are some general places defined in UIManager. The most
    # important is 'edge_up': in this case touch areas are associated with
    # edges going up. When left empty, touch area is associated with the node.

    touch_areas_when_dragging = {
        g.LEFT_ADD_TOP: {
            'condition':
            ['is_top_node', 'dragging_constituent', 'free_drawing_mode']
        },
        g.RIGHT_ADD_TOP: {
            'condition':
            ['is_top_node', 'dragging_constituent', 'free_drawing_mode']
        },
        g.LEFT_ADD_SIBLING: {
            'place': 'edge_up',
            'condition': ['dragging_constituent', 'free_drawing_mode']
        },
        g.RIGHT_ADD_SIBLING: {
            'place': 'edge_up',
            'condition': ['dragging_constituent', 'free_drawing_mode']
        },
        g.TOUCH_CONNECT_COMMENT: {
            'condition': 'dragging_comment'
        },
        g.TOUCH_CONNECT_FEATURE: {
            'condition': ['dragging_feature', 'free_drawing_mode']
        },
        g.TOUCH_CONNECT_GLOSS: {
            'condition': 'dragging_gloss'
        }
    }

    touch_areas_when_selected = {
        g.LEFT_ADD_TOP: {
            'condition': ['is_top_node', 'free_drawing_mode'],
            'action': 'add_top_left'
        },
        g.RIGHT_ADD_TOP: {
            'condition': ['is_top_node', 'free_drawing_mode'],
            'action': 'add_top_right'
        },
        g.MERGE_TO_TOP: {
            'condition': ['not:is_top_node', 'free_drawing_mode'],
            'action': 'merge_to_top'
        },
        g.INNER_ADD_SIBLING_LEFT: {
            'condition': ['inner_add_sibling', 'free_drawing_mode'],
            'place': 'edge_up',
            'action': 'inner_add_sibling_left'
        },
        g.INNER_ADD_SIBLING_RIGHT: {
            'condition': ['inner_add_sibling', 'free_drawing_mode'],
            'place': 'edge_up',
            'action': 'inner_add_sibling_right'
        },
        g.UNARY_ADD_CHILD_LEFT: {
            'condition': ['has_one_child', 'free_drawing_mode'],
            'action': 'unary_add_child_left'
        },
        g.UNARY_ADD_CHILD_RIGHT: {
            'condition': ['has_one_child', 'free_drawing_mode'],
            'action': 'unary_add_child_right'
        },
        g.LEAF_ADD_SIBLING_LEFT: {
            'condition': ['is_leaf', 'free_drawing_mode'],
            'action': 'leaf_add_sibling_left'
        },
        g.LEAF_ADD_SIBLING_RIGHT: {
            'condition': ['is_leaf', 'free_drawing_mode'],
            'action': 'leaf_add_sibling_right'
        },
        g.ADD_TRIANGLE: {
            'condition': 'can_have_triangle',
            'action': 'add_triangle'
        },
        g.REMOVE_TRIANGLE: {
            'condition': 'is_triangle_host',
            'action': 'remove_triangle'
        }
    }

    buttons_when_selected = {
        g.REMOVE_MERGER: {
            'condition': ['is_unnecessary_merger', 'free_drawing_mode'],
            'action': 'remove_merger'
        },
        g.NODE_EDITOR_BUTTON: {
            'action': 'toggle_node_edit_embed'
        },
        g.REMOVE_NODE: {
            'condition': ['not:is_unnecessary_merger', 'free_drawing_mode'],
            'action': 'remove_node'
        },
        #g.QUICK_EDIT_LABEL: {}, # 'condition': 'is_quick_editing'
    }

    def __init__(self, label=''):
        """ Most of the initiation is inherited from Node """
        Node.__init__(self)
        self.heads = []

        # ### Projection -- see also preferences that govern if these are used
        self.can_project = True
        self.projecting_to = set()

        self.index = ''
        self.label = label
        self.autolabel = ''
        self.gloss = ''

        self.is_trace = False
        self.merge_order = 0
        self.select_order = 0
        self.in_projections = []

        # ### Cycle index stores the order when node was originally merged to structure.
        # going up in trees, cycle index should go up too

    def after_init(self):
        """ After_init is called in 2nd step in process of creating objects:
        1st wave creates the objects and calls __init__, and then iterates through and sets the
        values. 2nd wave calls after_inits for all created objects. Now they can properly refer
        to each other and know their values.
        :return: None
        """
        self.update_gloss()
        self.update_label_shape()
        self.update_label()
        self.update_visibility()
        self.update_status_tip()
        self.announce_creation()
        if prefs.glow_effect:
            self.toggle_halo(True)
        ctrl.forest.store(self)

    @staticmethod
    def create_synobj(label, forest):
        """ ConstituentNodes are wrappers for Constituents. Exact
        implementation/class of constituent is defined in ctrl.
        :return:
        """
        if not label:
            label = forest.get_first_free_constituent_name()
        c = ctrl.syntax.Constituent(label)
        c.after_init()
        return c

    def load_values_from_parsernode(self, parsernode):
        """ Update constituentnode with values from parsernode
        :param parsernode:
        :return:
        """
        def remove_dot_label(inode, row_n):
            for i, part in enumerate(list(inode.parts)):
                if isinstance(part, str):
                    if part.startswith('.'):
                        inode.parts[i] = part[1:]
                    return True
                elif isinstance(part,
                                ICommandNode) and part.command == 'qroof':
                    self.triangle_stack = [self]
                    continue
                else:
                    return remove_dot_label(part, row_n)

        if parsernode.index:
            self.index = parsernode.index
        rows = parsernode.label_rows
        # Remove dotlabel

        for i, row in enumerate(list(rows)):
            if isinstance(row, str):
                if row.startswith('.'):
                    rows[i] = row[1:]
                break
            stop = remove_dot_label(row, i)
            if stop:
                break
        # △

        self.label = join_lines(rows)
        # now as rows are in one INode / string, we can extract the triangle part and put it to
        # end. It is different to qtree's way of handling triangles, but much simpler for us in
        # long run.
        triangle_part = extract_triangle(self.label, remove_from_original=True)
        if triangle_part:
            assert isinstance(self.label, ITextNode)

            self.label.parts.append('\n')
            self.label.parts.append(triangle_part)
        if self.index:
            base = as_html(self.label)
            if base.strip().startswith('t<sub>'):
                self.is_trace = True

    def get_syntactic_label(self):
        if self.syntactic_object:
            return self.syntactic_object.label

    # Other properties

    @property
    def gloss_node(self):
        """
        :return:
        """
        gs = self.get_children(visible=True, of_type=g.GLOSS_EDGE)
        if gs:
            return gs[0]

    def has_ordered_children(self):
        if self.syntactic_object:
            return getattr(self.syntactic_object, 'is_ordered', False)
        return True

    def update_label_shape(self):
        self.label_object.label_shape = ctrl.settings.get('label_shape')

    def should_show_gloss_in_label(self) -> bool:
        return ctrl.settings.get('lock_glosses_to_label') == 1

    def update_status_tip(self) -> None:
        """ Hovering status tip """

        if self.label:
            label = f'Label: "{as_text(self.label)}" '
        else:
            label = ''
        syn_label = self.get_syn_label()
        if syn_label:
            syn_label = f' Constituent: "{as_text(syn_label)}" '
        else:
            syn_label = ''
        if self.index:
            index = f' Index: "{self.index}"'
        else:
            index = ''

        if self.is_trace:
            name = "Trace"
        elif self.is_leaf():
            name = "Leaf "
        # elif self.is_top_node():
        #    name = "Set %s" % self.set_string() # "Root constituent"
        else:
            #name = f"Set {self.set_string()}"
            name = "Set "
        if self.use_adjustment:
            adjustment = f' w. adjustment ({self.adjustment[0]:.1f}, {self.adjustment[1]:.1f})'
        else:
            adjustment = ''
        heads = ', '.join([as_text(x.label) for x in self.heads])
        self.status_tip = f"{name} ({label}{syn_label}{index} pos: ({self.current_scene_position[0]:.1f}, " \
                          f"{self.current_scene_position[1]:.1f}){adjustment} head: {heads})"

    def short_str(self):
        label = as_text(self.label)
        if label:
            lines = label.splitlines()
            if len(lines) > 3:
                label = f'{lines[0]} ...\n{lines[-1]}'
        syn_label = as_text(self.get_syn_label())
        if label and syn_label:
            return f'{label} ({syn_label})'
        else:
            return label or syn_label or "no label"

    def set_string(self):
        """ This can be surprisingly expensive to calculate
        :return: 
        """
        if self.syntactic_object and hasattr(self.syntactic_object,
                                             'set_string'):
            return self.syntactic_object.set_string()
        else:
            return self._set_string()

    def _set_string(self):
        parts = []
        for child in self.get_children(similar=True, visible=False):
            parts.append(str(child._set_string()))
        if parts:
            return '{%s}' % ', '.join(parts)
        else:
            return self.label

    def __str__(self):
        label = as_text(self.label, single_line=True)
        syn_label = as_text(self.get_syn_label(), single_line=True)
        if label and syn_label:
            return f'CN {label} ({syn_label})'
        else:
            return f'CN {label or syn_label or "no label"}'

    def get_syn_label(self):
        if self.syntactic_object:
            return self.syntactic_object.label
        return ''

    def compose_html_for_viewing(self, peek_into_synobj=True):
        """ This method builds the html to display in label. For convenience, syntactic objects
        can override this (going against the containment logic) by having their own
        'compose_html_for_viewing' -method. This is so that it is easier to create custom
        implementations for constituents without requiring custom constituentnodes.

        Note that synobj's compose_html_for_viewing receives the node object as parameter,
        so you can replicate the behavior below and add your own to it.

        :param peek_into_synobj: allow syntactic object to override this method. If synobj in turn
        needs the result from this implementation (e.g. to append something to it), you have to
        turn this off to avoid infinite loop. See example plugins.
        :return:
        """

        # Allow custom syntactic objects to override this
        if peek_into_synobj and hasattr(self.syntactic_object,
                                        'compose_html_for_viewing'):
            return self.syntactic_object.compose_html_for_viewing(self)

        html = []

        label_text_mode = ctrl.settings.get('label_text_mode')
        l = ''
        if label_text_mode == g.NODE_LABELS:
            if self.label:
                l = self.label
            elif self.syntactic_object:
                l = self.syntactic_object.label
        elif label_text_mode == g.NODE_LABELS_FOR_LEAVES:
            if self.label:
                l = self.label
            elif self.syntactic_object and self.is_leaf(only_similar=True,
                                                        only_visible=False):
                l = self.syntactic_object.label
        elif label_text_mode == g.SYN_LABELS:
            if self.syntactic_object:
                l = self.syntactic_object.label
        elif label_text_mode == g.SYN_LABELS_FOR_LEAVES:
            if self.syntactic_object and self.is_leaf(only_similar=True,
                                                      only_visible=False):
                l = self.syntactic_object.label
        elif label_text_mode == g.SECONDARY_LABELS:
            if self.syntactic_object:
                l = self.syntactic_object.get_secondary_label()
        elif label_text_mode == g.XBAR_LABELS:
            l = self.get_autolabel()
        separate_triangle = bool(self.is_cosmetic_triangle()
                                 and self.triangle_stack[-1] is self)
        l_html = as_html(l,
                         omit_triangle=separate_triangle,
                         include_index=self.index)
        if l_html:
            html.append(l_html)

        if self.gloss and self.should_show_gloss_in_label():
            if html:
                html.append('<br/>')
            html.append(as_html(self.gloss))
        if html and html[-1] == '<br/>':
            html.pop()

        # Lower part
        lower_html = ''
        if separate_triangle:
            qroof_content = extract_triangle(l)
            if qroof_content:
                lower_html = as_html(qroof_content)
        return ''.join(html), lower_html

    def compose_html_for_editing(self):
        """ This is used to build the html when quickediting a label. It should reduce the label
        into just one field value that is allowed to be edited, in constituentnode this is
        either label or synobj's label. This can be overridden in syntactic object by having
        'compose_html_for_editing' -method there. The method returns a tuple,
          (field_name, setter, html).
        :return:
        """

        # Allow custom syntactic objects to override this
        if self.syntactic_object and hasattr(self.syntactic_object,
                                             'compose_html_for_editing'):
            return self.syntactic_object.compose_html_for_editing(self)
        label_text_mode = ctrl.settings.get('label_text_mode')
        if label_text_mode == g.NODE_LABELS or label_text_mode == g.NODE_LABELS_FOR_LEAVES:
            if self.label:
                if self.triangle_stack:
                    lower_part = extract_triangle(self.label)
                    return 'node label', as_html(self.label, omit_triangle=True) + \
                           '<br/>' + as_html(lower_part or '')
                else:
                    return 'node label', as_html(self.label)
            elif self.syntactic_object:
                return 'syntactic label', as_html(self.syntactic_object.label)
            else:
                return '', '', ''
        elif label_text_mode == g.SYN_LABELS or label_text_mode == g.SYN_LABELS_FOR_LEAVES:
            if self.syntactic_object:
                return 'syntactic label', as_html(self.syntactic_object.label)
            else:
                return '', ''

    def parse_edited_label(self, label_name, value):
        success = False
        if self.syntactic_object and hasattr(self.syntactic_object,
                                             'parse_edited_label'):
            success = self.syntactic_object.parse_edited_label(
                label_name, value)
        if not success:
            if label_name == 'node label':
                self.poke('label')
                self.label = value
                return True
            elif label_name == 'syntactic label':
                self.syntactic_object.label = value
                return True
            elif label_name == 'index':
                self.index = value
        return False

    def as_bracket_string(self):
        """ returns a simple bracket string representation """
        if self.label:
            children = list(self.get_children(similar=True, visible=False))
            if children:
                return '[.%s %s ]' % \
                       (self.label, ' '.join((c.as_bracket_string() for c in children)))
            else:
                return str(self.label)
        else:
            inside = ' '.join(
                (x.as_bracket_string()
                 for x in self.get_children(similar=True, visible=False)))
            if inside:
                return '[ ' + inside + ' ]'
            elif self.syntactic_object:
                return str(self.syntactic_object)
            else:
                return '-'

    def get_attribute_nodes(self, label_key=''):
        """

        :param label_k ey:
        :return:
        """
        atts = [
            x.end for x in self.edges_down if x.edge_type == g.ATTRIBUTE_EDGE
        ]
        if label_key:
            for a in atts:
                if a.attribute_label == label_key:
                    return a
        else:
            return atts

    def get_autolabel(self):
        return self.autolabel

    def is_unnecessary_merger(self):
        """ This merge can be removed, if it has only one child
        :return:
        """
        return len(list(self.get_children(similar=True, visible=False))) == 1

    # Conditions ##########################
    # These are called from templates with getattr, and may appear unused for IDE's analysis.
    # Check their real usage with string search before removing these.

    def inner_add_sibling(self):
        """ Node has child and it is not unary child. There are no other reasons preventing
        adding siblings
        :return: bool
        """
        return self.get_children(similar=True,
                                 visible=False) and not self.is_unary()

    def has_one_child(self):
        return len(self.get_children(similar=True, visible=False)) == 1

    def can_be_projection_of_another_node(self):
        """ Node can be projection from other nodes if it has other nodes
        below it.
        It may be necessary to move this check to syntactic level at some
        point.
        :return:
        """
        if ctrl.settings.get('use_projection'):
            if self.is_leaf(only_similar=True, only_visible=False):
                return False
            else:
                return True
        else:
            return False

    def set_heads(self, head):
        """ Set projecting head to be Node, list of Nodes or empty. Notice that this doesn't
        affect syntactic objects.
        :param head:
        :return:
        """
        if isinstance(head, list):
            self.heads = list(head)
        elif isinstance(head, Node):
            self.heads = [head]
        elif not head:
            self.heads = []
        else:
            raise ValueError

    def synobj_to_node(self):
        """ Update node's values from its synobj. Subclasses implement this.
        :return:
        """
        self.synheads_to_heads()

    def synheads_to_heads(self):
        """ Make sure that node's heads reflect synobjs heads.
        :return:
        """
        self.heads = []
        if self.syntactic_object:
            synlabel = self.syntactic_object.label
            parts = self.syntactic_object.parts
            if len(parts) == 0:
                self.heads = [self]
            if len(parts) == 1:
                if parts[0].label == synlabel:
                    self.heads = [ctrl.forest.get_node(parts[0])]
                else:
                    self.heads = [self]
            elif len(parts) == 2:
                if parts[0].label == synlabel:
                    self.heads = [ctrl.forest.get_node(parts[0])]
                elif parts[1].label == synlabel:
                    self.heads = [ctrl.forest.get_node(parts[1])]
                elif synlabel == f"({parts[0].label}, {parts[1].label})":
                    self.heads = [
                        ctrl.forest.get_node(parts[0]),
                        ctrl.forest.get_node(parts[1])
                    ]
                elif synlabel == f"({parts[1].label}, {parts[0].label})":
                    self.heads = [
                        ctrl.forest.get_node(parts[1]),
                        ctrl.forest.get_node(parts[0])
                    ]

    @property
    def contextual_color(self):
        """ Drawing color that is sensitive to node's state
        :return: QColor
        """

        if ctrl.is_selected(self):
            base = ctrl.cm.selection()
        elif self.in_projections:
            base = ctrl.cm.get(self.in_projections[0].color_id)
        else:
            base = self.color
        if self.drag_data:
            return ctrl.cm.lighter(base)
        elif ctrl.pressed is self:
            return ctrl.cm.active(base)
        elif self._hovering:
            return ctrl.cm.hovering(base)
        else:
            return base

    # ### Features #########################################

    def update_gloss(self, value=None):
        """


        """
        if not self.syntactic_object:
            return
        syn_gloss = self.gloss
        gloss_node = self.gloss_node
        if not ctrl.undo_disabled:
            if gloss_node and not syn_gloss:
                ctrl.free_drawing.delete_node(gloss_node)
            elif syn_gloss and not gloss_node:
                ctrl.free_drawing.create_gloss_node(host=self)
            elif syn_gloss and gloss_node:
                gloss_node.update_label()

    # ### Labels #############################################
    # things to do with traces:
    # if renamed and index is removed/changed, update chains
    # if moved, update chains
    # if copied, make sure that copy isn't in chain
    # if deleted, update chains
    # any other operations?

    def is_empty_node(self):
        """ Empty nodes can be used as placeholders and deleted or replaced without structural
        worries """
        return (not (self.syntactic_object or self.label
                     or self.index)) and self.is_leaf()

    # ## Indexes and chains ###################################

    def is_chain_head(self):
        """


        :return:
        """
        if self.index:
            return not (self.is_leaf() and self.label == 't')
        return False

    ### UI support

    def dragging_constituent(self):
        """ Check if the currently dragged item is constituent and can connect with me
        :return:
        """
        return self.is_dragging_this_type(g.CONSTITUENT_NODE)

    def dragging_feature(self):
        """ Check if the currently dragged item is feature and can connect with me
        :return:
        """
        return self.is_dragging_this_type(g.FEATURE_NODE)

    def dragging_gloss(self):
        """ Check if the currently dragged item is gloss and can connect with me
        :return:
        """
        return self.is_dragging_this_type(g.GLOSS_NODE)

    def dragging_comment(self):
        """ Check if the currently dragged item is comment and can connect with me
        :return:
        """
        return self.is_dragging_this_type(g.COMMENT_NODE)

    # ### Features #########################################

    def get_features(self):
        """ Returns FeatureNodes """
        return self.get_children(visible=True, of_type=g.FEATURE_EDGE)

    def get_features_as_string(self):
        """
        :return:
        """
        features = [f.syntactic_object for f in self.get_features()]
        feature_strings = [str(f) for f in features]
        return ', '.join(feature_strings)

    # ### Checks for callable actions ####

    def can_top_merge(self):
        """
        :return:
        """
        top = self.get_top_node()
        return self is not top and self not in top.get_children(similar=True,
                                                                visible=False)

    # ### Dragging #####################################################################

    # ## Most of this is implemented in Node

    def prepare_children_for_dragging(self, scene_pos):
        """ Implement this if structure is supposed to drag with the node
        :return:
        """
        children = ctrl.forest.list_nodes_once(self)

        for tree in self.trees:
            dragged_index = tree.sorted_constituents.index(self)
            for i, node in enumerate(tree.sorted_constituents):
                if node is not self and i > dragged_index and node in children:
                    node.start_dragging_tracking(host=False,
                                                 scene_pos=scene_pos)

    #################################

    # ### Parents & Children ####################################################

    def is_projecting_to(self, other):
        """

        :param other:
        """
        pass

    #
    # def paint(self, painter, option, widget=None):
    #     """ Painting is sensitive to mouse/selection issues, but usually with
    #     :param painter:
    #     :param option:
    #     :param widget:
    #     nodes it is the label of the node that needs complex painting """
    #     super().paint(painter, option, widget=widget)

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    label = SavedField("label")
    index = SavedField("index")
    gloss = SavedField("gloss", if_changed=update_gloss)
    heads = SavedField("heads")
Esempio n. 13
0
class KatajaDocument(SavedObject):
    """ Container and loader for Forest objects. Remember to not enable undo for any of the actions in here,
    as scope of undo should be a single Forest.

    :param name: Optional readable name for document
    :param filename: File name for saving the document. Initially empty, will be set on save
    """

    unique = True

    default_treeset_file = running_environment.resources_path + 'trees.txt'

    def __init__(self, name=None, filename=None, clear=False):
        super().__init__()
        self.name = name or filename or 'New project'
        self.filename = filename
        self.forests = [Forest()]
        self.current_index = 0
        self.forest = None
        self.lexicon = {}
        self.structures = OrderedDict()
        self.constituents = OrderedDict()
        self.features = OrderedDict()

    def new_forest(self):
        """ Add a new forest after the current one.
        :return: tuple (current_index (int), selected forest (Forest)
        """
        ctrl.undo_pile = set()
        #ctrl.undo_disabled = True
        if self.forest:
            self.forest.retire_from_drawing()
        forest = Forest()
        self.current_index += 1
        self.poke(
            'forests')  # <-- announce change in watched list-like attribute
        self.forests.insert(self.current_index, forest)
        self.forest = forest  # <-- at this point the signal is sent to update UI
        #ctrl.undo_disabled = False
        return self.current_index, self.forest

    def next_forest(self):
        """ Select the next forest in the list of forests. The list loops at end.
        :return: tuple (current_index (int), selected forest (Forest)
        """
        if not self.forests:
            return None
        if self.current_index < len(self.forests) - 1:
            self.current_index += 1
        else:
            self.current_index = 0
        ctrl.undo_pile = set()
        self.forest = self.forests[self.current_index]
        return self.current_index, self.forest

    def prev_forest(self):
        """ Select the previous forest in the list of forests. The list loops at -1.
        :return: tuple (current_index (int), selected forest (Forest)
        """
        if not self.forests:
            return None
        if self.current_index > 0:
            self.current_index -= 1
        else:
            self.current_index = len(self.forests) - 1
        ctrl.undo_pile = set()
        self.forest = self.forests[self.current_index]
        return self.current_index, self.forest

    def build_lexicon_dict(self):
        self.constituents = OrderedDict()
        self.features = OrderedDict()
        self.structures = OrderedDict()
        for key, data in sorted(self.lexicon.items()):
            if data.startswith('['):
                self.structures[key] = data
            elif data.startswith('<'):
                self.features[key] = data
            elif key != 'lexicon_info':
                self.constituents[key] = data

    @staticmethod
    def load_treelist_from_text_file(filename):
        """ Pretty dumb fileloader, to create a treelist (list of strings)
        :param filename: str, does nothing with the path.
        """
        try:
            f = open(filename, 'r', encoding='UTF-8')
            treelist = f.readlines()
            f.close()
        except FileNotFoundError:
            treelist = ['[A B]', '[ A [ C B ] ]', '']
        return treelist

    @time_me
    def create_forests(self, filename=None, clear=False):
        """ This will read list of strings where each line defines a trees or an element of trees.
        This can be used to reset the KatajaDocument if no treeset or an empty treeset is given.

        It is common to override this method in plugins to provide custom commands for e.g.
        running parsers.

        Example of tree this can read:

        [.AspP [.Asp\\Ininom] [.vP [.KP [.K\\ng ] [.DP [.D´ [.D ] [.NP\\lola ]] [.KP [.K\\ng]
        [.DP [.D´ [.D ] [.NP\\alila ] ] [.KP\\{ni Maria} ]]]]] [.v´ [.v ] [.VP [.V ] [.KP\\{ang tubig}]]]]]
        Ininom = drank
        ng = NG
        ng = NG
        lola = grandma
        alila = servant
        ni Maria = NG Maria
        ang tubig = ANG water
        'Maria's grandmother's servant drank the water'

        :param filename: (optional) file to load from
        :param clear: (optional) if True, start with an empty treeset and don't attempt to load
        examples
        """
        print('************* create forests ****************')

        if clear:
            treelist = []
        else:
            treelist = self.load_treelist_from_text_file(
                self.__class__.default_treeset_file) or []

        # Clear this screen before we start creating a mess
        ctrl.disable_undo()  # disable tracking of changes (e.g. undo)
        if self.forest:
            self.forest.retire_from_drawing()
        self.forests = []

        # buildstring is the bracket trees or trees.
        buildstring = []
        # definitions includes given definitions for constituents of this trees
        definitions = {}
        # gloss_text is the gloss for whole trees
        gloss_text = ''
        # comments are internal notes about the trees, displayed as help text or something
        comments = []
        started_forest = False

        syntax_class = classes.get('SyntaxConnection')

        for line in treelist:
            line = line.strip()
            #line.split('=', 1)
            parts = line.split('=', 1)
            # comment line
            if line.startswith('#'):
                if started_forest:
                    comments.append(line[1:])
            # Definition line
            elif len(parts) > 1 and not line.startswith('['):
                started_forest = True
                word = parts[0].strip()
                values = parts[1]
                definitions[word] = values
            # Gloss text:
            elif line.startswith("'"):
                if started_forest:
                    if line.endswith("'"):
                        line = line[:-1]
                    gloss_text = line[1:]
            # empty line: finalize this forest
            elif started_forest and not line:
                syn = syntax_class()
                syn.sentence = buildstring
                syn.lexicon = definitions
                forest = Forest(gloss_text=gloss_text,
                                comments=comments,
                                syntax=syn)
                self.forests.append(forest)
                started_forest = False
            # trees definition starts a new forest
            elif line and not started_forest:
                started_forest = True
                buildstring = line
                definitions = {}
                gloss_text = ''
                comments = []
            # another trees definition, append to previous
            elif line:
                buildstring += '\n' + line
        if started_forest:  # make sure that the last forest is also added
            syn = syntax_class()
            syn.sentence = buildstring
            syn.lexicon = definitions
            forest = Forest(gloss_text=gloss_text,
                            comments=comments,
                            syntax=syn)
            self.forests.append(forest)
        if not self.forests:
            syn = syntax_class()
            forest = Forest(gloss_text='', comments=[], syntax=syn)
            self.forests.append(forest)
        self.current_index = 0
        self.forest = self.forests[0]
        # allow change tracking (undo) again
        ctrl.resume_undo()

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    forests = SavedField("forests")
    current_index = SavedField("current_index")
    forest = SavedField("forest", watcher="forest_changed")
    lexicon = SavedField("lexicon")
Esempio n. 14
0
class DerivationStepManager(SavedObject):
    """ Stores derivation steps for one forest and takes care of related
    logic """

    def __init__(self, forest=None):
        super().__init__()
        self.forest = forest
        self.activated = False
        self.current = None
        self.derivation_steps = []
        self.derivation_step_index = 0

    def save_and_create_derivation_step(self, synobjs, numeration=None, other=None, msg='',
                                        gloss='', transferred=None, mover=None):
        """ Ok, new idea: derivation steps only include syntactic objects. Nodes etc. will be
        created in the fly. No problems from visualisations misbehaving, chains etc.
        :param synobjs: list of syntactic objects present in this snapshot
        :param numeration: optional list of items
        :param other: optional arbitrary data to store (must be Saveable or primitive data
        structures!)
        :param msg: optional message about derivation, to float when switching between derivations
        :param gloss: optional gloss for derivation
        :param transferred: items that have been transferred/spelt out
        :return:
        """
        d_step = DerivationStep(synobjs, numeration, other, msg, gloss, transferred, mover)
        # Use Kataja's save system to freeze objects into form where they can be stored and restored
        # without further changes affecting them.
        savedata = {}
        open_references = {}
        d_step.save_object(savedata, open_references)
        c = 0
        max_depth = 100 # constituent trees can be surprisingly deep, and we don't have any
        # general dict about them.
        while open_references and c < max_depth:
            c += 1
            for obj in list(open_references.values()):
                if hasattr(obj, 'uid'):
                    #print('saving obj ', obj.uid)
                    obj.save_object(savedata, open_references)
                else:
                    print('cannot save open reference object ', obj)
        assert(c < max_depth) # please raise the max depth if this is reached
        self.derivation_steps.append((d_step.uid, savedata, msg))

    @time_me
    def restore_derivation_step(self):
        if self.derivation_steps:
            uid, frozen_data, msg = self.derivation_steps[self.derivation_step_index]
            d_step = DerivationStep(uid=uid)
            d_step.load_objects(frozen_data, ctrl.main)
            self.activated = True
            self.current = d_step

            synobjs_to_nodes(self.forest, d_step.synobjs, d_step.numeration, d_step.other,
                             d_step.msg, d_step.gloss, d_step.transferred,
                             d_step.mover)
            if msg:
                log.info(msg)

    def next_derivation_step(self):
        """
        :return:
        """
        if self.derivation_step_index + 1 >= len(self.derivation_steps):
            return
        self.derivation_step_index += 1
        self.restore_derivation_step()

    def previous_derivation_step(self):
        """
        :return:
        """
        if self.derivation_step_index == 0:
            return
        self.derivation_step_index -= 1
        self.restore_derivation_step()

    def jump_to_derivation_step(self, i):
        """
        :return:
        """
        self.derivation_step_index = i
        self.restore_derivation_step()

    def is_first(self):
        return self.derivation_step_index == 0

    def is_last(self):
        return self.derivation_step_index == len(self.derivation_steps) - 1

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    derivation_steps = SavedField("derivation_steps")
    derivation_step_index = SavedField("derivation_step_index", watcher="forest_changed")
    forest = SavedField("forest")
Esempio n. 15
0
class Feature(MyBaseClass):
    role = "Feature"

    def __init__(self,
                 namestring='',
                 counter=0,
                 name='',
                 value='',
                 unvalued=False,
                 ifeature=False):
        if in_kataja:
            super().__init__(name=name, value=value)
        if namestring:
            if namestring.startswith('u'):
                namestring = namestring[1:]
                self.unvalued = True
                self.ifeature = False
            elif namestring.startswith('i'):
                namestring = namestring[1:]
                self.unvalued = False
                self.ifeature = True
            else:
                self.unvalued = False
                self.ifeature = False
            digits = ''
            digit_count = 0
            for char in reversed(namestring):
                if char.isdigit():
                    digits = char + digits
                    digit_count += 1
                else:
                    break
            if digit_count:
                self.counter = int(digits)
                namestring = namestring[:-digit_count]
            else:
                self.counter = 0
            self.name, part, self.value = namestring.partition(':')
            self.valued_by = None
        else:
            self.name = name
            self.counter = counter
            self.value = value
            self.unvalued = unvalued
            self.ifeature = ifeature
            self.valued_by = None

    def __str__(self):
        return repr(self)

    def __repr__(self):
        parts = []
        if self.ifeature:
            parts.append('i')
        elif self.unvalued:
            parts.append('u')
        parts.append(self.name)
        if self.value:
            parts.append(':' + self.value)
        if self.counter:
            parts.append(str(self.counter))
        #return "'"+''.join(parts)+"'"
        return ''.join(parts)

    def __eq__(self, o):
        if isinstance(o, Feature):
            return self.counter == o.counter and self.name == o.name and \
                   self.value == o.value and self.unvalued == o.unvalued and \
                   self.ifeature == o.ifeature
        elif isinstance(o, str):
            return str(self) == o
        else:
            return False

    def __lt__(self, other):
        return repr(self) < repr(other)

    def __gt__(self, other):
        return repr(self) > repr(other)

    def __hash__(self):
        return hash(str(self))

    def __contains__(self, item):
        if isinstance(item, Feature):
            if item.name != self.name:
                return False
            if item.ifeature and not self.ifeature:
                return False
            if item.value and self.value != item.value:
                return False
            if item.unvalued and not self.unvalued:
                return False
            return True
        else:
            return item in str(self)

    def copy(self):
        return Feature(counter=self.counter,
                       name=self.name,
                       value=self.value,
                       unvalued=self.unvalued,
                       ifeature=self.ifeature)

    if in_kataja:
        unvalued = SavedField("unvalued")
        ifeature = SavedField("ifeature")
        counter = SavedField("counter")
        valued_by = SavedField("valued_by")
Esempio n. 16
0
class SavedObject(object):
    """ Make the object to have internal .saved -object where saved data
    should go.
    Also makes it neater to check if item is Savable.
    """
    uid = SavedField("uid")
    class_name = SavedField("class_name")
    settings = SavedField("settings")
    syntactic_object = False
    unique = False
    dont_copy = []
    allowed_settings = []

    def __init__(self, uid=None, **kw):
        if self.unique:
            uid = self.__class__.__name__
        elif uid is None:
            uid = next_available_uid()
        self._saved = {}
        self._history = {}
        self.uid = uid
        self.settings = {}
        self.class_name = getattr(self.__class__, 'role',
                                  self.__class__.__name__)
        self._cd = 0  # / CREATED / DELETED
        self._can_be_deleted_with_undo = True
        self._skip_this = False  # temporary "ghost" objects can use this flag to avoid being stored

    def __str__(self):
        return self.class_name + str(self.uid)

    def copy(self):
        """ Make a new object of same type and copy its attributes.
        object class can define dont_copy, list of attribute names that shouldn't be copied (
        either because they refer to peers or objects above, or because they are handled
        manually.). Attributes starting with '_' are always ignored, and the copied object is
        assigned a new key.
        :return:
        """
        if self.__class__.unique:
            print('cannot copy unique object')
            return None
        new_obj = self.__class__()
        new_obj.uid = next_available_uid()
        dont_copy = self.__class__.dont_copy
        for key, value in vars(self).items():
            if (not key.startswith('_')) and key not in dont_copy:
                if hasattr(value, 'copy'):
                    new_value = value.copy()
                else:
                    new_value = copy.copy(value)
                setattr(new_obj, key, new_value)
        return new_obj

    def poke(self, attribute):
        """ Alert undo system that this (Saved) object is being changed.
        This is used manually for container-type objects in the model before
        changing adding or
        removing objects in them, as these operations would not be catch by
        setters. This doesn't check if
         the new value is different from previous, as this is used manually
         before actions that change
         the list/dict/set.
        :param attribute: string, name of the attribute
        :return: None
        """
        if ctrl.undo_disabled:
            return

        if not self._history:
            ctrl.undo_pile.add(self)
            self._history[attribute] = copy.copy(self._saved[attribute])
        elif attribute not in self._history:
            self._history[attribute] = copy.copy(self._saved[attribute])

    def announce_creation(self):
        """ Flag object to have been created in this undo cycle.
        If the object was created here, when moving to _previous_ cycle
        should launch removal of the object
        from scene.
        :return:None
        """
        if ctrl.undo_disabled:
            return
        self._cd = CREATED
        ctrl.undo_pile.add(self)

    def announce_deletion(self):
        """ Flag object to have been deleted in this undo cycle.
        :return:None
        """
        if ctrl.undo_disabled or not self._can_be_deleted_with_undo:
            return
        self._cd = DELETED
        ctrl.undo_pile.add(self)

    def call_watchers(self, signal, field_name=None, value=None):
        """ Alert (UI) objects that are watching for changes for given field
        in this object
        :param signal:
        :param field_name:
        :param value:
        :return:
        """
        ctrl.call_watchers(self, signal, field_name, value)

    def transitions(self):
        """ Create a dict of changes based on modified attributes of the item.
        result dict has tuples as value, where the first item is value
        before, and second item is value after the change.
        :return: (dict of changed attributes, 0=EDITED(default) | 1=CREATED |
        2=DELETED)
        """
        transitions = {}
        #print('item %s history: %s' % (self.uid, self._history))
        for key, old_value in self._history.items():
            new_value = self._saved[key]
            if old_value != new_value:
                if isinstance(new_value, Iterable):
                    transitions[key] = old_value, copy.copy(new_value)
                else:
                    transitions[key] = old_value, new_value
        return transitions, self._cd

    def flush_history(self):
        """ Call after getting storing a set of transitions. Prepare for next
        round of transitions.
        :return: None
        """
        self._history = {}
        self._cd = 0

    # don't know yet what to do with synobjs:
    #                elif attr_name.endswith('_synobj') and getattr(self,
    # attr_name, False):
    #                    transitions[attr_name] = (True, True)

    def revert_to_earlier(self, transitions, transition_type):
        """ Restore to earlier version with a given changes -dict
        :param transitions: dict of changes, values are tuples of (old,
        new) -pairs
        :return: None
        """
        #print('--- restore to earlier for ', self, ' ----------')
        for key, value in transitions.items():
            old, new = value
            if isinstance(old, Iterable):
                setattr(self, key, copy.copy(old))
            else:
                setattr(self, key, old)
        transition_type = -transition_type  # revert transition
        self.after_model_update(transitions.keys(), transition_type)

    def move_to_later(self, transitions, transition_type):
        """ Move to later version with a given changes -dict
        :param transitions: dict of changes, values are tuples of (old,
        new) -pairs
        :return: None
        """
        # print('--- move to later for ', self, ' ----------')
        for key, value in transitions.items():
            old, new = value
            if isinstance(new, Iterable):
                setattr(self, key, copy.copy(new))
            else:
                setattr(self, key, new)
        self.after_model_update(transitions.keys(), transition_type)

    def after_model_update(self, changed_fields, transition_type):
        """ Compute derived effects of updated values in sensible order.
        :param updated_fields: field keys of updates
        :param transition_type: 0:edit, 1:CREATED, -1:DELETED
        :return: None
        """

    def can_have_setting(self, key):
        return key in self.allowed_settings

    def after_init(self):
        """ Override this to do preparations necessary for object creation
        :return:
        """
        self.announce_creation()

    def save_object(self, saved_objs, open_refs):
        """ Flatten the object to saveable dict and recursively save the
        objects it contains
        :param saved_objs: dict where saved objects are stored.
        :param open_refs: set of open references. We cannot go jumping to
        save each referred object when one is
        met, as it would soon lead to circular references. Open references
        are stored and cyclically reduced.
        :return: None
        """
        def _simplify(data):
            """ Goes through common iterable datatypes and if common Qt types
            are found, replaces them
            with basic python tuples.

            If object is one of Kataja's own data classes, then save its uid
            """

            if isinstance(data, (int, float, str)):
                return data
            elif isinstance(data, ITextNode):
                r = data.as_latex()
                if r:
                    return 'INode', r
                else:
                    return ''

            elif isinstance(data, dict):
                result = {}
                for k, value in data.items():
                    value = _simplify(value)
                    result[k] = value
                return result
            elif isinstance(data, list):
                result = []
                for o in data:
                    result.append(_simplify(o))
                return result
            elif isinstance(data, tuple):
                result = []
                for o in data:
                    result.append(_simplify(o))
                result = tuple(result)
                return result
            elif isinstance(data, set):
                result = set()
                for o in data:
                    result.add(_simplify(o))
                return result
            elif isinstance(data, types.FunctionType):
                # if functions are stored in the dict, there should be some
                # original
                # version of the same dict, where these
                # are in their original form.
                raise SaveError('trying to save a function at object ', self)
            elif data is None:
                return data
            elif isinstance(data, QPointF):
                return 'QPointF', to_tuple(QPointF)
            elif isinstance(data, QPoint):
                return 'QPoint', to_tuple(QPoint)
            elif isinstance(data, QtGui.QColor):
                return 'QColor', data.red(), data.green(), data.blue(), \
                       data.alpha()
            elif isinstance(data, QtGui.QPen):
                pass
            elif isinstance(data, QtCore.QRectF):
                return 'QRectF', data.x(), data.y(), data.width(), data.height(
                )
            elif isinstance(data, QtCore.QRect):
                return 'QRect', data.x(), data.y(), data.width(), data.height()
            elif isinstance(data, QtGui.QFont):
                raise SaveError("We shouldn't save QFonts!: ", data)
            elif hasattr(data, 'uid'):
                k = getattr(data, 'uid')
                if k not in saved_objs and k not in open_refs:
                    # print('in %s adding open reference %s' % (
                    # self.uid, k))
                    open_refs[k] = data
                return '|'.join(('*r*', str(k)))
            else:
                raise SaveError("simplifying unknown data type:", data,
                                type(data))

        if self.uid in saved_objs:
            return

        obj_data = {}
        for key, item in self._saved.items():
            obj_data[key] = _simplify(item)

        # print('saving obj: ', self.uid, obj_data)
        saved_objs[self.uid] = obj_data
        if self.uid in open_refs:
            del open_refs[self.uid]

    @time_me
    def load_objects(self, data, kataja_main):
        """ Load and restore objects starting from given obj (probably Forest
        or KatajaMain instance)
        :param data:
        :param kataja_main:
        :param self:
        """
        full_map = {}
        restored = {}
        full_data = data

        # First we need a full index of objects that already exist within the
        #  existing objects.
        # This is to avoid recreating those objects. We just want to modify them
        def map_existing(obj):
            """ Take note of existing objects, as undo? will overwrite these.
            :param obj:
            :return:
            """
            if isinstance(obj, dict):
                for item in obj.values():
                    map_existing(item)
                return
            elif isinstance(obj, (list, tuple, set)):
                for item in obj:
                    map_existing(item)
                return
            # objects that support saving

            key = getattr(obj, 'uid', '')
            if key and key not in full_map:
                full_map[key] = obj
                for item in obj._saved.values():
                    map_existing(item)

        # Restore either takes existing object or creates a new 'stub' object
        #  and then loads it with given data
        map_existing(self)
        self.restore(self.uid,
                     full_data,
                     full_map,
                     restored,
                     kataja_main,
                     root=True)

    def restore(self,
                obj_key,
                full_data,
                full_map,
                restored,
                kataja_main,
                root=False):
        """ Recursively restore objects inside the scope of current obj. Used
        for loading kataja files.
        :param obj_key:
        :param root:
        :return:
        """

        if isinstance(obj_key, str):
            if obj_key.isdigit():
                obj_key = int(obj_key)

        def inflate(data):
            """ Recursively turn QObject descriptions back into actual
            objects and object references
            back into real objects
            :param data:
            :return:
            """
            # print('inflating %s in %s' % (str(data), self))
            if data is None:
                return data
            elif isinstance(data, (int, float)):
                return data
            elif isinstance(data, dict):
                result = {}
                for k, value in data.items():
                    result[k] = inflate(value)
                return result
            elif isinstance(data, list):
                result = []
                for item in data:
                    result.append(inflate(item))
                return result
            elif isinstance(data, str):
                if data.startswith('*r*'):
                    r, uid = data.split('|', 1)
                    return self.restore(uid, full_data, full_map, restored,
                                        kataja_main)
                else:
                    return data
            elif isinstance(data, tuple):
                if data and isinstance(data[0], str):
                    data_type = data[0]
                    if data_type == 'INode':
                        return ctrl.latex_field_parser.process(data[1])
                    elif data_type == 'QPointF':
                        return QPointF(data[1], data[2])
                    elif data_type == 'QPoint':
                        return QPoint(data[1], data[2])
                    elif data_type == 'QRectF':
                        return QtCore.QRectF(data[1], data[2], data[3],
                                             data[4])
                    elif data_type == 'QRect':
                        return QtCore.QRect(data[1], data[2], data[3], data[4])
                    elif data_type == 'QColor':
                        return QtGui.QColor(data[1], data[2], data[3], data[4])
                    elif data_type == 'QFont':
                        f = QtGui.QFont()
                        f.fromString(data[1])
                        return f
                    elif data_type.startswith('Q'):
                        raise SaveError('unknown QObject: %s' % str(data))
                    else:
                        result = []
                        for item in data:
                            result.append(inflate(item))
                        result = tuple(result)
                        return result
                else:
                    result = []
                    for item in data:
                        result.append(inflate(item))
                    result = tuple(result)
                    return result
            elif isinstance(data, set):
                result = set()
                for item in data:
                    result.add(inflate(item))
                return result
            return data

        # print('restoring %s , %s ' % (obj_key, class_key))
        # Don't restore object several times, even if the object is referred
        # in several places
        if obj_key in restored:
            return restored[obj_key]
        # If the object already exists (e.g. we are doing undo), the loaded
        # values overwrite existing values.
        obj = full_map.get(obj_key, None)

        # new data that the object should have
        new_data = full_data.get(obj_key, None)
        if not (obj or new_data):
            return None
        elif obj and not new_data:
            print(obj, " is not present in save data")
        class_key = new_data['class_name']

        if not obj:
            obj = classes.create(class_key)
        # when creating/modifying values inside forests, they may refer back
        # to ctrl.forest. That has to be the current

        # forest, or otherwise things go awry
        if class_key == 'Forest':
            kataja_main.forest = obj

        # keep track of which objects have been restored
        restored[obj_key] = obj

        for key, old_value in obj._saved.items():
            new_value = new_data.get(key, None)
            if new_value is not None:
                new_value = inflate(new_value)
            setattr(obj, key, new_value)

        # objects need to be finalized after setting values, do this only once per load.
        if root:
            for item in restored.values():
                if hasattr(item, 'after_init'):
                    #print('restoring item, calling after_init for ', type(item), item)
                    item.after_init()
        return obj
Esempio n. 17
0
class KatajaMain(SavedObject, QtWidgets.QMainWindow):
    """ Qt's main window. When this is closed, application closes. Graphics are
    inside this, in scene objects with view widgets. This window also manages
    keypresses and menus. """
    unique = True

    def __init__(self, kataja_app, no_prefs=False, reset_prefs=False):
        """ KatajaMain initializes all its children and connects itself to
        be the main window of the given application. Receives launch arguments:
        :param no_prefs: bool, don't load or save preferences
        :param reset_prefs: bool, don't attempt to load preferences, use defaults instead

        """
        QtWidgets.QMainWindow.__init__(self)
        kataja_app.processEvents()
        SavedObject.__init__(self)
        self.use_tooltips = True
        self.available_plugins = {}
        self.setDockOptions(QtWidgets.QMainWindow.AnimatedDocks)
        self.setCorner(QtCore.Qt.TopLeftCorner, QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.TopRightCorner, QtCore.Qt.RightDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomLeftCorner,
                       QtCore.Qt.LeftDockWidgetArea)
        self.setCorner(QtCore.Qt.BottomRightCorner,
                       QtCore.Qt.RightDockWidgetArea)
        x, y, w, h = (50, 50, 1152, 720)
        self.setMinimumSize(w, h)
        self.app = kataja_app
        self.save_prefs = not no_prefs
        self.forest = None
        self.fontdb = QtGui.QFontDatabase()
        self.color_manager = PaletteManager()
        self.settings_manager = Settings()
        self.forest_keepers = []
        self.forest_keeper = None
        ctrl.late_init(self)
        classes.late_init()
        prefs.import_node_classes(classes)
        self.syntax = SyntaxConnection()
        prefs.load_preferences(disable=reset_prefs or no_prefs)
        qt_prefs.late_init(running_environment, prefs, self.fontdb, log)
        self.settings_manager.set_prefs(prefs)
        self.color_manager.update_custom_colors()
        self.find_plugins(prefs.plugins_path
                          or running_environment.plugins_path)
        self.setWindowIcon(qt_prefs.kataja_icon)
        self.graph_scene = GraphScene(main=self, graph_view=None)
        self.graph_view = GraphView(main=self, graph_scene=self.graph_scene)
        self.graph_scene.graph_view = self.graph_view
        self.ui_manager = UIManager(self)
        self.settings_manager.set_ui_manager(self.ui_manager)
        self.ui_manager.populate_ui_elements()
        # make empty forest and forest keeper so initialisations don't fail because of their absence
        self.visualizations = VISUALIZATIONS
        self.init_forest_keepers()
        self.settings_manager.set_document(self.forest_keeper)
        kataja_app.setPalette(self.color_manager.get_qt_palette())
        self.forest = Forest()
        self.settings_manager.set_forest(self.forest)
        self.change_color_theme(prefs.color_theme, force=True)
        self.update_style_sheet()
        self.graph_scene.late_init()
        self.setCentralWidget(self.graph_view)
        self.setGeometry(x, y, w, h)
        self.setWindowTitle(self.tr("Kataja"))
        self.print_started = False
        self.show()
        self.raise_()
        kataja_app.processEvents()
        self.activateWindow()
        self.status_bar = self.statusBar()
        self.install_plugins()
        self.load_initial_treeset()
        log.info('Welcome to Kataja! (h) for help')
        #ctrl.call_watchers(self.forest_keeper, 'forest_changed')
        # toolbar = QtWidgets.QToolBar()
        # toolbar.setFixedSize(480, 40)
        # self.addToolBar(toolbar)
        gestures = [
            QtCore.Qt.TapGesture, QtCore.Qt.TapAndHoldGesture,
            QtCore.Qt.PanGesture, QtCore.Qt.PinchGesture,
            QtCore.Qt.SwipeGesture, QtCore.Qt.CustomGesture
        ]
        #for gesture in gestures:
        #    self.grabGesture(gesture)
        self.action_finished(undoable=False)
        self.forest.undo_manager.flush_pile()

    def update_style_sheet(self):
        c = ctrl.cm.drawing()
        ui = ctrl.cm.ui()
        f = qt_prefs.get_font(g.UI_FONT)
        self.setStyleSheet(
            stylesheet % {
                'draw': c.name(),
                'lighter': c.lighter().name(),
                'paper': ctrl.cm.paper().name(),
                'ui': ui.name(),
                'ui_lighter': ui.lighter().name(),
                'ui_font': f.family(),
                'ui_font_size': f.pointSize(),
                'ui_font_larger': int(f.pointSize() * 1.2),
                'ui_darker': ui.darker().name()
            })

    def find_plugins(self, plugins_path):
        """ Find the plugins dir for the running configuration and read the metadata of plugins.
        Don't try to load actual python code yet
        :return: None
        """
        if not plugins_path:
            return
        self.available_plugins = {}
        plugins_path = os.path.normpath(plugins_path)
        os.makedirs(plugins_path, exist_ok=True)
        sys.path.append(plugins_path)
        base_ends = len(plugins_path.split('/'))
        for root, dirs, files in os.walk(plugins_path):
            path_parts = root.split('/')
            if len(path_parts) == base_ends + 1 and not path_parts[base_ends].startswith('__') \
                    and 'plugin.json' in files:
                success = False
                try:
                    plugin_file = open(root + '/plugin.json', 'r')
                    data = json.load(plugin_file)
                    plugin_file.close()
                    success = True
                except:
                    log.error(sys.exc_info())
                    print(sys.exc_info())
                if success:
                    mod_name = path_parts[base_ends]
                    data['module_name'] = mod_name
                    data['module_path'] = root
                    self.available_plugins[mod_name] = data

    def enable_plugin(self, plugin_key, reload=False):
        """ Start one plugin: save data, replace required classes with plugin classes, load data.

        """
        self.active_plugin_setup = self.load_plugin(plugin_key)
        if not self.active_plugin_setup:
            return
        self.clear_all()
        ctrl.disable_undo()
        if reload:
            available = []
            for key in sys.modules:
                if key.startswith(plugin_key):
                    available.append(key)
            if getattr(self.active_plugin_setup, 'reload_order', None):
                to_reload = [
                    x for x in self.active_plugin_setup.reload_order
                    if x in available
                ]
            else:
                to_reload = sorted(available)
            for mod_name in to_reload:
                importlib.reload(sys.modules[mod_name])
                print('reloaded ', mod_name)
                log.info('reloaded module %s' % mod_name)

        if hasattr(self.active_plugin_setup, 'plugin_parts'):
            for classobj in self.active_plugin_setup.plugin_parts:
                base_class = classes.find_base_model(classobj)
                if base_class:
                    classes.add_mapping(base_class, classobj)
                    m = "replacing %s with %s " % (base_class.__name__,
                                                   classobj.__name__)
                else:
                    m = "adding %s " % classobj.__name__
                log.info(m)
                print(m)
        if hasattr(self.active_plugin_setup, 'help_file'):
            dir_path = os.path.dirname(
                os.path.realpath(self.active_plugin_setup.__file__))
            print(dir_path)
            self.ui_manager.set_help_source(dir_path,
                                            self.active_plugin_setup.help_file)
        if hasattr(self.active_plugin_setup, 'start_plugin'):
            self.active_plugin_setup.start_plugin(self, ctrl, prefs)
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = plugin_key

    def disable_current_plugin(self):
        """ Disable the current plugin and load the default trees instead.
        :param clear: if True, have empty treeset, if False, try to load default kataja treeset."""
        if not self.active_plugin_setup:
            return
        ctrl.disable_undo()
        if hasattr(self.active_plugin_setup, 'tear_down_plugin'):
            self.active_plugin_setup.tear_down_plugin(self, ctrl, prefs)
        self.clear_all()
        # print(classes.base_name_to_plugin_class)
        # if hasattr(self.active_plugin_setup, 'plugin_parts'):
        #     for classobj in self.active_plugin_setup.plugin_parts:
        #         class_name = classobj.__name__
        #         if class_name:
        #             log.info(f'removing {class_name}')
        #             print(f'removing {class_name}')
        #             classes.remove_class(class_name)
        classes.restore_default_classes()
        self.init_forest_keepers()
        ctrl.resume_undo()
        prefs.active_plugin_name = ''

    def load_plugin(self, plugin_module):
        setup = None
        importlib.invalidate_caches()
        if plugin_module in self.available_plugins:
            retry = True
            while retry:
                try:
                    setup = importlib.import_module(plugin_module + ".setup")
                    retry = False
                except:
                    e = sys.exc_info()
                    error_dialog = ErrorDialog(self)
                    error_dialog.set_error(
                        '%s, line %s\n%s: %s' %
                        (plugin_module + ".setup.py", e[2].tb_lineno,
                         e[0].__name__, e[1]))
                    error_dialog.set_traceback(traceback.format_exc())
                    retry = error_dialog.exec_()
                    setup = None
        return setup

    def install_plugins(self):
        """ If there are plugins defined in preferences to be used, activate them now.
        :return: None
        """
        if prefs.active_plugin_name:
            log.info('Installing plugin %s...' % prefs.active_plugin_name)
            self.enable_plugin(prefs.active_plugin_name, reload=False)
        self.ui_manager.update_plugin_menu()

    def reset_preferences(self):
        """

        :return:
        """
        prefs.restore_default_preferences(qt_prefs, running_environment,
                                          classes)
        ctrl.call_watchers(self, 'color_themes_changed')
        if self.ui_manager.preferences_dialog:
            self.ui_manager.preferences_dialog.close()
        self.ui_manager.preferences_dialog = PreferencesDialog(self)
        self.ui_manager.preferences_dialog.open()
        self.ui_manager.preferences_dialog.trigger_all_updates()

    def init_forest_keepers(self):
        """ Put empty forest keepers (Kataja documents) in place -- you want to do this after
        plugins have changed the classes that implement these.
        :return:
        """
        self.forest_keepers = [classes.get('KatajaDocument')()]
        self.forest_keeper = self.forest_keepers[0]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')

    def load_initial_treeset(self):
        """ Loads and initializes a new set of trees. Has to be done before
        the program can do anything sane.
        """

        self.forest_keeper.create_forests(clear=False)
        self.change_forest()
        self.ui_manager.update_projects_menu()

    def create_new_project(self):
        names = [fk.name for fk in self.forest_keepers]
        name_base = 'New project'
        name = 'New project'
        c = 1
        while name in names:
            name = '%s %s' % (name_base, c)
            c += 1
        self.forest.retire_from_drawing()
        self.forest_keepers.append(classes.KatajaDocument(name=name))
        self.forest_keeper = self.forest_keepers[-1]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    def switch_project(self, i):
        self.forest.retire_from_drawing()
        self.forest_keeper = self.forest_keepers[i]
        ctrl.call_watchers(self.forest_keeper, 'document_changed')
        self.change_forest()
        self.ui_manager.update_projects_menu()
        return self.forest_keeper

    # ### Visualization
    # #############################################################

    def change_forest(self):
        """ Tells the scene to remove current trees and related data and
        change it to a new one. Signal 'forest_changed' is already sent by forest keeper.
        """
        ctrl.disable_undo()
        if self.forest:
            self.forest.retire_from_drawing()
        if not self.forest_keeper.forest:
            self.forest_keeper.create_forests(clear=True)
        self.forest = self.forest_keeper.forest
        self.settings_manager.set_forest(self.forest)
        if self.forest.is_parsed:
            if self.forest.derivation_steps:
                ds = self.forest.derivation_steps
                if not ds.activated:
                    print('jumping to derivation step: ',
                          ds.derivation_step_index)
                    ds.jump_to_derivation_step(ds.derivation_step_index)
            else:
                print('no derivation steps')
        self.forest.prepare_for_drawing()
        ctrl.resume_undo()
        #if self.forest.undo_manager.

    def redraw(self):
        """ Call for forest redraw
        :return: None
        """
        self.forest.draw()

    def attach_widget_to_log_handler(self, browserwidget):
        """ This has to be done once: we have a logger set up before there is any output widget,
        once the widget is created it is connected to logger.
        :param browserwidget:
        :return:
        """
        self.app.log_handler.set_widget(browserwidget)

    #    def mousePressEvent(self, event):
#        """ KatajaMain doesn't do anything with mousePressEvents, it delegates
#        :param event:
#        them downwards. This is for debugging. """
#        QtWidgets.QMainWindow.mousePressEvent(self, event)

#    def keyPressEvent(self, event):
#        # if not self.key_manager.receive_key_press(event):
#        """
#
#        :param event:
#        :return:
#        """
#        return QtWidgets.QMainWindow.keyPressEvent(self, event)

# ## Menu management #######################################################

    def action_finished(self, m='', undoable=True, error=None):
        """ Write action to undo stack, report back to user and redraw trees
        if necessary
        :param m: message for undo
        :param undoable: are we supposed to take a snapshot of changes after
        this action.
        :param error message
        """
        if error:
            log.error(error)
        elif m:
            log.info(m)
        if ctrl.action_redraw:
            ctrl.forest.draw()
        if undoable and not error:
            ctrl.forest.undo_manager.take_snapshot(m)
        ctrl.graph_scene.start_animations()
        ctrl.ui.update_actions()

    def trigger_action(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.action_triggered(*args, **kwargs)

    def trigger_but_suppress_undo(self, name, *args, **kwargs):
        """ Helper for programmatically triggering actions (for tests and plugins)
        :param name: action name
        :param kwargs: keyword parameters
        :return:
        """
        action = self.ui_manager.actions[name]
        action.trigger_but_suppress_undo(*args, **kwargs)

    def enable_actions(self):
        """ Restores menus """
        for action in self.ui_manager.actions.values():
            action.setDisabled(False)

    def disable_actions(self):
        """ Actions shouldn't be initiated when there is other multi-phase
        action going on """
        for action in self.ui_manager.actions.values():
            action.setDisabled(True)

    def change_color_theme(self, mode, force=False):
        """
        triggered by color mode selector in colors panel

        :param mode:
        """
        if mode != ctrl.settings.get('color_theme') or force:
            if ctrl.settings.document:
                ctrl.settings.set('color_theme', mode, level=g.DOCUMENT)
            ctrl.settings.set('color_theme', mode, level=g.PREFS)
            self.update_colors()

    def timerEvent(self, event):
        """ Timer event only for printing, for 'snapshot' effect
        :param event:
        """
        def find_path(fixed_part, extension, counter=0):
            """ Generate file names until free one is found
            :param fixed_part: blah
            :param extension: blah
            :param counter: blah
            """
            if not counter:
                fpath = fixed_part + extension
            else:
                fpath = fixed_part + str(counter) + extension
            if os.path.exists(fpath):
                fpath = find_path(fixed_part, extension, counter + 1)
            return fpath

        if not self.print_started:
            return
        else:
            self.print_started = False
        self.killTimer(event.timerId())
        # Prepare file and path
        path = prefs.print_file_path or prefs.userspace_path or \
            running_environment.default_userspace_path
        if not path.endswith('/'):
            path += '/'
        if not os.path.exists(path):
            print("bad path for printing (print_file_path in preferences) , "
                  "using '.' instead.")
            path = './'
        filename = prefs.print_file_name
        if filename.endswith(('.pdf', '.png')):
            filename = filename[:-4]
        # Prepare image
        self.graph_scene.removeItem(self.graph_scene.photo_frame)
        self.graph_scene.photo_frame = None
        # Prepare printer
        png = prefs.print_format == 'png'
        source = self.graph_scene.print_rect()

        if png:
            full_path = find_path(path + filename, '.png', 0)
            scale = 4
            target = QtCore.QRectF(QtCore.QPointF(0, 0), source.size() * scale)
            writer = QtGui.QImage(target.size().toSize(),
                                  QtGui.QImage.Format_ARGB32_Premultiplied)
            writer.fill(QtCore.Qt.transparent)
            painter = QtGui.QPainter()
            painter.begin(writer)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            iwriter = QtGui.QImageWriter(full_path)
            iwriter.write(writer)
            log.info(
                "printed to %s as PNG (%spx x %spx, %sx size)." %
                (full_path, int(target.width()), int(target.height()), scale))

        else:
            dpi = 25.4
            full_path = find_path(path + filename, '.pdf', 0)
            target = QtCore.QRectF(0, 0,
                                   source.width() / 2.0,
                                   source.height() / 2.0)

            writer = QtGui.QPdfWriter(full_path)
            writer.setResolution(dpi)
            writer.setPageSizeMM(target.size())
            writer.setPageMargins(QtCore.QMarginsF(0, 0, 0, 0))
            painter = QtGui.QPainter()
            painter.begin(writer)
            self.graph_scene.render(painter, target=target, source=source)
            painter.end()
            log.info("printed to %s as PDF with %s dpi." % (full_path, dpi))

        # Thank you!
        # Restore image
        self.graph_scene.setBackgroundBrush(self.color_manager.gradient)

    # Not called from anywhere yet, but useful
    def release_selected(self, **kw):
        """

        :param kw:
        :return:
        """
        for node in ctrl.selected:
            node.release()
        self.action_finished()
        return True

    def clear_all(self):
        """ Empty everything - maybe necessary before loading new data. """
        if self.forest:
            self.forest.retire_from_drawing()
        self.forest_keeper = None
        # Garbage collection doesn't mix well with animations that are still running
        #print('garbage stats:', gc.get_count())
        #gc.collect()
        #print('after collection:', gc.get_count())
        #if gc.garbage:
        #    print('garbage:', gc.garbage)
        self.forest_keepers.append(classes.KatajaDocument(clear=True))
        self.forest_keeper = self.forest_keepers[-1]
        self.settings_manager.set_document(self.forest_keeper)
        self.forest = None

    # ## Other window events
    ###################################################

    def closeEvent(self, event):
        """ Shut down the program, give some debug info
        :param event:
        """
        QtWidgets.QMainWindow.closeEvent(self, event)
        if ctrl.print_garbage:
            # import objgraph
            log.debug('garbage stats: ' + str(gc.get_count()))
            gc.collect()
            log.debug('after collection: ' + str(gc.get_count()))
            if gc.garbage:
                log.debug('garbage: ' + str(gc.garbage))

                # objgraph.show_most_common_types(limit =40)
        if self.save_prefs:
            prefs.save_preferences()
        log.info('...done')

    @time_me
    def create_save_data(self):
        """
        Make a large dictionary of all objects with all of the complex stuff
        and circular references stripped out.
        :return: dict
        """
        savedata = {}
        open_references = {}
        savedata['save_scheme_version'] = 0.4
        self.save_object(savedata, open_references)
        max_rounds = 10
        c = 0
        while open_references and c < max_rounds:
            c += 1
            #print(len(savedata))
            #print('---------------------------')
            for obj in list(open_references.values()):
                if hasattr(obj, 'uid'):
                    obj.save_object(savedata, open_references)
                else:
                    print('cannot save open reference object ', obj)
        assert (c < max_rounds)
        print('total savedata: %s chars in %s items.' %
              (len(str(savedata)), len(savedata)))
        # print(savedata)
        return savedata

    def update_colors(self, randomise=False, animate=True):
        t = time.time()
        cm = self.color_manager
        old_gradient_base = cm.paper()
        cm.update_colors(randomise=randomise)
        self.app.setPalette(cm.get_qt_palette())
        self.update_style_sheet()
        ctrl.call_watchers(self, 'palette_changed')
        if cm.gradient:
            if old_gradient_base != cm.paper() and animate:
                self.graph_scene.fade_background_gradient(
                    old_gradient_base, cm.paper())
            else:
                self.graph_scene.setBackgroundBrush(cm.gradient)
        else:
            self.graph_scene.setBackgroundBrush(qt_prefs.no_brush)
        self.update()

    ### Applying specific preferences globally.
    # These are on_change -methods for various preferences -- these are called if changing a
    # preference should have immediate consequences. They are hosted here because they need to
    # have access to prefs, qt_prefs, main etc.

    def prepare_easing_curve(self):
        qt_prefs.prepare_easing_curve(prefs.curve, prefs.move_frames)

    def update_color_theme(self):
        self.change_color_theme(prefs.color_theme, force=True)

    def update_visualization(self):
        ctrl.forest.set_visualization(prefs.visualization)
        self.main.redraw()

    def resize_ui_font(self):
        qt_prefs.toggle_large_ui_font(prefs.large_ui_text, prefs.fonts)
        self.update_style_sheet()

    def watch_alerted(self, obj, signal, field_name, value):
        """ Receives alerts from signals that this object has chosen to listen. These signals
         are declared in 'self.watchlist'.

         This method will try to sort out the received signals and act accordingly.

        :param obj: the object causing the alarm
        :param signal: identifier for type of the alarm
        :param field_name: name of the field of the object causing the alarm
        :param value: value given to the field
        :return:
        """
        pass

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    forest_keeper = SavedField("forest_keeper")
    forest = SavedField("forest")
Esempio n. 18
0
class FeatureNode(Node):
    """
    Node to express a feature of a constituent
    """
    __qt_type_id__ = next_available_type_id()
    width = 20
    height = 20
    node_type = FEATURE_NODE
    display_name = ('Feature', 'Features')
    display = True
    wraps = 'feature'

    editable = {'name': dict(name='Name', prefill='name',
                              tooltip='Name of the feature, used as identifier',
                              syntactic=True),
                'value': dict(name='Value',
                              prefill='value',
                              tooltip='Value given to this feature',
                              syntactic=True),
                'family': dict(name='Family', prefill='family',
                               tooltip='Several distinct features can be '
                                       'grouped under one family (e.g. '
                                       'phi-features)',
                               syntactic=True)
                }

    default_style = {'fancy': {'color_id': 'accent2', 'font_id': g.SMALL_CAPS, 'font-size': 9},
                     'plain': {'color_id': 'accent2', 'font_id': g.SMALL_CAPS, 'font-size': 9}}

    default_edge = g.FEATURE_EDGE

    def __init__(self, label='', value='', family=''):
        Node.__init__(self)
        self.name = label
        self.value = value
        self.family = family
        self.repulsion = 0.25
        self._gravity = 2.5
        self.z_value = 60
        self.setZValue(self.z_value)

        # implement color() to map one of the d['rainbow_%'] colors here. Or if bw mode is on, then something else.

    @staticmethod
    def create_synobj(label, forest):
        """ FeatureNodes are wrappers for Features. Exact
        implementation/class of feature is defined in ctrl.
        :return:
        """
        if not label:
            label = 'Feature'
        obj = ctrl.syntax.Feature(name=label)
        obj.after_init()
        return obj

    def compute_start_position(self, host):
        """ Makes features start at somewhat predictable position, if they are of common kinds of features.
        If not, then some random noise is added to prevent features sticking together
        :param host:
        """
        x, y = host.current_position
        k = self.syntactic_object.key
        if k in color_map:
            x += color_map[k]
            y += color_map[k]
        else:
            x += random.uniform(-4, 4)
            y += random.uniform(-4, 4)
        self.set_original_position((x, y))


    def compose_html_for_viewing(self):
        """ This method builds the html to display in label. For convenience, syntactic objects
        can override this (going against the containment logic) by having their own
        'compose_html_for_viewing' -method. This is so that it is easier to create custom
        implementations for constituents without requiring custom constituentnodes.

        Note that synobj's compose_html_for_viewing receives the node object as parameter,
        so you can replicate the behavior below and add your own to it.
        :return:
        """

        # Allow custom syntactic objects to override this
        if hasattr(self.syntactic_object, 'compose_html_for_viewing'):
            return self.syntactic_object.compose_html_for_viewing(self)

        return str(self), ''

    def compose_html_for_editing(self):
        """ This is used to build the html when quickediting a label. It should reduce the label
        into just one field value that is allowed to be edited, in constituentnode this is
        either label synobj's label. This can be overridden in syntactic object by having
        'compose_html_for_editing' -method there. The method returns a tuple,
          (field_name, html).
        :return:
        """

        # Allow custom syntactic objects to override this
        if hasattr(self.syntactic_object, 'compose_html_for_editing'):
            return self.syntactic_object.compose_html_for_editing(self)

        return 'name', self.compose_html_for_viewing()[0]

    def parse_quick_edit(self, text):
        """ This is an optional method for node to parse quick edit information into multiple
        fields. Usually nodes do without this: quickediting only changes one field at a time and
        interpretation is straightforward. E.g. features can have more complex parsing.
        :param text:
        :return:
        """
        if hasattr(self.syntactic_object, 'parse_quick_edit'):
            return self.syntactic_object.parse_quick_edit(self, text)
        parts = text.split(':')
        name = ''
        value = ''
        family = ''
        if len(parts) >= 3:
            name, value, family = parts
        elif len(parts) == 2:
            name, value = parts
        elif len(parts) == 1:
            name = parts[0]
        if len(name) > 1 and name.startswith('u') and name[1].isupper():
            name = name[1:]
        self.name = name
        self.value = value
        self.family = family

    def update_relations(self, parents, shape=None, position=None):
        """ Cluster features according to feature_positioning -setting or release them to be
        positioned according to visualisation.
        :param parents: list where we collect parent objects that need to position their children
        :return:
        """

        if shape is None:
            shape = ctrl.settings.get('label_shape')
        if position is None:
            position = ctrl.settings.get('feature_positioning')
        if position or shape == g.CARD:
            for parent in self.get_parents(similar=False, visible=False):
                if parent.node_type == g.CONSTITUENT_NODE:
                    if parent.is_visible():
                        self.lock_to_node(parent)
                        parents.append(parent)
                        break
                    else:
                        self.release_from_locked_position()
                elif parent.node_type == g.FEATURE_NODE:
                    if self.locked_to_node == parent:
                        self.release_from_locked_position()
        else:
            self.release_from_locked_position()

    def paint(self, painter, option, widget=None):
        """ Painting is sensitive to mouse/selection issues, but usually with
        :param painter:
        :param option:
        :param widget:
        nodes it is the label of the node that needs complex painting """
        if ctrl.pressed == self or self._hovering or ctrl.is_selected(self):
            painter.setPen(ctrl.cm.get('background1'))
            painter.setBrush(self.contextual_background())
            painter.drawRoundedRect(self.inner_rect, 5, 5)
        Node.paint(self, painter, option, widget)

    @property
    def contextual_color(self):
        """ Drawing color that is sensitive to node's state """
        if ctrl.pressed == self:
            return ctrl.cm.get('background1')
        elif self._hovering:
            return ctrl.cm.get('background1')
        elif ctrl.is_selected(self):
            return ctrl.cm.get('background1')
            # return ctrl.cm.selected(ctrl.cm.selection())
        else:
            if getattr(self.syntactic_object, 'unvalued', False):  # fixme: Temporary hack
                return ctrl.cm.get('accent1')
            else:
                return self.color

    def contextual_background(self):
        """ Background color that is sensitive to node's state """
        if ctrl.pressed == self:
            return ctrl.cm.active(ctrl.cm.selection())
        elif self.drag_data:
            return ctrl.cm.hovering(ctrl.cm.selection())
        elif self._hovering:
            return ctrl.cm.hovering(ctrl.cm.selection())
        elif ctrl.is_selected(self):
            return ctrl.cm.selection()
        else:
            return qt_prefs.no_brush

    def special_connection_point(self, sx, sy, ex, ey, start=False):
        f_align = ctrl.settings.get('feature_positioning')
        br = self.boundingRect()
        left, top, right, bottom = (int(x * .8) for x in br.getCoords())
        if f_align == 0: # direct
            if start:
                return (sx, sy), BOTTOM_SIDE
            else:
                return (ex, ey), BOTTOM_SIDE
        elif f_align == 1: # vertical
            if start:
                if sx < ex:
                    return (sx + right, sy), RIGHT_SIDE
                else:
                    return (sx + left, sy), LEFT_SIDE
            else:
                if sx < ex:
                    return (ex + left, ey), LEFT_SIDE
                else:
                    return (ex + right, ey), RIGHT_SIDE
        elif f_align == 2:  # horizontal
            if start:
                if sy < ey:
                    return (sx, sy + bottom), BOTTOM_SIDE
                else:
                    return (sx, sy + top), TOP_SIDE
            else:
                if sy <= ey:
                    return (ex, ey + top), TOP_SIDE
                else:
                    return (ex, ey + bottom), BOTTOM_SIDE
        elif f_align == 3:  # card
            if start:
                return (sx + right, sy), RIGHT_SIDE
            else:
                return (ex + left, ey), LEFT_SIDE

        if start:
            return (sx, sy), 0
        else:
            return (ex, ey), 0

    def __str__(self):
        if self.syntactic_object:
            return str(self.syntactic_object)
        s = []
        signs = ('+', '-', '=', 'u', '✓')
        if self.value and (len(self.value) == 1 and self.value in signs or \
           len(self.value) == 2 and self.value[1] in signs):
            s.append(self.value + str(self.name))
        elif self.value or self.family:
            s.append(str(self.name))
            s.append(str(self.value))
            if self.family:
                s.append(str(self.family))
        else:
            s.append(str(self.name))
        return ":".join(s)

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    name = SavedField("name")
    value = SavedField("value")
    family = SavedField("family")
Esempio n. 19
0
class CommentNode(Node):
    """ Node to display comments, annotations etc. syntactically inert information """
    __qt_type_id__ = next_available_type_id()
    width = 20
    height = 20
    node_type = g.COMMENT_NODE
    display_name = ('Comment', 'Comments')
    display = True
    is_syntactic = False
    can_be_in_groups = False
    editable = {
        'text':
        dict(name='',
             prefill='comment',
             tooltip='freeform text, invisible for processing',
             input_type='expandingtext')
    }

    default_style = {
        'fancy': {
            'color_id': 'accent4',
            'font_id': g.MAIN_FONT,
            'font-size': 14
        },
        'plain': {
            'color_id': 'accent4',
            'font_id': g.MAIN_FONT,
            'font-size': 14
        }
    }

    default_edge = g.COMMENT_EDGE

    touch_areas_when_dragging = {
        g.DELETE_ARROW: {
            'condition': 'dragging_my_arrow'
        },
        g.TOUCH_CONNECT_COMMENT: {
            'condition': 'dragging_comment'
        },
    }

    touch_areas_when_selected = {
        g.DELETE_ARROW: {
            'condition': 'has_arrow',
            'action': 'delete_arrow'
        },
        g.ADD_ARROW: {
            'action': 'start_arrow_from_node'
        }
    }

    def __init__(self, label='comment'):
        self.image_object = None
        Node.__init__(self)
        if not label:
            label = 'comment'
        self.resizable = True
        self.label = label
        self.physics_x = False
        self.physics_y = False
        self.image_path = None
        self.image = None
        self.pos_relative_to_host = -50, -50
        self.preferred_host = None

    def after_init(self):
        Node.after_init(self)
        if self.user_size:
            w, h = self.user_size
            self.set_user_size(w, h)

    @property
    def hosts(self):
        """ A comment can be associated with nodes. The association uses the general connect/disconnect mechanism, but
        'hosts' is a shortcut to get the nodes.
        :return: list of Nodes
        """
        return self.get_parents(visible=False, of_type=g.COMMENT_EDGE)

    @property
    def text(self):
        """ The text of the comment. Uses the generic node.label as storage.
        :return: str or ITextNode
        """
        return self.label

    @text.setter
    def text(self, value):
        """ The text of the comment. Uses the generic node.label as storage.
        :param value: str or ITextNode
        """
        self.label = value

    def has_arrow(self):
        return bool(self.edges_down)

    def set_image_path(self, pixmap_path):
        if pixmap_path and (self.image_path != pixmap_path or not self.image):
            self.image_path = pixmap_path
            self.image = QtGui.QPixmap()
            success = self.image.load(pixmap_path)
            if success:
                if self.image_object:
                    print('removing old image object')
                    self.image_object.hide()
                    self.image_object.setParentItem(None)
                self.image_object = QtWidgets.QGraphicsPixmapItem(
                    self.image, self)
                self.image_object.setPos(self.image.width() / -2,
                                         self.image.height() / -2)
                self.text = ''
                self.update_label_visibility()
                self.update_bounding_rect()
        else:
            self.image = None
            if self.image_object:
                print('removing old image object 2')
                self.image_object.hide()
                self.image_object.setParentItem(None)
            self.image_object = None

    def set_user_size(self, width, height):
        if width < 1 or height < 1:
            return
        self.user_size = (width, height)
        if self.image_object:
            scaled = self.image.scaled(width, height,
                                       QtCore.Qt.KeepAspectRatio,
                                       QtCore.Qt.SmoothTransformation)
            self.image_object.prepareGeometryChange()
            self.image_object.setPixmap(scaled)
            self.image_object.setPos(-scaled.width() / 2, -scaled.height() / 2)
            # Update ui items around the label (or node hosting the label)
            ctrl.ui.update_position_for(self)

        elif self.label_object:
            self.label_object.resize_label()

    def dragging_my_arrow(self):
        return True

    def __str__(self):
        return 'comment: %s' % self.text

    def update_bounding_rect(self):
        """


        :return:
        """
        if self.image_object:
            my_class = self.__class__
            if self.user_size is None:
                user_width, user_height = 0, 0
            else:
                user_width, user_height = self.user_size

            lbr = self.image_object.boundingRect()
            lbw = lbr.width()
            lbh = lbr.height()
            lbx = self.image_object.x()
            lby = self.image_object.y()
            self.label_rect = QtCore.QRectF(lbx, lby, lbw, lbh)
            self.width = max((lbw, my_class.width, user_width))
            self.height = max((lbh, my_class.height, user_height))
            y = self.height / -2
            x = self.width / -2
            self.inner_rect = QtCore.QRectF(x, y, self.width, self.height)
            w4 = (self.width - 2) / 4.0
            w2 = (self.width - 2) / 2.0
            h2 = (self.height - 2) / 2.0

            self._magnets = [(-w2, -h2), (-w4, -h2), (0, -h2), (w4, -h2),
                             (w2, -h2), (-w2, 0), (w2, 0), (-w2, h2),
                             (-w4, h2), (0, h2), (w4, h2), (w2, h2)]
            if ctrl.ui.selection_group and self in ctrl.ui.selection_group.selection:
                ctrl.ui.selection_group.update_shape()

            return self.inner_rect

        else:
            return super().update_bounding_rect()

    def paint(self, painter, option, widget=None):
        """ Painting is sensitive to mouse/selection issues, but usually with
        :param painter:
        :param option:
        :param widget:
        nodes it is the label of the node that needs complex painting """
        if self.drag_data:
            p = QtGui.QPen(self.contextual_color)
            #b = QtGui.QBrush(ctrl.cm.paper())
            #p.setColor(ctrl.cm.hover())
            p.setWidth(1)
            painter.setPen(p)
            #painter.setBrush(self.drag_data.background)
            painter.drawRect(self.inner_rect)
            painter.setBrush(QtCore.Qt.NoBrush)

        elif self._hovering:
            p = QtGui.QPen(self.contextual_color)
            #p.setColor(ctrl.cm.hover())
            p.setWidth(1)
            painter.setPen(p)
            painter.drawRect(self.inner_rect)
        elif ctrl.pressed is self or ctrl.is_selected(self):
            p = QtGui.QPen(self.contextual_color)
            p.setWidth(1)
            painter.setPen(p)
            painter.drawRect(self.inner_rect)
        elif self.has_empty_label() and self.node_alone():
            p = QtGui.QPen(self.contextual_color)
            p.setStyle(QtCore.Qt.DotLine)
            p.setWidth(1)
            painter.setPen(p)
            painter.drawRect(self.inner_rect)

    def move(self, md):
        """ Override usual movement if comment is connected to some node. If so, try to keep the
        given position relative to that node.
        :return:
        """
        if self.preferred_host:
            x, y = self.preferred_host.current_scene_position
            dx, dy = self.pos_relative_to_host
            self.current_position = self.scene_position_to_tree_position(
                (x + dx, y + dy))
            return False, False
        else:
            return super().move(md)

    def drop_to(self, x, y, recipient=None):
        """

        :param recipient:
        :param x:
        :param y:
        :return: action finished -message (str)
        """
        message = super().drop_to(x, y, recipient=recipient)
        if self.preferred_host:
            x, y = self.preferred_host.current_scene_position
            mx, my = self.current_scene_position
            self.pos_relative_to_host = mx - x, my - y
            message = "Adjusted comment to be at %s, %s relative to '%s'" % (
                mx - x, my - y, self.preferred_host)
        return message

    def on_connect(self, other):
        print('on_connect called, hosts:', self.hosts)
        if other in self.hosts:
            self.preferred_host = other
            x, y = other.current_scene_position
            mx, my = self.current_scene_position
            self.pos_relative_to_host = mx - x, my - y

    def on_disconnect(self, other):
        print('on_disconnect called')
        if other is self.preferred_host:
            for item in self.hosts:
                if item != other:
                    self.preferred_host = item
                    x, y = item.current_scene_position
                    mx, my = self.current_scene_position
                    self.pos_relative_to_host = mx - x, my - y
                    return
            self.preferred_host = None

    def can_connect_with(self, other):
        return other not in self.hosts

    def dragging_comment(self):
        """ Check if the currently dragged item is comment and can connect with me
        :return:
        """
        return self.is_dragging_this_type(g.COMMENT_NODE)

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    image_path = SavedField("image_path", if_changed=set_image_path)
Esempio n. 20
0
class AttributeNode(Node):
    """ AttributeNodes can be used for diagnostics, to e.g. show properties of nodes that are not
    features and not participating in feature computation, but are otherwise interesting,
    e.g. weights.
    """
    width = 20
    height = 20
    node_type = ATTRIBUTE_NODE
    display_name = ('Attribute', 'Attributes')
    display = False
    __qt_type_id__ = next_available_type_id()

    default_style = {
        'fancy': {
            'color_id': 'accent4',
            'font_id': g.SMALL_CAPS,
            'font-size': 10
        },
        'plain': {
            'color_id': 'accent4',
            'font_id': g.SMALL_CAPS,
            'font-size': 10
        }
    }

    default_edge = g.ATTRIBUTE_EDGE

    def __init__(self,
                 forest=None,
                 host=None,
                 attribute_id=None,
                 label='',
                 show_label=False,
                 restoring=False):
        """

        :param host: 
        :param attribute_id: 
        :param attribute_label: 
        :param show_label: 
        :param forest: 
        :param restoring: 
        :raise: 
        """
        self.help_text = ""
        Node.__init__(self)
        self.host = host
        self.attribute_label = label or attribute_id
        self.attribute_id = attribute_id
        self._show_label = show_label
        # if self.attribute_label in color_map:
        # self.color = colors.feature_palette[color_map[self.attribute_label]]
        # else:
        # self.color = colors.feature
        if not restoring:
            # compute start position -- similar to FeatureNode, but happens on init
            # because host is given
            x, y = self.host.current_position
            k = self.attribute_label
            if k in color_map:
                x += color_map[k]
                y += color_map[k]
            else:
                x += random.uniform(-4, 4)
                y += random.uniform(-4, 4)
            self.set_original_position((x, y))
            self.update_help_text()
            self.update_label()
            self.update_bounding_rect()
            self.update_visibility()

    def update_help_text(self):
        """


        """
        if self.attribute_id == 'select_order':
            self.help_text = "'{host}' was Selected {value_ordinal} when constructing the trees."
        elif self.attribute_id == 'merge_order':
            self.help_text = "'{host}' was Merged {value_ordinal} when constructing the trees."

    def set_help_text(self, text):
        """

        :param text:
        """
        self.help_text = text
        self.update_status_tip()

    def update_status_tip(self):
        """


        """
        if self.help_text:
            self.status_tip = self.help_text.format(host=self.host,
                                                    value=self.value,
                                                    value_ordinal=ordinal(
                                                        self.value),
                                                    label=self.attribute_label)
        else:
            self.status_tip = "Attribute %s for %s" % (
                self.get_html_for_label(), self.host)

    def get_html_for_label(self):
        """ This should be overridden if there are alternative displays for label 
        :returns : str 
        """

        if self._show_label:
            return '%s:%s' % (self.attribute_label, self.value)
        else:
            return self.value
            # u'%s:%s' % (self.syntactic_object.key, self.syntactic_object.get_value_string())

    @property
    def value(self):
        """


        :return:
        """
        val = getattr(self.host, self.attribute_id, '')
        if isinstance(val, collections.Callable):
            return val()
        else:
            return val

    def __str__(self):
        """
        :returns : str
        """
        return 'AttributeNode %s' % self.attribute_label

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    host = SavedField("host")
    attribute_label = SavedField("attribute_label")
    attribute_id = SavedField("attribute_id")
Esempio n. 21
0
class Tree(Movable):
    """ Container for nodes that form a single trees. It allows operations that affect
    all nodes in one trees, e.g. translation of position.
    :param top:
    """
    __qt_type_id__ = next_available_type_id()
    display_name = ('Tree', 'Trees')

    def __init__(self, top=None, numeration=False):
        Movable.__init__(self)
        self.top = top
        if is_constituent(top):
            self.sorted_constituents = [top]
        else:
            self.sorted_constituents = []
        if top:
            self.sorted_nodes = [top]
        else:
            self.sorted_nodes = []
        self.numeration = numeration
        self.current_position = 100, 100
        self.drag_data = None
        self.tree_changed = True
        self._cached_bounding_rect = None
        self.setZValue(100)

    def __repr__(self):
        if self.numeration:
            suffix = " (numeration)"
        else:
            suffix = ''
        return "Tree '%s' and %s nodes.%s" % (self.top, len(
            self.sorted_nodes), suffix)

    def __contains__(self, item):
        return item in self.sorted_nodes

    def after_init(self):
        self.recalculate_top()
        self.update_items()
        self.announce_creation()

    def after_model_update(self, updated_fields, transition_type):
        """ Compute derived effects of updated values in sensible order.
        :param updated_fields: field keys of updates
        :param transition_type: 0:edit, 1:CREATED, -1:DELETED
        :return: None
        """
        super().after_model_update(updated_fields, transition_type)
        if transition_type != g.DELETED:
            self.update_items()

    def rebuild(self):
        self.recalculate_top()
        self.update_items()

    def recalculate_top(self):
        """ Verify that self.top is the topmost element of the trees. Doesn't handle consequences,
        e.g. it may now be that there are two identical trees at the top and doesn't update the
        constituent and node lists.
        :return: new top
        """
        passed = set()

        def walk_to_top(node):
            """ Recursive walk upwards
            :param node:
            :return:
            """
            passed.add(node)
            for parent in node.get_parents(similar=False, visible=False):
                if parent not in passed:
                    return walk_to_top(parent)
            return node

        if self.top:  # hopefully it is a short walk
            self.top = walk_to_top(self.top)
        elif self.sorted_nodes:  # take the long way if something strange has happened to top
            self.top = walk_to_top(self.sorted_nodes[-1])
        else:
            self.top = None  # hopefully this trees gets deleted.
        return self.top

    def is_empty(self):
        """ Empty trees should be deleted when found
        :return:
        """
        return bool(self.top)

    def is_valid(self):
        """ If trees top has parents, the trees needs to be recalculated or it is otherwise unusable
          before fixed.
        :return:
        """
        return not self.top.get_parents(similar=False, visible=False)

    def add_node(self, node):
        """ Add this node to given trees and possibly set it as parent for this graphicsitem.
        :param node: Node
        :return:
        """
        node.poke('trees')
        node.trees.add(self)
        node.update_graphics_parent()

    def remove_node(self, node, recursive_down=False):
        """ Remove node from trees and remove the (graphicsitem) parenthood-relation.
        :param node: Node
        :param recursive_down: bool -- do recursively remove child nodes from tree too
        :return:
        """
        if self in node.trees:
            node.poke('trees')
            node.trees.remove(self)
            node.update_graphics_parent()
        if recursive_down:
            for child in node.get_children(similar=False, visible=False):
                legit = False
                for parent in child.get_parents(similar=False, visible=False):
                    if self in parent.trees:
                        legit = True
                if not legit:
                    self.remove_node(child, recursive_down=True)

    def add_to_numeration(self, node):
        def add_children(node):
            if node not in self.sorted_nodes:
                self.sorted_nodes.append(node)
                if self not in node.trees:
                    self.add_node(node)
                for child in node.get_children(similar=False, visible=False):
                    if child:  # undoing object creation may cause missing edge ends
                        add_children(child)

        add_children(node)

    @time_me
    def update_items(self):
        """ Check that all children of top item are included in this trees and create the sorted
        lists of items. Make sure there is a top item before calling this!
        :return:
        """
        if self.numeration:
            to_be_removed = set()
            for item in self.sorted_nodes:
                for tree in item.trees:
                    if tree is not self:
                        to_be_removed.add(item)
                        break
            for item in to_be_removed:
                self.remove_node(item)
            return
        sorted_constituents = []
        sorted_nodes = []
        used = set()

        def add_children(node):
            """ Add node to this trees.
            :param node:
            :return:
            """
            if node not in used:
                used.add(node)
                if is_constituent(node):
                    sorted_constituents.append(node)
                sorted_nodes.append(node)
                if self not in node.trees:
                    self.add_node(node)
                for child in node.get_children(similar=False, visible=False):
                    if child:  # undoing object creation may cause missing edge ends
                        add_children(child)

        old_nodes = set(self.sorted_nodes)

        if is_constituent(self.top):
            add_children(self.top)

        self.sorted_constituents = sorted_constituents
        for i, item in enumerate(self.sorted_constituents):
            item.z_value = 10 + i
            item.setZValue(item.z_value)
        self.sorted_nodes = sorted_nodes

        to_be_removed = old_nodes - set(sorted_nodes)
        for item in to_be_removed:
            self.remove_node(item)

    def is_higher_in_tree(self, node_a, node_b):
        """ Compare two nodes, if node_a is higher, return True. Return False
        if not.
            Return None if nodes are not in the same trees -- cannot compare.
            (Be careful with the result,
            handle None and False differently.)
        :param node_a:
        :param node_b:
        :return:
        """
        if node_a in self and node_b in self:
            return self.sorted_nodes.index(node_a) < self.sorted_nodes.index(
                node_b)
        else:
            return None

    def start_dragging_tracking(self, host=False, scene_pos=None):
        """ Add this *Tree* to entourage of dragged nodes. These nodes will
        maintain their relative position to drag pointer while dragging.
        :return: None
        """
        self.drag_data = TreeDragData(self,
                                      is_host=host,
                                      mousedown_scene_pos=scene_pos)

    def boundingRect(self):
        if self.tree_changed or not self._cached_bounding_rect:
            if not self.sorted_nodes:
                return QtCore.QRectF()
            min_x, min_y = 10000, 10000
            max_x, max_y = -10000, -10000
            for node in self.sorted_nodes:
                if node.is_visible() and not node.locked_to_node:
                    nbr = node.future_children_bounding_rect()
                    if node.physics_x or node.physics_y:
                        x, y = node.x(), node.y()
                    else:
                        x, y = node.target_position
                    x1, y1, x2, y2 = nbr.getCoords()
                    x1 += x
                    y1 += y
                    x2 += x
                    y2 += y
                    if x1 < min_x:
                        min_x = x1
                    if x2 > max_x:
                        max_x = x2
                    if y1 < min_y:
                        min_y = y1
                    if y2 > max_y:
                        max_y = y2
            self._cached_bounding_rect = QtCore.QRectF(min_x, min_y,
                                                       max_x - min_x,
                                                       max_y - min_y)
            self.tree_changed = False
            return self._cached_bounding_rect
        else:
            return self._cached_bounding_rect

    def current_scene_bounding_rect(self):
        if not self.sorted_nodes:
            return QtCore.QRectF()
        min_x, min_y = 10000, 10000
        max_x, max_y = -10000, -10000
        for node in self.sorted_nodes:
            if node.is_visible():
                nbr = node.sceneBoundingRect()
                x1, y1, x2, y2 = nbr.getCoords()
                if x1 < min_x:
                    min_x = x1
                if x2 > max_x:
                    max_x = x2
                if y1 < min_y:
                    min_y = y1
                if y2 > max_y:
                    max_y = y2
        return QtCore.QRectF(min_x, min_y, max_x - min_x, max_y - min_y)

    # def normalize_positions(self):
    #     print('tree normalising positions')
    #     tx, ty = self.top.target_position
    #     for node in self.sorted_constituents:
    #         nx, ny = node.target_position
    #         node.move_to(nx - tx, ny - ty)

    def paint(self, painter, QStyleOptionGraphicsItem, QWidget_widget=None):
        if self.numeration:  # or True:
            br = self.boundingRect()
            painter.drawRect(br)
            #painter.drawText(br.topLeft() + QtCore.QPointF(2, 10), str(self))

    top = SavedField("top")
Esempio n. 22
0
class BaseConstituent(SavedObject, IConstituent):
    """ BaseConstituent is a default constituent used in syntax.
    IConstituent inherited here gives the abstract blueprint of what methods Constituents should
    implement -- it is an optional aid to validate that your Constituent is about ok.
    Constituents need to inherit SavedObject and use SavedFields for saving permanent data,
    otherwise structures won't get saved properly, undo and snapshots won't work.

    Object inheritance is not used to recognise what are constituent implementations and what
    are not, instead classes have attribute 'role' that tells what part they play in e.g. plugin.
    So your implementation of Constituent needs at least to have role = "Constituent", but not
    necessarily inherit BaseConstituent or IConstituent.

    However, often it is easisest just to inherit BaseConstituent and make minor modifications
    to it.
    """

    # info for kataja engine
    syntactic_object = True
    role = "Constituent"

    editable = {}
    addable = {
        'features': {
            'condition': 'can_add_feature',
            'add': 'add_feature',
            'order': 20
        }
    }

    # 'parts': {'check_before': 'can_add_part', 'add': 'add_part', 'order': 10},

    def __init__(self, label='', parts=None, uid='', features=None, **kw):
        """ BaseConstituent is a default constituent used in syntax.
        It is Savable, which means that the actual values are stored in separate object that is
        easily dumped to file. Extending this needs to take account if new elements should also
        be treated as savable, e.g. put them into. and make necessary property and setter.
         """
        SavedObject.__init__(self, **kw)
        self.label = label
        self.features = features or []
        self.parts = parts or []

    def __str__(self):
        return str(self.label)

    def __repr__(self):
        if self.is_leaf():
            return 'Constituent(id=%s)' % self.label
        else:
            return "[ %s ]" % (' '.join((x.__repr__() for x in self.parts)))

    def __contains__(self, c):
        if self == c:
            return True
        for part in self.parts:
            if c in part:
                return True
        else:
            return False

    def get_features(self):
        """ Getter for features, redundant for BaseConstituent (you could use c.features ) but it
        is better to use this consistently for compatibility with other implementations for
        constituent.
        :return:
        """
        return self.features

    def get_parts(self):
        """ Getter for parts, redundant for BaseConstituent (you could use c.parts ) but it
        is better to use this consistently for compatibility with other implementations for
        constituent.
        :return:
        """
        return self.parts

    def print_tree(self):
        """ Bracket trees representation of the constituent structure. Now it is same as str(self).
        :return: str
        """
        return self.__repr__()

    def can_add_part(self, **kw):
        """
        :param kw:
        :return:
        """
        return True

    def add_part(self, new_part):
        """ Add constitutive part to this constituent (append to parts)
        :param new_part:
        """
        self.poke('parts')
        self.parts.append(new_part)

    def insert_part(self, new_part, index=0):
        """ Insert constitutive part to front of the parts list. Usefulness
        depends on the linearization method.
        :param new_part:
        :param index:
        :return:
        """
        self.poke('parts')
        self.parts.insert(index, new_part)

    def remove_part(self, part):
        """ Remove constitutive part
        :param part:
        """
        self.poke('parts')
        self.parts.remove(part)

    def replace_part(self, old_part, new_part):
        """
        :param old_part:
        :param new_part:
        :return:
        """
        if old_part in self.parts:
            i = self.parts.index(old_part)
            self.poke('parts')
            self.parts[i] = new_part
        else:
            raise IndexError

    @property
    def is_ordered(self):
        return False

    def get_feature(self, key):
        """ Gets the first local feature (within this constituent, not of its children) with key
        'key'
        :param key: string for identifying feature type
        :return: feature object
        """
        for f in self.features:
            if f.name == key:
                return f

    def get_secondary_label(self):
        """ Visualisation can switch between showing labels and some other information in label
        space. If you want to support this, have "support_secondary_labels = True"
        in SyntaxConnection and provide something from this getter.
        :return:
        """
        raise NotImplementedError

    def has_feature(self, key):
        """ Check the existence of feature within this constituent
        :param key: string for identifying feature type or Feature instance
        :return: bool
        """
        if isinstance(key, BaseFeature):
            return key in self.features
        else:
            return bool(self.get_feature(key))

    def add_feature(self, feature):
        """ Add an existing Feature object to this constituent.
        :param feature:
        :return:
        """
        if isinstance(feature, BaseFeature):
            self.poke('features')
            self.features.append(feature)
        else:
            raise TypeError

    def remove_feature(self, name):
        """ Remove feature from a constituent. It's not satisfied, it is just gone.
        :param fname: str, the name for finding the feature or for convenience, a feature
        instance to be removed
        """
        if isinstance(name, BaseFeature):
            if name in self.features:
                self.poke('features')
                self.features.remove(name)
        else:
            for f in list(self.features):
                if f.name == name:
                    self.poke('features')
                    self.features.remove(f)

    def is_leaf(self):
        """ Check if the constituent is leaf constituent (no children) or inside a trees (has children).
        :return: bool
        """
        return not self.parts

    def ordered_parts(self):
        """ Tries to do linearization between two elements according to theory being used.
        Easiest, default case is to just store the parts as a list and return the list in its original order.
        This is difficult to justify theoretically, though.
        :return: len 2 list of ordered nodes, or empty list if cannot be ordered.
        """
        ordering_method = 1
        if ordering_method == 1:
            return list(self.parts)

    def copy(self):
        """ Make a deep copy of constituent. Useful for picking constituents from Lexicon.
        :return: BaseConstituent
        """
        new_parts = []
        for part in self.parts:
            new = part.copy()
            new_parts.append(new)
        new_features = self.features.copy()
        nc = self.__class__(label=self.label,
                            parts=new_parts,
                            features=new_features)
        return nc

    # ############## #
    #                #
    #  Save support  #
    #                #
    # ############## #

    features = SavedField("features")
    sourcestring = SavedField("sourcestring")
    label = SavedField("label")
    parts = SavedField("parts")