1
2from __future__ import print_function
3
4import itertools, os
5from glob import glob
6
7import pymol
8import pymol._gui
9
10from .pymol_gl_widget import PyMOLGLWidget
11from pymol.Qt import QtGui, QtCore
12from pymol.Qt import QtWidgets
13from pymol.Qt.utils import PopupOnException
14
15Qt = QtCore.Qt
16
17from pymol.wizard import Wizard
18from pymol.parsing import QuietException
19
20from pymol import editor
21from pymol import computing
22
23from chempy import cpv
24
25active_sele = "_builder_active" # object we're working on...
26newest_sele = "_builder_added" # last atom added?
27display_sele = "_build_display"
28
29undocontext = editor.undocontext
30
31def undoablemethod(sele):
32    def decorator(func):
33        def wrapper(self, *args, **kwargs):
34            with undocontext(self.cmd, sele):
35                return func(self, *args, **kwargs)
36        return wrapper
37    return decorator
38
39#############################################################
40# Action-directing Wizards
41
42class ActionWizard(Wizard):
43
44    def __init__(self, _self=pymol.cmd):
45        Wizard.__init__(self,_self)
46        self.actionHash = str(self.__class__)
47
48    def setActionHash(self, action_hash):
49        self.actionHash = action_hash
50
51    def activateOrDismiss(self):
52        activate_flag = 1
53        cur_wiz = self.cmd.get_wizard()
54        if cur_wiz is not None:
55            if cur_wiz.__class__ == self.__class__:
56                if cur_wiz.actionHash == self.actionHash:
57                    activate_flag = 0
58        if activate_flag:
59            self.cmd.set_wizard(self,replace=1)
60            self.cmd.refresh_wizard()
61        else:
62            self.actionWizardDone()
63        return activate_flag
64
65    def actionWizardDone(self):
66        self.cmd.delete(active_sele)
67        self.cmd.unpick()
68        self.cmd.set_wizard()
69        self.cmd.refresh_wizard()
70
71    def activeSeleValid(self):
72        if active_sele in self.cmd.get_names("selections"):
73            if self.cmd.select(active_sele, "byobj "+active_sele)<1:
74                self.cmd.delete(active_sele)
75            else:
76                enabled_list = self.cmd.get_names("objects",enabled_only=1)
77                active_obj_list = self.cmd.get_object_list(active_sele)
78                if len(active_obj_list) != 1:
79                    self.cmd.delete(active_sele)
80                elif active_obj_list[0] not in enabled_list:
81                    self.cmd.delete(active_sele)
82        if "pk1" in self.cmd.get_names("selections"):
83            self.cmd.select(active_sele,"byobj pk1")
84        else:
85            enabled_list = self.cmd.get_names("objects",enabled_only=1)
86            if len(enabled_list)==1:
87                if self.cmd.select(active_sele, enabled_list[0])<1:
88                    self.cmd.delete(active_sele)
89        return active_sele in self.cmd.get_names("selections")
90
91
92class CleanWizard(ActionWizard):
93
94    def __init__(self, _self=pymol.cmd):
95        self.clean_obj = None
96        ActionWizard.__init__(self,_self)
97
98    def run_job(self):
99        if active_sele in self.cmd.get_names("selections"):
100            obj_list = self.cmd.get_object_list(active_sele)
101            if len(obj_list)==1:
102                self.cmd.unpick()
103                self.cmd.set_wizard()
104                self.cmd.refresh_wizard()
105                self.cmd.do("_ cmd.clean('%s',message='''Cleaning %s...''',async_=1)"%(active_sele,obj_list[0]))
106
107    def do_pick(self, bondFlag):
108        if active_sele in self.cmd.get_names("selections"):
109            obj_list = self.cmd.get_object_list(active_sele)
110            if len(obj_list)!=1:
111                self.cmd.delete(active_sele)
112        else:
113            self.cmd.select(active_sele, "byobj pk1")
114        self.cmd.unpick()
115        self.cmd.deselect()
116        obj_list = self.cmd.get_object_list(active_sele)
117        if isinstance(obj_list,list) and (len(obj_list)==1):
118            self.run_job()
119        else:
120            print("Error: can only clean one object at a time")
121
122    def toggle(self):
123        if self.activateOrDismiss():
124            if self.activeSeleValid():
125                self.run_job()
126
127    def get_prompt(self):
128        return ["Pick object to clean..."]
129
130    def get_panel(self):
131        return [
132            [ 1, 'Clean', ''],
133            [ 2, 'Done','cmd.set_wizard()'],
134            ]
135
136
137class SculptWizard(ActionWizard):
138
139    def __init__(self, _self=pymol.cmd):
140        ActionWizard.__init__(self,_self)
141        self.sculpt_object = None
142
143    def sculpt_activate(self):
144        if active_sele in self.cmd.get_names("selections"):
145            obj_list = self.cmd.get_object_list(active_sele)
146            if len(obj_list)==1:
147                obj_name = obj_list[0]
148                self.cmd.push_undo(obj_name)
149                self.cmd.sculpt_activate(obj_name)
150                self.cmd.set("sculpting",1)
151                self.sculpt_object = obj_name
152                self.cmd.sculpt_activate(obj_name)
153                if int(self.cmd.get("sculpt_vdw_vis_mode")):
154                    self.cmd.show("cgo",obj_name)
155                self.cmd.unpick()
156                self.cmd.refresh_wizard()
157            else:
158                print("Error: cannot sculpt more than one object at a time")
159
160    def sculpt_deactivate(self):
161        if ((self.sculpt_object is not None) and
162            self.sculpt_object in self.cmd.get_names()):
163            self.cmd.set("sculpt_vdw_vis_mode","0",self.sculpt_object)
164            self.cmd.sculpt_iterate(self.sculpt_object,self.cmd.get_state(),0)
165            self.cmd.unset("sculpt_vdw_vis_mode",self.sculpt_object)
166            self.cmd.sculpt_deactivate(self.sculpt_object)
167            self.sculpt_object = None
168            self.cmd.refresh_wizard()
169
170    def do_pick(self, bondFlag):
171        if self.sculpt_object is None:
172            self.cmd.select(active_sele, "byobj pk1")
173            self.sculpt_activate()
174        else:
175            return 0 # already sculpting, so handle like a normal edit
176
177    def toggle(self):
178        if self.activateOrDismiss():
179            if self.activeSeleValid():
180                self.sculpt_activate()
181
182    def get_prompt(self):
183        if self.sculpt_object is None:
184            return ["Pick object to sculpt..."]
185        else:
186            return ["Sculpting %s..."%self.sculpt_object]
187
188    def finish_sculpting(self):
189        if self.sculpt_object:
190            self.sculpt_deactivate()
191        self.cmd.set("sculpting",0)
192        self.cmd.delete(active_sele)
193        self.cmd.set_wizard()
194        self.cmd.refresh_wizard()
195
196    def scramble(self,mode):
197        if self.cmd.count_atoms(self.sculpt_object):
198            sc_tmp = "_scramble_tmp"
199            if mode == 0:
200                self.cmd.select(sc_tmp,self.sculpt_object+
201                            " and not (fixed or restrained)")
202            if mode == 1:
203                self.cmd.select(sc_tmp,self.sculpt_object+
204                            " and not (fixed)")
205            extent = self.cmd.get_extent(sc_tmp)
206            center = self.cmd.get_position(sc_tmp)
207            radius = 1.25*cpv.length(cpv.sub(extent[0],extent[1]))
208            self.cmd.alter_state(self.cmd.get_state(), sc_tmp,
209                                 "(x,y,z)=rsp(pos,rds)",
210                space= { 'rsp' :  cpv.random_displacement,
211                         'pos' : center,
212                         'rds' : radius })
213            self.cmd.delete(sc_tmp)
214
215    def get_panel(self):
216        return [
217            [ 1, 'Sculpt', ''],
218            [ 2, 'Undo', 'cmd.undo()'],
219
220            [ 2, 'Switch Object', 'cmd.get_wizard().sculpt_deactivate()'],
221            [ 2, 'Scramble Unrestrained Coords.', 'cmd.get_wizard().scramble(0)'],
222            [ 2, 'Scramble Unfixed Coords.', 'cmd.get_wizard().scramble(1)'],
223            [ 2, 'Done','cmd.get_wizard().finish_sculpting()'],
224            ]
225
226    def cleanup(self):
227        self.sculpt_deactivate()
228        ActionWizard.cleanup(self)
229
230
231class RepeatableActionWizard(ActionWizard):
232
233    def __init__(self, _self=pymol.cmd):
234        ActionWizard.__init__(self,_self)
235        self.repeating = 0
236
237    def repeat(self):
238        self.repeating = 1
239        self.cmd.refresh_wizard()
240
241    def getRepeating(self):
242        return self.repeating
243
244    def activateRepeatOrDismiss(self):
245        activate_flag = 1
246        cur_wiz = self.cmd.get_wizard()
247        if cur_wiz is not None:
248            if cur_wiz.__class__ == self.__class__:
249                if cur_wiz.actionHash == self.actionHash:
250                    if cur_wiz.getRepeating():
251                        activate_flag = 0
252                    else:
253                        self.repeat()
254                elif cur_wiz.getRepeating():
255                    self.repeat()
256        if activate_flag:
257            self.cmd.set_wizard(self,replace=1)
258            self.repeat() # always repeating for now...
259            self.cmd.refresh_wizard()
260        else:
261            self.actionWizardDone()
262        return activate_flag
263
264    def cleanup(self):
265        self.cmd.unpick()
266        ActionWizard.cleanup(self)
267
268
269class ReplaceWizard(RepeatableActionWizard):
270
271    def do_pick(self, bondFlag):
272        self.cmd.select(active_sele, "bymol pk1")
273        self.cmd.replace(self.symbol, self.geometry, self.valence)
274        if not self.getRepeating():
275            self.actionWizardDone()
276
277    def toggle(self, symbol, geometry, valence, text):
278        self.symbol = symbol
279        self.geometry = geometry
280        self.valence = valence
281        self.text = text
282        self.setActionHash( (symbol,geometry,valence,text) )
283        self.activateRepeatOrDismiss()
284
285    def get_prompt(self):
286        if self.getRepeating():
287            return ["Pick atoms to replace with %s..."%self.text]
288        else:
289            return ["Pick atom to replace with %s..."%self.text]
290
291    def get_panel(self):
292        if self.getRepeating():
293            return [
294                [ 1, 'Replacing Multiple Atoms',''],
295                [ 2, 'Done','cmd.set_wizard()'],
296                ]
297        else:
298            return [
299                [ 1, 'Replacing an Atom',''],
300                [ 2, 'Replace Multiple Atoms','cmd.get_wizard().repeat()'],
301                [ 2, 'Done','cmd.set_wizard()'],
302                ]
303
304
305class AttachWizard(RepeatableActionWizard):
306
307    def __init__(self, _self=pymol.cmd):
308        RepeatableActionWizard.__init__(self,_self)
309        self.mode = 0
310
311    def do_pick(self, bondFlag):
312        if self.mode == 0:
313            self.cmd.select(active_sele, "bymol pk1")
314            editor.attach_fragment("pk1", self.fragment, self.position, self.geometry, _self=self.cmd)
315        elif self.mode == 1:
316            self.cmd.select(active_sele, "bymol pk1")
317            editor.combine_fragment("pk1", self.fragment, self.position,
318                                    self.geometry, _self=self.cmd)
319            self.mode = 0
320            self.cmd.refresh_wizard()
321        self.cmd.unpick()
322        if not self.getRepeating():
323            self.actionWizardDone()
324
325    def toggle(self, fragment, position, geometry, text):
326        self.fragment = fragment
327        self.position = position
328        self.geometry = geometry
329        self.text = text
330        self.setActionHash( (fragment, position, geometry, text) )
331        self.activateRepeatOrDismiss()
332
333    def create_new(self):
334        names = self.cmd.get_names("objects")
335        num = 1
336        while 1:
337            name = "obj%02d"%num
338            if name not in names:
339                break
340            num = num + 1
341        self.cmd.fragment(self.fragment, name)
342        if not self.getRepeating():
343            self.actionWizardDone()
344
345    def get_prompt(self):
346        if self.mode == 0:
347            if self.getRepeating():
348                return ["Pick locations to attach %s..."%self.text]
349            else:
350                return ["Pick location to attach %s..."%self.text]
351        else:
352            return ["Pick object to combine %s into..."%self.text]
353
354    def combine(self):
355        self.mode = 1
356        self.cmd.refresh_wizard()
357
358    def get_panel(self):
359        if self.getRepeating():
360            return [
361                [ 1, 'Attaching Multiple Fragmnts',''],
362                [ 2, 'Create As New Object','cmd.get_wizard().create_new()'],
363                [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'],
364                [ 2, 'Done','cmd.set_wizard()'],
365                ]
366        else:
367            return [
368                [ 1, 'Attaching One Fragment',''],
369                [ 2, 'Create As New Object','cmd.get_wizard().create_new()'],
370                [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'],
371                [ 2, 'Attach Multiple Fragments','cmd.get_wizard().repeat()'],
372                [ 2, 'Done','cmd.set_wizard()'],
373                ]
374
375
376class AminoAcidWizard(RepeatableActionWizard):
377
378    def __init__(self, _self=pymol.cmd, ss=-1):
379        RepeatableActionWizard.__init__(self,_self)
380        self.mode = 0
381        self.setSecondaryStructure(ss)
382
383    def setSecondaryStructure(self, ss):
384        self._secondary_structure = ss
385
386    def attach_monomer(self, objectname=""):
387         editor.attach_amino_acid("?pk1", self.aminoAcid, object=objectname,
388                ss=self._secondary_structure,
389                 _self=self.cmd)
390
391    def do_pick(self, bondFlag):
392        # since this function can change any position of atoms in a related
393        # molecule, bymol is used
394        if self.mode == 0:
395            self.cmd.select(active_sele, "bymol pk1")
396            try:
397                with undocontext(self.cmd, "bymol ?pk1"):
398                    self.attach_monomer(self.aminoAcid)
399            except QuietException:
400                fin = -1
401        elif self.mode == 1:
402            self.cmd.select(active_sele, "bymol pk1")
403            editor.combine_fragment("pk1", self.aminoAcid, 0, 1, _self=self.cmd)
404            self.mode = 0
405            self.cmd.refresh_wizard()
406
407        self.cmd.unpick()
408        if not self.getRepeating():
409            self.actionWizardDone()
410
411    def toggle(self, amino_acid):
412        self.aminoAcid = amino_acid
413        self.setActionHash( (amino_acid,) )
414        self.activateRepeatOrDismiss()
415
416    def create_new(self):
417        names = self.cmd.get_names("objects")
418        num = 1
419        while 1:
420            name = "obj%02d"%num
421            if name not in names:
422                break
423            num = num + 1
424        self.attach_monomer(self.aminoAcid)
425
426        if not self.getRepeating():
427            self.actionWizardDone()
428
429    def combine(self):
430        self.mode = 1
431        self.cmd.refresh_wizard()
432
433    def get_prompt(self):
434        if self.mode == 0:
435            if self.getRepeating():
436                return ["Pick locations to attach %s..."%self.aminoAcid]
437            else:
438                return ["Pick location to attach %s..."%self.aminoAcid]
439        else:
440            return ["Pick object to combine %s into..."%self.aminoAcid]
441
442    def get_panel(self):
443        if self.getRepeating():
444            return [
445                [ 1, 'Attaching Multiple Residues',''],
446                [ 2, 'Create As New Object','cmd.get_wizard().create_new()'],
447                [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'],
448                [ 2, 'Done','cmd.set_wizard()'],
449                ]
450        else:
451            return [
452                [ 1, 'Attaching Amino Acid',''],
453                [ 2, 'Create As New Object','cmd.get_wizard().create_new()'],
454                [ 2, 'Combine w/ Existing Object','cmd.get_wizard().combine()'],
455                [ 2, 'Attach Multiple...','cmd.get_wizard().repeat()'],
456                [ 2, 'Done','cmd.set_wizard()'],
457                ]
458
459
460class ValenceWizard(RepeatableActionWizard):
461
462    def cleanup(self):
463        self.cmd.button('single_left','none','PkAt')
464        self.cmd.button('double_left','none','MovA')
465
466    @undoablemethod("(?pk1 ?pk2) extend 1")
467    def do_pick(self, bondFlag):
468        self.cmd.select(active_sele, "bymol pk1")
469        if bondFlag:
470            if int(self.order)>=0:
471                self.cmd.valence(self.order, "pk1", "pk2")
472                self.cmd.h_fill()
473            else:
474                self.cmd.cycle_valence()
475        else:
476            self.cmd.button('double_left','none','PkBd')
477            self.cmd.button('single_left','none','PkBd')
478        self.cmd.unpick()
479        if not self.getRepeating():
480            self.actionWizardDone()
481
482    def toggle(self, order, text):
483        self.order = order
484        self.text = text
485        self.setActionHash( (order,text) )
486        self.activateRepeatOrDismiss()
487        if self.cmd.get_wizard() == self:
488            # get us into bond picking mode...
489            self.cmd.button('double_left','none','PkBd')
490            self.cmd.button('single_left','none','PkBd')
491
492    def get_prompt(self):
493        if self.getRepeating():
494            return ["Pick bonds to set as %s..."%self.text]
495        else:
496            return ["Pick bond to set as %s..."%self.text]
497
498    def get_panel(self):
499        if self.getRepeating():
500            return [
501                [ 1, 'Setting Multiple Valences',''],
502                [ 2, 'Done','cmd.set_wizard()'],
503                ]
504        else:
505            return [
506                [ 1, 'Set a Bond Valence',''],
507                [ 2, 'Set Multiple Valences','cmd.get_wizard().repeat()'],
508                [ 2, 'Done','cmd.set_wizard()'],
509                ]
510
511
512class ChargeWizard(RepeatableActionWizard):
513
514    @undoablemethod("bymol ?pk1")
515    def do_pick(self, bondFlag):
516        self.cmd.select(active_sele, "bymol pk1")
517        self.cmd.alter("pk1","formal_charge=%s" % self.charge)
518        self.cmd.h_fill()
519        if abs(float(self.charge))>0.0001:
520            self.cmd.label("pk1","'''"+self.text+"'''")
521        else:
522            self.cmd.label("pk1")
523        self.cmd.unpick()
524        if not self.getRepeating():
525            self.actionWizardDone()
526
527    def toggle(self, charge, text):
528        self.charge = charge
529        self.text = text
530        self.setActionHash( (charge,text) )
531        self.activateRepeatOrDismiss()
532
533    def get_prompt(self):
534        if self.getRepeating():
535            return ["Pick atoms to set charge = %s..."%self.text]
536        else:
537            return ["Pick atom to set charge = %s..."%self.text]
538
539    def get_panel(self):
540        if self.getRepeating():
541            return [
542                [ 1, 'Setting Multiple Charges',''],
543                [ 2, 'Done','cmd.set_wizard()'],
544                ]
545        else:
546            return [
547                [ 1, 'Setting Atom Charge',''],
548                [ 2, 'Modify Multiple Atoms','cmd.get_wizard().repeat()'],
549                [ 2, 'Done','cmd.set_wizard()'],
550                ]
551
552
553class InvertWizard(RepeatableActionWizard):
554
555    @PopupOnException.decorator
556    def do_pick(self, bondFlag):
557        self.cmd.select(active_sele, "bymol pk1")
558        picked = collectPicked(self.cmd)
559        if picked == ["pk1","pk2","pk3"]:
560            self.cmd.invert()
561            self.cmd.unpick()
562            if not self.getRepeating():
563                self.actionWizardDone()
564        self.cmd.refresh_wizard()
565
566    def toggle(self):
567        self.activateRepeatOrDismiss()
568
569    def get_prompt(self):
570        if "pk1" in self.cmd.get_names("selections"):
571            if "pk2" in self.cmd.get_names("selections"):
572                return ["Pick the second stationary atom..."]
573            else:
574                return ["Pick the first stationary atom..."]
575        else:
576            return ["Pick origin atom for inversion..."]
577
578    def get_panel(self):
579        if self.getRepeating():
580            return [
581                [ 1, 'Inverting Multiple',''],
582                [ 2, 'Done','cmd.set_wizard()'],
583                ]
584        else:
585            return [
586                [ 1, 'Inverting Stereocenter',''],
587                [ 2, 'Invert Multiple','cmd.get_wizard().repeat()'],
588                [ 2, 'Done','cmd.set_wizard()'],
589                ]
590
591
592class BondWizard(RepeatableActionWizard):
593
594    @staticmethod
595    def staticaction(cmd):
596        picked = collectPicked(cmd)
597        if picked != ["pk1","pk2"]:
598            return False
599
600        cmd.select(active_sele, "bymol ?pk1")
601
602        if (    cmd.count_atoms("?pk1&hydro") and
603                cmd.count_atoms("?pk2&hydro") and
604                cmd.count_atoms("(?pk1 extend 1)&!hydro") and
605                cmd.count_atoms("(?pk2 extend 1)&!hydro")):
606            # two hydrogens picked -> pick their heavy neighbors instead
607            cmd.select("pk1","(pk1 extend 1) and not hydro")
608            cmd.select("pk2","(pk2 extend 1) and not hydro")
609
610        with undocontext(cmd, "(?pk1 ?pk2) extend 1"):
611            cmd.bond("pk1", "pk2")
612            cmd.h_fill()
613
614        cmd.unpick()
615        return True
616
617    def do_pick(self, bondFlag):
618        if self.staticaction(self.cmd):
619            if not self.getRepeating():
620                self.actionWizardDone()
621        self.cmd.refresh_wizard()
622
623    def toggle(self):
624        self.activateRepeatOrDismiss()
625
626    def get_prompt(self):
627        if "pk1" in self.cmd.get_names("selections"):
628            return ["Pick second atom for bond..."]
629        else:
630            return ["Pick first atom for bond..."]
631
632    def get_panel(self):
633        if self.getRepeating():
634            return [
635                [ 1, 'Creating Multiple Bonds',''],
636                [ 2, 'Done','cmd.set_wizard()'],
637                ]
638        else:
639            return [
640                [ 1, 'Creating Bond',''],
641                [ 2, 'Create Multiple Bonds','cmd.get_wizard().repeat()'],
642                [ 2, 'Done','cmd.set_wizard()'],
643                ]
644
645
646class UnbondWizard(RepeatableActionWizard):
647
648    def cleanup(self):
649        self.cmd.button('single_left','none','PkAt')
650
651    @undoablemethod("(?pk1 ?pk2) extend 1")
652    def do_pick(self, bondFlag):
653        self.cmd.select(active_sele, "bymol pk1")
654        if bondFlag:
655            self.cmd.unbond("pk1", "pk2")
656            self.cmd.h_fill()
657            self.cmd.unpick()
658        else:
659            self.cmd.button('single_left','none','PkBd')
660            self.cmd.unpick()
661        if not self.getRepeating():
662            self.actionWizardDone()
663
664    def toggle(self):
665        self.activateRepeatOrDismiss()
666        if self.cmd.get_wizard() == self:
667            self.cmd.button('single_left','none','PkBd') # get us into bond picking mode...
668
669    def get_prompt(self):
670        if self.getRepeating():
671            return ["Pick bonds to delete..."]
672        else:
673            return ["Pick bond to delete..."]
674
675    def get_panel(self):
676        if self.getRepeating():
677            return [
678                [ 1, 'Deleting Multiple Bonds',''],
679                [ 2, 'Done','cmd.set_wizard()'],
680                ]
681        else:
682            return [
683                [ 1, 'Deleting a Bond',''],
684                [ 2, 'Delete Multiple Bonds','cmd.get_wizard().repeat()'],
685                [ 2, 'Done','cmd.set_wizard()'],
686                ]
687
688
689class HydrogenWizard(RepeatableActionWizard):
690
691    def run_add(self):
692        if self.mode == 'add':
693            self.cmd.h_add(active_sele)
694            self.cmd.delete(active_sele)
695
696    def do_pick(self, bondFlag):
697        self.cmd.select(active_sele, "bymol pk1")
698        if self.mode == 'fix':
699            self.cmd.h_fill()
700            self.cmd.unpick()
701        elif self.mode == 'add':
702            self.cmd.unpick()
703            self.run_add()
704        if not self.getRepeating():
705            self.actionWizardDone()
706
707    def toggle(self,mode):
708        self.mode = mode
709        self.setActionHash( (mode,) )
710
711        if self.mode == 'add':
712            if self.activateOrDismiss():
713                if self.activeSeleValid():
714                    self.run_add()
715        else:
716            self.activateRepeatOrDismiss()
717
718    def get_prompt(self):
719        if self.mode == 'fix':
720            if self.getRepeating():
721                return ["Pick atom upon which to fix hydrogens..."]
722            else:
723                return ["Pick atoms upon which to fix hydrogens..."]
724        else:
725            return ["Pick molecule upon which to add hydrogens..."]
726
727    def get_panel(self):
728        if self.getRepeating():
729            if self.mode == 'fix':
730                return [
731                    [ 1, 'Fixing Hydrogens',''],
732                    [ 2, 'Done','cmd.set_wizard()'],
733                    ]
734            else:
735                return [
736                    [ 1, 'Adding Hydrogens',''],
737                    [ 2, 'Done','cmd.set_wizard()'],
738                    ]
739        else:
740            if self.mode == 'fix':
741                return [
742                    [ 1, 'Fixing Hydrogens',''],
743                    [ 2, 'Fix Multiple Atoms','cmd.get_wizard().repeat()'],
744                    [ 2, 'Done','cmd.set_wizard()'],
745                    ]
746            else:
747                return [
748                    [ 1, 'Adding Hydrogens',''],
749                    [ 2, 'Add To Multiple...','cmd.get_wizard().repeat()'],
750                    [ 2, 'Done','cmd.set_wizard()'],
751                    ]
752
753
754class RemoveWizard(RepeatableActionWizard):
755
756    @undoablemethod("?pk1 extend 1")
757    def do_pick(self, bondFlag):
758        cnt = self.cmd.select(active_sele,
759                              "((pk1 and not hydro) extend 1) and not hydro")
760        self.cmd.remove_picked()
761        if cnt:
762            self.cmd.fix_chemistry(active_sele)
763            self.cmd.h_add(active_sele)
764        self.cmd.delete(active_sele)
765        if not self.getRepeating():
766            self.actionWizardDone()
767
768    def toggle(self):
769        self.activateRepeatOrDismiss()
770
771    def get_prompt(self):
772        if self.getRepeating():
773            return ["Pick atoms to delete..."]
774        else:
775            return ["Pick atom to delete..."]
776
777    def get_panel(self):
778        if self.getRepeating():
779            return [
780                [ 1, 'Deleting Atoms',''],
781                [ 2, 'Done','cmd.set_wizard()'],
782                ]
783        else:
784            return [
785                [ 1, 'Deleting an Atom',''],
786                [ 2, 'Delete Multiple Atoms','cmd.get_wizard().repeat()'],
787                [ 2, 'Done','cmd.set_wizard()'],
788                ]
789
790class AtomFlagWizard(ActionWizard):
791
792    def update_display(self):
793        if active_sele in self.cmd.get_names("selections"):
794            self.cmd.select(display_sele,active_sele+
795                            " and flag %d"%self.flag)
796            self.cmd.enable(display_sele)
797        else:
798            self.cmd.delete(display_sele)
799        self.cmd.refresh_wizard()
800
801    def do_pick(self, bondFlag):
802        if not(active_sele not in self.cmd.get_names("selections")):
803            if self.cmd.count_atoms("pk1 and flag %d"%self.flag):
804                self.cmd.flag(self.flag,"pk1","clear")
805            else:
806                self.cmd.flag(self.flag,"pk1","set")
807        self.cmd.select(active_sele, "byobj pk1")
808        self.cmd.unpick()
809        self.update_display()
810
811    def do_select(self,selection):
812        if selection == display_sele:
813            self.cmd.flag(self.flag,active_sele+" and "+display_sele,"set")
814            self.cmd.flag(self.flag,active_sele+" and not "+display_sele,"clear")
815        self.cmd.refresh_wizard()
816        self.update_display()
817
818    def get_prompt(self):
819        if active_sele not in self.cmd.get_names("selections"):
820            return ["Pick object to operate on..."]
821        else:
822            self.cmd.reference("validate",active_sele) # overbroad
823            if self.flag == 2:
824                return ["Toggle restrained atoms..."]
825            elif self.flag ==3:
826                return ["Toggle fixed atoms..."]
827            else:
828                return ["Toggle unknown atom flag..."]
829
830    def toggle(self,flag=0):
831        self.flag = flag
832        if self.activateOrDismiss():
833            if self.activeSeleValid():
834                self.update_display()
835            else:
836                self.cmd.deselect()
837            self.cmd.unpick()
838
839    def do_all(self):
840        if active_sele in self.cmd.get_names("selections"):
841            self.cmd.flag(self.flag,active_sele,"set")
842            self.update_display()
843
844    def do_less(self,mode):
845        if active_sele in self.cmd.get_names("selections"):
846            if mode == 0:
847                self.cmd.flag(self.flag,"(( byobj " + active_sele +
848                              " ) and not flag %d) extend 1"%self.flag,"clear")
849            elif mode == 1:
850                self.cmd.flag(self.flag,"byres ((( byobj " + active_sele +
851                              " ) and not flag %d) extend 1)"%self.flag,"clear")
852            self.update_display()
853
854    def do_cas(self,mode):
855        if active_sele in self.cmd.get_names("selections"):
856            if mode == 1:
857                self.cmd.flag(self.flag,active_sele,"clear")
858                self.cmd.flag(self.flag,active_sele+" and polymer and name ca","set")
859            elif mode == 0:
860                self.cmd.flag(self.flag,active_sele+
861                              " and flag %d and polymer and name ca"%self.flag,"set")
862                self.cmd.flag(self.flag,active_sele+
863                              " and not (polymer and name ca)","clear")
864            self.update_display()
865
866    def do_more(self,mode):
867        if active_sele in self.cmd.get_names("selections"):
868            if mode == 0:
869                self.cmd.flag(self.flag,active_sele +
870                              " and (flag %d extend 1)"%self.flag,"set")
871            elif mode == 1:
872                self.cmd.flag(self.flag,"byres ("+ active_sele +
873                              " and (byres flag %d) extend 1)"%self.flag,"set")
874            elif mode == 2:
875                self.cmd.flag(self.flag,"byres ("+ active_sele +
876                              " and flag %d )"%self.flag,"set")
877            self.update_display()
878
879    def do_none(self):
880        if active_sele in self.cmd.get_names("selections"):
881            self.cmd.flag(self.flag,active_sele,"clear")
882            self.update_display()
883
884    def do_store(self):
885        if active_sele in self.cmd.get_names("selections"):
886            self.cmd.reference("store",active_sele)
887
888    def do_recall(self):
889        if active_sele in self.cmd.get_names("selections"):
890            self.cmd.reference("recall",active_sele)
891
892    def do_swap(self):
893        if active_sele in self.cmd.get_names("selections"):
894            self.cmd.reference("swap",active_sele)
895
896    def get_panel(self):
897        title = {2:"Restrained Atoms",
898                 3:"Fixed Atoms"}.get(self.flag)
899        verb = {2:"Restrain", 3:"Fix"}.get(self.flag)
900
901        result = [
902            [ 1, title, ''],
903            [ 2, "All",'cmd.get_wizard().do_all()'],
904            [ 2, "All C-alphas",'cmd.get_wizard().do_cas(1)'],
905            [ 2, "More (byres)",'cmd.get_wizard().do_more(1)'],
906            [ 2, "More",'cmd.get_wizard().do_more(0)'],
907            [ 2, "Byresidue",'cmd.get_wizard().do_more(2)'],
908            [ 2, "Less", 'cmd.get_wizard().do_less(0)'],
909            [ 2, "Less (by residue)", 'cmd.get_wizard().do_less(1)'],
910            [ 2, "Only C-alphas",'cmd.get_wizard().do_cas(0)'],
911            [ 2, "None", 'cmd.get_wizard().do_none()'],
912            [ 2, 'Done','cmd.set_wizard()'],
913            ]
914
915        if self.flag == 2:
916            result[-1:-1] = [
917            [ 2, "Store Reference Coords.", 'cmd.get_wizard().do_store()'],
918            [ 2, "Recall Reference Coords.", 'cmd.get_wizard().do_recall()'],
919            [ 2, "Swap Reference Coords.", 'cmd.get_wizard().do_swap()']]
920
921        return result
922
923    def cleanup(self):
924        self.cmd.delete(display_sele)
925        Wizard.cleanup(self)
926
927
928class FixAtomWizard(AtomFlagWizard):
929    pass
930
931
932class RestAtomWizard(AtomFlagWizard):
933    pass
934
935
936#############################################################
937### Helper functions
938
939def getSeleDict(self_cmd):
940    result = {}
941    for sele in self_cmd.get_names("selections"):
942        result[sele] = 1
943    return result
944
945
946def collectPicked(self_cmd):
947    result = []
948    sele_dict = getSeleDict(self_cmd)
949    for sele in ["pk1","pk2","pk3","pk4"]:
950        if sele in sele_dict:
951            result.append(sele)
952    return result
953
954
955#############################################################
956### Actual GUI
957
958
959def makeFragmentButton():
960    btn = QtWidgets.QPushButton()
961    btn.setAttribute(Qt.WA_LayoutUsesWidgetRect) # OS X workaround
962    btn.setSizePolicy(
963            QtWidgets.QSizePolicy.Minimum,
964            QtWidgets.QSizePolicy.MinimumExpanding)
965    btn.setAutoDefault(False)
966    return btn
967
968
969class _BuilderPanel(QtWidgets.QWidget):
970
971    def __init__(self, parent=None, app=None):
972        super(_BuilderPanel, self).__init__(parent)
973
974        self.setWindowTitle("Builder")
975        self.setObjectName("builder")
976        self.cmd = app.pymol.cmd
977
978        self.layout = QtWidgets.QVBoxLayout()
979        self.setLayout(self.layout)
980        self.buttons_layout = QtWidgets.QVBoxLayout()
981
982        self.tabs = QtWidgets.QTabWidget(self)
983        self.layout.setContentsMargins(5, 5, 5, 5);
984        self.layout.setSpacing(5);
985        self.layout.addWidget(self.tabs)
986        self.layout.addLayout(self.buttons_layout)
987        self.layout.addStretch()
988
989        self.fragments_layout = QtWidgets.QGridLayout()
990        self.fragments_layout.setContentsMargins(5, 5, 5, 5);
991        self.fragments_layout.setSpacing(5);
992        self.fragments_tab = QtWidgets.QWidget()
993        self.fragments_tab.setLayout(self.fragments_layout)
994        self.protein_layout = QtWidgets.QGridLayout()
995        self.protein_layout.setContentsMargins(5, 5, 5, 5);
996        self.protein_layout.setSpacing(5);
997        self.protein_tab = QtWidgets.QWidget()
998        self.protein_tab.setLayout(self.protein_layout)
999
1000        self.tabs.addTab(self.fragments_tab, "Chemical")
1001        self.tabs.addTab(self.protein_tab, "Protein")
1002
1003        self.getIcons()
1004
1005        buttons = [
1006            [ ("H", "Hydrogen", lambda: self.replace("H", 1, 1, "Hydrogen")),
1007              ("C", "Carbon", lambda: self.replace("C", 4, 4, "Carbon")),
1008              ("N", "Nitrogen", lambda: self.replace("N", 4, 3, "Nitrogen")),
1009              ("O", "Oxygen", lambda: self.replace("O", 4, 2, "Oxygen")),
1010              ("P", "Phosphorus", lambda: self.replace("P",4,3, "Phosphorous")),
1011              ("S", "Sulfur", lambda: self.replace("S",2,2, "Sulfur")),
1012              ("F", "Fluorine", lambda: self.replace("F",1,1, "Fluorine")),
1013              ("Cl", "Chlorrine", lambda: self.replace("Cl",1,1, "Chlorine")),
1014              ("Br", "Bromine", lambda: self.replace("Br",1,1, "Bromine")),
1015              ("I", "Iodine", lambda: self.replace("I",1,1, "Iodine")),
1016              ("-CF3", "Trifluoromethane", lambda: self.grow("trifluoromethane",4,0, "trifluoro")),
1017              ("-OMe", "Methanol", lambda: self.grow("methanol",5,0, "methoxy")),
1018            ],
1019            [ ("CH4", "Methyl", lambda: self.grow("methane",1,0,"methyl")),
1020              ("C=C", "Ethylene", lambda: self.grow("ethylene",4,0,"vinyl")),
1021              ("C#C", "Acetylene", lambda: self.grow("acetylene",2,0,"alkynl")),
1022              ("C#N", "Cyanide", lambda: self.grow("cyanide",2,0,"cyano")),
1023              ("C=O", "Aldehyde", lambda: self.grow("formaldehyde",2,0,"carbonyl",)),
1024              ("C=OO", "Formic Acid", lambda: self.grow("formic",4,0,"carboxyl")),
1025              ("C=ON", "C->N amide", lambda: self.grow("formamide",5,0,"C->N amide")),
1026              ("NC=O", "N->C amide", lambda: self.grow("formamide",3,1,"N->C amide")),
1027              ("S=O2", "Sulfone", lambda: self.grow("sulfone",3,1,"sulfonyl")),
1028              ("P=O3", "Phosphite", lambda: self.grow("phosphite",4,0,"phosphoryl")),
1029              ("N=O2", "Nitro", lambda: self.grow("nitro",3,0,"nitro")),
1030            ],
1031            [
1032              ("#cyc3", "Cyclopropane", lambda: self.grow("cyclopropane",4,0,"cyclopropyl")),
1033              ("#cyc4", "Cyclobutane", lambda: self.grow("cyclobutane",4,0,"cyclobutyl")),
1034              ("#cyc5", "Cyclopentane", lambda: self.grow("cyclopentane",5,0,"cyclopentyl")),
1035              ("#cyc6", "Cyclohexane", lambda: self.grow("cyclohexane",7,0,"cyclohexyl")),
1036              ("#cyc7", "Cycloheptane", lambda: self.grow("cycloheptane",8,0,"cycloheptyl")),
1037              ("#aro5", "Cyclopentadiene", lambda: self.grow("cyclopentadiene",5,0,"cyclopentadienyl")),
1038              ("#aro6", "Benzene", lambda: self.grow("benzene",6,0,"phenyl")),
1039              ("#aro65", "Indane", lambda: self.grow("indane",12,0,"indanyl")),
1040              ("#aro66", "Napthylene", lambda: self.grow("napthylene",13,0,"napthyl")),
1041              ("#aro67", "Benzocycloheptane", lambda: self.grow("benzocycloheptane",13,0, "benzocycloheptyl")),
1042            ]
1043        ]
1044
1045        self.btn_icons = {}
1046
1047        requestsize = QtCore.QSize(48, 48)
1048        for row, btn_row in enumerate(buttons):
1049            for col, bb in enumerate(btn_row):
1050                btn_label, btn_tooltip, btn_command = bb
1051                btn = makeFragmentButton()
1052                if btn_label.startswith('#'):
1053                    icons = self.icons[btn_label[1:]]
1054                    btn.setIcon(icons[0])
1055                    btn.setIconSize(icons[1].actualSize(requestsize))
1056                    self.btn_icons[btn] = icons
1057                else:
1058                    btn.setText(btn_label)
1059                btn.setToolTip(btn_tooltip)
1060                btn.clicked.connect(btn_command)
1061                self.fragments_layout.addWidget(btn, row, col)
1062
1063        buttons = [
1064            [ 'Ace', 'Ala', 'Arg', 'Asn', 'Asp', 'Cys', 'Gln', 'Glu', 'Gly', 'His', 'Ile', 'Leu' ],
1065            [ 'Lys', 'Met', 'Phe', 'Pro', 'Ser', 'Thr', 'Trp', 'Tyr', 'Val', 'NMe', 'NHH' ]
1066        ]
1067        for row, btn_row in enumerate(buttons):
1068            for col, btn_label in enumerate(btn_row):
1069                btn = makeFragmentButton()
1070                btn.setText(btn_label)
1071                btn.setToolTip("Build %s residue" % btn_label)
1072                res = btn_label.lower()
1073                slot = lambda val=None, s=self,r=res: s.attach(r)
1074                btn.clicked.connect(slot)
1075                self.protein_layout.addWidget(btn, row, col)
1076
1077        lab = QtWidgets.QLabel('Secondary Structure:')
1078        lab_cols = 3
1079        self.ss_cbox = QtWidgets.QComboBox()
1080        self.ss_cbox.addItem("Alpha Helix")
1081        self.ss_cbox.addItem("Beta Sheet (Anti-Parallel)")
1082        self.ss_cbox.addItem("Beta Sheet (Parallel)")
1083        self.protein_layout.addWidget(lab, 2, 0, 1, lab_cols)
1084        self.protein_layout.addWidget(self.ss_cbox, 2, lab_cols, 1, 4)
1085        self.ss_cbox.currentIndexChanged[int].connect(self.ssIndexChanged)
1086
1087        buttons = [
1088            [
1089              ( "@Atoms:", None, None),
1090              ( "Fix H", "Fix hydrogens on picked atoms", self.fixH),
1091              ( "Add H", "Add hydrogens to entire molecule", self.addH),
1092              ( "Invert", "Invert stereochemistry around pk1 (pk2 and pk3 will remain fixed)", self.invert),
1093              ( "Delete", "Remove atoms", self.removeAtom),
1094              ( "Clear", "Delete everything", self.clear),
1095              ( "@   Charge:", None, None),
1096              ( " +1 ", "Positive Charge", lambda: self.setCharge(1,"+1")),
1097              ( "  0 ", "Neutral Charge", lambda: self.setCharge(0,"neutral")),
1098              ( " -1 ", "Negative Charge", lambda: self.setCharge(-1,"-1")),
1099            ],
1100            [
1101              ( "@Bonds:", None, None),
1102              ( "Create", "Create bond between pk1 and pk2", self.createBond),
1103              ( "Delete", "Delete bond between pk1 and pk2", self.deleteBond),
1104              ( "Cycle", "Cycle bond valence", self.cycleBond),
1105              ( "  |  ", "Create single bond", lambda: self.setOrder("1", "single")),
1106              ( " || ", "Create double bond", lambda: self.setOrder("2", "double")),
1107              ( " ||| ", "Create triple bond", lambda: self.setOrder("3", "triple")),
1108              ( "Arom", "Create aromatic bond", lambda: self.setOrder("4", "aromatic")),
1109              ( "@   Model:", None, None),
1110              ( "Clean", "Cleanup structure", self.clean),
1111              ( "Sculpt", "Molecular sculpting", self.sculpt),
1112              ( "Fix", "Fix atom positions", self.fix),
1113              ( "Rest", "Restrain atom positions", self.rest),
1114            ],
1115            [
1116              ( "$El-stat", "Electrostatics term for 'Clean' action", "clean_electro_mode"),
1117              ( "@   ", None, None),
1118              ( "$Bumps", "Show VDW contacts during sculpting", "sculpt_vdw_vis_mode"),
1119              ( "@   ", None, None),
1120              ( "#Undo Enabled", "", "suspend_undo"),
1121              ( "Undo", "Undo last change", self.undo),
1122              ( "Redo", "Redo last change", self.redo),
1123            ]
1124        ]
1125
1126        for row, btn_row in enumerate(buttons):
1127            btn_row_layout = QtWidgets.QHBoxLayout()
1128            self.buttons_layout.addLayout(btn_row_layout)
1129            for col, bb in enumerate(btn_row):
1130                btn_label, btn_tooltip, btn_command = bb
1131                if btn_label[0] == '@':
1132                    btn = QtWidgets.QLabel(btn_label[1:])
1133                elif btn_label[0] in ('#', '$'):
1134                    btn = QtWidgets.QCheckBox(btn_label[1:])
1135                    setting = btn_command
1136                    value = self.cmd.get_setting_int(setting)
1137                    if btn_label[0] == '$':
1138                        btn.setChecked(bool(value))
1139                        @btn.toggled.connect
1140                        def _(checked, n=setting):
1141                            self.cmd.set(n, checked, quiet=0)
1142                    else:
1143                        btn.setChecked(not value)
1144                        @btn.toggled.connect
1145                        def _(checked, n=setting):
1146                            self.cmd.set(n, not checked, quiet=0)
1147                else:
1148                    btn = makeFragmentButton()
1149                    btn.setText(btn_label)
1150                    btn.clicked.connect(btn_command)
1151                if btn_tooltip:
1152                    btn.setToolTip(btn_tooltip)
1153                btn_row_layout.addWidget(btn)
1154            btn_row_layout.addStretch()
1155
1156    def getIcons(self):
1157        self.icons = {}
1158        # use old Tk icons
1159        imgDir = os.path.join(os.environ['PYMOL_DATA'], "pmg_tk/bitmaps/builder")
1160        imgList = glob("%s/aro*.gif" % imgDir) + glob("%s/cyc*.gif" % imgDir)
1161        for imgFile in imgList:
1162            imgName = os.path.splitext(os.path.split(imgFile)[1])[0]
1163            if imgName not in list(self.icons.keys()):
1164                image = QtGui.QImage(imgFile)
1165                pixmap = QtGui.QPixmap.fromImage(image)
1166                image.invertPixels()
1167                inv_pixmap = QtGui.QPixmap.fromImage(image)
1168                self.icons[imgName] = (QtGui.QIcon(pixmap), QtGui.QIcon(inv_pixmap))
1169
1170    def showEvent(self, event):
1171        self.cmd.set("editor_auto_measure", 0)
1172        self.cmd.set("auto_overlay")
1173        self.cmd.set("valence")
1174        self.cmd.edit_mode(1)
1175
1176    def grow(self, name, pos, geom, text):
1177        if "pk1" in self.cmd.get_names("selections"):
1178            self.cmd.select(active_sele,"byobj pk1")
1179            editor.attach_fragment("pk1", name, pos, geom, _self=self.cmd)
1180            self.doAutoPick()
1181        else:
1182            self.cmd.unpick()
1183            AttachWizard(self.cmd).toggle(name, pos, geom, text)
1184
1185    def replace(self, atom, geometry, valence, text):
1186        picked = collectPicked(self.cmd)
1187        if len(picked):
1188            self.cmd.select(active_sele,"byobj "+picked[0])
1189            self.cmd.replace(atom, geometry, valence)
1190            self.doAutoPick()
1191        else:
1192            ReplaceWizard(_self=self.cmd).toggle(atom,geometry,valence,text)
1193
1194    def attach(self, aa):
1195        ss = self.ss_cbox.currentIndex() + 1
1196        picked = collectPicked(self.cmd)
1197        if len(picked)==1:
1198            try:
1199                with undocontext(self.cmd, "bymol %s" % picked[0]):
1200                    editor.attach_amino_acid(picked[0], aa,
1201                            ss=ss, _self=self.cmd)
1202            except:
1203                fin = -1
1204            self.doZoom()
1205        else:
1206            self.cmd.unpick()
1207            AminoAcidWizard(_self=self.cmd, ss=ss).toggle(aa)
1208
1209    def ssIndexChanged(self, index):
1210        w = self.cmd.get_wizard()
1211        if isinstance(w, AminoAcidWizard):
1212            w.setSecondaryStructure(index + 1)
1213
1214    def doAutoPick(self, old_atoms=None):
1215        self.cmd.unpick()
1216        if self.cmd.select(newest_sele,"(byobj "+active_sele+") and not "+active_sele)==0:
1217            self.cmd.select(newest_sele, active_sele)
1218        new_list = self.cmd.index(newest_sele+" and hydro")
1219        if len(new_list)==0:
1220            new_list = self.cmd.index(newest_sele)
1221        if new_list:
1222            index = new_list.pop()
1223            try:
1224                self.cmd.edit("%s`%d" % index)
1225                if self.cmd.get_wizard() is not None:
1226                    self.cmd.do("_ cmd.get_wizard().do_pick(0)")
1227            except pymol.CmdException:
1228                print(" doAutoPick-Error: exception")
1229        self.doZoom()
1230
1231    def doZoom(self, *ignore):
1232        if "pk1" in self.cmd.get_names("selections"):
1233            self.cmd.zoom("((neighbor pk1) extend 4)", 4.0, animate=-1)
1234
1235    def setCharge(self, charge, text):
1236        picked = collectPicked(self.cmd)
1237        if len(picked)>0:
1238            sele = "?pk1 ?pk2 ?pk3 ?pk4"
1239            with undocontext(self.cmd, sele):
1240                self.cmd.alter(sele,"formal_charge=%s" % charge)
1241                self.cmd.h_fill()
1242                self.cmd.label(sele,'"%+d" % formal_charge if formal_charge else ""')
1243            self.cmd.unpick()
1244        else:
1245            ChargeWizard(self.cmd).toggle(charge, text)
1246
1247    def createBond(self):
1248        if not BondWizard.staticaction(self.cmd):
1249            BondWizard(self.cmd).toggle()
1250
1251    def deleteBond(self):
1252        picked = collectPicked(self.cmd)
1253        if picked == ["pk1","pk2"]:
1254            with undocontext(self.cmd, "(?pk1 ?pk2) extend 1"):
1255                self.cmd.unbond("pk1", "pk2")
1256                self.cmd.h_fill()
1257            self.cmd.unpick()
1258        else:
1259            self.cmd.unpick()
1260            UnbondWizard(self.cmd).toggle()
1261
1262    def cycleBond(self):
1263        picked = collectPicked(self.cmd)
1264        if picked == ["pk1","pk2"]:
1265            with undocontext(self.cmd, "(?pk1 ?pk2) extend 1"):
1266                self.cmd.cycle_valence()
1267                self.cmd.unpick()
1268        else:
1269            ValenceWizard(_self=self.cmd).toggle(-1,"Cycle bond")
1270
1271    @undoablemethod("(?pk1 ?pk2) extend 1")
1272    def setOrder(self, order, text):
1273        picked = collectPicked(self.cmd)
1274        if picked == ["pk1","pk2"]:
1275            self.cmd.unbond("pk1", "pk2")
1276            self.cmd.bond("pk1", "pk2", order)
1277            self.cmd.h_fill()
1278            self.cmd.unpick()
1279        else:
1280            self.cmd.unpick()
1281            ValenceWizard(_self=self.cmd).toggle(order,text)
1282
1283    def fixH(self):
1284        picked = collectPicked(self.cmd)
1285        if len(picked):
1286            self.cmd.h_fill()
1287            self.cmd.unpick()
1288        else:
1289            HydrogenWizard(_self=self.cmd).toggle('fix')
1290
1291    def addH(self):
1292        picked = collectPicked(self.cmd)
1293        if len(picked):
1294            self.cmd.h_add("pkmol")
1295            self.cmd.unpick()
1296        else:
1297            HydrogenWizard(_self=self.cmd).toggle('add')
1298
1299    @PopupOnException.decorator
1300    def invert(self, _=None):
1301        picked = collectPicked(self.cmd)
1302        if picked == ["pk1","pk2","pk3"]:
1303            self.cmd.invert()
1304            self.cmd.unpick()
1305        else:
1306            self.cmd.unpick()
1307            InvertWizard(self.cmd).toggle()
1308
1309    def center(self):
1310        if "pk1" in self.cmd.get_names("selections"):
1311            self.cmd.zoom("pk1", 5.0, animate=-1)
1312        else:
1313            self.cmd.zoom("all", 3.0, animate=-1)
1314
1315    def removeAtom(self):
1316        picked = collectPicked(self.cmd)
1317        if len(picked):
1318            if self.cmd.count_atoms("?pkbond"):
1319                self.cmd.edit("(pk1)","(pk2)",pkbond=0)
1320            cnt = self.cmd.select(active_sele,
1321                   "(((?pkset or ?pk1) and not hydro) extend 1) and not hydro")
1322            with undocontext(self.cmd, "(?pkset ?pk1) extend 1"):
1323                self.cmd.remove_picked()
1324                if cnt:
1325                    self.cmd.fix_chemistry(active_sele)
1326                    self.cmd.h_add(active_sele)
1327            self.cmd.delete(active_sele)
1328            self.cmd.unpick()
1329        else:
1330            RemoveWizard(self.cmd).toggle()
1331
1332    def reset(self):
1333        self.cmd.unpick()
1334
1335    def clear(self):
1336        QMB = QtWidgets.QMessageBox
1337        check = QMB.question(None, "Confirm",
1338            "Really delete everything?", QMB.Yes | QMB.No)
1339        if check == QMB.Yes:
1340            self.cmd.delete("all")
1341            self.cmd.refresh_wizard()
1342
1343    def sculpt(self):
1344        picked = collectPicked(self.cmd)
1345        if len(picked):
1346            self.cmd.select(active_sele, ' or '.join(picked))
1347        SculptWizard(_self=self.cmd).toggle()
1348
1349    def clean(self):
1350        picked = collectPicked(self.cmd)
1351        if len(picked):
1352            self.cmd.select(active_sele, "pkmol")
1353            self.cmd.unpick()
1354        CleanWizard(_self=self.cmd).toggle()
1355
1356    def undo(self):
1357        self.cmd.undo()
1358
1359    def redo(self):
1360        self.cmd.redo()
1361
1362    def fix(self):
1363        picked = collectPicked(self.cmd)
1364        if len(picked):
1365            self.cmd.select(active_sele,"pk1")
1366            self.cmd.deselect()
1367        else:
1368            self.cmd.delete(active_sele)
1369        FixAtomWizard(_self=self.cmd).toggle(3)
1370
1371    def rest(self):
1372        picked = collectPicked(self.cmd)
1373        if len(picked):
1374            self.cmd.select(active_sele,"byobj ("+' or '.join(picked)+")")
1375            self.cmd.deselect()
1376        else:
1377            self.cmd.delete(active_sele)
1378        RestAtomWizard(_self=self.cmd).toggle(2)
1379
1380
1381def BuilderPanelDocked(parent, *args, **kwargs):
1382    widget = _BuilderPanel(parent, *args, **kwargs)
1383    window = QtWidgets.QDockWidget(parent)
1384    window.setWindowTitle("Builder")
1385    window.setWidget(widget)
1386    window.setFloating(True)
1387    return window
1388