# 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{PasteMode}
"""
import foundation.env as env
from PyQt4.Qt import SIGNAL
from commands.BuildAtoms.Ui_BuildAtomsPropertyManager import Ui_BuildAtomsPropertyManager
from model.bond_constants import btype_from_v6
from geometry.VQT import V
from utilities.Comparison import same_vals
NOBLEGASES = ["He", "Ne", "Ar", "Kr"]
PAMATOMS = ["Gv5", "Ax3"]
ALL_PAM_ATOMS = ["Gv5", "Ss5", "Pl5", "Ax3", "Ss3", "Ub3", "Ux3", "Uy3"]
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, parentMode):
"""
Constructor for the B{Build Atoms} property manager.
@param parentMode: The parent mode where this Property Manager is used
@type parentMode: L{depositMode}
"""
self.previousSelectionParams = None
Ui_BuildAtomsPropertyManager.__init__(self, parentMode)
# 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 ok_btn_clicked(self):
"""
Calls MainWindow.toolsDone to exit the current mode.
@attention: this method needs to be renamed. (this should be done in
PM_Dialog)
"""
self.w.toolsDone()
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{depositMode.connect_or_disconnect_signals} where this is called
"""
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)
def model_changed(self):
"""
Overrides basicMode.model_changed.
@WARNING: Ideally this property manager should implement both
model_changed and selection_changed methods in the mode API.
model_changed method will be used here when the selected atom is
dragged, transmuted etc. The selection_changed method will be
used when the selection (picking/ unpicking) changes.
At present, selection_changed and model_changed methods are
called too frequently that it doesn't matter which one you use.
Its better to use only a single method for preformance reasons
(at the moment). This should change when the original
methods in the API are revised to be called at appropiraite
time.
"""
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} or
L{self.selection_changed} methods are 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 name may be renamed in future.
Its 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.parentMode.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.parentMode.cutBondsAction.isChecked():
msg = " Cut Bonds tool is active. \
Click on bonds in order to delete them."
self.MessageGroupBox.insertHtmlMessage(msg)
return
if not hasattr(self.parentMode, 'bondclick_v6'):
return
if self.parentMode.bondclick_v6:
name = btype_from_v6(self.parentMode.bondclick_v6)
msg = "Click bonds or bondpoints to make them %s bonds." % name
# 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 parentMode.baggage and parentMode.nonbaggage seem to get
#cleared during left up. So that method is not useful.
#There needs to be a method in parentmode (selectMode or depositMode)
#to do the following (next code cleanup?) -- ninad 2007-09-27
self.parentMode.baggage, self.parentMode.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 parentMode so use that.
self.parentMode.drag_selected_atom(selectedAtom, delta)
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
# Disable signals out of these spinboxes, to fix bug 2564 [bruce 071015]
## self.xCoordOfSelectedAtom.setValue_with_signals_blocked(atomCoords[0]) # maybe someday we can just say this?
setValue_with_signals_blocked( self.xCoordOfSelectedAtom, atomCoords[0])
setValue_with_signals_blocked( self.yCoordOfSelectedAtom, atomCoords[1])
setValue_with_signals_blocked( self.zCoordOfSelectedAtom, atomCoords[2])
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