# Copyright 2007 Nanorex, Inc. See LICENSE file for details. """ BuildAtomsPropertyManager.py The BuildAtomsPropertyManager class provides the Property Manager for the B{Build Atoms mode}. The UI is defined in L{Ui_BuildAtomsPropertyManager} @author: Bruce, Huaicai, Mark, Ninad @version: $Id$ @copyright: 2007 Nanorex, Inc. See LICENSE file for details. History: Before Alpha9, (code that used Qt3 framework) Build Atoms mode had a 'Molecular Modeling Kit' (MMKit) and a dashboard. Starting Alpha 9, this functionality was integrated into a Property Manager. Since then several changes have been made. ninad 2007-08-29: Created to use PM module classes, thus deprecating old Property Manager class MMKit. Split out old 'clipboard' functionality into new L{PasteFromClipboard_Command} """ import foundation.env as env from PyQt4.Qt import SIGNAL from commands.BuildAtoms.Ui_BuildAtomsPropertyManager import Ui_BuildAtomsPropertyManager from geometry.VQT import V from utilities.Comparison import same_vals from utilities.prefs_constants import buildModeHighlightingEnabled_prefs_key from utilities.prefs_constants import buildModeWaterEnabled_prefs_key from widgets.prefs_widgets import connect_checkbox_with_boolean_pref from utilities import debug_flags from utilities.debug import print_compact_stack NOBLEGASES = ("He", "Ne", "Ar", "Kr") PAMATOMS = ("Gv5", "Ax3") ALL_PAM_ATOMS = ("Gv5", "Ss5", "Pl5", "Ax3", "Ss3", "Ub3", "Ux3", "Uy3") _superclass = Ui_BuildAtomsPropertyManager class BuildAtomsPropertyManager(Ui_BuildAtomsPropertyManager): """ The BuildAtomsPropertyManager class provides the Property Manager for the B{Build Atoms mode}. The UI is defined in L{Ui_BuildAtomsPropertyManager} """ def __init__(self, command): """ Constructor for the B{Build Atoms} property manager. @param command: The parent mode where this Property Manager is used @type command: L{BuildAtoms_Command} """ self.previousSelectionParams = None self.isAlreadyConnected = False self.isAlreadyDisconnected = False _superclass.__init__(self, command) # It is essential to make the following flag 'True' instead of False. # Program enters self._moveSelectedAtom method first after init, and # there and this flag ensures that it returns from that method # immediately. It is not clear why self.model_changed is not called # before the it enters that method. This flag may not be needed after # implementing connectWithState. self.model_changed_from_glpane = True def show(self): _superclass.show(self) self.updateMessage() def connect_or_disconnect_signals(self, isConnect): """ Connect or disconnect widget signals sent to their slot methods. @param isConnect: If True the widget will send the signals to the slot method. @type isConnect: boolean @see: L{BuildAtoms_Command.connect_or_disconnect_signals} where this is called """ if isConnect and self.isAlreadyConnected: if debug_flags.atom_debug: print_compact_stack("warning: attempt to connect widgets"\ "in this PM that are already connected." ) return if not isConnect and self.isAlreadyDisconnected: if debug_flags.atom_debug: print_compact_stack("warning: attempt to disconnect widgets"\ "in this PM that are already disconnected.") return self.isAlreadyConnected = isConnect self.isAlreadyDisconnected = not isConnect if isConnect: change_connect = self.w.connect else: change_connect = self.w.disconnect change_connect(self.atomChooserComboBox, SIGNAL("currentIndexChanged(int)"), self._updateAtomChooserGroupBoxes) change_connect(self.selectionFilterCheckBox, SIGNAL("stateChanged(int)"), self.set_selection_filter) change_connect(self.showSelectedAtomInfoCheckBox, SIGNAL("stateChanged(int)"), self.toggle_selectedAtomPosGroupBox) change_connect(self.xCoordOfSelectedAtom, SIGNAL("valueChanged(double)"), self._moveSelectedAtom) change_connect(self.yCoordOfSelectedAtom, SIGNAL("valueChanged(double)"), self._moveSelectedAtom) change_connect(self.zCoordOfSelectedAtom, SIGNAL("valueChanged(double)"), self._moveSelectedAtom) connect_checkbox_with_boolean_pref(self.waterCheckBox, buildModeWaterEnabled_prefs_key) connect_checkbox_with_boolean_pref(self.highlightingCheckBox, buildModeHighlightingEnabled_prefs_key) #New command API method -- implemented on 2008-08-27 def _update_UI_do_updates(self): """ Overrides superclass method @warning: This is called frequently, even when nothing has changed. It's used to respond to other kinds of changes as well (e.g. to the selection). So it needs to be fast when nothing has changed. (It will be renamed accordingly in the API.) @see: Command_PropertyManager._updateUI_do_updates() """ newSelectionParams = self._currentSelectionParams() if same_vals(newSelectionParams, self.previousSelectionParams): return self.previousSelectionParams = newSelectionParams #subclasses of BuildAtomsPM may not define self.selectedAtomPosGroupBox #so do the following check. if self.selectedAtomPosGroupBox: self._updateSelectedAtomPosGroupBox(newSelectionParams) def _currentSelectionParams(self): """ Returns a tuple containing current selection parameters. These parameters are then used to decide whether updating widgets in this property manager is needed when L{self.model_changed} method is called. In this case, the Seletion Options groupbox is updated when atom selection changes or when the selected atom is moved. @return: A tuple that contains following selection parameters - Total number of selected atoms (int) - Selected Atom if a single atom is selected, else None - Position vector of the single selected atom or None @rtype: tuple @NOTE: The method may be renamed in future. It's possible that there are other groupboxes in the PM that need to be updated when something changes in the glpane. """ #use selected atom dictionary which is already made by assy. #use this dict for length tests below. Don't create list from this #dict yet as that would be a slow operation to do at this point. selectedAtomsDictionary = self.win.assy.selatoms if len(selectedAtomsDictionary) == 1: #self.win.assy.selatoms_list() is same as # selectedAtomsDictionary.values() except that it is a sorted list #it doesn't matter in this case, but a useful info if we decide # we need a sorted list for multiple atoms in future. # -- ninad 2007-09-27 (comment based on Bruce's code review) selectedAtomList = self.win.assy.selatoms_list() selectedAtom = selectedAtomList[0] posn = selectedAtom.posn() return (len(selectedAtomsDictionary), selectedAtom, posn) elif len(selectedAtomsDictionary) > 1: #All we are interested in, is to check if multiple atoms are #selected. So just return a number greater than 1. This makes sure #that parameter difference test in self.model_changed doesn't # succeed much more often (i.e. whenever user changes the number of # selected atoms, but still keeping that number > 1 aNumberGreaterThanOne = 2 return (aNumberGreaterThanOne, None, None) else: return (0, None, None) def set_selection_filter(self, enabled): """ Slot for Atom Selection Filter checkbox that enables or disables the selection filter and updates the cursor. @param enabled: Checked state of L{self.selectionFilterStateBox} If checked, the selection filter will be enabled @type enabled: bool @see: L{self.update_selection_filter_list} """ #TODO: To be revised and moved to the Command or GM part. #This can be done when Bruce implements connectWithState API # -- Ninad 2008-01-03 if enabled != self.w.selection_filter_enabled: if enabled: env.history.message("Atom Selection Filter enabled.") else: env.history.message("Atom Selection Filter disabled.") self.w.selection_filter_enabled = enabled self.filterlistLE.setEnabled(enabled) self.update_selection_filter_list() self.command.graphicsMode.update_cursor() def update_selection_filter_list(self): """ Adds/removes the element selected in the Element Chooser to/from Atom Selection Filter based on what modifier key is pressed (if any). @see: L{self.set_selection_filter} @see: L{self.update_selection_filter_list_widget} """ #Don't update the filter list if selection filter checkbox is not active if not self.filterlistLE.isEnabled(): self.w.filtered_elements = [] self.update_selection_filter_list_widget() return element = self.elementChooser.element if self.o.modkeys is None: self.w.filtered_elements = [] self.w.filtered_elements.append(element) if self.o.modkeys == 'Shift': if not element in self.w.filtered_elements[:]: self.w.filtered_elements.append(element) elif self.o.modkeys == 'Control': if element in self.w.filtered_elements[:]: self.w.filtered_elements.remove(element) self.update_selection_filter_list_widget() return def update_selection_filter_list_widget(self): """ Updates the list of elements displayed in the Atom Selection Filter List. @see: L{self.update_selection_filter_list}. (Should only be called from this method) """ filtered_syms = '' for e in self.w.filtered_elements[:]: if filtered_syms: filtered_syms += ", " filtered_syms += e.symbol self.filterlistLE.setText(filtered_syms) return def setElement(self, elementNumber): """ Set the current element in the MMKit to I{elementNumber}. @param elementNumber: Element number. (i.e. 6 = Carbon) @type elementNumber: int """ self.regularElementChooser.setElement(elementNumber) return def updateMessage(self, msg = ""): """ Updates the message box with an informative message based on the current page and current selected atom type. @param msg: The message to display in the Property Manager message box. If called with an empty string (the default), a strandard message is displayed. @type msg: str """ if msg: self.MessageGroupBox.insertHtmlMessage(msg) return if not self.elementChooser: return element = self.elementChooser.element if element.symbol in ALL_PAM_ATOMS: atom_or_PAM_atom_string = ' pseudoatom' else: atom_or_PAM_atom_string = ' atom' if self.elementChooser.isVisible(): msg = "Double click in empty space to insert a single " \ + element.name + atom_or_PAM_atom_string + "." if not element.symbol in NOBLEGASES: msg += "Click on an atom's red bondpoint to attach a " \ + element.name + atom_or_PAM_atom_string +" to it." if element.symbol in PAMATOMS: msg ="Note: this pseudoatom can only be deposited onto a strand sugar"\ " and will disappear if deposited in free space" else: # Bonds Tool is selected if self.command.isDeleteBondsToolActive(): msg = " Cut Bonds tool is active. " \ "Click on bonds in order to delete them." self.MessageGroupBox.insertHtmlMessage(msg) return # Post message. self.MessageGroupBox.insertHtmlMessage(msg) def _updateSelectedAtomPosGroupBox(self, selectionParams): """ Update the Selected Atoms Position groupbox present within the B{Selection GroupBox" of this PM. This groupbox shows the X, Y, Z coordinates of the selected atom (if any). This groupbox is updated whenever selection in the glpane changes or a single atom is moved. This groupbox is enabled only when exactly one atom in the glpane is selected. @param selectionParams: A tuple that provides following selection parameters - Total number of selected atoms (int) - Selected Atom if a single atom is selected, else None - Position vector of the single selected atom or None @type: tuple @see: L{self._currentSelectionParams} @see: L{self.model_changed} """ totalAtoms, selectedAtom, atomPosn = selectionParams text = "" if totalAtoms == 1: self.enable_or_disable_selectedAtomPosGroupBox(bool_enable = True) text = str(selectedAtom.getInformationString()) text += " (" + str(selectedAtom.element.name) + ")" self._updateAtomPosSpinBoxes(atomPosn) elif totalAtoms > 1: self.enable_or_disable_selectedAtomPosGroupBox(bool_enable = False) text = "Multiple atoms selected" else: self.enable_or_disable_selectedAtomPosGroupBox(bool_enable = False) text = "No Atom selected" if self.selectedAtomLineEdit: self.selectedAtomLineEdit.setText(text) def _moveSelectedAtom(self, spinBoxValueJunk = None): """ Move the selected atom position based on the value in the X, Y, Z coordinate spinboxes in the Selection GroupBox. @param spinBoxValueJunk: This is the Spinbox value from the valueChanged signal. It is not used. We just want to know that the spinbox value has changed. @type spinBoxValueJunk: double or None """ if self.model_changed_from_glpane: #Model is changed from glpane ,do nothing. Fixes bug 2545 print "bug: self.model_changed_from_glpane seen; should never happen after bug 2564 was fixed." #bruce 071015 return totalAtoms, selectedAtom, atomPosn_junk = self._currentSelectionParams() if not totalAtoms == 1: return #@NOTE: This is important to determine baggage and nobaggage atoms. #Otherwise the bondpoints won't move! See also: # selectMode.atomSetup where this is done. # But that method gets called only when during atom left down. #Its not useful here as user may select that atom using selection lasso #or using other means (ctrl + A if only one atom is present) . Also, #the lists command.baggage and command.nonbaggage seem to get #cleared during left up. So that method is not useful. #There needs to be a method in parentmode (Select_Command or #BuildAtoms_Command) #to do the following (next code cleanup?) -- ninad 2007-09-27 self.command.baggage, self.command.nonbaggage = \ selectedAtom.baggage_and_other_neighbors() xPos= self.xCoordOfSelectedAtom.value() yPos = self.yCoordOfSelectedAtom.value() zPos = self.zCoordOfSelectedAtom.value() newPosition = V(xPos, yPos, zPos) delta = newPosition - selectedAtom.posn() #Don't do selectedAtom.setposn() because it needs to handle #cases where atom has bond points and/or monovalent atoms . It also #needs to modify the neighboring atom baggage. This is already done in #the following method in command so use that. self.command.drag_selected_atom(selectedAtom, delta, computeBaggage = True) self.o.gl_update() def _updateAtomPosSpinBoxes(self, atomCoords): """ Updates the X, Y, Z values in the Selection Options Groupbox. This method is called whenever the selected atom in the glpane is dragged. @param atomCoords: X, Y, Z coordinate position vector @type atomCoords: Vector """ self.model_changed_from_glpane = True #block signals while making setting values in these spinboxes self.xCoordOfSelectedAtom.setValue(atomCoords[0], blockSignals = True) self.yCoordOfSelectedAtom.setValue(atomCoords[1], blockSignals = True) self.zCoordOfSelectedAtom.setValue(atomCoords[2], blockSignals = True) self.model_changed_from_glpane = False return pass # TODO: setValue_with_signals_blocked is a useful helper function which should be refiled. def setValue_with_signals_blocked(widget, value): # bruce 071015 """ Call widget.setValue(value) while temporarily blocking all Qt signals sent from widget. (If they were already blocked, doesn't change that.) @param widget: a QDoubleSpinBox, or any Qt widget with a compatible setValue method @type widget: a Qt widget with a setValue method compatible with that of QDoubleSpinBox @param value: argument for setValue @type value: whatever is needed by setValue (depends on widget type) """ was_blocked = widget.blockSignals(True) try: widget.setValue(value) finally: widget.blockSignals(was_blocked) return # end