summaryrefslogtreecommitdiff
path: root/silx/gui/widgets/PeriodicTable.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/widgets/PeriodicTable.py')
-rw-r--r--silx/gui/widgets/PeriodicTable.py825
1 files changed, 825 insertions, 0 deletions
diff --git a/silx/gui/widgets/PeriodicTable.py b/silx/gui/widgets/PeriodicTable.py
new file mode 100644
index 0000000..2f1ca78
--- /dev/null
+++ b/silx/gui/widgets/PeriodicTable.py
@@ -0,0 +1,825 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Periodic table widgets
+
+Classes
+-------
+
+Widgets:
+
+ - :class:`PeriodicTable`
+ - :class:`PeriodicList`
+ - :class:`PeriodicCombo`
+
+Data model:
+
+ - :class:`PeriodicTableItem`
+ - :class:`ColoredPeriodicTableItem`
+
+
+Example of usage
+----------------
+
+This example uses the widgets with the standard builtin elements list.
+
+.. code-block:: python
+
+ from silx.gui import qt
+ from silx.gui.widgets.PeriodicTable import PeriodicTable, \
+ PeriodicCombo, PeriodicList
+
+ a = qt.QApplication([])
+
+ w = qt.QTabWidget()
+
+ ptable = PeriodicTable(w, selectable=True)
+ pcombo = PeriodicCombo(w)
+ plist = PeriodicList(w)
+
+ w.addTab(ptable, "PeriodicTable")
+ w.addTab(plist, "PeriodicList")
+ w.addTab(pcombo, "PeriodicCombo")
+
+ ptable.setSelection(['H', 'Fe', 'Si'])
+ plist.setSelectedElements(['H', 'Be', 'F'])
+ pcombo.setSelection("Li")
+
+ def change_list(items):
+ print("New list selection:", [item.symbol for item in items])
+
+ def change_combo(item):
+ print("New combo selection:", item.symbol)
+
+ def click_table(item):
+ print("New table click:", item.symbol)
+
+ def change_table(items):
+ print("New table selection:", [item.symbol for item in items])
+
+ ptable.sigElementClicked.connect(click_table)
+ ptable.sigSelectionChanged.connect(change_table)
+ plist.sigSelectionChanged.connect(change_list)
+ pcombo.sigSelectionChanged.connect(change_combo)
+
+ w.show()
+ a.exec_()
+
+
+The second example explains how to define custom elements.
+
+.. code-block:: python
+
+ from silx.gui import qt
+ from silx.gui.widgets.PeriodicTable import PeriodicTable, \
+ PeriodicCombo, PeriodicList
+ from silx.gui.widgets.PeriodicTable import PeriodicTableItem
+
+ # subclass PeriodicTableItem
+ class MyPeriodicTableItem(PeriodicTableItem):
+ "New item with added mass number and number of protons"
+ def __init__(self, symbol, Z, A, col, row, name, mass,
+ subcategory=""):
+ PeriodicTableItem.__init__(
+ self, symbol, Z, col, row, name, mass,
+ subcategory)
+
+ self.A = A
+ "Mass number (neutrons + protons)"
+
+ self.num_neutrons = A - Z
+ "Number of neutrons"
+
+ # build your list of elements
+ my_elements = [MyPeriodicTableItem("H", 1, 1, 1, 1, "hydrogen",
+ 1.00800, "diatomic nonmetal"),
+ MyPeriodicTableItem("He", 2, 4, 18, 1, "helium",
+ 4.0030, "noble gas"),
+ # etc ...
+ ]
+
+ app = qt.QApplication([])
+
+ ptable = PeriodicTable(elements=my_elements, selectable=True)
+ ptable.show()
+
+ def click_table(item):
+ "Callback function printing the mass number of clicked element"
+ print("New table click, mass number:", item.A)
+
+ ptable.sigElementClicked.connect(click_table)
+ app.exec_()
+
+"""
+
+__authors__ = ["E. Papillon", "V.A. Sole", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "26/01/2017"
+
+from collections import OrderedDict
+import logging
+from silx.gui import qt
+
+_logger = logging.getLogger(__name__)
+
+# Symbol Atomic Number col row name mass subcategory
+_elements = [("H", 1, 1, 1, "hydrogen", 1.00800, "diatomic nonmetal"),
+ ("He", 2, 18, 1, "helium", 4.0030, "noble gas"),
+ ("Li", 3, 1, 2, "lithium", 6.94000, "alkali metal"),
+ ("Be", 4, 2, 2, "beryllium", 9.01200, "alkaline earth metal"),
+ ("B", 5, 13, 2, "boron", 10.8110, "metalloid"),
+ ("C", 6, 14, 2, "carbon", 12.0100, "polyatomic nonmetal"),
+ ("N", 7, 15, 2, "nitrogen", 14.0080, "diatomic nonmetal"),
+ ("O", 8, 16, 2, "oxygen", 16.0000, "diatomic nonmetal"),
+ ("F", 9, 17, 2, "fluorine", 19.0000, "diatomic nonmetal"),
+ ("Ne", 10, 18, 2, "neon", 20.1830, "noble gas"),
+ ("Na", 11, 1, 3, "sodium", 22.9970, "alkali metal"),
+ ("Mg", 12, 2, 3, "magnesium", 24.3200, "alkaline earth metal"),
+ ("Al", 13, 13, 3, "aluminium", 26.9700, "post transition metal"),
+ ("Si", 14, 14, 3, "silicon", 28.0860, "metalloid"),
+ ("P", 15, 15, 3, "phosphorus", 30.9750, "polyatomic nonmetal"),
+ ("S", 16, 16, 3, "sulphur", 32.0660, "polyatomic nonmetal"),
+ ("Cl", 17, 17, 3, "chlorine", 35.4570, "diatomic nonmetal"),
+ ("Ar", 18, 18, 3, "argon", 39.9440, "noble gas"),
+ ("K", 19, 1, 4, "potassium", 39.1020, "alkali metal"),
+ ("Ca", 20, 2, 4, "calcium", 40.0800, "alkaline earth metal"),
+ ("Sc", 21, 3, 4, "scandium", 44.9600, "transition metal"),
+ ("Ti", 22, 4, 4, "titanium", 47.9000, "transition metal"),
+ ("V", 23, 5, 4, "vanadium", 50.9420, "transition metal"),
+ ("Cr", 24, 6, 4, "chromium", 51.9960, "transition metal"),
+ ("Mn", 25, 7, 4, "manganese", 54.9400, "transition metal"),
+ ("Fe", 26, 8, 4, "iron", 55.8500, "transition metal"),
+ ("Co", 27, 9, 4, "cobalt", 58.9330, "transition metal"),
+ ("Ni", 28, 10, 4, "nickel", 58.6900, "transition metal"),
+ ("Cu", 29, 11, 4, "copper", 63.5400, "transition metal"),
+ ("Zn", 30, 12, 4, "zinc", 65.3800, "transition metal"),
+ ("Ga", 31, 13, 4, "gallium", 69.7200, "post transition metal"),
+ ("Ge", 32, 14, 4, "germanium", 72.5900, "metalloid"),
+ ("As", 33, 15, 4, "arsenic", 74.9200, "metalloid"),
+ ("Se", 34, 16, 4, "selenium", 78.9600, "polyatomic nonmetal"),
+ ("Br", 35, 17, 4, "bromine", 79.9200, "diatomic nonmetal"),
+ ("Kr", 36, 18, 4, "krypton", 83.8000, "noble gas"),
+ ("Rb", 37, 1, 5, "rubidium", 85.4800, "alkali metal"),
+ ("Sr", 38, 2, 5, "strontium", 87.6200, "alkaline earth metal"),
+ ("Y", 39, 3, 5, "yttrium", 88.9050, "transition metal"),
+ ("Zr", 40, 4, 5, "zirconium", 91.2200, "transition metal"),
+ ("Nb", 41, 5, 5, "niobium", 92.9060, "transition metal"),
+ ("Mo", 42, 6, 5, "molybdenum", 95.9500, "transition metal"),
+ ("Tc", 43, 7, 5, "technetium", 99.0000, "transition metal"),
+ ("Ru", 44, 8, 5, "ruthenium", 101.0700, "transition metal"),
+ ("Rh", 45, 9, 5, "rhodium", 102.9100, "transition metal"),
+ ("Pd", 46, 10, 5, "palladium", 106.400, "transition metal"),
+ ("Ag", 47, 11, 5, "silver", 107.880, "transition metal"),
+ ("Cd", 48, 12, 5, "cadmium", 112.410, "transition metal"),
+ ("In", 49, 13, 5, "indium", 114.820, "post transition metal"),
+ ("Sn", 50, 14, 5, "tin", 118.690, "post transition metal"),
+ ("Sb", 51, 15, 5, "antimony", 121.760, "metalloid"),
+ ("Te", 52, 16, 5, "tellurium", 127.600, "metalloid"),
+ ("I", 53, 17, 5, "iodine", 126.910, "diatomic nonmetal"),
+ ("Xe", 54, 18, 5, "xenon", 131.300, "noble gas"),
+ ("Cs", 55, 1, 6, "caesium", 132.910, "alkali metal"),
+ ("Ba", 56, 2, 6, "barium", 137.360, "alkaline earth metal"),
+ ("La", 57, 3, 6, "lanthanum", 138.920, "lanthanide"),
+ ("Ce", 58, 4, 9, "cerium", 140.130, "lanthanide"),
+ ("Pr", 59, 5, 9, "praseodymium", 140.920, "lanthanide"),
+ ("Nd", 60, 6, 9, "neodymium", 144.270, "lanthanide"),
+ ("Pm", 61, 7, 9, "promethium", 147.000, "lanthanide"),
+ ("Sm", 62, 8, 9, "samarium", 150.350, "lanthanide"),
+ ("Eu", 63, 9, 9, "europium", 152.000, "lanthanide"),
+ ("Gd", 64, 10, 9, "gadolinium", 157.260, "lanthanide"),
+ ("Tb", 65, 11, 9, "terbium", 158.930, "lanthanide"),
+ ("Dy", 66, 12, 9, "dysprosium", 162.510, "lanthanide"),
+ ("Ho", 67, 13, 9, "holmium", 164.940, "lanthanide"),
+ ("Er", 68, 14, 9, "erbium", 167.270, "lanthanide"),
+ ("Tm", 69, 15, 9, "thulium", 168.940, "lanthanide"),
+ ("Yb", 70, 16, 9, "ytterbium", 173.040, "lanthanide"),
+ ("Lu", 71, 17, 9, "lutetium", 174.990, "lanthanide"),
+ ("Hf", 72, 4, 6, "hafnium", 178.500, "transition metal"),
+ ("Ta", 73, 5, 6, "tantalum", 180.950, "transition metal"),
+ ("W", 74, 6, 6, "tungsten", 183.920, "transition metal"),
+ ("Re", 75, 7, 6, "rhenium", 186.200, "transition metal"),
+ ("Os", 76, 8, 6, "osmium", 190.200, "transition metal"),
+ ("Ir", 77, 9, 6, "iridium", 192.200, "transition metal"),
+ ("Pt", 78, 10, 6, "platinum", 195.090, "transition metal"),
+ ("Au", 79, 11, 6, "gold", 197.200, "transition metal"),
+ ("Hg", 80, 12, 6, "mercury", 200.610, "transition metal"),
+ ("Tl", 81, 13, 6, "thallium", 204.390, "post transition metal"),
+ ("Pb", 82, 14, 6, "lead", 207.210, "post transition metal"),
+ ("Bi", 83, 15, 6, "bismuth", 209.000, "post transition metal"),
+ ("Po", 84, 16, 6, "polonium", 209.000, "post transition metal"),
+ ("At", 85, 17, 6, "astatine", 210.000, "metalloid"),
+ ("Rn", 86, 18, 6, "radon", 222.000, "noble gas"),
+ ("Fr", 87, 1, 7, "francium", 223.000, "alkali metal"),
+ ("Ra", 88, 2, 7, "radium", 226.000, "alkaline earth metal"),
+ ("Ac", 89, 3, 7, "actinium", 227.000, "actinide"),
+ ("Th", 90, 4, 10, "thorium", 232.000, "actinide"),
+ ("Pa", 91, 5, 10, "proactinium", 231.03588, "actinide"),
+ ("U", 92, 6, 10, "uranium", 238.070, "actinide"),
+ ("Np", 93, 7, 10, "neptunium", 237.000, "actinide"),
+ ("Pu", 94, 8, 10, "plutonium", 239.100, "actinide"),
+ ("Am", 95, 9, 10, "americium", 243, "actinide"),
+ ("Cm", 96, 10, 10, "curium", 247, "actinide"),
+ ("Bk", 97, 11, 10, "berkelium", 247, "actinide"),
+ ("Cf", 98, 12, 10, "californium", 251, "actinide"),
+ ("Es", 99, 13, 10, "einsteinium", 252, "actinide"),
+ ("Fm", 100, 14, 10, "fermium", 257, "actinide"),
+ ("Md", 101, 15, 10, "mendelevium", 258, "actinide"),
+ ("No", 102, 16, 10, "nobelium", 259, "actinide"),
+ ("Lr", 103, 17, 10, "lawrencium", 262, "actinide"),
+ ("Rf", 104, 4, 7, "rutherfordium", 261, "transition metal"),
+ ("Db", 105, 5, 7, "dubnium", 262, "transition metal"),
+ ("Sg", 106, 6, 7, "seaborgium", 266, "transition metal"),
+ ("Bh", 107, 7, 7, "bohrium", 264, "transition metal"),
+ ("Hs", 108, 8, 7, "hassium", 269, "transition metal"),
+ ("Mt", 109, 9, 7, "meitnerium", 268)]
+
+
+class PeriodicTableItem(object):
+ """Periodic table item, used as generic item in :class:`PeriodicTable`,
+ :class:`PeriodicCombo` and :class:`PeriodicList`.
+
+ This implementation stores the minimal amount of information needed by the
+ widgets:
+
+ - atomic symbol
+ - atomic number
+ - element name
+ - atomic mass
+ - column of element in periodic table
+ - row of element in periodic table
+
+ You can subclass this class to add additional information.
+
+ :param str symbol: Atomic symbol (e.g. H, He, Li...)
+ :param int Z: Proton number
+ :param int col: 1-based column index of element in periodic table
+ :param int row: 1-based row index of element in periodic table
+ :param str name: PeriodicTableItem name ("hydrogen", ...)
+ :param float mass: Atomic mass (gram per mol)
+ :param str subcategory: Subcategory, based on physical properties
+ (e.g. "alkali metal", "noble gas"...)
+ """
+ def __init__(self, symbol, Z, col, row, name, mass,
+ subcategory=""):
+ self.symbol = symbol
+ """Atomic symbol (e.g. H, He, Li...)"""
+ self.Z = Z
+ """Atomic number (Proton number)"""
+ self.col = col
+ """1-based column index of element in periodic table"""
+ self.row = row
+ """1-based row index of element in periodic table"""
+ self.name = name
+ """PeriodicTableItem name ("hydrogen", ...)"""
+ self.mass = mass
+ """Atomic mass (gram per mol)"""
+ self.subcategory = subcategory
+ """Subcategory, based on physical properties
+ (e.g. "alkali metal", "noble gas"...)"""
+
+ # pymca compatibility (elements used to be stored as a list of lists)
+ def __getitem__(self, idx):
+ if idx == 6:
+ _logger.warning("density not implemented in silx, returning 0.")
+
+ ret = [self.symbol, self.Z,
+ self.col, self.row,
+ self.name, self.mass,
+ 0.]
+ return ret[idx]
+
+ def __len__(self):
+ return 6
+
+
+class ColoredPeriodicTableItem(PeriodicTableItem):
+ """:class:`PeriodicTableItem` with an added :attr:`bgcolor`.
+ The background color can be passed as a parameter to the constructor.
+ If it is not specified, it will be defined based on
+ :attr:`subcategory`.
+
+ :param str bgcolor: Custom background color for element in
+ periodic table, as a RGB string *#RRGGBB*"""
+ COLORS = {
+ "diatomic nonmetal": "#7FFF00", # chartreuse
+ "noble gas": "#00FFFF", # cyan
+ "alkali metal": "#FFE4B5", # Moccasin
+ "alkaline earth metal": "#FFA500", # orange
+ "polyatomic nonmetal": "#7FFFD4", # aquamarine
+ "transition metal": "#FFA07A", # light salmon
+ "metalloid": "#8FBC8F", # Dark Sea Green
+ "post transition metal": "#D3D3D3", # light gray
+ "lanthanide": "#FFB6C1", # light pink
+ "actinide": "#F08080", # Light Coral
+ "": "#FFFFFF" # white
+ }
+ """Dictionary defining RGB colors for each subcategory."""
+
+ def __init__(self, symbol, Z, col, row, name, mass,
+ subcategory="", bgcolor=None):
+ PeriodicTableItem.__init__(self, symbol, Z, col, row, name, mass,
+ subcategory)
+
+ self.bgcolor = self.COLORS.get(subcategory, "#FFFFFF")
+ """Background color of element in the periodic table,
+ based on its subcategory. This should be a string of a hexadecimal
+ RGB code, with the format *#RRGGBB*.
+ If the subcategory is unknown, use white (*#FFFFFF*)
+ """
+
+ # possible custom color
+ if bgcolor is not None:
+ self.bgcolor = bgcolor
+
+
+_defaultTableItems = [ColoredPeriodicTableItem(*info) for info in _elements]
+
+
+class _ElementButton(qt.QPushButton):
+ """Atomic element button, used as a cell in the periodic table
+ """
+ sigElementEnter = qt.pyqtSignal(object)
+ """Signal emitted as the cursor enters the widget"""
+ sigElementLeave = qt.pyqtSignal(object)
+ """Signal emitted as the cursor leaves the widget"""
+ sigElementClicked = qt.pyqtSignal(object)
+ """Signal emitted when the widget is clicked"""
+
+ def __init__(self, item, parent=None):
+ """
+
+ :param parent: Parent widget
+ :param PeriodicTableItem item: :class:`PeriodicTableItem` object
+ """
+ qt.QPushButton.__init__(self, parent)
+
+ self.item = item
+ """:class:`PeriodicTableItem` object represented by this button"""
+
+ self.setText(item.symbol)
+ self.setFlat(1)
+ self.setCheckable(0)
+
+ self.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding,
+ qt.QSizePolicy.Expanding))
+
+ self.selected = False
+ self.current = False
+
+ # selection colors
+ self.selected_color = qt.QColor(qt.Qt.yellow)
+ self.current_color = qt.QColor(qt.Qt.gray)
+ self.selected_current_color = qt.QColor(qt.Qt.darkYellow)
+
+ # element colors
+
+ if hasattr(item, "bgcolor"):
+ self.bgcolor = qt.QColor(item.bgcolor)
+ else:
+ self.bgcolor = qt.QColor("#FFFFFF")
+
+ self.brush = qt.QBrush()
+ self.__setBrush()
+
+ self.clicked.connect(self.clickedSlot)
+
+ def sizeHint(self):
+ return qt.QSize(40, 40)
+
+ def setCurrent(self, b):
+ """Set this element button as current.
+ Multiple buttons can be selected.
+
+ :param b: boolean
+ """
+ self.current = b
+ self.__setBrush()
+
+ def isCurrent(self):
+ """
+ :return: True if element button is current
+ """
+ return self.current
+
+ def isSelected(self):
+ """
+ :return: True if element button is selected
+ """
+ return self.selected
+
+ def setSelected(self, b):
+ """Set this element button as selected.
+ Only a single button can be selected.
+
+ :param b: boolean
+ """
+ self.selected = b
+ self.__setBrush()
+
+ def __setBrush(self):
+ """Selected cells are yellow when not current.
+ The current cell is dark yellow when selected or grey when not
+ selected.
+ Other cells have no bg color by default, unless specified at
+ instantiation (:attr:`bgcolor`)"""
+ palette = self.palette()
+ # if self.current and self.selected:
+ # self.brush = qt.QBrush(self.selected_current_color)
+ # el
+ if self.selected:
+ self.brush = qt.QBrush(self.selected_color)
+ # elif self.current:
+ # self.brush = qt.QBrush(self.current_color)
+ elif self.bgcolor is not None:
+ self.brush = qt.QBrush(self.bgcolor)
+ else:
+ self.brush = qt.QBrush()
+ palette.setBrush(self.backgroundRole(),
+ self.brush)
+ self.setPalette(palette)
+ self.update()
+
+ def paintEvent(self, pEvent):
+ # get button geometry
+ widgGeom = self.rect()
+ paintGeom = qt.QRect(widgGeom.left() + 1,
+ widgGeom.top() + 1,
+ widgGeom.width() - 2,
+ widgGeom.height() - 2)
+
+ # paint background color
+ painter = qt.QPainter(self)
+ if self.brush is not None:
+ painter.fillRect(paintGeom, self.brush)
+ # paint frame
+ pen = qt.QPen(qt.Qt.black)
+ pen.setWidth(1 if not self.isCurrent() else 5)
+ painter.setPen(pen)
+ painter.drawRect(paintGeom)
+ painter.end()
+ qt.QPushButton.paintEvent(self, pEvent)
+
+ def enterEvent(self, e):
+ """Emit a :attr:`sigElementEnter` signal and send a
+ :class:`PeriodicTableItem` object"""
+ self.sigElementEnter.emit(self.item)
+
+ def leaveEvent(self, e):
+ """Emit a :attr:`sigElementLeave` signal and send a
+ :class:`PeriodicTableItem` object"""
+ self.sigElementLeave.emit(self.item)
+
+ def clickedSlot(self):
+ """Emit a :attr:`sigElementClicked` signal and send a
+ :class:`PeriodicTableItem` object"""
+ self.sigElementClicked.emit(self.item)
+
+
+class PeriodicTable(qt.QWidget):
+ """Periodic Table widget
+
+ The following example shows how to connect clicking to selection::
+
+ from silx.gui import qt
+ from silx.gui.widgets.PeriodicTable import PeriodicTable
+ app = qt.QApplication([])
+ pt = PeriodicTable()
+ pt.sigElementClicked.connect(pt.elementToggle)
+ pt.show()
+ app.exec_()
+
+ To print all selected elements each time a new element is selected::
+
+ def my_slot(item):
+ pt.elementToggle(item)
+ selected_elements = pt.getSelection()
+ for e in selected_elements:
+ print(e.symbol)
+
+ pt.sigElementClicked.connect(my_slot)
+
+ """
+ sigElementClicked = qt.pyqtSignal(object)
+ """When any element is clicked in the table, the widget emits
+ this signal and sends a :class:`PeriodicTableItem` object.
+ """
+
+ sigSelectionChanged = qt.pyqtSignal(object)
+ """When any element is selected/unselected in the table, the widget emits
+ this signal and sends a list of :class:`PeriodicTableItem` objects.
+
+ .. note::
+
+ To enable selection of elements, you must set *selectable=True*
+ when you instantiate the widget. Alternatively, you can also connect
+ :attr:`sigElementClicked` to :meth:`elementToggle` manually::
+
+ pt = PeriodicTable()
+ pt.sigElementClicked.connect(pt.elementToggle)
+
+
+ :param parent: parent QWidget
+ :param str name: Widget window title
+ :param elements: List of items (:class:`PeriodicTableItem` objects) to
+ be represented in the table. By default, take elements from
+ a predefined list with minimal information (symbol, atomic number,
+ name, mass).
+ :param bool selectable: If *True*, multiple elements can be
+ selected by clicking with the mouse. If *False* (default),
+ selection is only possible with method :meth:`setSelection`.
+ """
+
+ def __init__(self, parent=None, name="PeriodicTable", elements=None,
+ selectable=False):
+ self.selectable = selectable
+ qt.QWidget.__init__(self, parent)
+ self.setWindowTitle(name)
+ self.gridLayout = qt.QGridLayout(self)
+ self.gridLayout.setContentsMargins(0, 0, 0, 0)
+ self.gridLayout.addItem(qt.QSpacerItem(0, 5), 7, 0)
+
+ for idx in range(10):
+ self.gridLayout.setRowStretch(idx, 3)
+ # row 8 (above lanthanoids is empty)
+ self.gridLayout.setRowStretch(7, 2)
+
+ # Element information displayed when cursor enters a cell
+ self.eltLabel = qt.QLabel(self)
+ f = self.eltLabel.font()
+ f.setBold(1)
+ self.eltLabel.setFont(f)
+ self.eltLabel.setAlignment(qt.Qt.AlignHCenter)
+ self.gridLayout.addWidget(self.eltLabel, 1, 1, 3, 10)
+
+ self._eltCurrent = None
+ """Current :class:`_ElementButton` (last clicked)"""
+
+ self._eltButtons = OrderedDict()
+ """Dictionary of all :class:`_ElementButton`. Keys are the symbols
+ ("H", "He", "Li"...)"""
+
+ if elements is None:
+ elements = _defaultTableItems
+ # fill cells with elements
+ for elmt in elements:
+ self.__addElement(elmt)
+
+ def __addElement(self, elmt):
+ """Add one :class:`_ElementButton` widget into the grid,
+ connect its signals to interact with the cursor"""
+ b = _ElementButton(elmt, self)
+ b.setAutoDefault(False)
+
+ self._eltButtons[elmt.symbol] = b
+ self.gridLayout.addWidget(b, elmt.row, elmt.col)
+
+ b.sigElementEnter.connect(self.elementEnter)
+ b.sigElementLeave.connect(self._elementLeave)
+ b.sigElementClicked.connect(self._elementClicked)
+
+ def elementEnter(self, item):
+ """Update label with element info (e.g. "Nb(41) - niobium")
+ when mouse cursor hovers an element.
+
+ :param PeriodicTableItem item: Element entered by cursor
+ """
+ self.eltLabel.setText("%s(%d) - %s" % (item.symbol, item.Z, item.name))
+
+ def _elementLeave(self, item):
+ """Clear label when the cursor leaves the cell
+
+ :param PeriodicTableItem item: Element left
+ """
+ self.eltLabel.setText("")
+
+ def _elementClicked(self, item):
+ """Emit :attr:`sigElementClicked`,
+ toggle selected state of element
+
+ :param PeriodicTableItem item: Element clicked
+ """
+ if self._eltCurrent is not None:
+ self._eltCurrent.setCurrent(False)
+ self._eltButtons[item.symbol].setCurrent(True)
+ self._eltCurrent = self._eltButtons[item.symbol]
+ if self.selectable:
+ self.elementToggle(item)
+ self.sigElementClicked.emit(item)
+
+ def getSelection(self):
+ """Return a list of selected elements, as a list of :class:`PeriodicTableItem`
+ objects.
+
+ :return: Selected items
+ :rtype: list(PeriodicTableItem)
+ """
+ return [b.item for b in self._eltButtons.values() if b.isSelected()]
+
+ def setSelection(self, symbols):
+ """Set selected elements.
+
+ This causes the sigSelectionChanged signal
+ to be emitted, even if the selection didn't actually change.
+
+ :param list(str) symbols: List of symbols of elements to be selected
+ (e.g. *["Fe", "Hg", "Li"]*)
+ """
+ # accept list of PeriodicTableItems as input, because getSelection
+ # returns these objects and it makes sense to have getter and setter
+ # use same type of data
+ if isinstance(symbols[0], PeriodicTableItem):
+ symbols = [elmt.symbol for elmt in symbols]
+
+ for (e, b) in self._eltButtons.items():
+ b.setSelected(e in symbols)
+ self.sigSelectionChanged.emit(self.getSelection())
+
+ def setElementSelected(self, symbol, state):
+ """Modify *selected* status of a single element (select or unselect)
+
+ :param str symbol: PeriodicTableItem symbol to be selected
+ :param bool state: *True* to select, *False* to unselect
+ """
+ self._eltButtons[symbol].setSelected(state)
+ self.sigSelectionChanged.emit(self.getSelection())
+
+ def isElementSelected(self, symbol):
+ """Return *True* if element is selected, else *False*
+
+ :param str symbol: PeriodicTableItem symbol
+ :return: *True* if element is selected, else *False*
+ """
+ return self._eltButtons[symbol].isSelected()
+
+ def elementToggle(self, item):
+ """Toggle selected/unselected state for element
+
+ :param item: PeriodicTableItem object
+ """
+ b = self._eltButtons[item.symbol]
+ b.setSelected(not b.isSelected())
+ self.sigSelectionChanged.emit(self.getSelection())
+
+
+class PeriodicCombo(qt.QComboBox):
+ """
+ Combo list with all atomic elements of the periodic table
+
+ :param bool detailed: True (default) display element symbol, Z and name.
+ False display only element symbol and Z.
+ :param elements: List of items (:class:`PeriodicTableItem` objects) to
+ be represented in the table. By default, take elements from
+ a predefined list with minimal information (symbol, atomic number,
+ name, mass).
+ """
+ sigSelectionChanged = qt.pyqtSignal(object)
+ """Signal emitted when the selection changes. Send
+ :class:`PeriodicTableItem` object representing selected
+ element
+ """
+
+ def __init__(self, parent=None, detailed=True, elements=None):
+ qt.QComboBox.__init__(self, parent)
+
+ # add all elements from global list
+ if elements is None:
+ elements = _defaultTableItems
+ for i, elmt in enumerate(elements):
+ if detailed:
+ txt = "%2s (%d) - %s" % (elmt.symbol, elmt.Z, elmt.name)
+ else:
+ txt = "%2s (%d)" % (elmt.symbol, elmt.Z)
+ self.insertItem(i, txt)
+
+ self.currentIndexChanged[int].connect(self.__selectionChanged)
+
+ def __selectionChanged(self, idx):
+ """Emit :attr:`sigSelectionChanged`"""
+ self.sigSelectionChanged.emit(_defaultTableItems[idx])
+
+ def getSelection(self):
+ """Get selected element
+
+ :return: Selected element
+ :rtype: PeriodicTableItem
+ """
+ return _defaultTableItems[self.currentIndex()]
+
+ def setSelection(self, symbol):
+ """Set selected item in combobox by giving the atomic symbol
+
+ :param symbol: Symbol of element to be selected
+ """
+ # accept PeriodicTableItem for getter/setter consistency
+ if isinstance(symbol, PeriodicTableItem):
+ symbol = symbol.symbol
+ symblist = [elmt.symbol for elmt in _defaultTableItems]
+ self.setCurrentIndex(symblist.index(symbol))
+
+
+class PeriodicList(qt.QTreeWidget):
+ """List of atomic elements in a :class:`QTreeView`
+
+ :param QWidget parent: Parent widget
+ :param bool detailed: True (default) display element symbol, Z and name.
+ False display only element symbol and Z.
+ :param single: *True* for single element selection with mouse click,
+ *False* for multiple element selection mode.
+ """
+ sigSelectionChanged = qt.pyqtSignal(object)
+ """When any element is selected/unselected in the widget, it emits
+ this signal and sends a list of currently selected
+ :class:`PeriodicTableItem` objects.
+ """
+
+ def __init__(self, parent=None, detailed=True, single=False, elements=None):
+ qt.QTreeWidget.__init__(self, parent)
+
+ self.detailed = detailed
+
+ headers = ["Z", "Symbol"]
+ if detailed:
+ headers.append("Name")
+ self.setColumnCount(3)
+ else:
+ self.setColumnCount(2)
+ self.setHeaderLabels(headers)
+ self.header().setStretchLastSection(False)
+
+ self.setRootIsDecorated(0)
+ self.itemClicked.connect(self.__selectionChanged)
+ self.setSelectionMode(qt.QAbstractItemView.SingleSelection if single
+ else qt.QAbstractItemView.ExtendedSelection)
+ self.__fill_widget(elements)
+ self.resizeColumnToContents(0)
+ self.resizeColumnToContents(1)
+ if detailed:
+ self.resizeColumnToContents(2)
+
+ def __fill_widget(self, elements):
+ """Fill tree widget with elements """
+ if elements is None:
+ elements = _defaultTableItems
+
+ self.tree_items = []
+
+ previous_item = None
+ for elmt in elements:
+ if previous_item is None:
+ item = qt.QTreeWidgetItem(self)
+ else:
+ item = qt.QTreeWidgetItem(self, previous_item)
+ item.setText(0, str(elmt.Z))
+ item.setText(1, elmt.symbol)
+ if self.detailed:
+ item.setText(2, elmt.name)
+ self.tree_items.append(item)
+ previous_item = item
+
+ def __selectionChanged(self, treeItem, column):
+ """Emit a :attr:`sigSelectionChanged` and send a list of
+ :class:`PeriodicTableItem` objects."""
+ self.sigSelectionChanged.emit(self.getSelection())
+
+ def getSelection(self):
+ """Get a list of selected elements, as a list of :class:`PeriodicTableItem`
+ objects.
+
+ :return: Selected elements
+ :rtype: list(PeriodicTableItem)"""
+ return [_defaultTableItems[idx] for idx in range(len(self.tree_items))
+ if self.tree_items[idx].isSelected()]
+
+ # setSelection is a bad name (name of a QTreeWidget method)
+ def setSelectedElements(self, symbolList):
+ """
+
+ :param symbolList: List of atomic symbols ["H", "He", "Li"...]
+ to be selected in the widget
+ """
+ # accept PeriodicTableItem for getter/setter consistency
+ if isinstance(symbolList[0], PeriodicTableItem):
+ symbolList = [elmt.symbol for elmt in symbolList]
+ for idx in range(len(self.tree_items)):
+ self.tree_items[idx].setSelected(_defaultTableItems[idx].symbol in symbolList)