# Copyright (C) 2007 Maxim Fedorovsky, University of Fribourg (Switzerland).
# email : Maxim.Fedorovsky@unifr.ch, mutable@yandex.ru
#
# This file is part of PyVib2.
#
# PyVib2 is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PyVib2 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyVib2; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""GUI building blocks of the windows and dialogs of PyVib2.
The following classes are exported :
BaseWidget -- base class for all widgets
MoleculeRenderWidget -- widget for 3D rendering
MoleculeThumbnailWidget -- small frame for exploring a molecule
ButtonToolbar -- button toolbar
VibrationalToolbar -- navigation through vibrations
VibrationalToolbarLight -- for exploring single vibration
NavigationToolbar -- manipulating camera of MoleculeRenderWidget
WindowNavigationToolbar -- toolbar for switching between windows
GeometryMeasureToolbar -- widget for measuring distances, angles etc.
VibNavigationFrame -- component of VibrationalToolbar
SplashScreen -- shown during a long operation
InfoWidget -- widget for providing a help text
TwoDCircles -- canvas for rendering matrices
CorrelationResultsTable -- table for representing matrices
CorrelationResultsNoteBook -- exploring results of a correlation of v/m
WizardWidget -- wizard widget
ChooseColorWidget -- widget for choosing a color
AxesSettingsWidget -- widget for setting properties of axes
PropertiesWidget -- widget for showing text properties
The following functions are exported :
show_exception() -- show an exception in a text dialog
mouse_wheel() -- mouse wheel callback for a widget
bind_mouse_wheel() -- bind the mouse wheel events to a widget
"""
__author__ = 'Maxim Fedorovsky'
import os
import sys
import tempfile
from math import fabs, sqrt, ceil, floor, log
from time import clock
import Tkinter
import tkFileDialog
import tkFont
import tkMessageBox
import tkColorChooser
import tkSimpleDialog
import cPickle
from traceback import format_tb
import Pmw
from numpy import zeros, ndarray, array, any
import vtk
import vtk.tk.vtkTkRenderWidget as vtktkrw
from pyviblib import molecule
from pyviblib.calc.common import make_gcp, mass_center
from pyviblib.gui import rendering, resources
from pyviblib.gui.images import getimage
from pyviblib.util import misc
from pyviblib.util.constants import AMU2AU
from pyviblib.util.exceptions import ConstructorError, InvalidArgumentError
__all__ = ['BaseWidget', 'MoleculeRenderWidget', 'MoleculeThumbnailWidget',
'ButtonToolbar', 'VibrationalToolbar', 'VibrationalToolbarLight',
'NavigationToolbar', 'WindowNavigationToolbar',
'GeometryMeasureToolbar', 'VibNavigationFrame', 'SplashScreen',
'InfoWidget', 'TwoDCircles', 'CorrelationResultsTable',
'CorrelationResultsNoteBook', 'WizardWidget',
'ChooseColorWidget', 'AxesSettingsWidget', 'PropertiesWidget',
'show_exception', 'mouse_wheel', 'bind_mouse_wheel']
class BaseWidget(object) :
"""Base class for all widgets.
This class defines a set of protected methods which are called in the
constructor in the following sequence :
_init_vars() -- initialize some variables
_constructGUI() -- construct the GUI of the widget
_declare_properties() -- declare properties of the widget
_bind_help() -- bind help messages to the GUI components
_bind_events() -- bind events
These methods are intended to be overridden in subclasses. The base class
implementations do *nothing*.
The following protected instance variables are created :
_smartdict -- to store options (pyviblib.util.misc.SmartDict)
_varsdict -- to store GUI variables (dictionary)
_balloon -- to provide help message on the GUI (Pmw.Balloon)
"""
def __init__(self, **kw) :
"""Constructor of the class.
Keywords arguments are options of the widget.
"""
# smart properties
self._smartdict = misc.SmartDict(kw=kw)
self.__class__.smartdict = property(
misc.Command(misc.Command.fget_attr, '_smartdict'))
# dictionary for GUI variables
self._varsdict = {}
# help system
self._balloon = Pmw.Balloon()
# typical operations
self._init_vars()
self._constructGUI()
self._declare_properties()
self._bind_help()
self._bind_events()
def _init_vars(self) :
"""Initialize variables."""
pass
def _constructGUI(self) :
"""Construct the GUI of the widget."""
pass
def _declare_properties(self) :
"""Declare properties of the widget."""
pass
def _bind_help(self) :
"""Bind help messages to the GUI components."""
pass
def _bind_events(self) :
"""Bind events."""
pass
def _add_property(self, name, readonly=True, doc=None) :
"""Add a property.
Positional arguments :
name -- name of the property
self._smartdict[prop_name] must point to the variable
Keywords arguments :
readonly -- whether the property should be read-only (default True)
doc -- doc string for the property (default None)
"""
if name is None :
raise InvalidArgumentError('Property name must be supplied')
str_to_exec = '%s.%s = property(' % (self.__class__.__name__, name) + \
r"fget=misc.Command(self._get_property, '%s')," % name
if not readonly :
str_to_exec = ''.join((
str_to_exec,
r"fset=misc.Command(self._set_property, '%s')," % name))
str_to_exec = ''.join((str_to_exec, 'doc=doc)'))
exec str_to_exec
def _get_property(obj, name) :
"""Emulate the getter function for a property.
Positional arguments :
obj -- object whose property is to be read
name -- name of the property
"""
return obj._smartdict[name]
_get_property = staticmethod(_get_property)
def _set_property(obj, value, name) :
"""Emulate the setter function for a property.
Positional arguments :
obj -- object whose property is to be set
value -- value to set
name -- name of the property
"""
obj._smartdict[name] = value
_set_property = staticmethod(_set_property)
def _change_render_widget_size(self, rw) :
"""Show the dialog for changing size of a given render widget.
The function can be used as a callback for changing the size of the widget.
Positional arguments :
rw -- render widget
"""
cursize = rw.GetRenderWindow().GetSize()
kw = dict(parent = hasattr(self, 'tk') and self or self.interior(),
title='New size',
prompt='Enter the new size of the render widget',
initialvalue='%dx%d' % cursize)
newsize = tkSimpleDialog.askstring(**kw)
if newsize is not None :
# try to parse the size
try :
w, h = [ int(val) for val in newsize.strip().split('x') ]
if 0 >= w or 0 >= h :
raise InvalidArgumentError('Invalid size entered')
self._set_render_widget_size(rw, w, h)
except :
tkMessageBox.showerror(title='Error',
message=r'Invalid size : "%s"' % newsize)
def _set_render_widget_size(self, rw, w, h) :
"""Set the size of a given render widget.
Positional arguments :
rw -- render widget
w -- desired width in pixel for the render widget
h -- desired height in pixel for the render widget
"""
self.update_idletasks()
# current size of the render widget and of the whole window
cur_w, cur_h = rw.GetRenderWindow().GetSize()
geom = self.geometry()
delim = '+' in geom and '+' or '-'
w_, h_ = [ int(val) for val in geom[:geom.index(delim)].split('x') ]
# setting the new size (keep old coordinates of the upper left corner)
geom = (w_ + w - cur_w, h_ + h - cur_h, geom[geom.index(delim):])
self.geometry('%dx%d%s' % geom)
self.update_idletasks()
class MoleculeRenderWidget(BaseWidget, vtktkrw.vtkTkRenderWidget) :
"""Widget for 3D rendering.
The class inherits from vtkTkRenderWidget and overrides the mouse button
events.
The following readable and writable properties are exposed :
molecule -- molecule (pyviblib.molecule.Molecule)
molecule_mode -- molecule rendering mode
bonds_mode -- bonds rendering mode
color_sphere_1 -- used for rendering vibrational motion
color_sphere_2 -- used for rendering vibrational motion
color_picked_atom -- color of picked atoms
marked_vib_atoms -- mark vibrational motion of these atoms
mark_fragment -- whether to mark v/m on marked_vib_atoms
show_marked_only -- whether to show v/m motion only on fragment
do_picking -- whether the picking of atoms is allowed
do_render_picked -- whether picked atoms are to be rendered
do_sync_rotation -- whether the synchronized rotation is on
do_sync_zoom -- whether the synchronized zooming is om
clicked_atom_callback -- called when an atom has been clicked
clicked_bond_callback -- called when a bond has been clicked
clicked_vibatom_callback -- called when an v/m atom has been clicked
background -- background of the widget
perspective_projection -- whether the perspective projection is used
resolution -- resolution
camera -- camera installed in the widget
camera_state -- state of the camera currenly installed
The following read-only properties are exposed :
picked_atoms_indices -- list of picked atoms
Npicked -- number of picked atoms
renderer -- renderer (vtk.vtkRenderer)
The following public methods are exported :
cleanup() -- remove all actor from the widget
render_molecule() -- render the molecule
render_vibration() -- render a vibration
render_triangle() -- render a triangle
render_scalars() -- render scalar values e.g. ACPs
render_gcp() -- render group coupling matrices (GCPs)
rotate() -- rotate the molecule
zoom() -- zoom in/out
start_pairs_picking() -- start a subsequent selection of atom pairs
end_pairs_picking() -- end a subsequent selection of atom pairs
depick_atoms() -- unpick atoms
highlight_picked_atoms() -- highlight picked atoms
remove_triangles() -- remove triangles
synchronize_camera_state() -- specify a render widget for sync operations
get_node() -- get a certain node in the widget
snapshot() -- make a snapshot of the 3D window
create_animation_frames() -- create frames for animating vibration
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keywords arguments :
molecule -- molecule
(pyviblib.molecule.Molecule, default None)
vib_toolbar -- vibrational toolbar
(pyviblib.gui.widgets.VibrationalToolbar,
default None)
used for retrieving properties of vibrations
msgBar -- message bar (Pmw.MessageBar, default None)
molecule_mode -- render the molecule in the ball & stick, stick or
van der Waals radii representation
one of (resources.NUM_MODE_BALLSTICK,
resources.NUM_MODE_STICK,
resources.NUM_MODE_VDW)
(default resources.NUM_MODE_BALLSTICK)
bonds_mode -- render a bond in the molecule as a cylinder
or two cylinders
one of (resources.NUM_MODE_BONDS_MONOLITH_COLOR,
resources.NUM_MODE_BONDS_ATOMS_COLOR)
(default resources.NUM_MODE_BONDS_ATOMS_COLOR)
resolution -- resolution (default resources.NUM_RESOLUTION_VTK)
background -- background color in the HTML format
(default resources.COLOR_MOLECULE_WINDOW_BG)
hydrogen_bond -- whether hydrogen bonds are to be rendered
(default True)
atom_labels -- whether atom labels are to be rendered
(default False)
color_sphere_1 -- color of the first hemisphere in the sphere mode
representation of vibrational motion
(default resources.COLOR_VIB_HEMISPHERE_1)
color_sphere_2 -- color of the first hemisphere in the sphere mode
representation of vibrational motion
(default resources.COLOR_VIB_HEMISPHERE_2)
bonds_transparency -- transparency level of the bonds (default 0.)
a value between 0. (completely opaque) and 1.
(completely transparent)
color_picked_atom -- color of picked atoms in the HTML format
(default resources.COLOR_PICKED_ATOM)
invert_phase -- whether the phase of vibrations is to be inverted
mark_fragment -- whether to mark the vibrational motion of a fragment
defined by the marked_vib_atoms property
(default False)
show_marked_only -- whether to show vibrational motion only on a fragment
defined by the marked_vib_atoms property
(default False)
do_picking -- whether picking of atoms is allowed (default False)
do_render_picked -- whether picked atoms are to be rendered
(default True)
do_sync_rotation -- whether the synchronized rotation is to be enabled
the second render widget must be supplied for this to
work
see synchronize_camera_state()
(default False)
do_sync_zoom -- whether the synchronized zooming is to be enabled
the second render widget must be supplied for this to
work
see synchronize_camera_state()
(default False)
"""
vtktkrw.vtkTkRenderWidget.__init__(self, master,
**self._tk_widget_kw(**kw))
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
#
self.__molecule = self._smartdict['molecule']
self.__vib_toolbar = self._smartdict['vib_toolbar']
## default keywords for rendering
self._smartdict['molecule_mode'] = resources.NUM_MODE_BALLSTICK
self._smartdict['bonds_mode'] = resources.NUM_MODE_BONDS_ATOMS_COLOR
self._smartdict['resolution'] = resources.NUM_RESOLUTION_VTK
self._smartdict['background'] = resources.COLOR_MOLECULE_WINDOW_BG
self._smartdict['hydrogen_bond'] = True
self._smartdict['atom_labels'] = False
self._smartdict['color_sphere_1'] = resources.COLOR_VIB_HEMISPHERE_1
self._smartdict['color_sphere_2'] = resources.COLOR_VIB_HEMISPHERE_2
self._smartdict['bonds_transparency'] = 0.
self._smartdict['color_picked_atom'] = resources.COLOR_PICKED_ATOM
self._smartdict['invert_phase'] = False
# 0-based list of picked atoms
self.__picked_atoms_indices = []
# will be used if a synchronous picking is desired
# the users uses the left mouse button to pick atoms
self.__picked_atom_pairs = None
self.__picked_atom_pairs_callback = None
# do not marke a fragment by default
self._smartdict['mark_fragment'] = False
# do not show the vibrational motion on a fragment ONLY
self._smartdict['show_marked_only'] = False
# picking disabled by default
self._smartdict['do_picking'] = False
# a picked atom is rendered by default
self._smartdict['do_render_picked'] = True
# synchronous camera change
# can set synchronous rotation, zoom
self._sync_widget = None
# synchronous rotation is disabled by default
self._smartdict['do_sync_rotation'] = False
# synchronous zoom is disabled by default
self._smartdict['do_sync_zoom'] = False
# collecting all nodes is an internal dictionary
self.__nodes = dict(atoms=[],
atom_labels=[],
bonds=[],
vibatoms=[],
triangles=[])
# animator function id
self.__animator_id = None
def _constructGUI(self) :
"""Construct the GUI of the widget."""
self.__renderer = vtk.vtkRenderer()
self.GetRenderWindow().AddRenderer(self.__renderer)
# installing a picker for atom selection
# The picking is available with the left mouse button
# if mouse does not move
self.__picker = vtk.vtkPropPicker()
self.__mouse_motion = False
#self.background = self._smartdict['background']
self.__renderer.SetBackground(
misc.color_html_to_RGB(self._smartdict['background']))
# installing lights
lightKit = vtk.vtkLightKit()
lightKit.MaintainLuminanceOn()
lightKit.SetKeyLightIntensity(1.0)
# warmth of the lights
lightKit.SetKeyLightWarmth(0.65)
lightKit.SetFillLightWarmth(0.6)
# the function is called SetHeadLightWarmth starting from VTK 5.0
# this is a very big stupidity :(
try :
lightKit.SetHeadLightWarmth(0.45)
except :
lightKit.SetHeadlightWarmth(0.45)
# intensity ratios
# back lights will be very dimm ;)
lightKit.SetKeyToFillRatio(2.)
lightKit.SetKeyToHeadRatio(7.)
lightKit.SetKeyToBackRatio(1000.)
lightKit.AddLightsToRenderer(self.__renderer)
# first rendering of the molecule
self.render_molecule()
# install the camera if given
if self._smartdict['camera'] is not None :
self.camera = self._smartdict['camera']
def _declare_properties(self) :
"""Declare properties of the widget."""
# smart dictionary variables
for prop in ('color_sphere_1', 'color_sphere_2',
'marked_vib_atoms', 'mark_fragment', 'show_marked_only',
'do_picking', 'do_render_picked', 'color_picked_atom',
'do_sync_rotation', 'do_sync_zoom',
'molecule_mode', 'bonds_mode',
'clicked_atom_callback', 'clicked_bond_callback',
'clicked_vibatom_callback') :
self._add_property(prop, readonly=False)
# Number of picked atoms
self.__class__.Npicked = property(fget=self._get_Npicked)
# List of 0-based indices of picked atoms
self.__class__.picked_atoms_indices = property(
fget=misc.Command(misc.Command.fget_value, self.__picked_atoms_indices))
# Current molecule as an instance of pyviblic.molecule.Molecule
self.__class__.molecule = property(fget=self._get_molecule,
fset=self._set_molecule)
# Background color of a 3D-window in hex format
self.__class__.background = property(fget=self._get_background,
fset=self._set_background)
# Renderer object
self.__class__.renderer = property(fget=self._get_renderer)
# If the perspective projection
self.__class__.perspective_projection = property(
fget=self._get_perspective_projection,
fset=self._set_perspective_projection)
# resolution of all the nodes of the current widget
self.__class__.resolution = property(
fget=self._get_resolution, fset=self._set_resolution)
# Currently installed camera
self.__class__.camera = property(
fget=self._get_camera, fset=self._set_camera)
# State of the currently installed camera (as a dictionary)
self.__class__.camera_state = property(
fget=self._get_camera_state, fset=self._set_camera_state)
def _bind_events(self) :
"""Redefine the event binding of the base class.
Final bindings :
Left mouse button -> rotate
Right mouse button -> zoom in/out
Middle mouse button -> pan
Ctrl + left mouse button -> roll
"""
# events to be unbounded
to_be_unbound = ('<ButtonPress>', '<ButtonRelease>',
'<B1-Motion>', '<Shift-B1-Motion>',
'<B3-Motion>',
'<KeyPress-r>', '<KeyPress-u>',
'<KeyPress-w>', '<KeyPress-s>', '<KeyPress-p>' )
for ev in to_be_unbound :
self.unbind(ev)
# overriding the mouse press / release events
# for picking with the left mouse button
self.bind('<ButtonPress>' , misc.Command(self.__ButtonPress))
self.bind('<ButtonRelease>', misc.Command(self.__ButtonRelease))
# slower rotation
self.bind('<B1-Motion>', misc.Command(self.__rotate))
# rolling function
self.bind('<Control-B1-Motion>', misc.Command(self.__roll))
# overriding zoom for synchronized camera motion ;)
self.bind('<B3-Motion>', misc.Command(self.__zoom))
def _tk_widget_kw(**kw) :
"""Get the keywords of the vtkTkRenderWidget.
These are : width, height, rw
"""
if kw :
tk_widget_kw = {}
for key in ('width', 'height', 'rw') :
if key in kw :
tk_widget_kw[key] = kw[key]
return tk_widget_kw
else :
return {}
_tk_widget_kw = staticmethod(_tk_widget_kw)
def __ButtonPress(self, e) :
"""Handler for the mouse event <ButtonPress>."""
self.StartMotion(e.x, e.y)
self.__mouse_motion = False
def __ButtonRelease(self, e) :
"""Handler for the mouse event <ButtonRelease>."""
# superclass implementation
self.EndMotion(e.x, e.y)
if not self.__mouse_motion :
self.__clicked_node(e.x, e.y, e.num)
def __rotate(self, e) :
"""Reimplementation of the rotate function.
Affects the widget to be synchronized with.
"""
x, y = e.x, e.y
euler_azimuth = (self._LastX - x) * resources.NUM_MOUSE_SLOWING_FACTOR
euler_elevation = (y - self._LastY) * resources.NUM_MOUSE_SLOWING_FACTOR
self.rotate(euler_azimuth, euler_elevation)
self._LastX = x
self._LastY = y
if self._sync_widget is not None and self.do_sync_rotation :
self._sync_widget.rotate(euler_azimuth, euler_elevation)
def __roll(self, e) :
"""Rotate the camera about the direction of projection.
Affects the widget to be synchronized with.
"""
x, y = e.x, e.y
euler_roll = float(y - self._LastY) * resources.NUM_MOUSE_SLOWING_FACTOR
self._LastX = x
self._LastY = y
self.rotate(0., 0., euler_roll)
if self._sync_widget and self.do_sync_rotation :
self._sync_widget.rotate(0., 0., euler_roll)
def __zoom(self, e) :
"""Zoom in/out the camera.
Affects the widget to be synchronized with.
"""
x, y = e.x, e.y
zoomFactor = pow(1.02, (0.5*(self._LastY - y)))
self._LastX = x
self._LastY = y
self.zoom(zoomFactor)
if self._sync_widget and self.do_sync_zoom :
self._sync_widget.zoom(zoomFactor)
def __clicked_node(self, x, y, num) :
"""The user has clicked on a node.
num :
1 : left mouse button
2 : middle mouse button (the wheel)
3 : right mouse button
Callback functions must accept two args : (num, node_index)
node_index is 0-based.
Picking is handled *only* for the left mouse button.
"""
self.__picker.Pick(x, self.winfo_height() - y - 1, 0., self.__renderer)
node = self.__picker.GetAssembly()
# processing only registered nodes
if not isinstance(node, rendering.BaseNode ) :
return
node_index = -1
callback = None
# atoms
if isinstance(node, rendering.AtomNode) :
# first all events
if node in self.__nodes['atoms'] :
node_index = self.__nodes['atoms'].index(node)
callback = self.clicked_atom_callback
# only for picking
# one can suppress the picking by setting the nodes
# property "pickable" to False !
if 1 == num and self.do_picking and \
not node.get_picked() and node.get_pickable() :
if self.do_render_picked :
# render a picked atom
node.pick()
self.Render()
# saving the picked atom index
self.__picked_atoms_indices.append(node_index)
# handling atom pairs picking
if self.__picked_atom_pairs is not None and \
callable(self.__picked_atom_pairs_callback) :
pairs = self.__picked_atom_pairs
# the very first pair ;)
if 0 == len(pairs) :
pairs.append([node_index, -1])
else :
last_pair = pairs[len(pairs)-1]
# completing a pair
if -1 == last_pair[1] :
last_pair[1] = node_index
# starting the new pair
elif 0 == last_pair.count(-1) :
pairs.append([node_index, -1])
else :
return
self.__picked_atom_pairs_callback(self)
# bonds
elif isinstance(node, rendering.BondNode) :
if node in self.__nodes['bonds'] :
node_index = self.__nodes['bonds'].index(node)
callback = self.clicked_bond_callback
# vibrating atoms
elif isinstance(node, rendering.VibratingAtomNode) :
if node in self.__nodes['vibatoms'] :
node_index = self.__nodes['vibatoms'].index(node)
callback = self.clicked_vibatom_callback
# don't know how to handle
else :
return
# calling
if -1 != node_index and callable(callback) :
callback(num, node_index)
def __animate(self, Lx, scale_factor, nFrames, ms) :
"""Animate a normal node.
Positional arguments :
Lx : cartesian excursions (one-based ndarray)
shape : (1 + Natoms, 4) with Natoms being
the number of atoms
scale_factor : numeric factor with which the amplitude of vibrational
motion is multiplied
nFrames : number frames pro direction
ms : the time in milliseconds
"""
# animating
self.__render_animation_frame(Lx, scale_factor, self.__frameno, nFrames)
# frame number varies from -nFrames to +NFrames
if self.__plus_direction :
if self.__frameno == nFrames :
self.__plus_direction = False
self.__frameno -= 1
else :
self.__frameno += 1
else :
if self.__frameno == -nFrames :
self.__plus_direction = True
self.__frameno += 1
else :
self.__frameno -= 1
self.__animator_id = self.after(ms, self.__animate, Lx,
scale_factor, nFrames, ms)
def __render_animation_frame(self, Lx, scale_factor, frameno, nFrames) :
"""Render a frame."""
coeff = frameno * scale_factor / nFrames
# transforming atoms (translation)
atom_nodes = self.__nodes['atoms']
for a in xrange(len(atom_nodes)) :
t = vtk.vtkTransform()
t.Translate(coeff * Lx[1 + a, 1:])
atom_nodes[a].SetUserTransform(t)
#atom_nodes[a].translate(coeff * Lx[1 + a, 1:])
# transforming bonds (translation + rotation + scaling)
bond_nodes = self.__nodes['bonds']
for bond_node in bond_nodes :
bond = bond_node.get_bond()
bond_node.render_displaced(coeff * Lx[bond.atom1.index],
coeff * Lx[bond.atom2.index])
# making the changes visible
self.Render()
def _get_atom_labeldata(atom, mode) :
"""Get the label & radius of an Atom node."""
if resources.NUM_MODE_STICK == mode :
r = 0.1
elif resources.NUM_MODE_BALLSTICK == mode :
r = atom.element.r_coval * resources.NUM_FACTOR_SPHERE_RADIUS * 0.7
else :
r = atom.element.r_vdw * resources.NUM_FACTOR_SPHERE_RADIUS * 0.7
return r, '%s%d' % (atom.element.symbol, atom.index)
_get_atom_labeldata = staticmethod(_get_atom_labeldata)
def __animate_vibration_poll(self, Lx, scale_factor, nFrames, ms) :
"""Animate a vibration via the Tkinter polling."""
# current frame settings
self.__frameno = 0
self.__plus_direction = True
# if another animation already runs -> kill it and start a new one
if self.__animator_id is not None :
self.after_cancel(self.__animator_id)
self.__animator_id = self.after(ms,
self.__animate,
Lx,
scale_factor,
nFrames,
ms)
def _set_camera(obj, camera) :
"""Install a new camera to the widget."""
if camera is None :
return
MoleculeRenderWidget._set_camera_state(
obj, MoleculeRenderWidget._get_camera_state(obj, camera=camera))
_set_camera = staticmethod(_set_camera)
def _set_camera_state(obj, camera_state) :
"""Set a new camera state."""
for prop in resources.STRINGS_CAMERA_PROPERTIES :
if prop not in camera_state :
raise InvalidArgumentError('%s key missing' % prop)
camera = obj.camera
# set new values
camera.SetParallelScale(camera_state['ParallelScale'])
camera.SetFocalPoint(camera_state['FocalPoint'])
camera.SetPosition(camera_state['Position'])
camera.SetClippingRange(camera_state['ClippingRange'])
camera.ComputeViewPlaneNormal()
camera.SetViewUp(camera_state['ViewUp'])
camera.OrthogonalizeViewUp()
camera.SetParallelProjection(camera_state['ParallelProjection'])
obj.Render()
_set_camera_state = staticmethod(_set_camera_state)
def _get_camera_state(obj, **kw) :
"""Return the camera state of a given camera.
Keywords arguments :
camera -- camera (default None)
if None use the current camera
"""
camera = kw.get('camera', None) or obj.camera
camera_state = {}
# saving the camera properties
camera_state['ParallelProjection'] = camera.GetParallelProjection()
camera_state['ParallelScale'] = camera.GetParallelScale()
camera_state['FocalPoint'] = camera.GetFocalPoint()
camera_state['Position'] = camera.GetPosition()
camera_state['ClippingRange'] = camera.GetClippingRange()
camera_state['ViewUp'] = camera.GetViewUp()
return camera_state
_get_camera_state = staticmethod(_get_camera_state)
def _get_camera(obj) :
"""Get the current camera installed in the widget."""
return obj.__renderer.GetActiveCamera()
_get_camera = staticmethod(_get_camera)
def _set_molecule(obj, newmol) :
"""Install a new molecule to the widget."""
obj.__molecule = newmol
obj.render_molecule()
obj.Render()
_set_molecule = staticmethod(_set_molecule)
def _get_molecule(obj) :
"""Get the molecule."""
return obj.__molecule
_get_molecule = staticmethod(_get_molecule)
def _set_perspective_projection(obj, perspective) :
"""Turn the perspective projection on / off."""
if perspective :
obj.camera.ParallelProjectionOff()
else :
obj.camera.ParallelProjectionOn()
obj.Render()
_set_perspective_projection = staticmethod(_set_perspective_projection)
def _get_perspective_projection(obj) :
"""Whether the camera is instructed to do the perspective projection."""
return not obj.camera.GetParallelProjection()
_get_perspective_projection = staticmethod(_get_perspective_projection)
def _set_resolution(obj, resolution) :
"""Set the a resolution for all nodes of the widget.
Usefull when the user makes a snapshot and
wants to use the better rendering quality.
"""
for t in obj.__nodes :
for node in obj.__nodes[t] :
if isinstance(node, rendering.BaseNode) :
node.set_resolution(resolution)
obj.Render()
obj._smartdict.update_(dict(resolution=resolution))
_set_resolution = staticmethod(_set_resolution)
def _get_resolution(obj) :
"""Get the current resolution."""
return obj._smartdict['resolution']
_get_resolution = staticmethod(_get_resolution)
def _get_Npicked(obj) :
"""Get the number of picked atoms."""
return len(obj.__picked_atoms_indices)
_get_Npicked = staticmethod(_get_Npicked)
def _get_background(obj) :
"""Get the background color in the HTML format."""
return misc.color_RGB_to_html(obj.__renderer.GetBackground())
_get_background = staticmethod(_get_background)
def _set_background(obj, value) :
"""Set the new background."""
obj.__renderer.SetBackground(misc.color_html_to_RGB(value))
_set_background = staticmethod(_set_background)
def _get_renderer(obj) :
"""Get the renderer."""
return obj.__renderer
_get_renderer = staticmethod(_get_renderer)
def cleanup(self) :
"""Clean up the widget by removing all its actors."""
if self.__renderer is None :
return
# kill an animation if it runs
if self.__animator_id is not None :
self.after_cancel(self.__animator_id)
# clean of picked atoms
del self.__picked_atoms_indices[:]
# reinitializing the nodes
for key in self.__nodes.keys() :
del self.__nodes[key][:]
# finally cleaning the renderer
# vtkViewport::RemoveAllProps() is deprecated in VTK-5.0
try :
self.__renderer.RemoveAllViewProps()
except :
# for older VTK
self.__renderer.RemoveAllProps()
def render_molecule(self, **kw) :
"""Render the molecule.
Keywords arguments :
resolution -- resolution (default resources.NUM_RESOLUTION_VTK)
molecule_mode -- render the molecule in the ball & stick, stick or
van der Waals radii representation
one of (resources.NUM_MODE_BALLSTICK,
resources.NUM_MODE_STICK,
resources.NUM_MODE_VDW)
bonds_mode -- render a bond in the molecule as a cylinder
or two cylinders
one of (resources.NUM_MODE_BONDS_MONOLITH_COLOR,
resources.NUM_MODE_BONDS_ATOMS_COLOR)
(default resources.NUM_MODE_BONDS_ATOMS_COLOR)
color_picked_atom -- color of picked atoms in the HTML format
(default resources.COLOR_PICKED_ATOM)
rounded_bond -- whether the bonds are to be rendered rounded
(default False)
hydrogen_bond -- whether hydrogen bonds are to be rendered
(default True)
atom_labels -- whether atom labels are to be rendered
(default False)
"""
if self.__molecule is None :
return
# do not forget to clean !!!
self.cleanup()
self._smartdict.merge()
self._smartdict.kw = kw
props = self._smartdict
## let the control change without waiting for the rendering to complete
self.update_idletasks()
# atoms & labels
if ( resources.NUM_MODE_BALLSTICK == props['molecule_mode'] \
or resources.NUM_MODE_VDW == props['molecule_mode'] ) :
for atom in self.__molecule.atoms :
atom_node = rendering.AtomNode(
atom,
mode=props['molecule_mode'],
resolution=props['resolution'],
color_picked_atom=props['color_picked_atom'])
self.__nodes['atoms'].append(atom_node)
self.__renderer.AddActor(atom_node)
# rendering bonds
for bond in self.__molecule.bonds :
if not bond.is_hydrogen or bond.is_hydrogen and props['hydrogen_bond'] :
bond_node = rendering.BondNode(
bond,
mode=props['bonds_mode'],
transparency=props['bonds_transparency'],
resolution=props['resolution'],
rounded_bond=props['rounded_bond'])
self.__nodes['bonds'].append(bond_node)
self.__renderer.AddActor(bond_node)
# atom labels
if props['atom_labels'] :
self.Render()
camera = self.__renderer.GetActiveCamera()
for atom in self.__molecule.atoms :
r, text = self._get_atom_labeldata(atom,
self._smartdict['molecule_mode'])
atom_label = rendering.TextFollowerNode(text,
(r, r, r) + atom.coord[1:],
camera)
self.__nodes['atom_labels'].append(atom_label)
self.__renderer.AddActor(atom_label)
# reporting to the user
if self._smartdict['msgBar'] is not None :
self._smartdict['msgBar'].message('state', 'Structure')
def render_vibration(self, **kw) :
"""Render a vibration.
Keyword arguments :
vib_no -- number of the vibration
*must* be supplied if vib_toolbar is None
resolution -- resolution (default resources.NUM_RESOLUTION_VTK)
mode -- sphere, arrow representation or animation
one of
(resources.STRING_MODE_VIB_REPRESENTATION_SPHERES,
resources.STRING_MODE_VIB_REPRESENTATION_ARROWS,
resources.STRING_MODE_VIB_REPRESENTATION_ANIMATION)
*must* be supplied if vib_toolbar is None
rep_type -- cartesian or mass-weighted excursions
one of (resources.STRING_VIB_ENERGY,
STRING_VIB_EXCURSIONS)
*must* be supplied if vib_toolbar is None
rep_subtype -- representation subtype
one of (STRING_VIB_ENERGY_VOLUME,
STRING_VIB_ENERGY_VOLUME_ZERO_POINT,
STRING_VIB_EXCURSIONS_DIAMETER,
STRING_VIB_EXCURSIONS_DIAMETER_ZEROPOINT,
STRING_VIB_EXCURSIONS_DIAMETER_STANDARD)
*must* be supplied if vib_toolbar is None
scale_factor -- multiply factor for the amplitude of vibrational motion
*must* be supplied if vib_toolbar is None
invert_phase -- whether the phase of the vibration is to be inverted
*must* be supplied if vib_toolbar is None
color_sphere_1 -- color of the first hemisphere in the sphere mode, i.e.
mode = resources.STRING_MODE_VIB_REPRESENTATION_SPHERES
(default resources.COLOR_VIB_HEMISPHERE_1)
color_sphere_2 -- color of the first hemisphere in the sphere mode, i.e.
mode == resources.STRING_MODE_VIB_REPRESENTATION_ARROWS
(default resources.COLOR_VIB_HEMISPHERE_2)
"""
if self.__vib_toolbar is None and kw is None :
raise InvalidArgumentError(
'Vibrational toolbar or correspondent keywords must be given')
# do nothing if the representation type is not set
# for a static representation
if self.__vib_toolbar is not None and \
( self.__vib_toolbar.rep_type is None and \
resources.STRING_MODE_VIB_REPRESENTATION_ANIMATION != \
self.__vib_toolbar.mode ):
return
self._smartdict.merge()
self._smartdict.kw = kw
props = self._smartdict
## packing keywords for atoms
atoms_kw = dict()
# vibration properties
params_vib = ('mode', 'rep_type', 'rep_subtype', 'scale_factor',
'invert_phase')
for param in params_vib :
if self.__vib_toolbar :
atoms_kw[param] = getattr(self.__vib_toolbar, param)
else :
if props[param] is None :
raise InvalidArgumentError('Keyword %s must be supplied' % param)
atoms_kw[param] = props[param]
# vibration number
# negative vibrations mean translations / rotations
if self.__vib_toolbar is not None :
vib_no = self.__vib_toolbar.vib_no
else :
if props['vib_no'] is None :
raise InvalidArgumentError(
'vib_no must be given if a vibrational toolbar is not given')
vib_no = props['vib_no']
# frequency
# translations / rotations have the frequency of 0.
if 0 > vib_no :
freq = 0.
else :
freq = self.__molecule.freqs[vib_no]
if atoms_kw['invert_phase'] :
atoms_kw['color_sphere_1'] = self._smartdict['color_sphere_2']
atoms_kw['color_sphere_2'] = self._smartdict['color_sphere_1']
else :
atoms_kw['color_sphere_1'] = self._smartdict['color_sphere_1']
atoms_kw['color_sphere_2'] = self._smartdict['color_sphere_2']
atoms_kw['resolution'] = props['resolution']
## let the control change without waiting for the rendering to complete
self.tk.call('update', 'idletasks')
## choose an appropriate displacement type for a selected vibration
if resources.STRING_VIB_ENERGY == atoms_kw['rep_type'] :
if vib_no < 0 :
L_displ = self.__molecule.L_tr_rot[-vib_no]
else :
L_displ = self.__molecule.L[vib_no]
else :
if vib_no < 0 :
L_displ = self.__molecule.Lx_tr_rot[-vib_no]
else :
L_displ = self.__molecule.Lx[vib_no]
## rendering
self.cleanup()
## dynamic representation : animation
if resources.STRING_MODE_VIB_REPRESENTATION_ANIMATION == atoms_kw['mode'] :
# render the molecule in ball & stick representation
# do not render hydrogen bonds !
self.render_molecule(molecule_mode=resources.NUM_MODE_BALLSTICK,
bonds_mode=resources.NUM_MODE_BONDS_ATOMS_COLOR,
resolution=self._smartdict['resolution'],
rounded_bond=False,
hydrogen_bond=False,
atom_labels=False)
anim_kw = dict(Lx=sqrt(AMU2AU) * self.__molecule.Lx[vib_no],
scale_factor=atoms_kw['scale_factor'],
nFrames=5,
ms=30)
self.__animate_vibration_poll(**anim_kw)
## static representations : spheres & arrows
else :
# kill the animation if it runs
if self.__animator_id is not None :
self.after_cancel(self.__animator_id)
self.__animator_id = None
# render the molecule in monolith stick representation
# do not render hydrogen bonds !
self.render_molecule(molecule_mode=resources.NUM_MODE_STICK,
bonds_mode=resources.NUM_MODE_BONDS_MONOLITH_COLOR,
resolution=props['resolution'],
rounded_bond=False,
hydrogen_bond=False,
atom_labels=False)
# render
for atom in self.__molecule.atoms :
# exclude unnecessary atoms
if self.marked_vib_atoms is not None :
if self.show_marked_only and \
atom.index not in self.marked_vib_atoms :
continue
vibrating_atom_node = rendering.VibratingAtomNode(atom,
L_displ,
freq, **atoms_kw)
self.__nodes['vibatoms'].append(vibrating_atom_node)
self.__renderer.AddActor(vibrating_atom_node)
# mark the vibrational motion if all atoms will be shown
# and it is explicitely set by the 'mark fragment'
# do not mark if the user wants to see the fragment only !
mark_it = self.mark_fragment and self.marked_vib_atoms is not None \
and atom.index in self.marked_vib_atoms and \
not self.show_marked_only
if mark_it :
vibrating_atom_node.pick()
self.Render()
# reporting to the user
if self._smartdict['msgBar'] is not None :
p = (vib_no, freq, atoms_kw['rep_type'], atoms_kw['rep_type'])
self._smartdict['msgBar'].message(
'state', 'Vibration %d ( %.2f ), %s / %s' % p)
def render_triangle(self, i1, i2, i3, **kw) :
"""Render a triangle.
Positional arguments :
i1, i2, i3 -- coordinates of the vertices (one-based ndarray)
shape : (4,)
Keyword arguments :
see pyviblib.gui.rendering.BaseNode
"""
Natoms = self.__molecule.Natoms
indices = (i1, i2, i3)
for i in indices :
if 0 > i or i >= Natoms :
raise InvalidArgumentError('Invalid atom index passed : %d' % i)
# rendering
p = []
for i in indices :
p.append(self.__molecule.atoms[i].coord)
triag = rendering.TriangleNode(*p, **kw)
self.__nodes['triangles'].append(triag)
self.__renderer.AddActor(triag)
self.Render()
def render_scalars(self, scalars, scale_factor=1.0) :
"""Render scalar values on the atoms such as e.g. ACPs.
Position arguments :
scalars -- values to be rendered (one-based array)
shape : (1 + Natoms) with Natoms being the number of atoms
in the molecule
Keyword arguments :
scale_factor -- numeric factor with which the radii of the spheres are
multiplied
(default 1.)
"""
if self.__molecule is None :
return
if scalars is None or (1 + self.__molecule.Natoms) != len(scalars) :
raise InvalidArgumentError('Invalid arguments passed')
# render the molecule in monolith stick representation
self.render_molecule(molecule_mode=resources.NUM_MODE_STICK,
bonds_mode=resources.NUM_MODE_BONDS_MONOLITH_COLOR,
hydrogen_bond=False,
resolution=self._smartdict['resolution'])
# scalars on the atom positions
if 'scalars' not in self.__nodes :
self.__nodes['scalars'] = []
for atom in self.__molecule.atoms :
scalar_node = rendering.ScalarSphereNode(
scalars[atom.index],
atom.coord[1:],
mode=resources.NUM_MODE_PROPORTIONAL_TO_SURFACE,
resolution=self._smartdict['resolution'],
scale_factor=scale_factor)
self.__nodes['scalars'].append(scalar_node)
self.__renderer.AddActor(scalar_node)
def render_gcp(self, acp, groups, scale_factor=1.0) :
"""Render group contribution patterns (GCPs).
The contributions are placed in center of gravities of the groups.
Positional arguments :
acp -- atomic contribution patterns (one-based ndarray)
shape : (1 + Ngr,) with Ngr being the number of groups
groups -- groups (null-based list)
atom numbers are one-based
example : [[1, 4], [2, 7]]
Keyword arguments :
scale_factor -- numeric factor with which the radii of the spheres are
multiplied
(default 1.)
"""
if self.__molecule is None :
return
# trying to sum up the acps
acp_g = make_gcp(acp, groups)
# render the molecule in monolith stick representation
self.render_molecule(molecule_mode=resources.NUM_MODE_STICK,
bonds_mode=resources.NUM_MODE_BONDS_MONOLITH_COLOR,
resolution=self._smartdict['resolution'],
rounded_bond=True,
hydrogen_bond=False,
atom_labels=False)
# saving references to the vtk objects
if 'acp_groups' not in self.__nodes :
self.__nodes['acp_groups'] = []
# rendering the scalars acp_g in the mass centers of the groups
for g in xrange(len(groups)) :
# mass center
grp = array([0] + groups[g], 'l')
M_g = mass_center(self.__molecule.coords, self.__molecule.masses, grp)
# sphere
scalar_node = rendering.ScalarSphereNode(
acp_g[1 + g],
M_g[1:],
mode=resources.NUM_MODE_PROPORTIONAL_TO_SURFACE,
resolution=self._smartdict['resolution'],
scale_factor=scale_factor)
self.__nodes['acp_groups'].append(scalar_node)
self.__renderer.AddActor(scalar_node)
# text following labeling the group
r = scalar_node.get_radius()
group_label = rendering.TextFollowerNode('%d' % (1 + g),
(r, r, r) + M_g[1:],
self.camera)
self.__nodes['acp_groups'].append(group_label)
self.__renderer.AddActor(group_label)
def rotate(self, euler_azimuth=0., euler_elevation=0., euler_roll=0.):
"""Rotate the active camera at given Euler angles.
Keyword arguments :
euler_azimuth -- angle of rotation about the view up vector
centered at the focal point of the camera
(default 0.)
euler_elevation -- angle of rotation about the cross product of the
direction of projection and the view up vector
centered on the focal point
(default 0.)
euler_roll -- angle of rotation about the direction of projection
of the camera
(default 0.)
The rotation does *not* affect the synchronized widget.
"""
self._CurrentCamera = self.camera
if self._CurrentCamera is None :
return
self._CurrentCamera.Azimuth(euler_azimuth)
self._CurrentCamera.Elevation(euler_elevation)
self._CurrentCamera.Roll(euler_roll)
self._CurrentCamera.OrthogonalizeViewUp()
self.__renderer.ResetCameraClippingRange()
self.Render()
self.__mouse_motion = True
def zoom(self, zoomFactor):
"""Zoom in/out.
Positional arguments :
zoomFactor -- the height of the viewport in world-coordinate distances.
Note that this factor works as an "inverse scale" i.e.
larger numbers produce smaller images.
This factor has no meaning in perspective projection mode.
The zooming does *not* affect the synchronized widget.
"""
renderer = self.__renderer
self._CurrentZoom *= zoomFactor
if self.camera.GetParallelProjection():
parallelScale = self.camera.GetParallelScale() / zoomFactor
self.camera.SetParallelScale(parallelScale)
else:
self.camera.Dolly(zoomFactor)
renderer.ResetCameraClippingRange()
self.Render()
def start_pairs_picking(self, pairs, callback) :
"""Start a synchronous picking of atoms in two render widgets.
Positional arguments :
pairs -- reference to the picked atom pairs
callback -- callable function which accepts one argument being the
the widget
"""
if pairs is None :
raise InvalidArgumentError('Invalid pairs parameter')
if not callable(callback) :
raise InvalidArgumentError('callback must be a callable')
self.do_picking = True
self.__picked_atom_pairs = pairs
self.__picked_atom_pairs_callback = callback
def end_pairs_picking(self) :
"""End the synchronous picking of atoms started with start_pairs_picking().
"""
self.do_picking = False
self.__picked_atom_pairs = None
self.__picked_atom_pairs_callback = None
def depick_atoms(self, atom_index_list=None) :
"""Deselect the picked atoms.
Keyword arguments :
atom_index_list -- list of 0-based indices of *PICKED* atoms
these are NOT indices of atoms to be unpicked !!!
If None, use all the *PICKED* atoms
If the only element in the list is -1 -> depick the last picked atom.
"""
if self.__picked_atoms_indices is None :
return
Natoms_picked = len(self.__picked_atoms_indices)
# if list is not given -> remove all picked atoms
if atom_index_list is None :
atom_index_list = xrange(Natoms_picked)
elif 1 == len(atom_index_list) and -1 in atom_index_list :
atom_index_list = [Natoms_picked - 1]
# depicking
for a in atom_index_list :
if 0 <= a and a < Natoms_picked :
self.__nodes['atoms'][self.__picked_atoms_indices[a]].unpick()
# finally removing from the list
misc.remove_indices_from_list(self.__picked_atoms_indices, atom_index_list)
self.Render()
def pick_atoms(self, atom_index_list=None) :
"""Pick given atoms.
If the atom list is not supplied, all the atoms are picked.
Positional arguments :
atom_index_list -- atom indices (null-based ndarray)
indices are null-based
"""
# if list is None pick all atoms
if atom_index_list is None :
atom_index_list = xrange(self.molecule.Natoms)
for a in atom_index_list :
self.__nodes['atoms'][a].pick()
# save the picked atom if it is not in the list
if a not in self.__picked_atoms_indices :
self.__picked_atoms_indices.append(a)
self.Render()
def highlight_picked_atoms(self, atom_index_list, highlight=True) :
"""Highlight/unhighlight the picked atoms.
Nothing is done the atom list is not supplied.
Positional arguments :
atom_index_list -- atom indices (null-based ndarray)
indices are null-based
Keyword arguments :
highlight -- whether to highlight (default True)
"""
if self.__picked_atoms_indices is None :
return
Natoms_picked = len(self.__picked_atoms_indices)
# highlight
for a in atom_index_list :
if 0 > a or a >= Natoms_picked :
continue
atom_node = self.__nodes['atoms'][self.__picked_atoms_indices[a]]
atom_node.highlight_picked(highlight)
self.Render()
def remove_triangles(self) :
"""Remove the triangles."""
if not self.__renderer or 0 == len(self.__nodes['triangles']) :
return
for triag in self.__nodes['triangles'] :
self.__renderer.RemoveActor(triag)
del self.__nodes['triangles'][:]
self.Render()
def synchronize_camera_state(self, sync_widget) :
"""Set a render widget for synchronous rotation/zooming.
By default the rotation and zooming are synchronized.
Consider the do_sync_rotation and do_sync_zoom properties to control the
synchronized behaviour.
Positional arguments :
sync_widget -- the render widget
can be None
"""
self._sync_widget = sync_widget
self.do_sync_rotation = True
self.do_sync_zoom = True
def get_node(self, category, index) :
"""Get a node.
Positional arguments :
category -- type of the node
one of ('atoms', 'bonds', 'vibatoms', 'atom_labels')
index -- index of the node (null-based)
Return None unless argument are valid or nothing is found.
"""
if category is None or category not in self.__nodes :
return None
if 0 > index or len(self.__nodes[category]) <= index :
return None
return self.__nodes[category][index]
def snapshot(self, filename,
format='tiff', magnification=1, resolution=None) :
"""Make a snapshot of the 3D render window.
Positional arguments :
filename -- file name of the snapshot
Keyword arguments :
format -- image format
one of ('jpeg', 'tiff', 'png', 'eps', 'ppm')
(default 'tiff')
magnification -- integer magnification factor for the 3D render window
(default 1)
resolution -- resolution (default None)
if None, use the current resolution
"""
# assigning an appropriate writer
format = format.lower()
if format not in resources.STRINGS_VTK_SNAPSHOT_FORMATS :
raise InvalidArgumentError('Unsupported format : %s' % format)
writer = None
if 'jpeg' == format :
writer = vtk.vtkJPEGWriter()
writer.SetQuality(100)
elif 'tiff' == format :
writer = vtk.vtkTIFFWriter()
elif 'png' == format :
writer = vtk.vtkPNGWriter()
elif 'eps' == format :
writer = vtk.vtkPostScriptWriter()
elif 'ppm' == format :
writer = vtk.vtkPNMWriter()
# setting the resolution
if resolution :
self.resolution = resolution
self.Render()
# setting the image filter & saving
image_filter = vtk.vtkWindowToImageFilter()
image_filter.SetMagnification(magnification)
image_filter.SetInput(self.GetRenderWindow())
writer.SetInput(image_filter.GetOutput())
writer.SetFileName(filename)
writer.Write()
# reminding to vtk that the actual object should be seen in the window
self.Render()
def create_animation_frames(self, Lx, scale_factor, nFrames,
resolution=10, format='ppm',
transparent_bg=False) :
"""Create a series of files for animation of a vibration.
The function does not compile the result images to the animation.
It produces the files and returns the name of file with a list
of the created frames.
The files will be stored in a temporaral directory.
Positional arguments :
Lx -- cartesian excursion of the vibration (one-based ndarray)
shape : (1 + Natoms, 4) with Natoms being the number of
atoms
scale_factor -- numeric factor with which the radii of the spheres are
multiplied
nFrames -- number of frame pro direction
Keyword arguments :
resolution -- resolution (default 10)
format -- image format
one of ('jpeg', 'tiff', 'png', 'eps', 'ppm', 'gif')
(default 'ppm')
if 'gif' is supplied, then the ppmtogif utility from the
Netpbm package should be installed
See http://netpbm.sourceforge.net/
transparent_bg -- whether the background should be made transparent
(default False)
"""
if Lx is None or 0. > scale_factor or 0 > nFrames or 0 > resolution or \
not (format.lower() in resources.STRINGS_VTK_SNAPSHOT_FORMATS or \
format.lower() == 'gif') :
raise InvalidArgumentError('Invalid input parameter(s) passed')
# if an animation runs -> stop it
if self.__animator_id :
self.after_cancel(self.__animator_id)
# rendering the molecule with the specified resolution
self.render_molecule(resolution=resolution,
molecule_mode=resources.NUM_MODE_BALLSTICK,
bonds_mode=resources.NUM_MODE_BONDS_ATOMS_COLOR,
rounded_bond=False,
hydrogen_bond=False,
atom_labels=False)
# make sure that all GUI elements are updated
self.tk.call('update')
## making snapshots & a list file :
# 1) from 0 to nFrames
# 2) from nFrames to -nFrames
# 3) from -nFrames to 0
range_ = range(nFrames) + range(nFrames, -nFrames, -1) + range(-nFrames, 0)
# creating a temporaral directory for storing of the files
# list file will be saved there
tmpdir = tempfile.mkdtemp()
listfile = os.path.join(tmpdir, 'list')
file_ = open(listfile, 'w+')
# total number of frames
nTotalFrames = 4 * nFrames
#
no_fields = 1 + int(floor(log(nTotalFrames, 10)))
format_file = os.path.join(tmpdir, 'frame%%0%dd.%%s' % no_fields)
start_time = clock()
for i in xrange(len(range_)) :
frameno = range_[i]
# saving to the list file
filename = format_file % (i, format)
file_.write(filename + '\n')
# sending a message to the status bar of the molecule window
if self._smartdict['msgBar'] is not None :
self._smartdict['msgBar'].message('state',
'Saving frame %d / %d' % \
(1 + i, nTotalFrames))
self.__render_animation_frame(Lx, scale_factor, frameno, nFrames)
self.update()
# if format is gif one needs first to generate a ppm file
# and then start ppmtogif to convert it finally to gif
# raising an exception if this conversion failed
if 'gif' == format.lower() :
# producing ppm
ppmname = format_file % (i, 'ppm')
self.snapshot(ppmname, format='ppm', magnification=1)
# using Netpbm utilities to convert it to gif
palette = os.path.join(tmpdir, 'palette')
gifname = format_file % (i, 'gif')
cmd = r'pnmcolormap 256 "%s" > "%s" && ' % (ppmname, palette) + \
r'pnmremap -mapfile "%s" "%s" | ppmtogif ' % \
(palette, ppmname)
if transparent_bg :
cmd = ''.join((cmd, r'--transparent="%s"' % self.background))
cmd = ''.join((cmd, r'> "%s"' % gifname))
if os.system(cmd) :
raise RuntimeError(
r'Conversion to "%s" failed :(' % os.path.basename(gifname))
else :
self.snapshot(filename, format=format, magnification=1)
file_.close()
if self._smartdict['msgBar'] is not None :
total_time = clock() - start_time
self._smartdict['msgBar'].message(
'state',
'Completed in %.3fs (%.3fs pro frame)' % \
(total_time, total_time / nTotalFrames))
return listfile
class ButtonToolbar(BaseWidget, Tkinter.Frame) :
"""Button toolbar.
The widget is based on Tkinter.Frame.
The following public methods are exported :
add_button() -- add a button
add_separator() -- add a separator
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
style -- style of the toolbar
0 : raised with a thin border
1 : flat without any border
(default 0)
horizontal -- whether the toolbar is horizontal (default True)
button_pad -- padding for the buttons being added (default 3)
"""
Tkinter.Frame.__init__(self, master, **self._frame_kw(**kw))
BaseWidget.__init__(self, **kw)
def _frame_kw(**kw) :
"""Retrieve the keywords for the Tkinter.Frame."""
frame_kw = {}
style = kw.get('style', 0)
# raised frame with a thin border
if 0 == style :
frame_kw['borderwidth'] = 1
frame_kw['relief'] = 'raised'
# flat frame without any border
elif 1 == style :
frame_kw['borderwidth'] = 0
frame_kw['relief'] = 'flat'
else :
raise ConstructorError('Invalid style: %s' % style)
return frame_kw
_frame_kw = staticmethod(_frame_kw)
def _init_vars(self) :
"""Initialize variables."""
self._smartdict['style'] = 0
self._smartdict['horizontal'] = True
self._smartdict['button_pad'] = 3
# default keywords for buttons
if 0 == self._smartdict['style'] :
self._varsdict['button_relief'] = 'flat'
self._varsdict['button_overrelief'] = 'raised'
elif 1 == self._smartdict['style'] :
self._varsdict['button_relief'] = 'raised'
self._varsdict['button_overrelief'] = 'raised'
# last value of row or column depending on if the toolbar is horizonal
self._varsdict['grid_counter'] = 0
# list of the buttons
self._varsdict['ar_buttons'] = []
def _constructGUI(self) :
"""Nothing is done since the buttons are added dynamically."""
pass
def __get_grid_params(self, forbutton=True) :
"""Get the grid parameters for self.grid().
Keyword arguments :
forbutton -- True for an usual button, False for a separator (default True)
The grid counter will not be incremented !
"""
if self._smartdict['horizontal'] :
row = 0
column = self._varsdict['grid_counter']
if forbutton :
sticky = 'w'
else :
sticky = 'ns'
else :
row = self._varsdict['grid_counter']
column = 0
if forbutton :
sticky = 'n'
else :
sticky = 'we'
return dict(row=row,
column=column,
padx=self._smartdict['button_pad'],
pady=self._smartdict['button_pad'],
sticky=sticky)
def add_button(self, **kw) :
"""Add a button.
The methods accepts the keyword arguments for Tkinter.Button.
Keyword arguments (specific for this method):
helptext -- help message which is shown if the mouse is over the button
(default None)
imagename -- name of the image resource
either the imagename or image argument must be supplied
Return the reference to the button created.
"""
# customizing the button's keywords
if 'helptext' in kw :
helptext = kw['helptext']
del kw['helptext']
# images
if 'image' in kw and 'imagename' in kw :
raise InvalidArgumentError(
'Either the image or imagename arguments must be supplied')
if 'imagename' in kw :
kw['image'] = getimage(kw['imagename'])
del kw['imagename']
kw['relief'] = kw.get('relief', self._varsdict['button_relief'])
kw['overrelief'] = kw.get('overrelief', self._varsdict['button_overrelief'])
btn = Tkinter.Button(self, **kw)
btn.grid(**self.__get_grid_params(forbutton=True))
# saving
self._varsdict['ar_buttons'].append(btn)
# help
if helptext :
self._balloon.bind(btn, helptext)
self._varsdict['grid_counter'] += 1
return btn
def add_separator(self) :
"""Add a separator."""
kw = dict(bd=1, relief='sunken')
if self._smartdict['horizontal'] :
kw['width'] = 2
else :
kw['height'] = 2
separator = Tkinter.Frame(self, **kw)
separator.grid(**self.__get_grid_params(forbutton=False))
self._varsdict['grid_counter'] += 1
class VibrationalToolbar (BaseWidget, Pmw.ScrolledFrame) :
"""Widget for navigating through vibrations.
The widget is based on Pmw.ScrolledFrame.
The following read-only properties are exposed :
mode -- sphere, arrow, animation or structure
rep_type -- cartesian or mass-weighted excursions
rep_subtype -- representation subtype
invert_phase -- whether the phase is to be inverted
scale factor -- factor for the amplitude of vibrational motion
The following readable and writable properties are exposed :
vib_no -- number of the current vibration
The following public methods are exported :
go_backward() -- go one vibration backward
go_forward() -- go one vibration forward
go_first() -- go to the first vibration
go_last() -- go to the last vibration
increase_scale_factor() -- increase the scale factor
decrease_scale_factor() -- decrease the scale factor
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
The size of the toolbar is hardcoded to be 1000x105.
Positional arguments :
master -- parent widget
Keyword arguments :
freqs -- wavenumbers of the vibrations (one-based ndarray)
shape : (1 + NFreq, ) with NFreq being the number of
vibrations
render_callback -- called if a vibration is to rendered (default None)
the callable does not require any arguments.
"""
Pmw.ScrolledFrame.__init__(self, master,
usehullsize=True,
hull_width=1000,
hull_height=105,
horizflex='expand',
vertflex='expand',
hscrollmode='static')
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
if self._smartdict['freqs'] is not None :
self.__NFreq = len(self._smartdict['freqs']) - 1
else :
self.__NFreq = -1
if self._smartdict['render_callback'] is not None \
and not callable(self._smartdict['render_callback']) :
raise InvalidArgumentError('Invalid render_callback supplied')
def _constructGUI(self) :
"""Construct the GUI of the widget."""
## create a group with the title "Vibrational toolbar"
group = Pmw.Group(self.interior(), tag_text='Vibrational toolbar')
group.pack(padx=3, pady=3)
column = 0
## vibrational rendering mode
width = max(len(resources.STRINGS_MODE_VIB_REPRESENTATION[0]),
len(resources.STRINGS_MODE_VIB_REPRESENTATION[1]),
len(resources.STRINGS_MODE_VIB_REPRESENTATION[2]))
self._varsdict['options_mode'] = Pmw.OptionMenu(
group.interior(),
labelpos='w',
label_text='Mode : ',
items=resources.STRINGS_MODE_VIB_REPRESENTATION,
menubutton_width=width,
command=self.__change_mode)
self._varsdict['options_mode'].grid(row=0, column=column,
padx=3, pady=0, sticky='w')
column += 1
## frame with vibrations navigation
self._varsdict['vibframe'] = VibNavigationFrame(
group.interior(),
self._smartdict['freqs'],
changed_callback=self.__changed_vibrations)
self._varsdict['vibframe'].grid(row=0, column=column,
padx=3, pady=3, sticky='w')
column += 1
## frame with selection of the displacement types
frame_select = Tkinter.Frame(group.interior(),
borderwidth=2, relief='sunken',
padx=3, pady=3)
frame_select.grid(row=0, column=column, padx=3, pady=3)
column += 1
# Energy or excursions choice
widget = Pmw.OptionMenu(frame_select,
menubutton_width=20,
command=self.__change_rep_type)
self._varsdict['options_respresentation'] = widget
self._varsdict['options_respresentation'].grid(row=0, column=1,
padx=3, pady=0, sticky='ew')
# radio buttons
widget = Pmw.RadioSelect(frame_select,
buttontype='radiobutton',
orient='horizontal',
command=self.__change_rep_type)
self._varsdict['radio_select'] = widget
self._varsdict['radio_select'].add(resources.STRING_VIB_ENERGY)
self._varsdict['radio_select'].add(resources.STRING_VIB_EXCURSIONS)
self._varsdict['radio_select'].grid(row=0, column=0, padx=3, pady=0)
## frame with the scale factor and show button
frame_show = Tkinter.Frame(group.interior(), borderwidth=2,
relief='sunken', padx=3, pady=3)
frame_show.grid(row=0, column=column, padx=3, pady=3)
column += 1
# entry field scale factor with a validator :)
validate = dict(validator='real',
min=0.,
max=10.,
separator='.')
widget = Pmw.Counter(frame_show,
labelpos='w',
label_text='Scale factor:',
label_justify='left',
entry_width=4,
entryfield_value='1.0',
entryfield_modifiedcommand=\
self._smartdict['render_callback'],
datatype=dict(counter='real', separator='.'),
entryfield_validate=validate,
autorepeat=False,
increment = 0.1)
self._varsdict['counter_scale_factor'] = widget
self._varsdict['counter_scale_factor'].pack(side='left', padx=5, pady=3)
# invert phase checkbox (off by default)
self._varsdict['var_invert_phase'] = Tkinter.IntVar()
self._varsdict['var_invert_phase'].set(0)
widget = Tkinter.Checkbutton(frame_show,
text='Invert phase',
variable=self._varsdict['var_invert_phase'],
command=self.__invert_phase)
self._varsdict['check_invert'] = widget
self._varsdict['check_invert'].pack(side='left', padx=3, pady=3)
# disabling the controlls if the frequencies were not given
self.__enable_controls(-1 != self.__NFreq)
def _declare_properties(self) :
"""Declare properties of the widget."""
# Mode : spheres, arrows, animation or structure.
self.__class__.mode = property(fget=self._get_mode)
# Vibration number from 1 to N (writable).
self.__class__.vib_no = property(fget=self._get_vib_no,
fset=self._set_vib_no)
# Radius scale factor.
self.__class__.scale_factor = property(fget=self._get_scale_factor)
# Volume or surface excursions
self.__class__.rep_type = property(fget=self._get_rep_type)
# Subtype according to value of rep_type
self.__class__.rep_subtype = property(fget=self._get_rep_subtype)
# Invert phase
self.__class__.invert_phase = property(fget=self._get_invert_phase)
def _bind_help(self) :
"""Bind help messages to the GUI components."""
self._balloon.bind(self._varsdict['options_mode'],
'Rendering mode of vibrational motion.')
self._balloon.bind(self._varsdict['options_respresentation'],
'Options appropriate for the current representation type.')
self._balloon.bind(self._varsdict['radio_select'],
'Choose between vibrational energy and ' + \
'surface excursions representations.')
self._balloon.bind(self._varsdict['counter_scale_factor'],
'Set the scale factor for the diameter of spheres.')
self._balloon.bind(self._varsdict['check_invert'],
'Invert the phase of a vibration.')
def __enable_controls(self, enable_=True) :
"""Enable/disable the controls."""
if enable_ :
state = 'normal'
else :
state = 'disabled'
# vib frame
self._varsdict['vibframe'].enable_controls(enable_)
# the rest controls
for control in (self._varsdict['radio_select'].button(0),
self._varsdict['radio_select'].button(1)) :
control.configure(state=state)
for option_control in (self._varsdict['options_mode'],
self._varsdict['options_respresentation']) :
option_control.configure(menubutton_state=state)
self._varsdict['counter_scale_factor'].configure(entry_state=state)
self._varsdict['check_invert'].configure(state=state)
def __change_mode(self, tag) :
"""Change the vibrational representation mode."""
# if changed to the animation -> set Lx / standard & block it
state = 'normal'
if resources.STRING_MODE_VIB_REPRESENTATION_ANIMATION == self.mode :
state = 'disabled'
self._varsdict['radio_select'].setvalue(resources.STRING_VIB_EXCURSIONS)
self._varsdict['options_respresentation'].setvalue(
resources.STRING_VIB_EXCURSIONS_DIAMETER_STANDARD)
self._varsdict['radio_select'].button(0).configure(state=state)
self._varsdict['radio_select'].button(1).configure(state=state)
self._varsdict['options_respresentation'].configure(menubutton_state=state)
if callable(self._smartdict['render_callback']) :
self._smartdict['render_callback']()
def __changed_vibrations(self, p) :
"""VibNavigation frame changed callback."""
if callable(self._smartdict['render_callback']) :
self._smartdict['render_callback']()
def __change_rep_type(self, tag) :
"""Changes the representation option menu."""
cur_sel = self._varsdict['radio_select'].getvalue()
if resources.STRING_VIB_ENERGY == cur_sel :
self._varsdict['options_respresentation'].setitems(
resources.STRINGS_ENERGY_VOLUME_REPRESENTATIONS)
else :
self._varsdict['options_respresentation'].setitems(
resources.STRINGS_EXCURSIONS_DIAMETER_REPRESENTATIONS)
if callable(self._smartdict['render_callback']) :
self._smartdict['render_callback']()
def __invert_phase(self) :
"""Invert the phase."""
if callable(self._smartdict['render_callback']) :
self._smartdict['render_callback']()
def _set_vib_no(obj, vib_no) :
"""Setter function for the vib_no property."""
obj._varsdict['vibframe'].vib_no = vib_no
_set_vib_no = staticmethod(_set_vib_no)
def _get_vib_no(obj) :
"""Getter function for the vib_no property."""
return obj._varsdict['vibframe'].vib_no
_get_vib_no = staticmethod(_get_vib_no)
def _get_mode(obj) :
"""Getter function for the mode property."""
return obj._varsdict['options_mode'].getvalue()
_get_mode = staticmethod(_get_mode)
def _get_scale_factor(obj) :
"""Getter function for the scale_factor property.
Return 0 if failed to convert the user input to the float value.
"""
try :
scale_factor = float(obj._varsdict['counter_scale_factor'].get())
except :
scale_factor = 0.
return scale_factor
_get_scale_factor = staticmethod(_get_scale_factor)
def _get_rep_type(obj) :
"""Getter function for the rep_type property."""
return obj._varsdict['radio_select'].getvalue()
_get_rep_type = staticmethod(_get_rep_type)
def _get_rep_subtype(obj) :
"""Getter function for the rep_subtype property."""
return obj._varsdict['options_respresentation'].getvalue()
_get_rep_subtype = staticmethod(_get_rep_subtype)
def _get_invert_phase(obj) :
"""Getter function for the invert_phase property."""
return obj._varsdict['var_invert_phase'].get()
_get_invert_phase = staticmethod(_get_invert_phase)
def go_backward(self, *dummy) :
"""Go one vibration back."""
self.vib_no = self.vib_no - 1
def go_forward(self, *dummy) :
"""Go one vibration forward."""
self.vib_no = self.vib_no + 1
def go_first(self) :
"""Go to the first vibration."""
self.vib_no = 1
def go_last(self) :
"""Go to the last vibration."""
self.vib_no = self.__NFreq
def increase_scale_factor(self, *dummy) :
"""Increase the scale factor."""
self._varsdict['counter_scale_factor'].increment()
def decrease_scale_factor(self, *dummy) :
"""Decrease the scale factor."""
self._varsdict['counter_scale_factor'].decrement()
class VibrationalToolbarLight(BaseWidget, Tkinter.Frame) :
"""Like VibrationalToolbar but for exploring single vibration.
Supports also marking of vibrational motion on a fragment.
The following readable and writable properties are exposed :
vib_no -- number of the vibration
rep_type -- cartesian or mass-weighted excursions
rep_subtype -- representation subtype
invert_phase -- whether the phase is to be inverted
sync_toolbar -- VibrationalToolbarLight to synchronize with
The following properties are exposed uf fragment_controls=True was supplied
in the constructor of the class :
mark_fragment -- fragment to be marked
show_marked_only -- show vibrational motion only on the fragment
The following public methods are exported :
increase_scale_factor() -- increase the scale factor
decrease_scale_factor() -- decrease the scale factor
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
rep_type -- cartesian or mass-weighted excursions
one of (resources.STRING_VIB_ENERGY,
STRING_VIB_EXCURSIONS)
(no default)
rep_subtype -- representation subtype
one of (STRING_VIB_ENERGY_VOLUME,
STRING_VIB_ENERGY_VOLUME_ZERO_POINT,
STRING_VIB_EXCURSIONS_DIAMETER,
STRING_VIB_EXCURSIONS_DIAMETER_ZEROPOINT,
STRING_VIB_EXCURSIONS_DIAMETER_STANDARD)
(no default)
scale_factor -- multiply factor for the amplitude of
vibrational motion
(no default)
invert_phase -- whether the phase of the vibration is to be inverted
(no default)
fragment_controls -- whether to create the controls for marking fragment
(default True)
mark_fragment -- whether to mark vibrational motion of a fragment
(default False)
show_marked_only -- whether to show vibrational motion on a fragment only
(default False)
show_gcm -- whether to show the Raman/ROA generation button
(default False)
sync_toolbar -- VibrationalToolbarLight to synchronize with
(default None)
invert_roa -- whether to invert the sign of ROA (default False)
"""
Tkinter.Frame.__init__(self, master, relief='raised', borderwidth=2)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
# default keywords values
self._smartdict['invert_phase'] = False
self._smartdict['scale_factor'] = 1.0
# show the fragment GUI controls by default
self._smartdict['fragment_controls'] = True
# fragment gui keywords
#self._smartdict['mark_fragment'] = True
self._smartdict['mark_fragment'] = False
self._smartdict['show_marked_only'] = False
# show_acp
self._smartdict['show_gcm'] = False
# validating
if self._smartdict['rep_type'] is not None and self._smartdict['rep_type']\
not in resources.STRINGS_VIB_REPRESENTATIONS :
raise ConstructorError(
'Invalid representation type : %s' % self._smartdict['rep_type'])
if self._smartdict['rep_subtype'] is not None and \
self._smartdict['rep_subtype'] not in \
resources.STRINGS_VIB_REPRESENTATIONS_ALL :
raise ConstructorError(
'Invalid representation subtype : %s' % self._smartdict['rep_subtype'])
if not isinstance(self._smartdict['scale_factor'], float) and \
self._smartdict['scale_factor'] < 0. :
raise ConstructorError(
'Invalid scale factor : %s' % self._smartdict['scale_factor'])
if not isinstance(self._smartdict['invert_phase'], int) :
raise ConstructorError(
'Invalid value of invert_phase : %s' % self._smartdict['invert_phase'])
# if the synchronized toolbar is given it must be
# instance of the same class
if self._smartdict['sync_toolbar'] is not None and not \
isinstance(self._smartdict['sync_toolbar'], VibrationalToolbarLight) :
raise ConstructorError('Invalid sync_toolbar argument')
def _constructGUI(self) :
"""Construct the GUI of the widget."""
# saving variables for the controls
self._varsdict['var_invert_phase'] = Tkinter.IntVar()
self._varsdict['var_scale_factor'] = Tkinter.StringVar()
self._varsdict['var_invert_phase'].set(self._smartdict['invert_phase'])
self._varsdict['var_scale_factor'].set(self._smartdict['scale_factor'])
# these depend on if one wants to see the fragment GUI controls
if self._smartdict['fragment_controls'] :
self._varsdict['var_mark_fragment'] = Tkinter.IntVar()
self._varsdict['var_show_marked_only'] = Tkinter.IntVar()
self._varsdict['var_mark_fragment'].set(self._smartdict['mark_fragment'])
self._varsdict['var_show_marked_only'].set(
self._smartdict['show_marked_only'])
#
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
## frame with selection of the displacement types
frm_select = Tkinter.Frame(self, borderwidth=2, relief='sunken',
padx=3, pady=3)
frm_select.grid(row=0, column=0, padx=3, pady=3, sticky='we')
frm_select.grid_rowconfigure(0, weight=1)
frm_select.grid_columnconfigure(0, weight=1)
frm_select.grid_columnconfigure(1, weight=1)
# Energy or excursions choice
widget = Pmw.RadioSelect(frm_select,
buttontype='radiobutton',
orient='horizontal',
command=self.__change_rep_type)
self._varsdict['radio_select'] = widget
self._varsdict['radio_select'].add(resources.STRING_VIB_ENERGY)
self._varsdict['radio_select'].add(resources.STRING_VIB_EXCURSIONS)
self._varsdict['radio_select'].grid(row=0, column=0,
padx=3, pady=0, sticky='w')
# options for the chosen type
widget = Pmw.OptionMenu(frm_select,
menubutton_width=20,
command=self.__change_rep_subtype)
self._varsdict['options_respresentation'] = widget
self._varsdict['options_respresentation'].grid(row=0, column=1,
padx=3, pady=0, sticky='w')
# installing the smaller fonts for monitors narrow or equal than 1024
if 1024 >= self.winfo_screenwidth() :
font = tkFont.Font(
self, font=self._varsdict['radio_select'].button(0).cget('font'))
font.configure(size=int(font.cget('size')) - 1)
self._varsdict['radio_select'].button(0).configure(font=font)
self._varsdict['radio_select'].button(1).configure(font=font)
self._varsdict['options_respresentation'].configure(menubutton_font=font)
self._varsdict['options_respresentation'].configure(menu_font=font)
## second line of the toolbar
# scale factor & invert button
frm_show = Tkinter.Frame(self, borderwidth=2, relief='sunken',
padx=3, pady=3)
frm_show.grid(row=1, column=0, padx=3, pady=3, sticky='we')
frm_show.grid_rowconfigure(0, weight=1)
frm_show.grid_columnconfigure(0, weight=1)
column = 0
validate = dict(validator='real',
min=0.0,
max=66.0,
separator='.')
widget = Pmw.Counter(frm_show,
entry_width=3,
entryfield_value='1.0',
entry_textvariable=self._varsdict['var_scale_factor'],
entryfield_modifiedcommand=self.__render,
datatype=dict(counter='real', separator='.'),
entryfield_validate=validate,
autorepeat=False,
increment = 0.1)
self._varsdict['counter_scale_factor'] = widget
self._varsdict['counter_scale_factor'].grid(row=0, column=column,
padx=3, pady=3, sticky='w')
column += 1
# show the structure
widget = Pmw.RadioSelect(frm_show,
selectmode='multiple',
command=misc.Command(self.__render))
self._varsdict['radio_structure'] = widget
self._varsdict['radio_structure'].grid(row=0, column=column,
padx=3, pady=3, sticky='e')
column += 1
self._varsdict['radio_structure'].add('structure',
image=getimage('structure'))
# optional gcm button
# add if a reference to the main application is given and
# the Raman/ROA data are available
if self._smartdict['show_gcm'] and \
self._smartdict['mainApp'] is not None and \
self.__are_raman_roa_available() :
self._varsdict['btn_show_gcm'] = Tkinter.Button(frm_show,
image=getimage('gcm'),
relief='flat',
overrelief='raised',
command=self.__show_gcm)
self._varsdict['btn_show_gcm'].grid(row=0, column=column,
padx=3, pady=3, sticky='e')
column += 1
# invert the phase of a vibration
self._varsdict['check_invert'] = Tkinter.Checkbutton(
frm_show,
text='Invert phase',
variable=self._varsdict['var_invert_phase'],
command=self.__render)
self._varsdict['check_invert'].grid(row=0, column=column,
padx=3, pady=3, sticky='e')
column += 1
# optional fragment controls
if self._smartdict['fragment_controls'] :
self._varsdict['check_mark_fragment'] = Tkinter.Checkbutton(
frm_show,
text='Mark fragment',
variable=self._varsdict['var_mark_fragment'],
command=self.__change_mark_fragment)
self._varsdict['check_mark_fragment'].grid(row=0, column=column,
padx=3, pady=3, sticky='e')
column += 1
self._varsdict['check_show_marked_only'] = Tkinter.Checkbutton(
frm_show,
text='Fragment only',
variable=self._varsdict['var_show_marked_only'],
command=self.__change_show_marked_only)
self._varsdict['check_show_marked_only'].grid(row=0, column=column,
padx=3, pady=3, sticky='e')
column += 1
## setting the initial values if given
if self._smartdict['rep_type'] is not None :
#
self._varsdict['radio_select'].setvalue(self._smartdict['rep_type'])
if resources.STRING_VIB_ENERGY == self._smartdict['rep_type'] :
self._varsdict['options_respresentation'].setitems(
resources.STRINGS_ENERGY_VOLUME_REPRESENTATIONS)
else :
self._varsdict['options_respresentation'].setitems(
resources.STRINGS_EXCURSIONS_DIAMETER_REPRESENTATIONS)
if self._smartdict['rep_subtype'] is not None :
self._varsdict['options_respresentation'].setvalue(
self._smartdict['rep_subtype'])
self.__render()
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.rep_type = property(fget=self._get_rep_type,
fset=self._set_rep_type)
self.__class__.rep_subtype = property(fget=self._get_rep_subtype,
fset=self._set_rep_subtype)
self.__class__.invert_phase = property(fget=self._get_invert_phase,
fset=self._set_invert_phase)
self.__class__.sync_toolbar = property(fget=self._get_sync_toolbar,
fset=self._set_sync_toolbar)
self.__class__.vib_no = property(fget=self._get_vib_no,
fset=self._set_vib_no)
# these depend on whether one wants to see the fragment GUI controls
if self._smartdict['fragment_controls'] :
self.__class__.mark_fragment = property(fget=self._get_mark_fragment,
fset=self._set_mark_fragment)
self.__class__.show_marked_only = property(
fget=self._get_show_marked_only,
fset=self._set_show_marked_only)
def _bind_help(self) :
"""Bind help messages to the GUI components."""
self._balloon.bind(self._varsdict['options_respresentation'],
'Options appropriate for the current representation type.')
self._balloon.bind(self._varsdict['radio_select'],
'Choose between differenct representations.')
self._balloon.bind(self._varsdict['counter_scale_factor'],
'Set the scale factor for the diameter of spheres.')
self._balloon.bind(self._varsdict['check_invert'],
'Invert the phase of a vibration.')
# these controls can be absent
if self._smartdict['fragment_controls'] :
self._balloon.bind(self._varsdict['check_mark_fragment'],
'Mark the fragment being analized with partially transparent spheres.')
self._balloon.bind(self._varsdict['check_show_marked_only'],
'Show vibrational motion on the fragment being analized only.')
if 'btn_show_gcm' in self._varsdict :
self._balloon.bind(self._varsdict['btn_show_gcm'], 'ACP / GCM.')
self._balloon.bind(self._varsdict['radio_structure'],
'Show only the structure of the molecule.')
def __are_raman_roa_available(self) :
"""Whethe the Raman/ROA data are available."""
if self._smartdict['widget'] is None :
return False
if self._smartdict['widget'].molecule is None :
return False
return self._smartdict['widget'].molecule.raman_roa_tensors is not None
def __change_rep_type(self, tag) :
"""Changes the representation type in the option menu."""
cur_sel = self._varsdict['radio_select'].getvalue()
self._set_rep_type(self, cur_sel)
# synchronized toolbar
if self._smartdict['sync_toolbar'] :
self._smartdict['sync_toolbar'].rep_type = cur_sel
def __change_rep_subtype(self, tag) :
"""Changes the representation subtype."""
cur_sel = self._varsdict['options_respresentation'].getvalue()
self._set_rep_subtype(self, cur_sel)
# synchronized toolbar
if self._smartdict['sync_toolbar'] :
self._smartdict['sync_toolbar'].rep_subtype = cur_sel
def __change_mark_fragment(self) :
"""Changes the state of the mark fragment checkbox."""
mark_fragment = self._varsdict['var_mark_fragment'].get()
self._set_mark_fragment(self, mark_fragment)
# synchronized toolbar
if self._smartdict['sync_toolbar'] :
self._smartdict['sync_toolbar'].mark_fragment = mark_fragment
def __change_show_marked_only(self) :
"""Changes the show marked only checkbox."""
show_marked_only = self._varsdict['var_show_marked_only'].get()
self._set_show_marked_only(self, show_marked_only)
# synchronized toolbar
if self._smartdict['sync_toolbar'] :
self._smartdict['sync_toolbar'].show_marked_only = show_marked_only
def __render(self, *dummy, **dummy_kw) :
"""Called if the user changes the settings."""
if self._smartdict['widget'] is None :
return
self.tk.call('update', 'idletasks')
# render a vibration if the structure button is released
if 1 == len(self._varsdict['radio_structure'].getvalue()) :
self._smartdict['widget'].render_molecule(
molecule_mode=resources.NUM_MODE_BALLSTICK,
bonds_mode=resources.NUM_MODE_BONDS_ATOMS_COLOR,
hydrogen_bond=True)
self._smartdict['widget'].Render()
else :
# packing vars
kw = {}
kw['mode'] = resources.STRING_MODE_VIB_REPRESENTATION_SPHERES
kw['vib_no'] = self._smartdict['vib_no']
kw['rep_type'] = self._varsdict['radio_select'].getvalue()
kw['rep_subtype'] = self._varsdict['options_respresentation'].getvalue()
kw['scale_factor'] = misc.str_to_float(
self._varsdict['var_scale_factor'].get(), 0.)
kw['invert_phase'] = self._varsdict['var_invert_phase'].get()
if self._smartdict['fragment_controls'] :
kw['mark_fragment'] = self._varsdict['var_mark_fragment'].get()
kw['show_marked_only'] = self._varsdict['var_show_marked_only'].get()
else :
kw['mark_fragment'] = False
kw['show_marked_only'] = False
# calling
self._smartdict['widget'].render_vibration(**kw)
def __show_gcm(self) :
"""Show the Raman/ROA generation interface for the current vibration."""
from pyviblib.gui.windows import RamanROAMatricesWindow
self.tk.call('update', 'idletasks')
splash = SplashScreen(self, 'Please wait...')
wnd = RamanROAMatricesWindow(self._smartdict['mainApp'],
self._smartdict['widget'].molecule,
vib_no=self._smartdict['vib_no'],
camera=self._smartdict['widget'].camera,
molinv=self._smartdict['molinv'] or 'a2',
tabname='ACP',
invert_roa=self._smartdict['invert_roa'])
splash.destroy()
def _set_vib_no(obj, vib_no) :
"""Setter function for the vib_no property."""
if obj._smartdict['widget'] is None :
return
if not vib_no in xrange(1, 1 + obj._smartdict['widget'].molecule.NFreq) :
raise InvalidArgumentError(
'Invalid number of vibration %s' % str(vib_no))
obj._smartdict.kw['vib_no'] = vib_no
obj.__render()
_set_vib_no = staticmethod(_set_vib_no)
def _get_vib_no(obj) :
"""Getter function for the vib_no property."""
return obj._smartdict['vib_no']
_get_vib_no = staticmethod(_get_vib_no)
def _set_rep_type(obj, rep_type) :
"""Setter function for the rep_type property."""
if rep_type not in resources.STRINGS_VIB_REPRESENTATIONS :
raise InvalidArgumentError(
'Invalid representation type : %s' % rep_type)
# set value without invoking of the command !
obj._varsdict['radio_select'].setvalue(rep_type)
if resources.STRING_VIB_ENERGY == rep_type :
obj._varsdict['options_respresentation'].setitems(
resources.STRINGS_ENERGY_VOLUME_REPRESENTATIONS)
else :
obj._varsdict['options_respresentation'].setitems(
resources.STRINGS_EXCURSIONS_DIAMETER_REPRESENTATIONS)
obj.__render()
_set_rep_type = staticmethod(_set_rep_type)
def _get_rep_type(obj) :
"""Getter function for the rep_type property."""
return obj._varsdict['radio_select'].getvalue()
_get_rep_type = staticmethod(_get_rep_type)
def _set_rep_subtype(obj, rep_subtype) :
"""Setter function for the rep_subtype property."""
if rep_subtype not in resources.STRINGS_ENERGY_VOLUME_REPRESENTATIONS and\
rep_subtype not in \
resources.STRINGS_EXCURSIONS_DIAMETER_REPRESENTATIONS :
raise InvalidArgumentError(
'Invalid representation subtype : %s' % rep_subtype)
# set value without invoking of the command !
obj._varsdict['options_respresentation'].setvalue(rep_subtype)
obj.__render()
_set_rep_subtype = staticmethod(_set_rep_subtype)
def _get_rep_subtype(obj) :
"""Getter function for the rep_subtype property."""
return obj._varsdict['options_respresentation'].getvalue()
_get_rep_subtype = staticmethod(_get_rep_subtype)
def _set_mark_fragment(obj, mark_fragment) :
"""Setter function for the mark_fragment property.
Raise an exception if the control was not created.
"""
if not obj._smartdict['fragment_controls'] :
raise RuntimeError(
'fragment_controls is set to False, cannot call the function')
# set value without invoking of the command !
if mark_fragment :
obj._varsdict['check_mark_fragment'].select()
else:
obj._varsdict['check_mark_fragment'].deselect()
obj.__render()
_set_mark_fragment = staticmethod(_set_mark_fragment)
def _get_mark_fragment(obj) :
"""Getter function for the mark_fragment property."""
return obj._varsdict['var_mark_fragment'].get()
_get_mark_fragment = staticmethod(_get_mark_fragment)
def _set_sync_toolbar(obj, sync_toolbar) :
"""Setter function for the sync_toolbar property."""
obj._smartdict['sync_toolbar'] = sync_toolbar
_set_sync_toolbar = staticmethod(_set_sync_toolbar)
def _get_sync_toolbar(obj) :
"""Getter function for the sync_toolbar property."""
return obj._smartdict['sync_toolbar']
_get_sync_toolbar = staticmethod(_get_sync_toolbar)
def _set_show_marked_only(obj, show_marked_only) :
"""Setter function for the show_marked_only property."""
if not obj._smartdict['fragment_controls'] :
raise RuntimeError(
'fragment_controls is set to False, cannot call the function')
if show_marked_only :
obj._varsdict['check_show_marked_only'].select()
else:
obj._varsdict['check_show_marked_only'].deselect()
obj.__render()
_set_show_marked_only = staticmethod(_set_show_marked_only)
def _get_show_marked_only(obj) :
"""Getter function for the show_marked_only property."""
return obj._varsdict['var_show_marked_only'].get()
_get_show_marked_only = staticmethod(_get_show_marked_only)
def _get_invert_phase(obj) :
"""Getter function for the invert_phase property."""
return obj._varsdict['var_invert_phase'].get()
_get_invert_phase = staticmethod(_get_invert_phase)
def _set_invert_phase(obj, invert_phase) :
"""Getter function for the invert_phase property."""
obj._varsdict['var_invert_phase'].set(invert_phase)
obj.__render()
_set_invert_phase = staticmethod(_set_invert_phase)
def increase_scale_factor(self, *dummy) :
"""Increase the scale factor."""
self._varsdict['counter_scale_factor'].increment()
def decrease_scale_factor(self, *dummy) :
"""Decrease the scale factor."""
self._varsdict['counter_scale_factor'].decrement()
class VibNavigationFrame(BaseWidget, Tkinter.Frame) :
"""Frame for navigating through vibrations.
Component of VibrationalToolbar.
The following readable and writable property is exposed :
vib_no -- number of the current vibration
The following public method is exported :
enable_controls() -- enable / disable the controls
"""
def __init__(self, master, freqs, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
freqs -- wavenumbers (one-based ndarray)
shape : (1 + NFreq,) with NFreq being the number of
vibrations
Keyword arguments :
changed_callback -- callable (default None)
accepts one argument being the number of vibration
"""
self.__freqs = freqs
Tkinter.Frame.__init__(self, master,
borderwidth=2, relief='sunken', padx=3, pady=3)
BaseWidget.__init__(self, **kw)
def _constructGUI(self) :
"""Construct the GUI of the widget."""
# variables
self._varsdict['vib_no'] = Tkinter.StringVar()
self._varsdict['freq'] = Tkinter.StringVar()
self._varsdict['NFreq'] = Tkinter.StringVar()
if self.__freqs is not None and 1 < len(self.__freqs) :
self.__NFreq = len(self.__freqs) - 1
# user can specify a valid start value
if self._smartdict['vib_no'] is not None and \
self._smartdict['vib_no'] <= self.__NFreq :
self.__vib_no = self._smartdict['vib_no']
else :
self.__vib_no = 1
self._varsdict['vib_no'].set(self.__vib_no)
self._varsdict['freq'].set('%.2f' % self.__freqs[self.__vib_no])
self._varsdict['NFreq'].set(self.__NFreq)
else :
self.__NFreq = -1
self.__vib_no = -1
self._varsdict['vib_no'].set('-')
self._varsdict['freq'].set('----')
self._varsdict['NFreq'].set('-')
# back button
column = 0
self._varsdict['btn_back'] = Tkinter.Button(self,
image=getimage('back'),
command=self.__go_backward)
self._varsdict['btn_back'].grid(row=0, column=column, padx=3, pady=3)
column += 1
# vib_no label
widget = Tkinter.Label(self,
textvariable=self._varsdict['vib_no'],
relief='ridge',
borderwidth=3,
width=3)
self._varsdict['lbl_vib_no'] = widget
self._varsdict['lbl_vib_no'].grid(row=0, column=column, padx=0, pady=3)
column += 1
# '/' label
label_from = Tkinter.Label(self, text='/')
label_from.grid(row=0, column=column, padx=0, pady=3)
column += 1
# total number of frequencies label
widget = Tkinter.Label(self,
textvariable=self._varsdict['NFreq'],
relief='ridge',
borderwidth=3,
width=3)
self._varsdict['lbl_nfreq'] = widget
self._varsdict['lbl_nfreq'].grid(row=0, column=column, padx=0, pady=3)
column += 1
# forward button
self._varsdict['btn_forward'] = Tkinter.Button(self,
image=getimage('forward'),
command=self.__go_forward)
self._varsdict['btn_forward'].grid(row=0, column=column, padx=5, pady=3)
column += 1
# freq label
widget = Tkinter.Label(self,
textvariable=self._varsdict['freq'],
relief='ridge',
borderwidth=2,
width=8)
self._varsdict['lbl_freq'] = widget
self._varsdict['lbl_freq'].grid(row=0, column=column, padx=0, pady=3)
column += 1
# label for cm**(-1)
label_cm = Tkinter.Label(self, image=getimage('cm-1'))
label_cm.grid(row=0, column=column, padx=5, pady=3)
column += 1
# select vibration button
widget = Tkinter.Button(self,
image=getimage('freq_list'),
command=self.__show_freq_list)
self._varsdict['btn_freq_list'] = widget
self._varsdict['btn_freq_list'].grid(row=0, column=column, padx=3, pady=3)
column += 1
def _declare_properties(self) :
"""Declare properties of the widget."""
# Number of the currently selected vibration
self.__class__.vib_no = property(fget=self.__get_current_vib_no,
fset=self.__set_current_vib_no)
def _bind_help(self) :
"""Bind help messages to the GUI components."""
self._balloon.bind(self._varsdict['btn_back'],
'Go one vibration backward.')
self._balloon.bind(self._varsdict['lbl_vib_no'],
'Number of the current vibration.')
self._balloon.bind(self._varsdict['lbl_nfreq'],
'Total number of vibrations.')
self._balloon.bind(self._varsdict['btn_forward'],
'Go one vibration forward.')
self._balloon.bind(self._varsdict['lbl_freq'],
'Wavenumber of the current vibration.')
self._balloon.bind(self._varsdict['btn_freq_list'],
'Select a vibration from the list.')
def __show_freq_list(self) :
"""Show the list of frequencies to the user for selection."""
list_items = ['%3d%20.2f' % (i, self.__freqs[i]) for i \
in xrange(1, 1 + self.__NFreq) ]
dlg = Pmw.SelectionDialog(self,
title='Available vibrations',
buttons=\
resources.STRINGS_BUTTONS_OK_APPLY_CANCEL,
defaultbutton=resources.STRING_BUTTON_APPLY,
scrolledlist_labelpos='n',
label_text='Select a vibration :',
scrolledlist_items=list_items)
dlg.configure(command=misc.Command(self.__dlg_freq_command, dlg))
dlg.component('listbox').see(self.__vib_no - 1)
dlg.component('listbox').selection_set((self.__vib_no - 1, ))
dlg.show()
def __dlg_freq_command(self, btn_name, dlg) :
"""Callback for the frequency selection dialog."""
sel_indices = dlg.component('listbox').curselection()
# close the dialog if pressed Ok or Cancel
if btn_name in resources.STRINGS_BUTTONS_OK_CANCEL or not btn_name :
dlg.destroy()
self.tk.call('update')
if btn_name in resources.STRINGS_BUTTONS_OK_APPLY :
if 1 == len(sel_indices) :
self.__set_current_vib_no(self, 1 + int(sel_indices[0]))
def __go_backward(self) :
"""Go one vibration backward."""
self.vib_no = self.__vib_no - 1
def __go_forward(self) :
"""Go one vibration forward."""
self.vib_no = self.__vib_no + 1
def __set_current_vib_no(obj, new_vib_no) :
"""Setter function for the vib_no property."""
if 0 < new_vib_no and -1 != obj.__NFreq and \
new_vib_no != obj._varsdict['vib_no'].get() and \
new_vib_no <= obj.__NFreq :
obj.__vib_no = new_vib_no
obj._varsdict['vib_no'].set(new_vib_no)
obj._varsdict['freq'].set('%.2f' % obj.__freqs[new_vib_no])
if callable(obj._smartdict['changed_callback']) :
obj._smartdict['changed_callback'](new_vib_no)
__set_current_vib_no = staticmethod(__set_current_vib_no)
def __get_current_vib_no(obj) :
"""Getter function for the vib_no property."""
return obj.__vib_no
__get_current_vib_no = staticmethod(__get_current_vib_no)
def enable_controls(self, enable_) :
"""Enable / disable the controls.
Positional arguments :
enable_ -- whether the controls are to be enabled.
"""
if enable_ :
state = 'normal'
else :
state = 'disabled'
for name in ('btn_back', 'btn_forward', 'btn_freq_list') :
self._varsdict[name].configure(state=state)
class NavigationToolbar(BaseWidget, Tkinter.Frame) :
"""Manipulating camera of MoleculeRenderWidget.
The following public methods are exported :
save_camera_state() -- save the camera state to a dictionary
update_camera() -- update the camera state
"""
def __init__(self, master, renderWidget, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
renderWidget -- render widget to be manipulated
Keyword arguments :
orientation -- orientation for the toolbar
0 : horizontal
1 : vertical
(default 0)
"""
# the only required parameter
if not isinstance(renderWidget, MoleculeRenderWidget) :
raise ConstructorError('Invalid render widget')
self._master = master
self.__renderWidget = renderWidget
Tkinter.Frame.__init__(self, master, borderwidth=2, relief='ridge')
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
self._smartdict['molecule_name'] = ''
self._smartdict['orientation'] = 0
def _constructGUI(self) :
"""Construct the GUI of the widget."""
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
# projection types
widget = Pmw.RadioSelect(self,
orient='vertical',
command=self.__set_projection)
self._varsdict['radio_projection'] = widget
self._varsdict['radio_projection'].grid(row=0, column=0, sticky='n')
self._varsdict['radio_projection'].add('perspective',
image=getimage('perspective'))
self._varsdict['radio_projection'].add('parallel',
image=getimage('orthogonal'))
# set the camera projection type properly
if self.__renderWidget.perspective_projection :
self._varsdict['radio_projection'].setvalue('perspective')
else :
self._varsdict['radio_projection'].setvalue('parallel')
## button toolbar with the camera navigation buttons
btn_toolbar = ButtonToolbar(self, horizontal=False)
btn_toolbar.configure(borderwidth=0)
btn_toolbar.grid(row=1, column=0, sticky='n')
row = 0
# restore camera state
btn_toolbar.add_button(imagename='restore',
command=self.__restore_camera,
helptext='Restore the last saved camera state.')
# remember camera state
btn_toolbar.add_button(imagename='set_pos',
command=self.__remember_camera_state,
helptext='Remember the current camera state.')
# save camera state
btn_toolbar.add_button(imagename='save_camera',
command=self.__save_camera,
helptext='Save the camera state to a file.')
# load camera state
btn_toolbar.add_button(imagename='load_camera',
command=self.__load_camera,
helptext='Load the camera state from a file.')
## initial camera position
self.__camera_state = {}
def _bind_help(self) :
"""Bind help messages to the GUI components."""
self._balloon.bind(self._varsdict['radio_projection'],
'Set the perspective or orthogonal projection.')
def __set_projection(self, tag) :
"""Set the perspective or orthogonal projection."""
renderer = self.__renderWidget.renderer
if not renderer :
return
if 'parallel' == tag :
renderer.GetActiveCamera().ParallelProjectionOn()
else :
renderer.GetActiveCamera().ParallelProjectionOff()
self.__renderWidget.GetRenderWindow().Render()
def __restore_camera(self) :
"""Restore to the initial state of the camera."""
self.update_camera(self.__camera_state)
def __serialize_camera_state(self, filename) :
"""Save the properties of the CURRENT camera to the file."""
f = open(filename, 'w+')
renderer = self.__renderWidget.renderer
if renderer is not None :
camera = renderer.GetActiveCamera()
# saving properties
cPickle.dump(camera.GetParallelProjection(), f)
cPickle.dump(camera.GetParallelScale(), f)
cPickle.dump(camera.GetFocalPoint(), f)
cPickle.dump(camera.GetPosition(), f)
cPickle.dump(camera.GetClippingRange(), f)
cPickle.dump(camera.GetViewUp(), f)
f.close()
def _format_tuple_string(name, name_tuple) :
"""Format name and 3-tuple: name_tuple.
Used by saving the camera state to the file.
"""
s = '%20s' % name
for t in name_tuple :
s = ''.join((s, '%20.6f' % t))
return ''.join((s, '\n'))
_format_tuple_string = staticmethod(_format_tuple_string)
def __save_camera(self) :
"""Save the camera to a file."""
filename = tkFileDialog.SaveAs(
parent=self._master,
filetypes=[(resources.STRING_FILETYPE_CAMERAFILE_DESCRIPTION, '*.cam')],
initialfile='%s.cam' % self.__renderWidget.molecule.name,
defaultextension='.cam'
).show()
if filename :
self.__serialize_camera_state(filename)
def __load_camera(self) :
"""Load the camera from a file."""
filename = tkFileDialog.Open(
parent=self._master,
filetypes=[(resources.STRING_FILETYPE_CAMERAFILE_DESCRIPTION, '*.cam')]
).show()
if filename :
# de-serialize a camera state previosly stored with the cPickle package
f = open(filename, 'r')
camera_state = {}
for prop in resources.STRINGS_CAMERA_PROPERTIES :
camera_state[prop] = cPickle.load(f)
f.close()
self.update_camera(camera_state)
def __remember_camera_state(self) :
"""Remember the camera state."""
self.save_camera_state()
def save_camera_state(self, camera_state=None) :
"""Save the camera camera state to a dictionary.
Keyword arguments :
camera_state -- the dictionary to save to (default None)
if None save to the internal dictionary
"""
renderer = self.__renderWidget.renderer
if renderer :
camera = renderer.GetActiveCamera()
if camera_state is None :
camera_state = self.__camera_state
# saving the camera properties
camera_state['ParallelProjection'] = camera.GetParallelProjection()
camera_state['ParallelScale'] = camera.GetParallelScale()
camera_state['FocalPoint'] = camera.GetFocalPoint()
camera_state['Position'] = camera.GetPosition()
camera_state['ClippingRange'] = camera.GetClippingRange()
camera_state['ViewUp'] = camera.GetViewUp()
def update_camera(self, camera_state) :
"""Update the current camera.
Positional arguments :
camera_state -- dictionary with the camera state
"""
if camera_state is None :
return
renderer = self.__renderWidget.renderer
if renderer :
camera = renderer.GetActiveCamera()
# set new values
camera.SetParallelScale(camera_state['ParallelScale'])
camera.SetFocalPoint(camera_state['FocalPoint'])
camera.SetPosition(camera_state['Position'])
camera.SetClippingRange(camera_state['ClippingRange'])
camera.ComputeViewPlaneNormal()
camera.SetViewUp(camera_state['ViewUp'])
camera.OrthogonalizeViewUp()
self.__renderWidget.Render()
# update buttons
if camera_state['ParallelProjection'] :
self._varsdict['radio_projection'].invoke('parallel')
else :
self._varsdict['radio_projection'].invoke('perspective')
class SplashScreen(BaseWidget, Tkinter.Toplevel) :
"""Splash screen which can be shown during a long operation.
"""
def __init__(self, master, text, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
text -- text to be shown
Keyword arguments :
font -- font for rendering text
(default ('Helvetica', 12, 'bold'))
overrideredirect -- if True the splash will be ignored be the
window manager
"""
self.__text = text
Tkinter.Toplevel.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
self._smartdict['font'] = ('Helvetica', 12, 'bold')
self._smartdict['overrideredirect'] = True
def _constructGUI(self) :
"""Construct the GUI of the widget."""
self.withdraw()
self.wm_title('Operation in progress')
# showing text
text = Tkinter.Label(self,
font=self._smartdict['font'],
relief = 'raised',
borderwidth = 2,
padx=50, pady=50,
text=self.__text,
image=self._smartdict['image'],
compound='left')
text.pack(side='top', fill = 'both', expand = 1)
self.update_idletasks()
width = self.winfo_reqwidth()
height = self.winfo_reqheight()
x = (self.winfo_screenwidth() - width) / 2 - self.winfo_vrootx()
y = (self.winfo_screenheight() - height) / 3 - self.winfo_vrooty()
if x < 0:
x = 0
if y < 0:
y = 0
geometry = '%dx%d+%d+%d' % (width, height, x, y)
self.overrideredirect(self._smartdict['overrideredirect'])
self.geometry(geometry)
self.update_idletasks()
self.deiconify()
self.update()
class CorrelationResultsTable(BaseWidget, Pmw.ScrolledText) :
"""Table for representing matrices e.g. overlaps or similarities.
The widget is based on Pmw.ScrolledText.
The following read-only properties are exposed :
csv -- contents in the CSV format
data -- contents as a matrix
labels -- labels as a dictionary
The following public methods are exported :
update_table() -- update the table
ij_to_vibno() -- convert rows and columns to vibration numbers
vibno_to_ij() -- convert vibration numbers to rows and columns
"""
def __init__(self, master, matrix,
freqs_ref, freqs_tr, include_tr_rot, **kw) :
"""Constructor of the class.
Positional arguments :
matrix -- matrix to show (one-based ndarray)
shape : (1 + NFreq_ref, 1 + NFreq_tr)
freqs_ref -- wavenumbers of the reference molecule
(one-based ndarray)
shape : (1 + NFreq_ref, )
freqs_tr -- wavenumbers of the trial molecule
(one-based ndarray)
shape : (1 + NFreq_tr, )
include_tr_rot -- whether translations/rotations are in the matrix
Keyword keywords :
dblclick_callback -- called when double clicked on an element
(default None)
if supplied, must accept 2 arguments :
ref_no and tr_no
msgBar -- message bar (Pmw.MessageBar, default None)
"""
if matrix is None or freqs_ref is None or freqs_tr is None :
raise ConstructorError('Invalid arguments')
self._matrix = matrix
self._freqs_ref = freqs_ref
self._freqs_tr = freqs_tr
self._include_tr_rot = include_tr_rot
Pmw.ScrolledText.__init__(self,
master,
columnheader=True,
rowheader=True,
rowcolumnheader=True,
columnheader_height= 2,
rowheader_width= 4 + 5,
rowcolumnheader_width=3,
Header_font= resources.FONT_TABLE_ELEMENT,
text_font= resources.FONT_TABLE_ELEMENT,
text_wrap='none',)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
if self._smartdict['dblclick_callback'] is not None and \
not callable(self._smartdict['dblclick_callback']) :
raise ConstructorError('dblclick_callback must be a callable')
def _constructGUI(self) :
"""Construct the GUI of the widget."""
# marking of the matrix elements
self.tag_configure('marked',
background=resources.COLOR_MARKED_TABLE_ELEMENT_BG)
# unmarking the headers when mouse leaves
self.tag_bind('matrix_element', '<Leave>', self.__mouse_leave)
# keep track of the mouse motion on the matrix elements
self.tag_bind('matrix_element', '<Motion>', self.__mouse_motion)
# keep track of double clicks on the matrix elements
self.tag_bind('matrix_element', '<Button-1>',
self.__mouse_doubleclick_element)
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.csv = property(fget=self._get_csv)
self.__class__.data = property(fget=self._get_data)
self.__class__.labels = property(fget=self._get_labels)
def _bind_events(self) :
"""Bind events."""
bind_mouse_wheel(self.component('text'))
def __mouse_motion(self, e) :
"""Callback for the mouse motion."""
pos = self.index('@%d,%d' % (e.x, e.y)).split('.')
row_index = int(pos[0])
column_index = 1 + int(pos[1]) / (5 + self.__current_precision)
# one has to mark the selected tags
text_rowheaders = self.component('rowheader')
text_columnheaders = self.component('columnheader')
kw_marked = dict(foreground='red',
font=resources.FONT_TABLE_SEL_HEADER2)
kw_default = dict(foreground=self.cget('text_foreground'),
font=self.cget('text_font'))
# frequecy labels and numbers
kw_head1 = dict(foreground='blue',
font=resources.FONT_TABLE_SEL_HEADER1)
kw_head2 = dict(foreground='blue',
font=resources.FONT_TABLE_SEL_HEADER2)
kw_head1_def = dict(foreground=self.cget('text_foreground'),
font=resources.FONT_TABLE_HEADER1)
kw_head2_def = dict(foreground=self.cget('text_foreground'),
font=resources.FONT_TABLE_HEADER2)
# mark only the selected tag
# column headers
for i in xrange(1, 1 + self.__last_tr_header) :
if i == column_index :
kw1 = kw_head1
kw2 = kw_head2
else :
kw1 = kw_head1_def
kw2 = kw_head2_def
text_columnheaders.tag_configure('tr_header%d' % i, **kw1)
text_columnheaders.tag_configure('tr2_header%d' % i, **kw2)
# row headers
for i in xrange(1, 1 + self.__last_ref_header) :
if i == row_index :
kw1 = kw_head1
kw2 = kw_head2
else :
kw1 = kw_head1_def
kw2 = kw_head2_def
text_rowheaders.tag_configure('ref_header%d' % i, **kw1)
text_rowheaders.tag_configure('ref2_header%d' % i, **kw2)
# sending info to the message bar
ref_index, tr_index = self.__text_indices2vib_indices(
[ int(str_ind) for str_ind in pos ])
msg = ''
labels = resources.STRINGS_TRANSLATIONS_ROTATIONS_LABELS
if ref_index < 0 :
msg = ''.join((msg, '%s -> ' % labels[-ref_index - 1]))
else :
msg = ''.join((
msg,
'%d / %6.2f -> ' % (ref_index, self._freqs_ref[ref_index])))
if tr_index < 0 :
msg = ''.join((msg, '%s' % labels[-tr_index - 1]))
else :
msg = ''.join((msg, '%d / %6.2f' % (tr_index, self._freqs_tr[tr_index])))
if self._smartdict['msgBar'] :
self._smartdict['msgBar'].message('help', msg)
def __mouse_leave(self, e) :
"""Callback for <Leave>."""
kw_head1_def = dict(foreground=self.cget('text_foreground'),
font=resources.FONT_TABLE_HEADER1)
kw_head2_def = dict(foreground=self.cget('text_foreground'),
font=resources.FONT_TABLE_HEADER2)
# column headers
for i in xrange(1, 1 + self.__last_tr_header) :
self.component('columnheader').tag_configure(
'tr_header%d' % i, **kw_head1_def)
self.component('columnheader').tag_configure(
'tr2_header%d' % i, **kw_head2_def)
# row headers
for i in xrange(1, 1 + self.__last_ref_header) :
self.component('rowheader').tag_configure(
'ref_header%d' % i, **kw_head1_def)
self.component('rowheader').tag_configure(
'ref2_header%d' % i, **kw_head2_def)
if self._smartdict['msgBar'] :
self._smartdict['msgBar'].message('help', '')
def __mouse_doubleclick_element(self, e) :
"""Callback for a double click on an element of the table."""
indices = self.index('@%d,%d' % (e.x, e.y)).split('.')
text_indices = [ int(str_ind) for str_ind in indices ]
ref_no, tr_no = self.__text_indices2vib_indices(text_indices)
if callable(self._smartdict['dblclick_callback']) :
self.interior().tk.call('update', 'idletasks')
self._smartdict['dblclick_callback'](ref_no, tr_no)
def __text_indices2vib_indices(self, text_indices) :
"""Converts a text character index to the vibrational index.
Lines indices are one-based, column indices are null-based.
"""
columnheader_width = 5 + self.__current_precision
i = text_indices[0]
j = 1 + text_indices[1] / columnheader_width
return self.ij_to_vibno(i, j)
def _get_csv(obj) :
"""Return the contents of the table as in the CSV format."""
rowheader_width = int(obj.component('rowheader').cget('width'))
# first two lines - trial frequecies and labels
indent = ('%%%ds' % rowheader_width) % ''
text_csv = ''
colheaders = obj.component('columnheader').get('1.0', 'end').split('\n')
text_csv = ''.join((text_csv, indent, colheaders[0], '\n'))
text_csv = ''.join((text_csv, indent, colheaders[1], '\n'))
# following line are row data
rowheaders = obj.component('rowheader').get('1.0', 'end').split('\n')
elements = obj.get('1.0', 'end').split('\n')
for i in xrange(len(elements)) :
text_csv = ''.join((text_csv, rowheaders[i], elements[i], '\n'))
return text_csv
_get_csv = staticmethod(_get_csv)
def _get_data(obj) :
"""Get the contents of the table as a matrix.
Usefull to update TwoDCircles.
"""
data = []
for line in obj.get('1.0', 'end').split('\n') :
line = line.strip()
if 0 == len(line) :
continue
data.append( [ float(val) for val in line.split() ] )
n, m = len(data), len(data[0])
ans = zeros((1 + n, 1 + m), 'd')
ans[1:, 1:] = array(data, 'd')
return ans
_get_data = staticmethod(_get_data)
def _get_labels(obj) :
"""Return a dictionary with labels.
All values of the dictionary are one-based lists.
Keys of the dictionary :
labels1_cols : first column headers
labels2_cols : second column headers
labels1_rows : first row headers
labels2_cols : second row headers
"""
colheaders = obj.component('columnheader').get('1.0', 'end').split('\n')
rowheaders = obj.component('rowheader').get('1.0', 'end').split('\n')
# saving in a dictionary
ans = dict(labels1_cols=[0.],
labels2_cols=[0.],
labels1_rows=[0.],
labels2_rows=[0.])
# columns
ans['labels1_cols'].extend(colheaders[0].split())
ans['labels2_cols'].extend(colheaders[1].split())
# rows
for row in rowheaders :
vals = row.split()
if 2 == len(vals) :
ans['labels1_rows'].append(vals[0])
ans['labels2_rows'].append(vals[1])
return ans
_get_labels = staticmethod(_get_labels)
def update_table(self, ref_from, ref_to, tr_from, tr_to,
precision, show_tr_rot, nrot_ref, nrot_tr,
mark=False, threshold_marked=0.) :
"""Update the contents of the table.
Positional arguments :
ref_from -- start vibration number of the reference molecule
ref_to -- end vibration number of the reference molecule
tr_from -- start vibration number of the trial molecule
tr_to -- end vibration number of the reference molecule
precision -- number of decimal points to show
show_tr_rot -- whether translations/rotations are to be shown
nrot_ref -- number of rotations in the reference molecule
nrot_tr -- number of rotations in the trial molecule
Keyword arguments :
mark -- whether to mark elements exceeding a threshold
supplied by the threshold_marked argument
(default False)
threshold_marked -- threshold for marking elements (default 0.)
"""
matrix = self._matrix
# unblocking
self.configure(text_state = 'normal', Header_state = 'normal')
# cleaning
self.clear()
self.component('columnheader').delete('1.0', 'end')
self.component('rowheader').delete('1.0', 'end')
# column headers
columnheader_width = 5 + precision
format_rowheader = '%%%ds\n' % int(self.cget('rowheader_width'))
format_rowheader_1 = '%5.0f'
format_rowheader_2 = '%4s\n'
format_columnheader_1 = '%%%d.0f' % columnheader_width
format_columnheader_2 = '%%%ds' % columnheader_width
format_el_f = '%%%d.%df' % (columnheader_width, precision)
format_el_i = '%%%dd' % columnheader_width
# user can show translations / rotations if they were calculated
if self._include_tr_rot :
include_tr_rot = show_tr_rot
else :
include_tr_rot = False
labels_tr_rot = resources.STRINGS_TRANSLATIONS_ROTATIONS_LABELS
## column headers
# vibrational frequencies(first line)
self.__last_tr_header = 0
if include_tr_rot :
for i in xrange(1, 4 + nrot_tr) :
self.__last_tr_header += 1
tag_name = 'tr_header%d' % self.__last_tr_header
self.component('columnheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER1)
self.component('columnheader').insert('end',
format_columnheader_1 % 0.,
tag_name)
for i in xrange(tr_from, 1 + tr_to) :
self.__last_tr_header += 1
tag_name = 'tr_header%d' % self.__last_tr_header
self.component('columnheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER1)
self.component('columnheader').insert(
'end', format_columnheader_1 % self._freqs_tr[i], tag_name)
self.component('columnheader').insert('end', '\n')
# vibrational labels (second line)
self.__last_tr_header = 0
if include_tr_rot :
for i in xrange(1, 4 + nrot_tr) :
self.__last_tr_header += 1
tag_name = 'tr2_header%d' % self.__last_tr_header
self.component('columnheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER2)
self.component('columnheader').insert(
'end', format_columnheader_2 % labels_tr_rot[i-1], tag_name)
for i in xrange(tr_from, 1 + tr_to) :
self.__last_tr_header += 1
tag_name = 'tr2_header%d' % self.__last_tr_header
self.component('columnheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER2)
self.component('columnheader').insert(
'end', format_columnheader_2 % str(i), tag_name)
# row headers : freq label
self.__last_ref_header = 0
if include_tr_rot :
for i in xrange(1, 4 + nrot_ref) :
self.__last_ref_header += 1
# freq
tag_name = 'ref_header%d' % self.__last_ref_header
self.component('rowheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER1)
self.component('rowheader').insert(
'end', format_rowheader_1 % 0., tag_name)
# label
tag_name = 'ref2_header%d' % self.__last_ref_header
self.component('rowheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER2)
self.component('rowheader').insert(
'end', format_rowheader_2 % labels_tr_rot[i-1], tag_name)
for i in xrange(ref_from, 1 + ref_to) :
self.__last_ref_header += 1
# freq
tag_name = 'ref_header%d' % self.__last_ref_header
self.component('rowheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER1)
self.component('rowheader').insert(
'end', format_rowheader_1 % self._freqs_ref[i], tag_name)
# label
tag_name = 'ref2_header%d' % self.__last_ref_header
self.component('rowheader').tag_configure(
tag_name, font=resources.FONT_TABLE_HEADER2)
self.component('rowheader').insert(
'end', format_rowheader_2 % str(i), tag_name)
# data
range_ref = xrange(ref_from, 1 + ref_to)
range_tr = xrange(tr_from, 1 + tr_to)
threshold = pow(10., -1. * precision)
for i in xrange(1, matrix.shape[0]) :
# print only valid i !
if include_tr_rot :
if i > (3 + nrot_ref) and (i - 3 - nrot_ref) not in range_ref :
continue
else :
if i not in range_ref :
continue
print_ij = True
for j in xrange(1, matrix.shape[1]) :
el_ij = matrix[i, j]
if include_tr_rot :
if i <= 3 + nrot_ref :
if j <= 3 + nrot_tr :
print_ij = True
else :
print_ij = (j - 3 - nrot_tr) in range_tr
else :
if j <= 3 + nrot_tr :
print_ij = (i - 3 - nrot_ref) in range_ref
else :
print_ij = (i - 3 - nrot_ref) in range_ref and \
(j - 3 - nrot_tr) in range_tr
else :
if i in range_ref and j in range_tr :
print_ij = True
if self._include_tr_rot :
el_ij = matrix[i + 3 + nrot_ref, j + 3 + nrot_tr]
else :
print_ij = False
#
if print_ij :
if threshold > el_ij :
self.insert('end', format_el_i % 0, 'matrix_element')
else :
tags = ['matrix_element']
if mark and threshold_marked <= el_ij :
tags.append('marked')
self.insert('end', format_el_f % el_ij, tuple(tags))
self.insert('end', '\n')
# blocking
self.configure(text_state = 'disabled', Header_state = 'disabled')
self.__current_precision = precision
self.__show_tr_rot = show_tr_rot
self.__nrot_ref = nrot_ref
self.__nrot_tr = nrot_tr
self.__vib_ranges = ref_from, ref_to, tr_from, tr_to
def ij_to_vibno(self, i, j) :
"""Get the numbers of vibrations.
Positional arguments :
i -- row number
j -- column number
Return (ref_index, tr_index). Negative indices correspond to
translations/rotations.
"""
ref_from, ref_to, tr_from, tr_to = self.__vib_ranges
if self._include_tr_rot and self.__show_tr_rot :
if i <= 3 + self.__nrot_ref :
ref_index = -i
else :
ref_index = ref_from - 1 + i - (3 + self.__nrot_ref)
if j <= 3 + self.__nrot_tr :
tr_index = -j
else :
tr_index = tr_from -1 + j - (3 + self.__nrot_tr)
else :
ref_index = min(ref_from + i - 1, ref_to)
tr_index = min(tr_from + j - 1, tr_to)
return ref_index, tr_index
def vibno_to_ij(self, ref_index, tr_index) :
"""Get the number of the row and column.
Positional arguments :
ref_index -- vibration number of the reference molecule
tr_index -- vibration number of the trial molecule
Opposite destination to ij_to_vibno().
"""
ref_from, ref_to, tr_from, tr_to = self.__vib_ranges
# reference i (rows)
if 0 > ref_index :
i = -ref_index
else :
if self._include_tr_rot and self.__show_tr_rot :
i = 3 + self.__nrot_ref + ref_index - ref_from + 1
else :
i = max(ref_index - ref_from + 1, ref_index - ref_to)
# trial j (columns)
if 0 > tr_index :
j = -tr_index
else :
if self._include_tr_rot and self.__show_tr_rot :
j = 3 + self.__nrot_tr + tr_index - tr_from + 1
else :
j = max(tr_index - tr_from + 1, tr_index - tr_to)
return i, j
def show_exception(exception_info) :
"""Show an exception in a dialog.
Positional arguments :
exception_info -- must be the return value of sys.exc_info()
"""
dialog = Pmw.TextDialog(None,
scrolledtext_labelpos = 'n',
title='Error occured',
defaultbutton = 0,
text_height=10,
text_font=\
Pmw.logicalfont('Times', sizeIncr=-1, weight='bold'),
text_wrap='char',
label_text='Details:')
dialog.withdraw()
dialog.focus_set()
text = dialog.component('text')
text.tag_configure('msg_head', foreground='red')
# exception details are to be shown with the red color
text.insert('end', exception_info[1], 'msg_head')
# traceback - in black
msg_trace = '\n\nTraceback :\n'
for exc_line in format_tb(exception_info[2]) :
msg_trace = ''.join((msg_trace, exc_line))
text.insert('end', msg_trace)
dialog.configure(text_state = 'disabled')
# center the dialog
Pmw.setgeometryanddeiconify(dialog, dialog._centreonscreen())
def mouse_wheel(e, widget) :
"""Mouse wheel callback for a widget.
Positional arguments :
widget -- widget
Usage :
>>> from pyviblib.util.misc import Command
>>> from pyviblib.gui.widgets import mouse_wheel
>>> widget.bind('<Button-4>', Command(mouse_wheel, widget))
>>> widget.bind('<Button-5>', Command(mouse_wheel, widget))
"""
# scroll direction : always 1 unit
if 4 == e.num :
number = -1
else :
number = 1
widget.yview('scroll', number, 'units')
def bind_mouse_wheel(widget) :
"""Bind the mouse wheel events to a widget."""
widget.bind('<Button-4>', misc.Command(mouse_wheel, widget))
widget.bind('<Button-5>', misc.Command(mouse_wheel, widget))
class WindowNavigationToolbar(BaseWidget, Tkinter.Frame) :
"""Toolbar for switching between windows.
Can have a home and back buttons.
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
mainApp -- reference to the main window of PyVib2
(pyviblib.gui.main.Main, default None)
backbutton -- whether to create a back button (default False)
homebutton -- whether to create a home button (default False)
backbutton_command -- command for the back button (default None)
"""
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
if self._smartdict['mainApp'] is not None :
self._smartdict['homebutton_command'] = \
self._smartdict['mainApp'].activate
def _constructGUI(self) :
"""Construct the GUI of the widget."""
current_column = 0
if self._smartdict['backbutton'] :
widget = Tkinter.Button(self,
image=getimage('goback'),
relief='flat',
overrelief='raised',
command=self._smartdict['backbutton_command'])
self._varsdict['btn_back'] = widget
self._varsdict['btn_back'].grid(row=0, column=current_column,
padx=3, pady=3, sticky='w')
current_column += 1
if self._smartdict['homebutton'] :
widget = Tkinter.Button(self,
image=getimage('home'),
relief='flat',
overrelief='raised',
command=self._smartdict['homebutton_command'])
self._varsdict['btn_home'] = widget
self._varsdict['btn_home'].grid(row=0, column=current_column,
padx=3, pady=3, sticky='w')
def _bind_help(self) :
"""Bind help messages to the GUI components."""
if self._smartdict['backbutton'] :
self._balloon.bind(self._varsdict['btn_back'], 'Go back.')
if self._smartdict['homebutton'] :
self._balloon.bind(self._varsdict['btn_home'],
'Switch to the main window.')
class InfoWidget(BaseWidget, Tkinter.Frame) :
"""Widget for providing a help text."""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
text -- text to be shown (default '')
height -- number of rows in the widget (default 4)
icon -- whether to render the icon to the left of the text (default True)
"""
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
self._smartdict['text'] = ''
self._smartdict['height'] = 4
self._smartdict['icon'] = True
def _constructGUI(self) :
"""Construct the GUI of the widget."""
column = 0
# image
if self._smartdict['icon'] :
lbl_image = Tkinter.Label(self, image=getimage('message'))
lbl_image.grid(row=0, column=column, padx=3, pady=3, sticky='w')
column += 1
# scrolled text
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(column, weight=1)
# setting the background of a readonly text
bg = Tkinter.Entry(None, state='readonly').cget('readonlybackground')
scrolled_text = Pmw.ScrolledText(self,
text_height=self._smartdict['height'],
text_bg=bg)
scrolled_text.grid(row=0, column=column, padx=3, pady=3, sticky='we')
scrolled_text.insert('end', self._smartdict['text'])
scrolled_text.configure(text_state='disabled')
class TwoDCircles(BaseWidget, Tkinter.Canvas) :
"""Canvas for rendering matrices such as e.g. GCMs, overlaps or similarities.
The widget is based on Tkinter.Canvas.
The following public methods are exported :
update() -- update the canvas
mark_rect_ij() -- mark a rectangle on the canvas
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
All arrays supplied in the keyword arguments must be one-based ndarrays.
Positional arguments :
master -- parent widget
Keyword arguments :
data -- matrix with numeric values (default None)
mode -- how to render the matrix
one of (resources.NUM_MODE_WHOLE,
resources.NUM_MODE_UPPERHALFONLY,
resources.NUM_MODE_LOWERHALFONLY
(default resources.NUM_MODE_WHOLE)
sumup_diag -- whether to sum the elements in the upper- or
lowerhalf mode (default False)
type -- whether the square or the diameters of the circles
are proportional to the values
one of (resources.NUM_TYPE_PROPORTIONAL_TO_SQUARE,
resources.NUM_TYPE_PROPORTIONAL_TO_DIAMETER)
(default resources.NUM_TYPE_PROPORTIONAL_TO_SQUARE)
labels_on -- render the labels (default True)
labels1_cols -- first column headers (default None)
labels2_cols -- second column headers (default None)
labels1_rows -- first row headers (default None)
labels2_cols -- second row headers (default None)
bbox_on -- whether to render the bounding box around the
circles (default False)
font_labels1 -- font for the headers1
(default resources.FONT_TWODCIRCLES_LABEL1)
font_labels2 -- font for the headers2
(default resources.FONT_TWODCIRCLES_LABEL2)
color_positive -- filling color for positive values (default 'black')
color_negative -- filling color for negative values (default 'white')
color_rect -- color of the bounding rectangles (default 'black')
scale_factor -- number for the radii of the circles (default 1.0)
max_circlewidth -- maximum width of the bounding box of a circle
in pixel (default 30)
indent -- indent from the upper left corner of the canvas
(default 5)
text_padding -- padding with a label box (default 15)
full_circle -- value of a completely filled circle (default 1.0)
color_highlight -- highlighting color for a selected pair
(default 'red')
ndigits -- show numbers under 10**(-ndigits) in the
message bar as 0 (default None)
if None, show the number is a scientific notation
msgBar -- message bar for messages (default None)
dblclick_callback -- called on a double click (default None)
mouse_motion_callback -- called when the mouse enters the canvas
(args : i j) (default None)
mouse_leave_callback -- called when the mouse leaves the canvas (no args)
"""
Tkinter.Canvas.__init__(self, master, background='white')
BaseWidget.__init__(self, **kw)
self.update()
def _init_vars(self) :
"""Initialize variables."""
self._props = self._smartdict
defaults = dict(data=None,
mode=resources.NUM_MODE_WHOLE,
sumup_diag=False,
type=resources.NUM_TYPE_PROPORTIONAL_TO_SQUARE,
labels_on=True,
labels1_cols=None,
labels2_cols=None,
labels1_rows=None,
labels2_rows=None,
bbox_on=False,
font_labels1=resources.FONT_TWODCIRCLES_LABEL1,
font_labels2=resources.FONT_TWODCIRCLES_LABEL2,
color_positive='black',
color_negative='white',
color_rect='black',
scale_factor=1.0,
max_circlewidth=50,
indent=15,
text_padding=5,
full_circle=1.0,
color_highlight='blue',
dblclick_callback=None,
msgBar=None)
# copy defaults to the smart dictionary
for key in defaults.keys() :
self._props[key] = defaults[key]
# validate
if self._props['data'] is not None :
if not isinstance(self._props['data'], ndarray) or \
2 != len(self._props['data'].shape) :
raise ConstructorError('Invalid matrix')
if (self._props['labels1_rows'] is not None and \
self._props['labels1_cols'] is None ) or \
(self._props['labels1_rows'] is None and \
self._props['labels1_cols'] is not None ) :
raise ConstructorError(
'labels1 for rows and cols must be given or not given at all')
if (self._props['labels2_rows'] is not None and \
self._props['labels2_cols'] is None ) or \
(self._props['labels2_rows'] is None and \
self._props['labels2_cols'] is not None ) :
raise ConstructorError(
'labels2 for rows and cols must be given or not given at all')
# controlling dimensions
if self._props['data'] is None :
return
n, m = self._props['data'].shape
if self._props['labels1_rows'] is not None :
if m != len(self._props['labels1_cols']) or \
n != len(self._props['labels1_rows']) :
raise ConstructorError(
'Labels1 dimensions are incompatible with the matrix')
if self._props['labels2_rows'] is not None :
if m != len(self._props['labels2_cols']) or \
n != len(self._props['labels2_rows']) :
raise ConstructorError(
'Labels2 dimensions are incompatible with the matrix')
# number of digits must be positive
if self._smartdict['ndigits'] is not None and \
0 >= self._smartdict['ndigits'] :
raise ConstructorError('ndigits must be positive')
def _maxwidth_labels(labels) :
"""Calculate the maximum width of symbols in a given tuple of labels.
Return 0 if labels are not given.
"""
if labels is None or 0 == len(labels) :
return 0
maxwidth = len(str(labels[0]))
for l in labels[1:] :
if len(str(l)) > maxwidth :
maxwidth = len(str(l))
return maxwidth
_maxwidth_labels = staticmethod(_maxwidth_labels)
def _create_string_n(n) :
"""Return a string with a given number of spaces
"""
if not n or 0 > n :
return ''
return ('%%%ds' % n) % ''
_create_string_n = staticmethod(_create_string_n)
def __get_text_height(self, font) :
"""Get the height of a text of a given font."""
if font is None :
return 0
metrics = tkFont.Font(self, font=font).metrics()
return metrics['ascent']
def __get_text_width(self, text, font) :
"""
Return the width of a given text with a given font.
"""
if not text or not font :
return 0
return tkFont.Font(self, font=font).measure(text)
def __mousepos_to_ij(self, x, y) :
"""Return i, j from the mouse position."""
items = self.find_closest(self.canvasx(x), self.canvasy(y))
for item in items :
if item in self.__id_to_ij :
return self.__id_to_ij[item]
else :
return None
def __mouse_motion(self, e) :
"""Handler for the mouse move event."""
if self._props['data'] is None :
return
ij = self.__mousepos_to_ij(e.x, e.y)
if ij is None :
return
i, j = ij
# mouse motion callback
if callable(self._props['mouse_motion_callback']) :
self._props['mouse_motion_callback'](i, j)
# message bar message
if self._props['msgBar'] :
message = ''
if self._props['labels1_rows'] is not None :
message = ''.join((message, '%s / ' % self._props['labels1_rows'][i]))
if self._props['labels2_rows'] is not None :
message = ''.join((message, '%s -> ' % self._props['labels2_rows'][i]))
if self._props['labels1_cols'] is not None :
message = ''.join((message, '%s / ' % self._props['labels1_cols'][j]))
if self._props['labels2_cols'] is not None :
message = ''.join((message, '%s : ' % self._props['labels2_cols'][j]))
# do not forget about summed diagonals
# very small values format in scientific notation
if ((resources.NUM_MODE_UPPERHALFONLY == \
self._props['mode'] and j > i ) or \
(resources.NUM_MODE_LOWERHALFONLY == \
self._props['mode'] and j < i ) ) \
and self._props['sumup_diag'] :
val = self._props['data'][i, j] + self._props['data'][j, i]
else :
val = self._props['data'][i, j]
str_val = ''
if self._smartdict['ndigits'] is not None :
if pow(10., -self._smartdict['ndigits']) > abs(val) :
str_val = '0'
else :
str_val = ('%%.%df' % self._smartdict['ndigits']) % val
else :
str_val = '%.2e' % val
message = ''.join((message, 'value = %s' % str_val))
self._props['msgBar'].message('help', message)
def __mouse_leave(self, e) :
"""When the mouse leaves the canvas."""
# cleaning the message bar
if self._props['msgBar'] :
self._props['msgBar'].message('help', '')
# callback
if callable(self._props['mouse_leave_callback']) :
self._props['mouse_leave_callback']()
def __deselect_all(self) :
"""Deselect all labels."""
if self._props['data'] is None :
return
# default labels text foreground
fg_default = 'black'
# rows
for i in xrange(1, self._props['data'].shape[0]) :
self.itemconfig('row%d' % i, fill=fg_default)
# columns
for j in xrange(1, self._props['data'].shape[1]) :
self.itemconfig('col%d' % j, fill=fg_default)
def __mouse_doubleclick_element(self, e) :
"""Double clicked on an element."""
ij = self.__mousepos_to_ij(e.x, e.y)
if ij is not None :
# callback : arguments are row & column indices of the given matrix
# NOT vibration numbers !!!
if callable(self._props['dblclick_callback']) :
self.tk.call('update', 'idletasks')
self._props['dblclick_callback'](*ij)
def __find_rect_ij(self, ij) :
"""Find the rectangle identifier for given ij.
Return None unless found.
"""
ids = self.find_withtag('rect%d_%d' % ij)
if 1 == len(ids) :
return ids[0]
else :
return None
def update(self, **kw) :
"""Update the canvas.
See the constructor __init__() for the keyword arguments.
"""
# refresh properties
self._smartdict.merge()
self._smartdict.kw = kw
props = self._smartdict
self._props = self._smartdict
# do nothing if data not given
if props['data'] is None :
return
# cleaning the canvas and waiting for the operation to be completed
self.delete(Tkinter.ALL)
self.tk.call('update')
# dictionary for mapping between items ids and position in the table
self.__id_to_ij = {}
# define the maximum width of the labels
# the can suppress the labels with labels_on = False
# even if they are given
if props['labels_on'] :
maxwidth_labels1_rows = self._maxwidth_labels(props['labels1_rows'])
maxwidth_labels2_rows = self._maxwidth_labels(props['labels2_rows'])
maxwidth_labels1_cols = self._maxwidth_labels(props['labels1_cols'])
maxwidth_labels2_cols = self._maxwidth_labels(props['labels2_cols'])
else :
maxwidth_labels1_rows = 0
maxwidth_labels2_rows = 0
maxwidth_labels1_cols = 0
maxwidth_labels2_cols = 0
# text metrics
text_labels1_width = self.__get_text_width(
self._create_string_n(maxwidth_labels1_rows), props['font_labels1'])
text_labels2_width = self.__get_text_width(
self._create_string_n(maxwidth_labels2_rows), props['font_labels2'])
text_labels1_height = self.__get_text_height(props['font_labels1'])
text_labels2_height = self.__get_text_height(props['font_labels2'])
# width of the labels
labels1_box_width = text_labels1_width + 2 * props['text_padding']
labels2_box_width = text_labels2_width + 2 * props['text_padding']
labels1_box_height = text_labels1_height + 2 * props['text_padding']
labels2_box_height = text_labels2_height + 2 * props['text_padding']
# quadrat face around the circles
list_sizes = [ props['max_circlewidth'] ]
if 0 < maxwidth_labels1_rows :
list_sizes.append(labels1_box_height)
list_sizes.append(labels1_box_width)
if 0 < maxwidth_labels2_rows :
list_sizes.append(labels2_box_height)
list_sizes.append(labels2_box_width)
a = max(list_sizes)
## set the approptiate size of the canvas
n = props['data'].shape[0] - 1
m = props['data'].shape[1] - 1
canvas_width = 2 * props['indent'] + m * a
canvas_height = 2 * props['indent'] + n * a
if 0 < maxwidth_labels1_rows :
canvas_width += labels1_box_width
canvas_height += labels1_box_height
if 0 < maxwidth_labels2_rows :
canvas_width += labels2_box_width
canvas_height += labels2_box_height
self.configure(width=canvas_width, height=canvas_height)
## rendering labels1 and labels2 in rows
# define start positions
start_x = props['indent']
start_y = props['indent']
if 0 < maxwidth_labels1_rows :
start_y += labels1_box_height
if 0 < maxwidth_labels2_rows :
start_y += labels2_box_height
# starting position for circles - x
circles_start_x = start_x
# labels1 rows
if 0 < maxwidth_labels1_rows :
for i in xrange(1, 1 + n) :
x = start_x + labels1_box_width / 2
y = start_y + (i - 1) * a + a / 2
self.create_text(x, y,
text=props['labels1_rows'][i],
font=props['font_labels1'],
anchor='center',
tags='row%d' % i)
start_x += labels1_box_width
circles_start_x += labels1_box_width
# labels2 rows
if 0 < maxwidth_labels2_rows :
for i in xrange(1, 1 + n) :
x = start_x + labels2_box_width / 2
y = start_y + (i - 1) * a + a / 2
self.create_text(x, y,
text=props['labels2_rows'][i],
font=props['font_labels2'],
anchor='center',
tags='row%d' % i)
circles_start_x += labels2_box_width
## rendering labels1 and labels2 in columns
start_x = props['indent']
if 0 < maxwidth_labels1_cols :
start_x += labels1_box_width
if 0 < maxwidth_labels2_cols :
start_x += labels2_box_width
start_y = props['indent']
# starting position for circles - y
circles_start_y = start_y
# labels1 cols
if 0 < maxwidth_labels1_cols :
for i in xrange(1, 1 + m) :
x = start_x + (i - 1) * a + a / 2
y = start_y + labels1_box_height / 2
self.create_text(x, y,
text=props['labels1_cols'][i],
font=props['font_labels1'],
anchor='center',
tags='col%d' % i)
start_y += labels1_box_height
circles_start_y += labels1_box_height
# labels2 cols
if 0 < maxwidth_labels2_cols :
for i in xrange(1, 1 + m) :
x = start_x + (i - 1) * a + a / 2
y = start_y + labels2_box_height / 2
self.create_text(x, y,
text=props['labels2_cols'][i],
font=props['font_labels2'],
anchor='center',
tags='col%d' % i)
circles_start_y += labels2_box_height
## rendering the bounding rectangles for the circles
for i in xrange(1, 1 + n) :
y = circles_start_y + (i - 1) * a
for j in xrange(1, 1 + m) :
if ( resources.NUM_MODE_UPPERHALFONLY == props['mode'] and j < i ) or \
( resources.NUM_MODE_LOWERHALFONLY == props['mode'] and j > i ) :
continue
#
x = circles_start_x + (j - 1) * a
tag_rect = 'rect%d_%d' % (i, j)
element_id = self.create_rectangle(x,
y,
x + a,
y + a,
tags=('element', tag_rect),
outline=props['color_rect'],
fill='white')
self.__id_to_ij[element_id] = (i, j)
## rendering a bounding frame around the circles
## very usefull if one has only circles !
if props['bbox_on'] :
self.create_rectangle(circles_start_x, circles_start_y,
circles_start_x + m * a, circles_start_y + n * a)
## rendering the circles after the bounding boxes
## to prevent the "cutted" view
for i in xrange(1, 1 + n) :
y = circles_start_y + (i - 1) * a
for j in xrange(1, 1 + m) :
if (resources.NUM_MODE_UPPERHALFONLY == props['mode'] and j < i) or \
(resources.NUM_MODE_LOWERHALFONLY == props['mode'] and j > i) :
continue
# variable val is the final value to show
# sum up elements if demanded (useful for the molecular invariants)
if ((resources.NUM_MODE_UPPERHALFONLY == props['mode'] and j > i ) or \
(resources.NUM_MODE_LOWERHALFONLY == props['mode'] and j < i )) \
and props['sumup_diag'] :
val = props['data'][i, j] + props['data'][j, i]
else :
val = props['data'][i, j]
#
x = circles_start_x + (j - 1) * a
abs_value = fabs(val)
if val > 0. :
color = props['color_positive']
else :
color = props['color_negative']
if resources.NUM_TYPE_PROPORTIONAL_TO_SQUARE == props['type'] :
r = float(0.5 * a * sqrt(abs_value/props['full_circle']))
else :
r = float(0.5 * a * (abs_value/props['full_circle']))
r *= props['scale_factor']
# do not render null contributions
if r < 1e-5 :
continue
if 0. != r :
x_left = x + int(ceil(0.5 * a - r))
y_left = y + int(ceil(0.5 * a - r))
x_right = x + int(float(0.5 * a + r))
y_right = y + int(float(0.5 * a + r))
element_id = self.create_oval(x_left, y_left, x_right, y_right,
fill=color,
tags=('element',),
outline=color)
self.__id_to_ij[element_id] = (i, j)
# binding
self.tag_bind('element', '<Motion>', self.__mouse_motion)
self.tag_bind('element', '<Leave>', self.__mouse_leave)
self.tag_bind('element', '<Button-1>', self.__mouse_doubleclick_element)
def mark_rect_ij(self, ij, mark=True) :
"""Mark/unmark a rectangle.
Positional arguments :
ij -- identifier of the rectangle
Keyword arguments :
mark -- whether to mark (default True)
"""
id_ = self.__find_rect_ij(ij)
if id_ is not None :
if mark :
self.itemconfig(id_, outline='red', fill='red')
else :
self.itemconfig(id_, outline='black', fill='')
class CorrelationResultsNoteBook(BaseWidget, Pmw.NoteBook) :
"""Widget for representing results of a correlation of vibrational motions.
The widget is based on Pmw.NoteBook.
The following read-only properties are exposed :
table -- table with the text, see CorrelationResultsTable
circles_frame -- frame with the circles
The following public methods are exported :
update_contents() -- update the contents of the widget
show_A4() -- switch to the A4 representation of circles
"""
def __init__(self, master, matrix,
freqs_ref, freqs_tr, include_tr_rot, **kw) :
"""Constructor of the class.
Positional arguments :
matrix -- matrix (one-based ndarray)
freqs_ref -- wavenumbers of the reference molecule
(one-based ndarray)
freqs_tr -- wavenumbers of the trial molecule
(one-based ndarray)
include_tr_rot -- whether translations / rotations are present
in the table
Keyword arguments :
dblclick_callback -- called on a double clicke on an element (default None)
function of 2 arguments : ref_no and tr_no
msgBar -- message bar (default None)
"""
Pmw.NoteBook.__init__(self,
master,
raisecommand=None,
lowercommand=None,
arrownavigation=False)
# saving data
self._matrix = matrix
self._freqs_ref = freqs_ref
self._freqs_tr = freqs_tr
self._include_tr_rot = include_tr_rot
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
# last visited directory
self.__lastdir = None
# construct filetypes
# add pdf if ps2pdf is found
self.__save_filetypes = []
if misc.is_command_on_path('ps2pdf') :
self.__save_filetypes.append(
(resources.STRING_FILETYPE_PDFFILE_DESCRIPTION, '*.pdf'))
self.__save_filetypes.append(
(resources.STRING_FILETYPE_EPSFILE_DESCRIPTION, '*.ps *.eps'))
def _constructGUI(self) :
"""Construct the GUI of the widget."""
## first tab - values
tabValues = self.add('Values')
tabValues.grid_rowconfigure(0, weight=1)
tabValues.grid_columnconfigure(1, weight=1)
# save csv button
widget = Tkinter.Button(tabValues,
image=getimage('oo_calc'),
relief='flat',
overrelief='raised',
command=self.__export_table)
self._varsdict['btn_save_values'] = widget
self._varsdict['btn_save_values'].grid(row=0, column=0,
padx=3, pady=3, sticky='n')
# table with the values
widget = CorrelationResultsTable(tabValues,
matrix=self._matrix,
freqs_ref=self._freqs_ref,
freqs_tr=self._freqs_tr,
include_tr_rot=self._include_tr_rot,
msgBar=self._smartdict['msgBar'],
dblclick_callback=\
self._smartdict['dblclick_callback'])
self._varsdict['table_values'] = widget
self._varsdict['table_values'].grid(row=0, column=1,
padx=3, pady=3, sticky='news')
## second tab - circles
tabCircles = self.add('Circles')
tabCircles.grid_rowconfigure(1, weight=1)
tabCircles.grid_columnconfigure(2, weight=1)
# buttons (left side)
buttons = Tkinter.Frame(tabCircles)
buttons.grid(row=0, column=0, rowspan=2, padx=3, pady=3, sticky='n')
# save button
widget = Tkinter.Button(buttons,
image=getimage('save'),
relief='flat',
overrelief='raised',
command=self.__export_circles)
self._varsdict['btn_save_circles'] = widget
self._varsdict['btn_save_circles'].grid(row=0, column=0,
padx=3, pady=3, sticky='n')
# plot all_ref -> all_tr on an A4 page
self._varsdict['btn_A4'] = Tkinter.Button(buttons,
image=getimage('A4'),
relief='flat',
overrelief='raised',
command=self.__resize_canvas)
self._varsdict['btn_A4'].grid(row=1, column=0, padx=3, pady=3, sticky='n')
# counter to control the size of the canvas
self._varsdict['var_scale_size'] = Tkinter.DoubleVar()
self._varsdict['var_scale_size'].set(1.0)
validate = dict(validator='real', min=0.5, max=10.0)
counter_size = Pmw.Counter(buttons,
autorepeat=False,
orient='vertical',
entry_width=3,
entryfield_value=1.0,
datatype=dict(counter='real', separator='.'),
entry_textvariable=\
self._varsdict['var_scale_size'],
entryfield_validate=validate,
entryfield_modifiedcommand=self.__resize_canvas,
increment=0.1)
counter_size.grid(row=2, column=0, padx=3, pady=3, sticky='n')
## properties in the upper left corner of the circles
self._varsdict['btn_settings'] = Tkinter.Button(tabCircles,
image=getimage('prefs'),
relief='flat',
overrelief='raised',
command=self.__settings)
self._varsdict['btn_settings'].grid(row=0, column=1, padx=3, pady=3)
## scrollable area for circles and
## headers with freq/no as text controls
# row header
widget = Tkinter.Text(tabCircles,
height=2,
font=resources.FONT_TABLE_SEL_HEADER1,
foreground='blue',
state='disabled',
wrap='none')
self._varsdict['canvas_columnheader'] = widget
self._varsdict['canvas_columnheader'].grid(row=0, column=2,
padx=1, pady=1, sticky='ew')
# column headers
widget = Tkinter.Text(tabCircles,
width=9,
font = resources.FONT_TABLE_SEL_HEADER1,
foreground='blue',
state='disabled')
self._varsdict['canvas_rowheader'] = widget
self._varsdict['canvas_rowheader'].grid(row=1, column=1,
padx=1, pady=1, sticky='ns')
# circles
self._varsdict['scrolled_frame'] = Pmw.ScrolledFrame(tabCircles)
self._varsdict['scrolled_frame'].grid(row=1, column=2, columnspan=2,
padx=3, pady=3, sticky='news')
self._varsdict['scrolled_frame'].interior().grid_rowconfigure(0, weight=1)
self._varsdict['scrolled_frame'].interior().grid_columnconfigure(0,
weight=1)
# do not update the circles
self._varsdict['circles'] = TwoDCircles(
self._varsdict['scrolled_frame'].interior(),
msgBar=self._smartdict['msgBar'],
dblclick_callback=self.__dblclick_circle,
mouse_motion_callback=self.__show_ij_freqno,
mouse_leave_callback=self.__clean_circle_headers,
ndigits=3)
self._varsdict['circles'].grid(row=0, column=0,
padx=0, pady=0, sticky='news')
# circles in A4 representation should be shown
self.selectpage(1)
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.table = property(fget=self._get_table)
self.__class__.circles_frame = property(fget=self._get_circles_frame)
def _bind_events(self) :
"""Bind events."""
cmd = misc.Command(mouse_wheel, self._varsdict['scrolled_frame'])
self._varsdict['circles'].bind('<Button-4>', cmd)
self._varsdict['circles'].bind('<Button-5>', cmd)
def _bind_help(self) :
"""Bind help messages to the GUI components."""
self._balloon.bind(self._varsdict['btn_save_values'],
'Save the table to a CSV file.')
self._balloon.bind(self._varsdict['btn_save_circles'],
'Save the matrix in PS/EPS/PDF formats.')
def _get_table(obj) :
"""Getter function for the table property."""
return obj.__table_values
_get_table = staticmethod(_get_table)
def _get_circles_frame(obj) :
"""Getter function for the circles_frame property."""
return obj.__scrolled_frame
_get_circles_frame = staticmethod(_get_circles_frame)
def __dblclick_circle(self, i, j) :
"""Double clicked on a circle."""
if callable(self._smartdict['dblclick_callback']) :
# convert the circle positions to the vibrational numbers
ref_index, tr_index = self._varsdict['table_values'].ij_to_vibno(i, j)
self._smartdict['dblclick_callback'](ref_index, tr_index)
# saving the last clicked vibrational pair
# marking the currently clicked pair
# unmarking the previous one
if not hasattr(self, '_last_clicked_vibno') :
self._last_clicked_vibno = None
if self._last_clicked_vibno is not None :
self._varsdict['circles'].mark_rect_ij(
self._varsdict['table_values'].vibno_to_ij(*self._last_clicked_vibno),
False)
self._varsdict['circles'].mark_rect_ij(
self._varsdict['table_values'].vibno_to_ij(ref_index, tr_index))
self._last_clicked_vibno = (ref_index, tr_index)
def __export_table(self) :
"""Export the table as a *.csv file."""
filetypes = [ (resources.STRING_FILETYPE_CSVFILE_DESCRIPTION, '*.csv') ]
filename = tkFileDialog.SaveAs(parent=self.interior(),
filetypes=filetypes,
defaultextension='.csv',
initialdir=self.__lastdir).show()
if filename is not None :
file_csv = open(filename, 'w+')
file_csv.write(self._varsdict['table_values'].csv)
file_csv.close()
self.__lastdir = os.path.dirname(filename)
def __export_circles(self) :
"""Export the circles."""
filename = tkFileDialog.SaveAs(parent=self.interior(),
filetypes=self.__save_filetypes,
initialdir=self.__lastdir).show()
if filename is not None :
try :
base, ext = os.path.splitext(filename)
ext = ext.lower()
if ext not in ('.eps', '.ps', '.pdf') :
raise InvalidArgumentError('Unsupported type : %s' % ext)
# unmark the rectangle before saving
if self._last_clicked_vibno is not None :
self._varsdict['circles'].mark_rect_ij(
self._varsdict['table_values'].vibno_to_ij
(*self._last_clicked_vibno),
mark=False)
# just ps
if ext in ('.eps', '.ps') :
self._varsdict['circles'].postscript(file=filename)
# pdf
else :
# as a first step save to ps, and then to pdf
psname = '%s.ps' % base
self._varsdict['circles'].postscript(file=psname)
status = misc.ps2pdf(psname,
removesrc=True,
CompatibilityLevel='1.3')
self.__lastdir = os.path.dirname(filename)
except :
show_exception(sys.exc_info())
def __find_font(self, max_vibno, maxwidth) :
"""Find a font for which the width of the number max_vibno would
be smaller than maxwidth.
"""
# ('Courier', 13, 'bold')
for size in xrange(13, 1, -1) :
font = ('Courier', size, 'bold')
if tkFont.Font(self.interior(), font=font).\
measure(str(max_vibno)) < maxwidth :
break
return font
def __resize_canvas(self) :
"""Resize the canvas with circles."""
try :
kw = dict(scale_factor=self._varsdict['var_scale_size'].get(),
show_tr_rot=self.__show_tr_rot,
nrot_ref=self.__nrot_ref,
nrot_tr=self.__nrot_tr)
self.show_A4(**kw)
except :
pass
def __show_ij_freqno(self, i, j) :
"""Show frequency / no in the text widgets associated with the circles."""
# position of the mouse poiter and upper left coordinates of the headers
# x is important for the column header, y for the row headers
x, y = self._varsdict['circles'].winfo_pointerxy()
colrootx = self._varsdict['canvas_columnheader'].winfo_rootx()
colrooty = self._varsdict['canvas_rowheader'].winfo_rooty()
# position of the text
delta_x = x - colrootx
delta_y = y - colrooty
#
if 0 > delta_x or 0 > delta_y :
return
# calculating the width and height of a single symbol to
# place the labels properly
# the font should be with fixed width
font = tkFont.Font(self.interior(), font=resources.FONT_TABLE_SEL_HEADER1)
symbol_width, symbol_height = font.measure(' '), font.metrics()['ascent']
# current sizes of the headers = size of the circles frame
self.interior().tk.call('update', 'idletasks')
cur_colheader_width = self._varsdict['canvas_columnheader'].winfo_width()
cur_rowheader_height = self._varsdict['canvas_rowheader'].winfo_height()
# enabling the text editing
self._varsdict['canvas_columnheader'].configure(state='normal')
self._varsdict['canvas_rowheader'].configure(state='normal')
# cleaning the headers
self._varsdict['canvas_columnheader'].delete('1.0', 'end')
self._varsdict['canvas_rowheader'].delete('1.0', 'end')
# transforming the raw ij of the canvas to
# the indices of the reference & trial molecules
ref_index, tr_index = self._varsdict['table_values'].ij_to_vibno(i, j)
## column headers (trial molecule)
nempty = int(cur_colheader_width / symbol_width) + 20
dummytext = ('%%%ds' % nempty) % ''
format_no = '%s'
format_freq = '%.0f'
# consider the translations / rotations
if 0 > tr_index :
freq_col = format_freq % 0.
no_col = format_no % \
resources.STRINGS_TRANSLATIONS_ROTATIONS_LABELS[-tr_index-1]
else :
freq_col = format_freq % self._freqs_tr[tr_index]
no_col = format_no % str(tr_index)
self._varsdict['canvas_columnheader'].configure(width=nempty)
self._varsdict['canvas_columnheader'].insert('end',
dummytext + '\n' + dummytext)
self._varsdict['canvas_columnheader'].insert(
'@%d,%d' % (delta_x, symbol_height), freq_col)
self._varsdict['canvas_columnheader'].insert(
'@%d,%d' % (delta_x, 2*symbol_height), no_col)
## row headers
rowheader_width = int(self._varsdict['canvas_rowheader'].cget('width'))
nlines = int(cur_rowheader_height / symbol_height)
dummylineformat = '%%%ds\n' % rowheader_width
format_rowheader = '%5s%4s'
dummytext = ''
for q in xrange(nlines) :
dummytext = ''.join((dummytext, dummylineformat % ''))
# consider the translations / rotations
if 0 > ref_index :
freq_row = format_freq % 0.
no_row = format_no % \
resources.STRINGS_TRANSLATIONS_ROTATIONS_LABELS[-ref_index-1]
else :
freq_row = format_freq % self._freqs_ref[ref_index]
no_row = format_no % str(ref_index)
self._varsdict['canvas_rowheader'].insert('end', dummytext)
self._varsdict['canvas_rowheader'].insert(
'@0,%d' % (delta_y + symbol_height/2),
format_rowheader % (freq_row, no_row))
## disabling the text editing to prevent its modification
self._varsdict['canvas_columnheader'].configure(state='disabled')
self._varsdict['canvas_rowheader'].configure(state='disabled')
def __clean_circle_headers(self) :
"""Clean the headers of the circles canvas.
Called when the mouse leaves the widget.
"""
# unblocking the text controlls so that they could be edited
self._varsdict['canvas_columnheader'].configure(state='normal')
self._varsdict['canvas_rowheader'].configure(state='normal')
self._varsdict['canvas_columnheader'].delete('1.0', 'end')
self._varsdict['canvas_rowheader'].delete('1.0', 'end')
# blocking it to prevent modification
self._varsdict['canvas_columnheader'].configure(state='disabled')
self._varsdict['canvas_rowheader'].configure(state='disabled')
def __settings(self) :
"""Callback for the settings button."""
if 'dlg_settings' not in self._varsdict :
from pyviblib.gui.dialogs import TwoDCirclesSettingsDialog
self._varsdict['dlg_settings'] = TwoDCirclesSettingsDialog(
self.page(0), self._varsdict['circles'])
self._varsdict['dlg_settings'].update_controls()
self._varsdict['dlg_settings'].show()
def update_contents(self, ref_from, ref_to, tr_from, tr_to,
precision, show_tr_rot, nrot_ref, nrot_tr,
mark=False, threshold_marked=0.) :
"""Update the contents of the table and circles.
Positional arguments :
ref_from -- start vibration number of the reference molecule
ref_to -- end vibration number of the reference molecule
tr_from -- start vibration number of the trial molecule
tr_to -- end vibration number of the reference molecule
precision -- number of decimal points to show
show_tr_rot -- whether translations/rotations are to be shown
nrot_ref -- number of rotations in the reference molecule
nrot_tr -- number of rotations in the trial molecule
Keyword arguments :
mark -- whether to mark elements exceeding a threshold
supplied by the threshold_marked argument
(default False)
threshold_marked -- threshold for marking elements (default 0.)
"""
# saving the information
self.__show_tr_rot = show_tr_rot
self.__nrot_ref = nrot_ref
self.__nrot_tr = nrot_tr
# table
self._varsdict['table_values'].update_table(
ref_from, ref_to, tr_from, tr_to, precision, show_tr_rot, nrot_ref,
nrot_tr, mark, threshold_marked)
# circles
labels = self._varsdict['table_values'].labels
self.__max_circlewidth = 50
self._varsdict['circles'].update(
data=self._varsdict['table_values'].data,
labels1_cols=labels['labels1_cols'],
labels1_rows=labels['labels1_rows'],
labels2_cols=labels['labels2_cols'],
labels2_rows=labels['labels2_rows'],
dblclick_callback=self.__dblclick_circle,
font_labels1=resources.FONT_TWODCIRCLES_LABEL1,
font_labels2=resources.FONT_TWODCIRCLES_LABEL2,
text_padding=5,
indent=15,
max_circlewidth=50)
self.__a4_enabled = False
# marking the last clicked element
if hasattr(self, '_last_clicked_vibno') and \
self._last_clicked_vibno is not None :
# prevent marking of tr / rot in the a4 representation & if is not desired
if (self.__a4_enabled and any(0 > array(self._last_clicked_vibno))) or \
(not show_tr_rot and any(0 > array(self._last_clicked_vibno))) :
pass
else :
self._varsdict['circles'].mark_rect_ij(
self._varsdict['table_values'].vibno_to_ij(*self._last_clicked_vibno))
def show_A4(self, scale_factor=1.0,
show_tr_rot=False, nrot_ref=0, nrot_tr=0) :
"""Switch to the A4 representation of the circles.
Keyword arguments :
scale_factor -- number for scaling the canvas size (default 1.)
show_tr_rot -- whether to show translations/rotations (default False)
nrot_ref -- number of rotations in the reference molecule (default 0)
nrot_tr -- number of rotations in the trial molecule (default 0)
"""
if self._freqs_ref is None or self._freqs_tr is None :
return
# can show tr/rot if they are present in the matrix
show_tr_rot = show_tr_rot and self._include_tr_rot
# number of frequencies in the reference and trial molecules
# m defines the width of the whole matrix
n = len(self._freqs_ref) - 1
m = len(self._freqs_tr) - 1
if show_tr_rot :
n += 3 + nrot_ref
m += 3 + nrot_tr
# resolution dots per mm
# width of an A4-page in pixels
width_A4 = scale_factor * self.winfo_fpixels('210m')
# indent on the page - 5 mm
indent = self.winfo_pixels('5m')
# approximate width of circles
maxwidth = int( float(width_A4 - 2. * indent) / float(m + 1) )
# appropriate font
font_labels2 = self.__find_font(m, maxwidth)
# width of the label text
labels2text_width = tkFont.Font(
self.interior(), font=font_labels2).measure(str(m))
# text_padding
text_padding = max(0, (maxwidth - labels2text_width) / 2)
# all circles must pass to the width of an A4 page : 210 mm
self.__max_circlewidth = int(
float(width_A4 - 2. * indent - maxwidth) / float(m+1))
# updating the controls
self._varsdict['table_values'].update_table(ref_from=1,
ref_to=len(self._freqs_ref)-1,
tr_from=1,
tr_to=len(self._freqs_tr)-1,
precision=3,
show_tr_rot=show_tr_rot,
nrot_ref=nrot_ref,
nrot_tr=nrot_tr,
mark=False,
threshold_marked=0.)
# circles : set the labels properly
labels = resources.STRINGS_TRANSLATIONS_ROTATIONS_LABELS
if show_tr_rot :
labels_ref = [None] + list(labels[:3+nrot_ref]) + \
range(1, len(self._freqs_ref))
labels_tr = [None] + list(labels[:3+nrot_tr]) + \
range(1, len(self._freqs_tr))
else :
labels_ref = range(len(self._freqs_ref))
labels_tr = range(len(self._freqs_tr))
self._varsdict['circles'].update(data=self._varsdict['table_values'].data,
labels1_cols=None,
labels1_rows=None,
labels2_cols=labels_tr,
labels2_rows=labels_ref,
max_circlewidth=self.__max_circlewidth,
indent=indent,
text_padding=text_padding,
font_labels2=font_labels2)
self.__a4_enabled = True
# marking the last clicked element
if hasattr(self, '_last_clicked_vibno') and \
self._last_clicked_vibno is not None :
# prevent marking of tr / rot in the a4 representation
if any(0 > array(self._last_clicked_vibno)) :
pass
else :
self._varsdict['circles'].mark_rect_ij(
self._varsdict['table_values'].vibno_to_ij(
*self._last_clicked_vibno))
class WizardWidget(BaseWidget, Tkinter.Frame) :
"""Wizard widget.
The following read-only properties are exposed :
notebook -- encapsulated internal notebook
buttonbox -- Back and Next buttons
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
back_command -- command for the Back button (default None)
next_command -- command for the Next button (default None)
"""
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _constructGUI(self) :
"""Construct the GUI of the widget."""
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
# notebook without tabs
self._varsdict['notebook'] = Pmw.NoteBook(self, tabpos=None)
self._varsdict['notebook'].grid(row=0, column=0,
padx=3, pady=3, sticky='news')
# separator
separator = Tkinter.Frame(self, height=2, bd=1, relief='sunken')
separator.grid(row=1, column=0, padx=3, pady=3, sticky='we')
# buttons
self._varsdict['buttonbox'] = Pmw.ButtonBox(self)
self._varsdict['buttonbox'].grid(row=2, column=0,
padx=3, pady=3, sticky='we')
self._varsdict['buttonbox'].add(resources.STRING_BUTTON_BACK,
command=self._smartdict['back_command'])
self._varsdict['buttonbox'].add(resources.STRING_BUTTON_NEXT,
command=self._smartdict['next_command'])
self._varsdict['buttonbox'].setdefault(resources.STRING_BUTTON_NEXT)
self._varsdict['buttonbox'].alignbuttons()
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.notebook = property(
fget=misc.Command(misc.Command.fget_value, self._varsdict['notebook']))
self.__class__.buttonbox = property(
fget=misc.Command(misc.Command.fget_value, self._varsdict['buttonbox']))
class GeometryMeasureToolbar(ButtonToolbar) :
"""Toolbar for measuring distances, angles and dihedral angles.
The following readable and writable property is exposed :
resolution -- resolution of the connected render widget
"""
def __init__(self, master, renderWidget, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
widget -- render widget to connect to
Keyword arguments :
horizontal -- whether the toolbar is horizontal (default False)
"""
if not isinstance(renderWidget, MoleculeRenderWidget) :
raise ConstructorError('Invalid renderWidget argument')
self._master = master
self.__renderWidget = renderWidget
ButtonToolbar.__init__(self, master, **self._toolbar_kw(**kw))
self.configure(relief='ridge', borderwidth=2)
def _toolbar_kw(**kw) :
"""Retrieve the keywords of the parent class."""
tb_kw = dict(kw)
tb_kw['horizontal'] = kw.get('horizontal', False)
tb_kw['style'] = 0
return tb_kw
_toolbar_kw = staticmethod(_toolbar_kw)
def _init_vars(self) :
"""Initialize variables."""
ButtonToolbar._init_vars(self)
self._smartdict['resolution'] = resources.NUM_RESOLUTION_VTK
def _constructGUI(self) :
"""Construct the GUI of the widget."""
# distance
self.add_button(imagename='distance',
command=misc.Command(self.__start_measure, 2),
helptext='Measure a distance.')
# angle
self.add_button(imagename='angle',
command=misc.Command(self.__start_measure, 3),
helptext='Measure an angle.')
# dihedral angle
self.add_button(imagename='dihedral_angle',
command=misc.Command(self.__start_measure, 4),
helptext='Measure a dihedral angle.')
# remove selection
widget = self.add_button(imagename='remove',
command=self.__remove,
helptext='Measure a dihedral angle.')
self._varsdict['btn_remove'] = widget
# settings
self._varsdict['color_angle'] = resources.COLOR_MARKING_TRIANGLE
self._varsdict['color_dihedral'] = misc.color_complementary(
self._varsdict['color_angle'])
self.__renderWidget.clicked_atom_callback = self.__clicked_atom_callback
self.__updateGUI()
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.resolution = property(fget=self._get_resolution,
fset=self._set_resolution)
def _bind_help(self) :
"""Bind help messages to the GUI components."""
pass
def __remove(self) :
"""Remove marked atoms."""
self.__renderWidget.cleanup()
self.__renderWidget.render_molecule(
resolution=self._smartdict['resolution'],
molecule_mode=resources.NUM_MODE_BALLSTICK,
bonds_mode=resources.NUM_MODE_BONDS_ATOMS_COLOR)
self.__renderWidget.Render()
self.__renderWidget.do_picking = False
self.__updateGUI()
def _set_resolution(obj, res) :
"""Setter function for the resolution property."""
obj._smartdict.kw['resolution'] = res
_set_resolution = staticmethod(_set_resolution)
def _get_resolution(obj) :
"""Getter function for the resolution property."""
return obj._smartdict['resolution']
_get_resolution = staticmethod(_get_resolution)
def __start_measure(self, Nmaxpicked) :
"""Prepare the widget for measuring.
Enable the picking and set the number of maximaly allowed picked atoms.
"""
# cleaning the list of picked atoms
self.__remove()
self.__renderWidget.do_picking = True
self._Nmaxpicked = Nmaxpicked
def __clicked_atom_callback(self, num, node_index) :
"""Called when the user left clicks on an atom.
max_picked : maximal number of atoms that can be picked
(disable picking if exceeded)
"""
# processing only left clicks on non-picked atoms
if 1 != num or not hasattr(self, '_Nmaxpicked') or \
not self.__renderWidget.do_picking:
return
node = self.__renderWidget.get_node('atoms', node_index)
if node is None :
return
# disabling the picking if maximal number of picked atoms is reached
self.__renderWidget.do_picking = (self.__renderWidget.Npicked < \
self._Nmaxpicked)
self.__updateGUI()
if self.__renderWidget.Npicked == self._Nmaxpicked :
# render the triangles for the bonds
atoms_list = self.__renderWidget.picked_atoms_indices
if 3 <= self._Nmaxpicked :
# making the middle atom completely opaque for an angle !
if 3 == self._Nmaxpicked :
middle_node = self.__renderWidget.get_node('atoms', atoms_list[1])
middle_node.highlight_picked(True, transparency=0.6)
self.__renderWidget.render_triangle(
atoms_list[0],
atoms_list[1],
atoms_list[2],
color=self._varsdict['color_angle'])
if 4 == self._Nmaxpicked :
self.__renderWidget.render_triangle(
atoms_list[1],
atoms_list[2],
atoms_list[3],
color=self._varsdict['color_dihedral'])
self.__show_result(atoms_list)
def __show_result(self, atoms_list) :
"""Show the result of to the user.
Positional arguments :
atoms_list -- list of 0-based atoms
"""
if atoms_list is None :
return
len_list = len(atoms_list)
if len_list not in (2, 3, 4) :
return
mol = self.__renderWidget.molecule
if 2 == len_list :
d = mol.distance(1 + atoms_list[0], 1 + atoms_list[1])
msg = u'Distance (%d,%d) is %.3f \u00C5' % \
(1 + atoms_list[0], 1 + atoms_list[1], d)
if 3 == len_list :
a = mol.angle(1 + atoms_list[0], 1 + atoms_list[1], 1 + atoms_list[2])
msg = u'Angle (%d,%d,%d) is %.3f\u00B0' % \
(1 + atoms_list[0], 1 + atoms_list[1], 1 + atoms_list[2], a)
if 4 == len_list :
dh = mol.dihedral(1 + atoms_list[0], 1 + atoms_list[1],
1 + atoms_list[2], 1 + atoms_list[3])
msg = u'Dihedral angle (%d,%d,%d,%d) is %.3f\u00B0' % \
(1 + atoms_list[0], 1 + atoms_list[1], 1 + atoms_list[2],
1 + atoms_list[3], dh)
if self._smartdict['msgBar'] is not None :
self._smartdict['msgBar'].message('state', msg)
tkMessageBox.showinfo(parent=self._master, title='Result', message=msg)
def __updateGUI(self) :
"""Update the remove button."""
if 0 == self.__renderWidget.Npicked :
state = 'disabled'
else :
state = 'normal'
self._varsdict['btn_remove'].configure(state=state)
class ChooseColorWidget(BaseWidget, Tkinter.Frame) :
"""Widget for choosing a color.
Consists of a label with a description and a button for changing the color.
The following readable and writable property is exposed :
color -- color in the HTML format
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
text -- description (default '')
initialcolor -- initial color of the button (default default)
sticky -- sticky option for the button (default 'w')
label_width -- width of the label (default length of the text)
button_width -- width of the button (default 7)
"""
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
self._smartdict['text'] = ''
self._smartdict['initialcolor'] = None
self._smartdict['sticky'] = 'w'
self._smartdict['label_width'] = len(self._smartdict['text'])
self._smartdict['button_width'] = 7
def _constructGUI(self) :
"""Construct the GUI of the widget."""
# label with the description (empty unless given)
# text aligned to the left
lbl = Tkinter.Label(self,
text=self._smartdict['text'],
width=self._smartdict['label_width'],
anchor='w',
padx=3, pady=3)
lbl.grid(row=0, column=0, sticky='w')
# button which allows to change the color
# sticky option can be specified (default : 'w')
self.grid_columnconfigure(1, weight=1)
widget = Tkinter.Button(self,
bg=self._smartdict['initialcolor'],
activebackground=self._smartdict['initialcolor'],
width=self._smartdict['button_width'])
self._varsdict['btn_color'] = widget
self._varsdict['btn_color'].configure(command=self.__set_button_color)
self._varsdict['btn_color'].grid(row=0, column=1,
padx=3, pady=3,
sticky=self._smartdict['sticky'])
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.color = property(fget=self._get_color,
fset=self._set_color)
def _set_color(obj, color) :
"""Setter function for the color property."""
obj._varsdict['btn_color'].configure(bg=color, activebackground=color)
_set_color = staticmethod(_set_color)
def _get_color(obj) :
"""Getter function for the color property."""
return obj._varsdict['btn_color'].cget('bg')
_get_color = staticmethod(_get_color)
def __set_button_color(self) :
"""
Choose a color and set it to the background of the button.
"""
color_chosen = tkColorChooser.Chooser(
parent=self._varsdict['btn_color'],
initialcolor=self._varsdict['btn_color'].cget('bg'),
title=self._smartdict['text']).show()
if color_chosen[0] is not None :
self.color = color_chosen[1]
class AxesSettingsWidget(BaseWidget, Tkinter.Frame) :
"""Widget for setting properties of axes.
The following readable and writable properties are exposed
limits_auto -- whether the limits are to be set automatically (*)
from_ -- upper range of the y axis (*)
to_ -- lower range of the y axis (*)
multfactor -- order of magnitude for y values (*)
ticks_number -- number of ticks(*)
ticks_scaling_factor -- scaling factor (*)
linewidth -- line width
linecolor -- line color
The following read-only properties are exposed :
ticks_option -- how to render ticks (*)
ticks_auto -- whether the ticks are rendered automatically (*)
Properties marked with an asterisk (*) are exposed if the add_limits argument
of the constructor was set to True.
"""
"""List of properties."""
LIST_PROPERTIES = ('limits_auto', 'from_', 'to_', 'multfactor',
'ticks_option', 'ticks_auto', 'ticks_number',
'ticks_scaling_factor', 'linewidth', 'linecolor',
'invert')
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
add_limits -- whether to add the controls for the y values
(default True)
add_invert -- whether to add a checkbox for inverting y values
(default False)
buttons_to_validate -- list of buttons to block if the user
input is invalid (default None)
"""
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
# add limits by default
self._smartdict['add_limits'] = True
def _declare_properties(self) :
"""Declare properties of the widget."""
# auto adjust limits ?
self.__class__.limits_auto = property(fget=self.__get_limits_auto,
fset=self.__set_limits_auto)
# can be definied automatically
if self._smartdict['add_limits'] :
# limits
self.__class__.from_ = self.__create_property('from_')
self.__class__.to_ = self.__create_property('to_')
self.__class__.multfactor = self.__create_property('multfactor')
# ticks
self.__class__.ticks_option = property(fget=self.__get_ticks_option)
self.__class__.ticks_auto = property(fget=self.__get_ticks_auto)
self.__class__.ticks_number = self.__create_property('ticks_number')
self.__class__.ticks_scaling_factor = \
self.__create_property('ticks_scaling_factor')
if self._smartdict['add_invert'] :
self.__class__.invert = self.__create_property('invert')
# appearance
self.__class__.linewidth = self.__create_property('linewidth')
self.__class__.linecolor = property(fget=self.__get_linecolor,
fset=self.__set_linecolor)
def _constructGUI(self) :
"""Construct the GUI of the widget."""
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
row = 0
### axis settings
if self._smartdict['add_limits'] :
group_axis = Pmw.Group(self, tag_text='Axis')
group_axis.grid(row=row, column=0, padx=3, pady=3, sticky='nwe')
row += 1
## Axis limits : manual or auto
group_axis.interior().grid_rowconfigure(0, weight=1)
group_axis.interior().grid_columnconfigure(0, weight=1)
widget = Pmw.RadioSelect(group_axis.interior(),
buttontype='radiobutton',
orient='vertical',
labelpos='w',
label_text='Limits ')
self._varsdict['radio_axis_lim'] = widget
self._varsdict['radio_axis_lim'].grid(row=0, column=0,
padx=3, pady=3, sticky='w')
self._varsdict['radio_axis_lim'].add('manual')
self._varsdict['radio_axis_lim'].add('auto')
self._varsdict['radio_axis_lim'].setvalue('manual')
# from, to for the manual choice
# one can specify values between 0 and 1
self._varsdict['var_axis_from_'] = Tkinter.DoubleVar()
self._varsdict['var_axis_to_'] = Tkinter.DoubleVar()
validate = dict(validator='real',
min=-1.0,
max=1.0,
separator='.')
var = self._varsdict['var_axis_from_']
entryfield_from = Pmw.EntryField(group_axis.interior(),
entry_textvariable=var,
label_text='From ',
labelpos='w',
entry_width=7,
validate=validate,
modifiedcommand=self.__updateGUI)
entryfield_from.grid(row=0, column=1, padx=3, pady=8, sticky='wn')
var = self._varsdict['var_axis_to_']
entryfield_xto = Pmw.EntryField(group_axis.interior(),
entry_textvariable=var,
label_text='to ',
labelpos='w',
entry_width=7,
validate=validate,
modifiedcommand=self.__updateGUI)
entryfield_xto.grid(row=0, column=2, padx=3, pady=8, sticky='wn')
# multiply factor 10**(x)
self._varsdict['var_axis_multfactor'] = Tkinter.IntVar()
validate = dict(validator='integer',
min=-25,
max=+25)
var = self._varsdict['var_axis_multfactor']
counter_multfactor = Pmw.Counter(group_axis.interior(),
entry_textvariable=var,
label_text='Multiply factor ',
labelpos='w',
datatype=dict(counter='integer'),
entry_state='readonly',
entry_width=3,
entryfield_validate=validate,
increment=1,
autorepeat=True,
entryfield_value=-14)
counter_multfactor.grid(row=0, column=3, padx=3, pady=8, sticky='wn')
## Ticks' settings
group_axis.interior().grid_rowconfigure(1, weight=1)
widget = Pmw.RadioSelect(group_axis.interior(),
buttontype='radiobutton',
orient='vertical',
labelpos='w',
label_text='Ticks ')
self._varsdict['radio_axis_ticks'] = widget
self._varsdict['radio_axis_ticks'].grid(row=1, column=0,
padx=3, pady=3, sticky='w')
self._varsdict['radio_axis_ticks'].add('scaling factor')
self._varsdict['radio_axis_ticks'].add('fixed number')
self._varsdict['radio_axis_ticks'].add('auto')
self._varsdict['radio_axis_ticks'].setvalue('auto')
# vertical frame for the options
group_axis.interior().grid_rowconfigure(1, weight=1)
group_axis.interior().grid_columnconfigure(1, weight=1)
frm = Tkinter.Frame(group_axis.interior())
frm.grid(row=1, column=1, padx=3, pady=3, sticky='nw')
# scaling factor : from 0. to 1. (default : 0.1)
self._varsdict['var_axis_ticks_scaling_factor'] = Tkinter.DoubleVar()
self._varsdict['var_axis_ticks_scaling_factor'].set(0.1)
validate = dict(validator='real',
min=0.0,
max=1.0,
separator='.')
var = self._varsdict['var_axis_ticks_scaling_factor']
entryfield_sf = Pmw.EntryField(frm,
labelpos='w',
label_text='',
entry_textvariable=var,
entry_width=10,
validate=validate,
modifiedcommand=self.__updateGUI)
entryfield_sf.grid(row=0, column=0, padx=3, pady=4, sticky='wn')
# fixed number of ticks (from 0 to 10, default : 3)
self._varsdict['var_axis_ticks_number'] = Tkinter.IntVar()
self._varsdict['var_axis_ticks_number'].set(3)
validate = dict(validator='integer',
min=0,
max=10)
var = self._varsdict['var_axis_ticks_number']
counter_multfactor = Pmw.Counter(frm,
labelpos='w',
label_text='',
entry_textvariable=var,
datatype=dict(counter='integer'),
entry_state='readonly',
entry_width=3,
entryfield_validate=validate,
increment=1,
autorepeat=True)
counter_multfactor.grid(row=1, column=0, padx=3, pady=4, sticky='wn')
# invert axes (useful for ROA:)
if self._smartdict['add_invert'] :
self._varsdict['var_axis_invert'] = Tkinter.IntVar()
self._varsdict['var_axis_invert'].set(0)
checkbtn = Tkinter.Checkbutton(group_axis.interior(),
text='Invert values',
variable=\
self._varsdict['var_axis_invert'])
checkbtn.grid(row=2, column=0, padx=3, pady=6, sticky='w')
# align the controls of the first row
Pmw.alignlabels((entryfield_sf, counter_multfactor))
### Appearance
self.grid_rowconfigure(row, weight=1)
group_appearance = Pmw.Group(self, tag_text='Appearance')
group_appearance.grid(row=row, column=0, padx=3, pady=3, sticky='nwe')
group_appearance.interior().grid_rowconfigure(0, weight=1)
group_appearance.interior().grid_columnconfigure(0, weight=1)
# linewidth
self._varsdict['var_axis_linewidth'] = Tkinter.DoubleVar()
validate = dict(validator='real',
min=0.5,
max=3.0,
separator='.')
var = self._varsdict['var_axis_linewidth']
counter_linewidth = Pmw.Counter(group_appearance.interior(),
entry_textvariable=var,
label_text='Line width ',
labelpos='w',
datatype=dict(counter='real'),
entry_state='readonly',
entry_width=3,
entryfield_validate=validate,
increment=0.5,
autorepeat=True,
entryfield_value=1.0)
counter_linewidth.grid(row=0, column=0, padx=3, pady=3, sticky='w')
# linecolor : label + button with a desired background
widget = ChooseColorWidget(group_appearance.interior(),
text='Line color',
sticky='w')
self._varsdict['widget_axis_linecolor'] = widget
self._varsdict['widget_axis_linecolor'].grid(row=0, column=1,
padx=3, pady=3, sticky='w')
def __create_property(self, name) :
"""Create a readable and writable property."""
if 'var_axis_%s' % name in self._varsdict :
stmt = r"property(fget=misc.Command(self._get_property_, '%s')," + \
r"fset=misc.Command(self._set_property_, '%s'))"
return eval(stmt % (name, name))
else :
return None
def _get_property_(obj, name) :
"""Getter function for a property."""
return obj._varsdict['var_axis_%s' % name].get()
_get_property_ = staticmethod(_get_property_)
def _set_property_(obj, value, name) :
"""Setter function for a property."""
obj._varsdict['var_axis_%s' % name].set(value)
_set_property_ = staticmethod(_set_property_)
def __get_limits_auto(obj) :
"""Getter function for the limits_auto property."""
return 'auto' == obj._varsdict['radio_axis_lim'].getvalue()
__get_limits_auto = staticmethod(__get_limits_auto)
def __set_limits_auto(obj, val) :
"""Setter function for the limits_auto property."""
obj._varsdict['radio_axis_lim'].setvalue(val and 'auto' or 'manual')
__set_limits_auto = staticmethod(__set_limits_auto)
def __get_ticks_option(obj) :
"""Getter function for the ticks_option property."""
return obj._varsdict['radio_axis_ticks'].getvalue()
__get_ticks_option = staticmethod(__get_ticks_option)
def __get_ticks_auto(obj) :
"""Getter function for the ticks_auto property."""
return 'auto' == obj._varsdict['radio_axis_ticks'].getvalue()
__get_ticks_auto = staticmethod(__get_ticks_auto)
def __get_linecolor(obj) :
"""Getter function for the linecolor property."""
return obj._varsdict['widget_axis_linecolor'].color
__get_linecolor = staticmethod(__get_linecolor)
def __set_linecolor(obj, value) :
"""Setter function for the linecolor property."""
obj._varsdict['widget_axis_linecolor'].color = value
__set_linecolor = staticmethod(__set_linecolor)
def __updateGUI(self) :
"""Check the validity of the input data."""
if self._smartdict['buttons_to_validate'] is None :
return
state = 'normal'
# axes limits should be valid
try :
from_ = self._varsdict['var_axis_from_'].get()
to_ = self._varsdict['var_axis_to_'].get()
except ValueError:
state = 'disabled'
else :
if from_ == to_ :
state = 'disabled'
# axis scaling factor cannot be 0.
try :
scaling_factor = self._varsdict['var_axis_ticks_scaling_factor'].get()
except ValueError :
state = 'disabled'
else :
if 0. == scaling_factor :
state = 'disabled'
# configuring the buttons
for btn in self._smartdict['buttons_to_validate'] :
btn.configure(state=state)
class PropertiesWidget(BaseWidget, Pmw.ScrolledFrame) :
"""Widget for showing text properties.
Each property is represented by one line with the name of the property
shown at the left and its value at the right.
The following public methods are exported :
add_line() -- add a new line
add_separator() -- add a separator
"""
def __init__(self, master, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
title -- title at the top of the widget (default '')
width -- width in pixel (default 500)
height -- heigh in pixel (default 250)
"""
Pmw.ScrolledFrame.__init__(self, master, **self._scrolled_frame_kw(**kw))
BaseWidget.__init__(self, **kw)
def _scrolled_frame_kw(**kw) :
"""Retrieve the keywords for initialization of the scrolled frame."""
sf_kw = dict(usehullsize=True, horizflex='expand', vertflex='expand')
# no title by default
if kw.get('title', None) is not None :
sf_kw['labelpos'] = 'n'
sf_kw['label_text'] = kw['title']
# size is 500 x 250 by default
sf_kw['hull_width'] = kw.get('width', 500)
sf_kw['hull_height'] = kw.get('height', 250)
return sf_kw
_scrolled_frame_kw = staticmethod(_scrolled_frame_kw)
def _init_vars(self) :
"""Initialize variables."""
self._varsdict['cur_row'] = 0
def _constructGUI(self) :
"""GUI is constructed dynamically."""
pass
def add_line(self, key, value) :
"""Add a new line.
Positional arguments :
key -- property name
value -- property value
"""
if 'ar_entries' not in self._varsdict :
self._varsdict['ar_entries'] = []
self.interior().grid_columnconfigure(0, weight=1)
self.interior().grid_rowconfigure(self._varsdict['cur_row'], weight=1)
item = Pmw.EntryField(self.interior(),
labelpos='w',
label_text=key,
value=value,
labelmargin=3,
entry_state='readonly')
item.grid(row=self._varsdict['cur_row'],
column=0,
padx=3,
pady=3,
sticky='nwe')
self._varsdict['ar_entries'].append(item)
self._varsdict['cur_row'] += 1
Pmw.alignlabels(self._varsdict['ar_entries'])
def add_separator(self) :
"""Add a separator."""
self.interior().grid_rowconfigure(self._varsdict['cur_row'], weight=1)
separator = Tkinter.Frame(self.interior(), height=2, bd=1, relief='sunken')
separator.grid(row=self._varsdict['cur_row'],
column=0,
padx=3,
pady=3,
sticky='nwe')
self._varsdict['cur_row'] += 1
class MoleculeThumbnailWidget(BaseWidget, Tkinter.Frame) :
"""Small frame for viewing a molecule.
The widget is made up of a Molecule menu at the top, a 3D render widget
and a check box. The contents of the menu reflects the data which
the molecule possesses.
The following readable and writable property is exposed :
checked -- if the widget is "checked"
The following read-only properties are exposed :
molecule -- molecule
renderWidget -- render widget
"""
def __init__(self, master, mol, **kw) :
"""Constructor of the class.
Positional arguments :
master -- parent widget
Keyword arguments :
mainApp -- reference to the main window (default None)
if supplied, the Molecule menu is added
parser -- parser object (default None)
must be supplied if mainApp is not None
check_command -- called if the status of the check box is changed
(default None)
"""
if not isinstance(mol, molecule.Molecule) :
raise ConstructorError('Invalid mol argument')
# validate
if kw.get('mainApp', None) is not None and kw.get('parser', None) is None :
raise ConstructorError(
'The mainApp and parser arguments must be given simultaneously !')
self.__mol = mol
Tkinter.Frame.__init__(self, master)
BaseWidget.__init__(self, **kw)
def _init_vars(self) :
"""Initialize variables."""
# size of the render widgets (pixel).
self._varsdict['SIZE_RENDERWIDGET'] = 150
def _constructGUI(self) :
"""Construct the GUI of the widget."""
row = 0
## menubar at the top
## add only if the mainApp is given
if self._smartdict['mainApp'] is not None :
self._varsdict['menubar'] = self.__constructMenubar(self)
self._varsdict['menubar'].grid(row=row, column=0, sticky='w')
row += 1
## molecule widget
kw = dict(molecule=self.__mol,
width=self._varsdict['SIZE_RENDERWIDGET'],
height=self._varsdict['SIZE_RENDERWIDGET'],
resolution=self._smartdict['resolution'] or \
resources.NUM_RESOLUTION_VTK,
molecule_mode=resources.NUM_MODE_STICK,
rounded_bond=True,
hydrogen_bond=True,
atom_labels=False,
background='#FFFFFF')
self._varsdict['renderWidget'] = MoleculeRenderWidget(self, **kw)
self._varsdict['renderWidget'].grid(row=row, column=0,
padx=3, pady=3, sticky='w')
row += 1
## checkpoint with the name
self._varsdict['var_checkname'] = Tkinter.IntVar()
widget = Tkinter.Checkbutton(self,
text=self.__mol.name,
font=resources.get_font_molecules(self),
variable=self._varsdict['var_checkname'],
command=self._smartdict['check_command'])
self._varsdict['check_name'] = widget
self._varsdict['check_name'].grid(row=row, column=0,
padx=3, pady=3, sticky='w')
def _declare_properties(self) :
"""Declare properties of the widget."""
self.__class__.molecule = property(fget=self._get_molecule)
self.__class__.renderWidget = property(
fget=misc.Command(misc.Command.fget_value,
self._varsdict['renderWidget']))
self.__class__.checked = property(fget=self._get_checked,
fset=self._set_checked)
def __constructMenubar(self, parent) :
"""Construct the menu bar and return it."""
menubar = Pmw.MenuBar(parent, hotkeys=False)
# the only menu
# fill the menu depending on which properties the molecule has
menubar.addmenu('Molecule', None, tearoff=False)
# Information
menubar.addmenuitem('Molecule',
'command',
label='Info...',
command=self.__info)
menubar.addmenuitem('Molecule', 'separator')
# Explore - show a molecule window
menubar.addmenuitem('Molecule',
'command',
label='Explore...',
command=self.__explore)
# Spectra
if self.__mol.raman_roa_tensors is not None or \
self.__mol.ir_vcd_tensors is not None :
menubar.addcascademenu('Molecule', 'Spectra')
if self.__mol.raman_roa_tensors is not None :
menubar.addmenuitem('Spectra',
'command',
label='Raman / ROA / Degree of circularity',
command=self.__spectra_raman_roa_degcirc)
if self.__mol.ir_vcd_tensors is not None :
menubar.addmenuitem('Spectra',
'command',
label='IR / VCD',
command=self.__spectra_ir_vcd)
# GCM / ACP
if self.__mol.raman_roa_tensors is not None :
menubar.addmenuitem('Molecule',
'command',
label='Raman / ROA generation',
command=self.__raman_roa_matrices)
menubar.addmenuitem('Molecule', 'separator')
# Remove - remove oneself from the main application window
menubar.addmenuitem('Molecule',
'command',
label='Remove',
command=self.__remove)
return menubar
def __info(self) :
"""Molecule / Info... menu file handler."""
from pyviblib.gui.dialogs import FileInfoDialog
self.tk.call('update', 'idletasks')
info_dlg = FileInfoDialog(self, self.__mol, self._smartdict['parser'])
info_dlg.configure(title = r'Information for "%s"' % self.__mol.name)
info_dlg.show()
def __explore(self) :
"""Molecule / Explore command."""
from pyviblib.gui.windows import MoleculeWindow
self.tk.call('update', 'idletasks')
splash = SplashScreen(self, 'Opening %s ...' % self.__mol.name)
MoleculeWindow(self._smartdict['mainApp'],
self._smartdict['parser'],
molecule=self.__mol,
camera=self.renderWidget.camera,
size='500x500')
splash.destroy()
def __spectra_raman_roa_degcirc(self) :
"""Molecule / Spectra / Raman/ROA/Degree of circularity command."""
from pyviblib.gui.windows import RamanROADegcircCalcWindow
self.tk.call('update', 'idletasks')
splash = SplashScreen(self, 'Calculating the Raman/ROA invariants...')
RamanROADegcircCalcWindow(self._smartdict['mainApp'],
self.__mol,
camera=self.renderWidget.camera)
splash.destroy()
def __spectra_ir_vcd(self) :
"""Molecule / Spectra / IR/VCD of circularity command."""
from pyviblib.gui.windows import IRVCDCalcWindow
self.tk.call('update', 'idletasks')
splash = SplashScreen(self, 'Calculating the IR/VCD invariants...')
IRVCDCalcWindow(self._smartdict['mainApp'], self.__mol)
splash.destroy()
def __raman_roa_matrices(self) :
"""Molecule / GCM / ACP menu command handler."""
self.tk.call('update', 'idletasks')
from pyviblib.gui.windows import RamanROAMatricesWindow
splash = SplashScreen(self.master,
'Calculating the Raman/ROA invariants...')
RamanROAMatricesWindow(self._smartdict['mainApp'], self.__mol,
vib_no=1,
camera=self.renderWidget.camera)
splash.destroy()
def __remove(self) :
"""Molecule / Remove command."""
if self in self._smartdict['mainApp']._varsdict['thumbnails'] :
self._smartdict['mainApp'].remove_thumbnail(self)
def _get_molecule(obj) :
"""Getter function for the molecule property."""
return obj.__mol
_get_molecule = staticmethod(_get_molecule)
def _set_checked(obj, check) :
"""Setter function for the checked property."""
obj._varsdict['var_checkname'].set(check)
if callable(obj.smartdict['check_command']) :
obj.smartdict['check_command']()
_set_checked = staticmethod(_set_checked)
def _get_checked(obj) :
"""Getter function for the checked property."""
return obj._varsdict['var_checkname'].get()
_get_checked = staticmethod(_get_checked)