summaryrefslogtreecommitdiff
path: root/cad/src/model/BorrowerChunk.py
blob: 4f61df76982e264eae29f5fc1e2c6907a7d726de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# Copyright 2006-2009 Nanorex, Inc.  See LICENSE file for details. 
"""
BorrowerChunk.py -- a chunk which temporarily borrows the atoms
from another one, for an experimental optimization of display lists
when moving subsets of the atoms of a chunk.

Not used by default. Deprecated, since some more principled and
more general loosening of the chunk <-> display list correspondence
would be much better.

@author: Bruce
@version: $Id$
@copyright: 2006-2009 Nanorex, Inc.  See LICENSE file for details.

History:

Bruce wrote this inside chunk.py

Bruce 080314 split this into its own file.
"""

# note: this may not have been tested since it was split out of chunk.py.

from utilities.debug import register_debug_menu_command

from model.chunk import Chunk

from model.global_model_changedicts import _changed_parent_Atoms

import foundation.env as env

from utilities.Log import orangemsg, redmsg, quote_html

from utilities.debug import safe_repr

# ==

# Some notes about BorrowerChunk [bruce 060411]
##    If an undo checkpoint occurs while the atoms are stolen,
##    it won't contain them (it will be as if they were deleted, I think).
##    This might be tolerable for A7 (if tested for safety), since cp's during drag
##    are rare -- or it might not, if problems are caused by bonds from existing to dead atoms!
##
##    When we want to fix it, here are some possible ways:
#     - make the missing-atoms scheme work, by making the bonds from existing to dead atoms also seem to be missing, somehow.
##    - let this chunk be scanned by Undo (ie make it a child of the assy, in a special place
##    so not in the MT), and let it have an _undo_update method
##    (or the like) which merges its atoms back into their homes. (It might turn out this is required
##    anyway, if having it missing during a cp causes unforseen problems.)
##    - disable cp's during drag.
##    - merge undo diffs from the drag.

# ==

class BorrowerChunk(Chunk):
    """
    A temporary Chunk (mostly transparent to users and Undo, when not added to MT) whose atoms belong in other chunks.
    Useful for optimizing redraws, since it has one perhaps-small display list, and the other chunks' display lists
    won't be modified when our atoms change (except once when we first steal their atoms, and when we add them back).

    @warning: removing atoms from, or adding atoms to, this pseudo-chunk is not supported.
    Except for debugging purposes, it should never be added to the MT, or permitted to exist when arbitrary user-ops are possible.
    Its only known safe usage pattern is to be created, used, and destroyed, during one extended operation such as a mouse-drag.
    [If more uses are thought of, these limitations could all be removed. #e]

    update 060412: trying to make it fully safe for Undo cp, and in case it's accidently left alive (in GLPane or MT).
    But not trying to make results perfectly transparent or "correct" in those cases, since we'll try to prevent them.
    E.g. for mmp save, it'll save as a normal Chunk would.
    """
    def __init__(self, assy, atomset = None, name = None): # revised 060413
        """
        #doc; for doc of atomset, see take_atomset
        """
        Chunk.__init__(self, assy, self._name_when_empty(assy))
        if atomset is not None:
            self.take_atomset(atomset, name = name)
        return

    def _name_when_empty(self, assy = None):
        if assy is None:
            assy = self.assy
        del assy # not yet used; might use id or repr someday
        return "(empty borrower id %#x)" % id(self)

    def take_atomset(self, atomset, name = None):
        """
        #doc; atomset maps atom.key -> atom for some atoms we'll temporarily own
        @warning: if all of another chunk's atoms are in atomset, creating us will kill that chunk.
        @warning: it's up to the caller to make sure all singlet neighbors of atoms in atomset
                  are also in atomset! Likely bugs if it doesn't.
        If you want to call this again on new atoms, call self.demolish first.
        """
        if not name:
            name = "(borrower of %d atoms, id %#x)" % (len(atomset), id(self)) #e __repr__ also incls this info
        self.name = name # no use for prior value of self.name #k is there a set_name we should be using??
        atoms = atomset.values() #e could optim this -- only use is to let us pick one arbitrary atom
        egatom = atoms[0]
        egmol = egatom.molecule # do this now, since we're going to change it in the loop
        del atoms, egatom
        assy = egmol.assy
        assert assy is self.assy
        # now steal the atoms, but remember their homes and don't add ourselves to assy.tree.
        # WARNING: if we steal *all* atoms from another chunk, that will cause trouble,
        # but preventing this is up to the caller! [#e put this into another method, so it can be called again later??]
        # We optimize this by lots of inlining, since we need it to be fast for lots of atoms.
        harmedmols = {} # id(mol) -> mol for all mols whose atoms we steal from
        origmols = {} # atom.key - original atom.molecule
        self.origmols = origmols
        self.harmedmols = harmedmols
        # self.atoms was initialized to {} in Chunk.__init__, or restored to that in self.demolish()
        for key, atom in atomset.iteritems():
            mol = atom.molecule
            assert mol is not self
            if isinstance(mol, self.__class__):
                print "%r: borrowing %r from another borrowerchunk %r is not supported" % (self, atom, mol)
                    # whether it might work, I have no idea
            harmedmols[id(mol)] = mol
            # inline part of mol.delatom(atom):
            #e do this later: mol.invalidate_atom_lists()
            _changed_parent_Atoms[key] = atom
            del mol.atoms[key] # callers can check for KeyError, always an error
            # don't do this (but i don't think it prevents all harm from stealing all mol's atoms):
            ## if not mol.atoms:
            ##     mol.kill()
            # inline part of self.addatom(atom):
            atom.molecule = self
            atom.index = -1 # illegal value
            self.atoms[key] = atom
            #e do this later: self.invalidate_atom_lists()
            # remember where atom came from:
            origmols[key] = mol
        # do what we saved for later in the inlined delatom and addatom calls:
        for mol in harmedmols.itervalues():
            natoms = len(mol.atoms)
            if not natoms:
                print "bug: BorrowerChunk stole all atoms from %r; potential for harm is not yet known" % mol
            mol.invalidate_atom_lists()
        self.invalidate_atom_lists()

        try:
            part = egmol.part
            part.add(self) ###e not 100% sure this is ok; need to call part.remove too (and we do)
            assert part is self.part # Part.add should do this (if it was not already done)
        except:
            print "data from following exception: egmol = %r, its part = %r, self.part = %r" % \
                  ( egmol, part, self.part )
            raise

        return # from take_atomset
    
    # instead of overriding _draw_for_main_display_list
    # (now in ChunkDrawer rather than in our superclass Chunk,
    #  so overriding it would require defining a custom _drawer_class),
    # it's enough to define _colorfunc and _dispfunc to help it:
    
    def _colorfunc(self, atm):
        """
        Define this to use atm's home mol's color instead of self.color, and also so that self._dispfunc gets called
        [overrides self._colorfunc = None; this scheme will get messed up if self ever gets copied,
         since the copy code (two methods in Chunk) will set an instance attribute pointing to the bound method of the original]
        """
        #e this has bugs if we removed atoms from self -- that's not supported (#e could override delatom to support it)
        return self.origmols[atm.key].drawing_color()

    def _dispfunc(self, atm):
        origmol = self.origmols[atm.key]
        glpane = origmol.glpane # set shortly before this call, in origmol.draw (kluge)
        disp = origmol.get_dispdef(glpane)
        return disp

    def restore_atoms_to_their_homes(self):
        """
        put your atoms back where they belong
        (calling this multiple times should be ok)
        """
        #e this has bugs if we added atoms to self -- that's not supported (#e could override addatom to support it)
        origmols = self.origmols
        for key, atom in self.atoms.iteritems():
            _changed_parent_Atoms[key] = atom
            origmol = origmols[key]
            atom.molecule = origmol
            atom.index = -1 # illegal value
            origmol.atoms[key] = atom
        for mol in self.harmedmols.itervalues():
            mol.invalidate_atom_lists()
        self.atoms = {}
        self.origmols = {}
        self.harmedmols = {}
        ## self.part = None
        self.invalidate_atom_lists() # might not matter anymore; hope it's ok when we have no atoms
        if self.part is not None:
            self.part.remove(self)
        return
    
    def demolish(self):
        """
        Restore atoms, and make self reusable
        (but up to caller to remove self from any .dad it might have)
        """
        self.restore_atoms_to_their_homes()
        self.name = self._name_when_empty()
        return
    
    def take_atoms_from_list(self, atomlist):
        """
        We must be empty (ready for reuse).
        Divide atoms in atomlist by chunk; take the atoms we can (without taking all atoms from any chunk);
        return a pair of lists (other_chunks, other_atoms), where other_chunks are chunks whose atoms were all in atomlist,
        and other_atoms is a list of atoms we did not take for some other reason
        (presently always [] since there is no other reason we can't take an atom).
        """
        # note: some recent selectMode code for setting up dragatoms is similar enough (in finding other_chunks)
        # that it might make sense to pull out a common helper routine
        other_chunks = []
        other_atoms = [] # never changed
        our_atoms = []
        chunks_and_atoms = divide_atomlist_by_chunk(atomlist) # list of pairs (chunk, atoms in it)
        for chunk, atlist in chunks_and_atoms:
            if len(chunk.atoms) == len(atlist):
                other_chunks.append(chunk)
            else:
                our_atoms.extend(atlist)
        atomset = dict([(a.key, a) for a in our_atoms])
        self.take_atomset( atomset)
        return other_chunks, other_atoms
    
    def kill(self):
        self.restore_atoms_to_their_homes() # or should we delete them instead?? (this should never matter in our planned uses)
            # this includes self.part = None
        Chunk.kill(self)
    
    def destroy(self):
        self.kill()
        self.name = "(destroyed borrowerchunk)"
    
    # for testing, we might let one of these show up in the MT, and then we need these cmenu methods for it:
    
    def __CM_Restore_Atoms_To_Their_Homes(self):
        self.restore_atoms_to_their_homes()
        assy = self.assy
        self.kill()
        assy.w.win_update() # at least mt_update is needed
    
    #e we might need destroy and/or kill methods which call restore_atoms_to_their_homes
    
    pass # end of class BorrowerChunk

# ==

def divide_atomlist_by_chunk(atomlist): # only used in this file as of 080314, but might be more generally useful
    # note: similar to some recent code for setting up dragatoms in selectMode, but not identical
    """
    Given a list of atoms, return a list of pairs
    (chunk, atoms in that chunk from that list).
    Assume no atom appears twice.
    """
    resdict = {} # id(chunk) -> list of one or more atoms from it
    for at in atomlist:
        chunk = at.molecule
        resdict.setdefault(id(chunk), []).append(at)
    return [(atlist[0].molecule, atlist) for atlist in resdict.itervalues()]

def debug_make_BorrowerChunk(target):
    """
    (for debugging only)
    """
    debug_make_BorrowerChunk_raw(True)

def debug_make_BorrowerChunk_no_addmol(target):
    """
    (for debugging only)
    """
    debug_make_BorrowerChunk_raw(False)

def debug_make_BorrowerChunk_raw(do_addmol = True):
    win = env.mainwindow()
    atomset = win.assy.selatoms
    if not atomset:
        env.history.message(redmsg("Need selected atoms to make a BorrowerChunk (for debugging only)"))
    else:
        atomset = dict(atomset) # copy it, since we shouldn't really add singlets to assy.selatoms...
        for atom in atomset.values(): # not itervalues, we're changing it in the loop!
            # BTW Python is nicer about this than I expected:
            # exceptions.RuntimeError: dictionary changed size during iteration
            for bp in atom.singNeighbors(): # likely bugs if these are not added into the set!
                atomset[bp.key] = bp
            assy = atom.molecule.assy # these are all the same, and we do this at least once
        chunk = BorrowerChunk(assy, atomset)
        if do_addmol:
            win.assy.addmol(chunk)
        import __main__
        __main__._bc = chunk
        env.history.message(orangemsg("__main__._bc = %s (for debugging only)" % quote_html(safe_repr(chunk))))
    win.win_update() #k is this done by caller?
    return

# ==

register_debug_menu_command("make BorrowerChunk", debug_make_BorrowerChunk)
register_debug_menu_command("make BorrowerChunk (no addmol)", debug_make_BorrowerChunk_no_addmol)

# end