1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3# elements_gui.py
4
5# Copyright (c) 2005-2014, Christoph Gohlke
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions are met:
10#
11# * Redistributions of source code must retain the above copyright
12#   notice, this list of conditions and the following disclaimer.
13# * Redistributions in binary form must reproduce the above copyright
14#   notice, this list of conditions and the following disclaimer in the
15#   documentation and/or other materials provided with the distribution.
16# * Neither the name of the copyright holders nor the names of any
17#   contributors may be used to endorse or promote products derived
18#   from this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30# POSSIBILITY OF SUCH DAMAGE.
31
32"""Periodic Table of Elements - A user interface for elements.py.
33
34:Author: `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_
35
36:Version: 2013.03.18
37
38Requirements
39------------
40
41* `CPython 2.7 <http://www.python.org>`_
42* `wxPython 2.8 <http://www.wxpython.org>`_
43* `Elements.py 2013.03.18 <http://www.lfd.uci.edu/~gohlke/>`_
44
45"""
46from __future__ import division, print_function
47
48import sys
49import math
50import io
51import webbrowser
52
53import wxversion
54#wxversion.ensureMinimal('2.8')
55import wx
56from wx.lib import fancytext, buttons, rcsizer
57
58from elements import ELEMENTS, SERIES
59
60from abipy.gui.awx.panels import ListCtrlFromTable
61try:
62    from pseudo_dojo.refdata.nist import database as nist
63except ImportError:
64    pass
65
66
67class MainApp(wx.App):
68    """Main application."""
69
70    name = "Periodic Table of Elements"
71    version = "2012.04.05"
72    website = "http://www.lfd.uci.edu/~gohlke/"
73    copyright = ("Christoph Gohlke\n"
74                 "Laboratory for Fluorescence Dynamics\n"
75                 "University of California, Irvine")
76    icon = "elements"
77
78    def OnInit(self):
79        _ = wx.LogNull()
80        wx.InitAllImageHandlers()
81        mainframe = WxPeriodicTable(None, -1)
82        self.SetTopWindow(mainframe)
83        if wx.Platform != "__WXMAC__":
84            mainframe.Centre()
85        mainframe.ApplyLayout(False)
86        mainframe.Show()
87        return 1
88
89
90class ElementButton(buttons.GenToggleButton):
91    """Button representing chemical element."""
92
93    def __init__(self, *args, **kwds):
94        buttons.GenToggleButton.__init__(self, *args, **kwds)
95        self.color = wx.Colour(255, 255, 255)
96        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
97        self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
98
99    @property
100    def Z(self):
101        """Atomic number corresponding to this button."""
102        return self.GetId() - 100
103
104    def OnEraseBackground(self, event):
105        pass
106
107    def SetButtonColour(self, color):
108        self.color = color
109
110    def OnPaint(self, event):
111        width, height = self.GetClientSizeTuple()
112        dc = wx.BufferedPaintDC(self)
113        brush = wx.Brush(self.GetBackgroundColour(), wx.SOLID)
114        if wx.Platform == "__WXMAC__":
115            brush.MacSetTheme(1)  # kThemeBrushDialogBackgroundActive
116        dc.SetBackground(brush)
117        dc.SetPen(wx.TRANSPARENT_PEN)
118        dc.SetBrush(wx.Brush(self.color if self.up else 'WHITE', wx.SOLID))
119        dc.Clear()
120        dc.DrawRectanglePointSize((1, 1), (width - 2, height - 2))
121        if self.up:
122            self.DrawBezel(dc, 0, 0, width - 1, height - 1)
123        self.DrawLabel(dc, width, height)
124        if (self.hasFocus and self.useFocusInd):
125            self.DrawFocusIndicator(dc, width, height)
126
127    def DrawLabel(self, dc, width, height):
128        font = self.GetFont()
129        font.SetWeight(wx.FONTWEIGHT_BOLD)
130        dc.SetFont(font)
131
132        if self.IsEnabled():
133            dc.SetTextForeground(self.GetForegroundColour())
134        else:
135            dc.SetTextForeground(wx.SystemSettings.GetColour(
136                wx.SYS_COLOUR_GRAYTEXT))
137
138        label = self.GetLabel()
139        txtwidth, txtheight = dc.GetTextExtent(label)
140        xpos = (width - txtwidth) // 2
141        ypos = (height*0.75 - txtheight) // 2 - 1
142        dc.DrawText(label, xpos, ypos)
143
144        font.SetWeight(wx.FONTWEIGHT_LIGHT)
145        font.SetPointSize((font.GetPointSize()*6) // 8)
146        dc.SetFont(font)
147        label = "%i" % (self.GetId() - 100)
148        txtwidth, txtheight = dc.GetTextExtent(label)
149        dc.DrawText(label, (width-txtwidth)//2, 4+ypos+(height-txtheight)//2)
150
151    def makePopupMenu(self):
152        """Build and return the popup menu."""
153        menu = wx.Menu()
154
155        self.ID_POPUP_NIST_LDA = wx.NewId()
156        menu.Append(self.ID_POPUP_NIST_LDA, "NIST SCF data (LDA)")
157
158        # Associate menu/toolbar items with their handlers.
159        menu_handlers = [
160            (self.ID_POPUP_NIST_LDA, self.onNistLda),
161        ]
162
163        for combo in menu_handlers:
164            mid, handler = combo[:2]
165            self.Bind(wx.EVT_MENU, handler, id=mid)
166
167        return menu
168
169    def onRightDown(self, event):
170        """Called when right button is pressed."""
171        popup_menu = self.makePopupMenu()
172        self.PopupMenu(popup_menu, event.GetPosition())
173
174    def onNistLda(self, event):
175        """
176        Show the LDA levels of the neutral atom.
177        (useful to decide if semicore states should be included in the valence).
178        """
179        try:
180            entry = nist.get_neutral_entry(self.Z)
181        except KeyError:
182            return
183
184        table = entry.to_table()
185        frame = wx.Frame(self, title="LDA levels for atom %s (NIST database) " % entry.symbol)
186        ListCtrlFromTable(frame, table)
187        frame.Show()
188
189
190class PeriodicPanel(wx.Panel):
191    """Periodic table of elements panel."""
192
193    layout = """
194        .  1  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  18 .
195        1  H  2  .  .  .  .  .  .  .  .  .  .  13 14 15 16 17 He .
196        2  Li Be .  .  .  .  .  .  .  .  .  .  B  C  N  O  F  Ne .
197        3  Na Mg 3  4  5  6  7  8  9  10 11 12 Al Si P  S  Cl Ar .
198        4  K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr .
199        5  Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe .
200        6  Cs Ba *  Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn .
201        7  Fr Ra ** Rf Db Sg Bh Hs Mt .  .  .  .  .  .  .  .  .  .
202        .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
203        .  .  .  *  La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu .
204        .  .  .  ** Ac Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr .
205    """
206    # Class used to instanciates the buttons in the panel.
207    element_button_class = ElementButton
208
209    def __init__(self, *args, **kwds):
210        kwds["style"] = wx.TAB_TRAVERSAL
211        wx.Panel.__init__(self, *args, **kwds)
212        rows = len(self.layout.splitlines()) - 2
213        cols = len(self.layout.splitlines()[1].split())
214        self.sizer = wx.FlexGridSizer(rows, cols, 0, 0)
215        self.buttons = list(range(0, len(ELEMENTS)))
216        self.selected = -1
217
218        self.info = ElementPanel(self, -1, pos=(0, 0))
219
220        # create element buttons
221        buttonsize = math.ceil(float(self.info.GetSize()[0] + 4) / 9.0)
222        if buttonsize < 30:
223            buttonsize = 30
224        for row in self.layout.splitlines()[1:-1]:
225            for col in row.split():
226                if col == '.':
227                    self.sizer.Add((SPACER, SPACER))
228                elif col[0] in '123456789*':
229                    static = wx.StaticText(self, -1, col,
230                        style=wx.ALIGN_CENTER|wx.ALIGN_BOTTOM)
231                    self.sizer.Add(static, 0,
232                        (wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|
233                         wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE), SPACER//2)
234                else:
235                    ele = ELEMENTS[col]
236                    button = self.element_button_class(self, ele.number+100, ele.symbol,
237                                           size=(buttonsize, buttonsize))
238                    self.buttons[ele.number - 1] = button
239                    button.SetBezelWidth(1)
240                    button.SetToolTipString(ele.name)
241                    col = COLORS[ele.series]
242                    button.SetButtonColour(wx.Colour(col[0], col[1], col[2]))
243                    self.sizer.Add(button, 0, (wx.LEFT|wx.BOTTOM|
244                        wx.FIXED_MINSIZE|wx.ALIGN_CENTER_HORIZONTAL|
245                        wx.ALIGN_CENTER_VERTICAL), 0)
246                    self.Bind(wx.EVT_BUTTON, self.OnSelect, button)
247
248        self.SetAutoLayout(True)
249        self.SetSizer(self.sizer)
250        self.sizer.SetSizeHints(self)
251        self.sizer.Fit(self)
252
253        # position element info panel
254        cw = self.sizer.GetColWidths()
255        rh = self.sizer.GetRowHeights()
256        self.info.Move((sum(cw[:3])+cw[3]//2, (rh[0]-SPACER)//2-1))
257
258        # legend of chemical series
259        self.legendpos = (sum(cw[:13]), (rh[0]-SPACER)//2-1)
260        self.legendsize = (sum(cw[13:17]) + cw[17]//2, -1)
261        self.legend = wx.StaticText(self, -1, " Alkaline earth metals ",
262            style=wx.ALIGN_CENTER, pos=self.legendpos, size=self.legendsize)
263        self.legend.SetToolTipString("Chemical series")
264
265        # blinking element button
266        self.highlight = False
267        self.timer = wx.Timer(self)
268        self.timer.Start(750)
269        self.Bind(wx.EVT_TIMER, self.OnTimer)
270
271    def AddCtrl(self, ctrl, pos=200):
272        self.sizer.Remove(pos)
273        self.sizer.Insert(pos, ctrl, 0, (wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|
274                                         wx.ALIGN_BOTTOM|wx.FIXED_MINSIZE), 0)
275        self.Layout()
276
277    def OnTimer(self, evt):
278        button = self.buttons[self.selected]
279        button.SetToggle(button.up)
280
281    def OnSelect(self, evt):
282        self.GetParent().SetSelection(evt.GetId() - 101)
283
284    def SetSelection(self, select):
285        """Set active element."""
286        if self.selected == select:
287            return
288        # reset old selection
289        self.buttons[self.selected].SetToggle(False)
290        # highlight new selection
291        self.selected = select
292        self.buttons[select].SetToggle(True)
293        ele = ELEMENTS[select + 1]
294        col = COLORS[ele.series]
295        self.legend.SetBackgroundColour(wx.Colour(col[0], col[1], col[2]))
296        self.legend.SetLabel(SERIES[ele.series])
297        self.legend.Move(self.legendpos)
298        self.legend.SetSize(self.legendsize)
299        self.info.SetSelection(select)
300
301
302class ElementPanel(wx.Panel):
303    """Element information panel."""
304
305    def __init__(self, *args, **kwds):
306        kwds["style"] = wx.NO_BORDER | wx.TAB_TRAVERSAL
307        wx.Panel.__init__(self, *args, **kwds)
308        self.selected = -1
309
310        # create controls
311        self.number = wx.StaticText(self, -1, "808",
312            style=wx.ALIGN_RIGHT)
313        self.position = wx.StaticText(self, -1, "6, 88, 9",
314            style=wx.ALIGN_LEFT)
315        self.symbol = wx.StaticText(self, -1, "Mm",
316            style=wx.ALIGN_CENTER_HORIZONTAL)
317        self.name = wx.StaticText(self, -1, "Praseodymium ",
318            style=wx.ALIGN_CENTER_HORIZONTAL)
319        self.mass = wx.StaticText(self, -1, "123.4567890 ",
320            style=wx.ALIGN_CENTER_HORIZONTAL)
321        self.massnumber = wx.StaticText(self, -1, "123 A ",
322            style=wx.ALIGN_RIGHT)
323        self.protons = wx.StaticText(self, -1, "123 P ",
324            style=wx.ALIGN_RIGHT)
325        self.neutrons = wx.StaticText(self, -1, "123 N ",
326            style=wx.ALIGN_RIGHT)
327        self.electrons = wx.StaticText(self, -1, "123 e ",
328            style=wx.ALIGN_RIGHT)
329        self.eleshell = wx.StaticText(self, -1, "2, 8, 18, 32, 32, 15, 2",
330            style=wx.ALIGN_LEFT)
331        self.eleconfig = StaticFancyText(self, -1,
332            "[Xe] 4f<sup>14</sup> 5d<sup>10</sup>"
333            " 6s<sup>2</sup> 6p<sup>6</sup> ",
334            style=wx.ALIGN_LEFT)
335        self.oxistates = wx.StaticText(self, -1, "1*, 2, 3, 4, 5, 6, -7 ")
336        self.atmrad = wx.StaticText(self, -1, "1.234 A ",
337            style=wx.ALIGN_RIGHT)
338        self.ionpot = wx.StaticText(self, -1, "123.4567890 eV ")
339        self.eleneg = wx.StaticText(self, -1, "123.45678 ")
340
341        # set control properties
342        font = self.GetFont()
343        font.SetWeight(wx.FONTWEIGHT_BOLD)
344        self.number.SetFont(font)
345        self.name.SetFont(font)
346        font.SetPointSize(font.GetPointSize() * 1.9)
347        self.symbol.SetFont(font)
348
349        self.number.SetToolTipString("Atomic number")
350        self.position.SetToolTipString("Group, Period, Block")
351        self.symbol.SetToolTipString("Symbol")
352        self.name.SetToolTipString("Name")
353        self.mass.SetToolTipString("Relative atomic mass")
354        self.eleshell.SetToolTipString("Electrons per shell")
355        self.massnumber.SetToolTipString("Mass number (most abundant isotope)")
356        self.protons.SetToolTipString("Protons")
357        self.neutrons.SetToolTipString("Neutrons (most abundant isotope)")
358        self.electrons.SetToolTipString("Electrons")
359        self.eleconfig.SetToolTipString("Electron configuration")
360        self.oxistates.SetToolTipString("Oxidation states")
361        self.atmrad.SetToolTipString("Atomic radius")
362        self.ionpot.SetToolTipString("Ionization potentials")
363        self.eleneg.SetToolTipString("Electronegativity")
364
365        # layout
366        sizer = rcsizer.RowColSizer()
367        sizer.col_w = SPACER
368        sizer.row_h = SPACER
369        sizer.Add(self.number, row=0, col=0,
370            flag=wx.ALIGN_CENTER_HORIZONTAL|wx.FIXED_MINSIZE)
371        sizer.Add(self.position, row=0, col=1,
372            flag=wx.ALIGN_LEFT|wx.FIXED_MINSIZE)
373        sizer.Add(self.symbol, row=1, col=0, rowspan=2, colspan=2,
374            flag=(wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|
375                  wx.FIXED_MINSIZE))
376        sizer.Add(self.name, row=3, col=0, colspan=2,
377            flag=wx.ALIGN_CENTER_HORIZONTAL|wx.FIXED_MINSIZE)
378        sizer.Add(self.mass, row=4, col=0, colspan=2,
379            flag=wx.ALIGN_CENTER_HORIZONTAL|wx.FIXED_MINSIZE)
380        sizer.Add(self.massnumber, row=0, col=2,
381            flag=wx.ALIGN_RIGHT|wx.FIXED_MINSIZE)
382        sizer.Add(self.protons, row=1, col=2,
383            flag=wx.ALIGN_RIGHT|wx.FIXED_MINSIZE)
384        sizer.Add(self.neutrons, row=2, col=2,
385            flag=wx.ALIGN_RIGHT|wx.FIXED_MINSIZE)
386        sizer.Add(self.electrons, row=3, col=2,
387            flag=wx.ALIGN_RIGHT|wx.FIXED_MINSIZE)
388        sizer.Add(self.atmrad, row=4, col=2,
389            flag=wx.ALIGN_RIGHT|wx.FIXED_MINSIZE)
390        sizer.Add(self.eleconfig, row=0, col=4, flag=wx.ADJUST_MINSIZE)
391        sizer.Add(self.eleshell, row=1, col=4, flag=wx.ADJUST_MINSIZE)
392        sizer.Add(self.oxistates, row=2, col=4, flag=wx.ADJUST_MINSIZE)
393        sizer.Add(self.ionpot, row=3, col=4, flag=wx.ADJUST_MINSIZE)
394        sizer.Add(self.eleneg, row=4, col=4, flag=wx.ADJUST_MINSIZE)
395
396        self.SetAutoLayout(True)
397        self.SetSizer(sizer)
398        sizer.Fit(self)
399        sizer.SetSizeHints(self)
400        self.Layout()
401
402    def SetSelection(self, select):
403        """Set active element."""
404        if self.selected == select:
405            return
406        self.Freeze()
407        self.selected = select
408        ele = ELEMENTS[select + 1]
409
410        self.number.SetLabel("%i" % ele.number)
411        self.position.SetLabel("%i, %i, %s" % (
412            ele.group, ele.period, ele.block))
413        self.mass.SetLabel("%.10g" % ele.mass)
414        self.eleshell.SetLabel(', '.join("%i" % i for i in ele.eleshells))
415        self.massnumber.SetLabel('%i A ' % ele.nominalmass)
416        self.protons.SetLabel('%i P ' % ele.protons)
417        self.neutrons.SetLabel('%i N ' % ele.neutrons)
418        self.electrons.SetLabel('%i e ' % ele.electrons)
419        self.oxistates.SetLabel(ele.oxistates)
420        self.atmrad.SetLabel(_u("%.10g \xc5 ") % ele.atmrad if ele.atmrad else
421                             "")
422        self.eleneg.SetLabel("%.10g" % ele.eleneg if ele.eleneg else "")
423        self.ionpot.SetLabel(
424            "%.10g eV" % ele.ionenergy[0] if ele.ionenergy else "")
425        self.symbol.SetLabel(ele.symbol)
426        self.name.SetLabel(ele.name)
427
428        label = []
429        for orb in ele.eleconfig.split():
430            if not orb.startswith('[') and len(orb) > 2:
431                orb = orb[:2] + '<sup>' + orb[2:] + '</sup>'
432            label.append(orb)
433        label.append("<sup> </sup>")  # fix ADJUST_MINSIZE
434        self.eleconfig.SetLabel(' '.join(label))
435
436        self.Thaw()
437        self.Layout()
438
439
440class DetailsPanel(wx.Panel):
441    """Element details panel."""
442
443    def __init__(self, *args, **kwds):
444        kwds["style"] = wx.NO_BORDER|wx.TAB_TRAVERSAL
445        wx.Panel.__init__(self, *args, **kwds)
446        self.selected = -1
447
448        # create controls
449        cb_style = wx.CB_READONLY|wx.CB_SORT
450        if wx.Platform == "__WXMAC__":
451            cb_style = wx.CB_READONLY
452
453
454        self.names = LabeledCtrl(self, wx.ComboBox, "Element Name",
455            choices=[p.name for p in ELEMENTS],
456            style=cb_style, size=(1, -1))
457        self.symbols = LabeledCtrl(self, wx.ComboBox, "Symbol", '',
458            choices=[p.symbol for p in ELEMENTS],
459            style=cb_style, size=(1, -1))
460        self.numbers = LabeledCtrl(self, wx.ComboBox, "Number",
461            choices=["%s" % p.number for p in ELEMENTS],
462            style=wx.CB_READONLY, size=(1, -1))
463        self.mass = LabeledCtrl(self, wx.ComboBox, "Relative Atomic Mass",
464            choices=["%-.10g" % p.mass for p in ELEMENTS],
465            style=wx.CB_READONLY, size=(1, -1))
466        self.atmrad = LabeledCtrl(self, wx.ComboBox,
467                                  _u("Atomic Radius (\xc5)"),
468            choices=["%-.10g" % p.atmrad for p in ELEMENTS],
469            style=wx.CB_READONLY, size=(1, -1))
470        self.covrad = LabeledCtrl(self, wx.ComboBox,
471                                  _u("Covalent Radius (\xc5)"),
472            choices=["%-.10g" % p.covrad for p in ELEMENTS],
473            style=wx.CB_READONLY, size=(1, -1))
474        self.vdwrad = LabeledCtrl(self, wx.ComboBox,
475                                  _u("V.d.Waals Radius (\xc5)"),
476            choices=["%-.10g" % p.vdwrad for p in ELEMENTS],
477            style=wx.CB_READONLY, size=(1, -1))
478        self.eleneg = LabeledCtrl(self, wx.ComboBox, "Electronegativity",
479            choices=["%-.10g" % p.eleneg for p in ELEMENTS],
480            style=wx.CB_READONLY, size=(1, -1))
481        self.eleconfig = LabeledCtrl(self, wx.ComboBox, "e- Config",
482            choices=[p.eleconfig for p in ELEMENTS],
483            style=wx.CB_READONLY, size=(1, -1))
484        self.eleshells = LabeledCtrl(self, wx.ComboBox, "Electrons per Shell",
485            choices=[', '.join("%i" % i for i in p.eleshells)
486                     for p in ELEMENTS],
487            style=wx.CB_READONLY, size=(1, -1))
488        self.oxistates = LabeledCtrl(self, wx.ComboBox, "Oxidation States",
489            choices=[p.oxistates for p in ELEMENTS],
490            style=wx.CB_READONLY, size=(1, -1))
491        self.ionpot = LabeledCtrl(self, wx.Choice,
492            "Ionization Potentials (eV)", choices=[], size=(1, -1))
493        self.isotopes = LabeledCtrl(self, wx.Choice, "Isotopes",
494            choices=[], size=(1, -1))
495
496        # layout
497        sizer = wx.BoxSizer(wx.VERTICAL)
498        sizer_top = wx.BoxSizer(wx.HORIZONTAL)
499        sizer_left = wx.BoxSizer(wx.VERTICAL)
500        sizer_right = wx.BoxSizer(wx.VERTICAL)
501        sizer_num = wx.BoxSizer(wx.HORIZONTAL)
502        style = wx.RIGHT|wx.BOTTOM|wx.EXPAND|wx.ADJUST_MINSIZE
503        sizer_left.Add(self.names, 0, style, SPACER)
504        sizer_left.Add(self.mass, 0, style, SPACER)
505        sizer_left.Add(self.atmrad, 0, style, SPACER)
506        sizer_left.Add(self.covrad, 0, style, SPACER)
507        sizer_left.Add(self.vdwrad, 0, style, SPACER)
508        sizer_left.Add(self.eleneg, 0, style, SPACER)
509        sizer_top.Add(sizer_left, 1, wx.LEFT|wx.RIGHT, 0)
510        style = wx.BOTTOM|wx.EXPAND|wx.ADJUST_MINSIZE
511        sizer_num.Add(self.symbols, 1, style, 0)
512        sizer_num.Add((SPACER, 5), 0, 0, 0)
513        sizer_num.Add(self.numbers, 1, style, 0)
514        sizer_right.Add(sizer_num, 0, style, SPACER)
515        sizer_right.Add(self.eleconfig, 0, style, SPACER)
516        sizer_right.Add(self.eleshells, 0, style, SPACER)
517        sizer_right.Add(self.oxistates, 0, style, SPACER)
518        sizer_right.Add(self.ionpot, 0, style, SPACER)
519        sizer_right.Add(self.isotopes, 0, style, SPACER)
520        sizer_top.Add(sizer_right, 1, wx.TOP|wx.RIGHT, 0)
521        sizer.Add(sizer_top, 1,
522            wx.LEFT|wx.RIGHT|wx.TOP|wx.EXPAND|wx.ADJUST_MINSIZE, SPACER)
523        self.SetAutoLayout(True)
524        self.SetSizerAndFit(sizer, True)
525        sizer.SetSizeHints(self)
526        self.Layout()
527
528        # bind events
529        self.Bind(wx.EVT_COMBOBOX, self.OnSelectName, self.names.ctrl)
530        self.Bind(wx.EVT_COMBOBOX, self.OnSelectSymbol, self.symbols.ctrl)
531        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.numbers.ctrl)
532        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.mass.ctrl)
533        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.atmrad.ctrl)
534        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.covrad.ctrl)
535        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.vdwrad.ctrl)
536        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.eleshells.ctrl)
537        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.eleneg.ctrl)
538        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.eleconfig.ctrl)
539        self.Bind(wx.EVT_COMBOBOX, self.OnSelect, self.oxistates.ctrl)
540
541    def SetSelection(self, select):
542        """Set active element."""
543        if self.selected == select:
544            return
545        self.selected = select
546        ele = ELEMENTS[select+1]
547
548        self.names.ctrl.SetStringSelection(ele.name)
549        self.symbols.ctrl.SetStringSelection(ele.symbol)
550        self.numbers.ctrl.SetSelection(select)
551        self.mass.ctrl.SetSelection(select)
552        self.eleconfig.ctrl.SetSelection(select)
553        self.atmrad.ctrl.SetSelection(select)
554        self.covrad.ctrl.SetSelection(select)
555        self.vdwrad.ctrl.SetSelection(select)
556        self.eleneg.ctrl.SetSelection(select)
557        self.eleshells.ctrl.SetSelection(select)
558        self.oxistates.ctrl.SetSelection(select)
559
560        self.isotopes.ctrl.Clear()
561        for index, massnum in enumerate(sorted(ele.isotopes)):
562            iso = ele.isotopes[massnum]
563            self.isotopes.ctrl.Append("%3i:  %8.4f , %8.4f%%" % (
564                massnum, iso.mass, iso.abundance*100.0))
565            if massnum == ele.nominalmass:
566                self.isotopes.ctrl.SetSelection(index)
567
568        self.ionpot.ctrl.Clear()
569        for ion in ele.ionenergy:
570            self.ionpot.ctrl.Append("%8.4f" % ion)
571        self.ionpot.ctrl.SetSelection(0)
572
573    def OnSelect(self, evt):
574        self.SetSelection(evt.GetSelection())
575        event = SelectionEvent(pteEVT_ELE_CHANGED, self.GetId(), self.selected)
576        self.GetEventHandler().ProcessEvent(event)
577        evt.Skip()
578
579    def OnSelectName(self, evt):
580        name = self.names.ctrl.GetValue()
581        self.SetSelection(ELEMENTS[name].number - 1)
582        event = SelectionEvent(pteEVT_ELE_CHANGED, self.GetId(), self.selected)
583        self.GetEventHandler().ProcessEvent(event)
584        evt.Skip()
585
586    def OnSelectSymbol(self, evt):
587        name = self.symbols.ctrl.GetValue()
588        self.SetSelection(ELEMENTS[name].number - 1)
589        event = SelectionEvent(pteEVT_ELE_CHANGED, self.GetId(), self.selected)
590        self.GetEventHandler().ProcessEvent(event)
591        evt.Skip()
592
593
594class DecriptionPanel(wx.Panel):
595    """Element description panel."""
596
597    def __init__(self, *args, **kwds):
598        kwds["style"] = wx.NO_BORDER|wx.TAB_TRAVERSAL
599        wx.Panel.__init__(self, *args, **kwds)
600        self.selected = -1
601
602        self.description = wx.TextCtrl(self, -1, " \n \n",
603            style=wx.TE_MULTILINE|wx.TE_READONLY)
604        font = self.description.GetFont()
605        font.SetPointSize((font.GetPointSize() + 1))
606        self.description.SetFont(font)
607        sizer = wx.BoxSizer(wx.VERTICAL)
608        sizer.Add(self.description, 1,
609            wx.TOP|wx.LEFT|wx.RIGHT|wx.BOTTOM|wx.EXPAND|wx.FIXED_MINSIZE,
610            SPACER)
611
612        self.SetAutoLayout(True)
613        self.SetSizerAndFit(sizer, True)
614        sizer.SetSizeHints(self)
615        self.Layout()
616
617    def SetSelection(self, select):
618        """Set active element."""
619        if self.selected == select:
620            return
621        self.selected = select
622
623        ele = ELEMENTS[select + 1]
624        self.description.SetValue(ele.description)
625
626
627class LabeledCtrl(wx.BoxSizer):
628    """BoxSizer containing label, control, and unit."""
629
630    def __init__(self, parent, control, label, unit=None, space="  ",
631                 *args, **kwds):
632        wx.BoxSizer.__init__(self, wx.HORIZONTAL)
633        self.label = wx.StaticText(parent, -1, label + space)
634        self.ctrl = control(parent, -1, *args, **kwds)
635        self.Add(self.label, 0, wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE)
636        self.Add(self.ctrl, 1, (wx.LEFT|wx.EXPAND|wx.ALIGN_CENTER_VERTICAL|
637                                wx.ALIGN_RIGHT|wx.ADJUST_MINSIZE), 0)
638        if unit:
639            self.unit = wx.StaticText(parent, -1, unit)
640            self.Add(self.unit, 0,
641                wx.RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.FIXED_MINSIZE, 0)
642        else:
643            self.unit = None
644
645
646class StaticFancyText(fancytext.StaticFancyText):
647    """StaticFancyText with SetLabel function."""
648
649    def SetLabel(self, label):
650        bmp = fancytext.RenderToBitmap(
651            label, wx.Brush(self.GetBackgroundColour(), wx.SOLID))
652        self.SetBitmap(bmp)
653
654
655class DisclosureCtrl(buttons.GenBitmapTextToggleButton):
656    """Disclosure triangle button."""
657
658    bmp0 = (b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\r\x00\x00'
659            b'\x00\r\x08\x06\x00\x00\x00r\xeb\xe4|\x00\x00\x00\x04sBIT'
660            b'\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\xd5IDAT(\x91\x9d'
661            b'\x921n\x83@\x10E\xdf.!\x96\x9c\x90P"\xd9\x86\x92\x82\x03p'
662            b'\x02h\xa9i9A\xe2\xb38)9\x02w\xc0\t\x94I\xa0H\xd2\xc7'
663            b'\x96(\x918\xc0\xa6Z\x17\x91\x05&_\x1a\xe9k4O\xfa3\x1a!'
664            b'\xa4\xc1\\Im>\xde\xdf\xd4l\xa8,K\x9e\x9fv\xeax\xf8\x99\x84'
665            b'\x85\x8e\xb7}|P\x00\xb6m\x13\x04\x01q\x1cc\xdd\xdd\x8bs\xd0'
666            b'\xd5\xdfF\xdf\xf7TUE\xdb\xb6\xbc\xbe\xecU\x18\x86\x98\xd7'
667            b'\x0b1\ni\r\xc3@Q\x14\xd4u\xcd\xf7\xd7\xa7r]\x97\xe5\xcd'
668            b'\xad\x18\x85\xb4\xba\xae#\xcfs|\xdf?\xf5\xe4\xc8<\x00\x8e'
669            b'\xe3\x90e\x19i\x9aN\xc7\xb3,\x8b(\x8a\xb8h\xa7Y\xd7'
670            b'\xf3<\x0f\xd34I\x92\x84\xd5zsv\xf8$!\r\x844h\x9aFi?Y\xff'
671            b'\xf9\xbd_\xd7\x8c7Z\xc0k\x8d8\x00\x00\x00\x00IEND\xaeB`\x82')
672
673    bmp1 = (b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\r\x00\x00'
674            b'\x00\r\x08\x06\x00\x00\x00r\xeb\xe4|\x00\x00\x00\x04sBIT'
675            b'\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\xc1IDAT(\x91\x9d\xd2;'
676            b'\x8e\xc20\x10\x80\xe1\x7fB\x0c\x95\xc1\xa5%\xc0)S\xb8I\xd2'
677            b'\xb8Le\x97\xf6\x99s\x81\xdd\xbdBN\xb2\xdb!Y\t\xac`\xbay|'
678            b'\xa3)F\xa49\xf0n4o\x8bOQ\x0b\xf0\xf3\xfd\xf5\xbb,\x0b\xeb'
679            b'\xba>\x1d\xec\xba\x8ey\x9e\x19\xc6I\x1a\x80a\x9cD)\xf5r'
680            b'\xbbR\x8aa\x9c\xa4:\xaf\x94\x821f\x17\x18c(\xa5<\xf2\x07'
681            b'\xba\xde\xee\xe2\xbd\xdfE\xde{\xae\xb7\xbbl\x10@J\t\xadu\x05'
682            b'\xb4\xd6\xa4\x94\xaaZ\x85\xf4\xf9"1\xc6j \xc6\x88>_\xe4)\x02'
683            b'\x08!`\xad\x05\xc0ZK\x08as\xee\x06\xa9\xe3Ir\xce\xb4mK\xce'
684            b'\x19u<\xc9\xbf\x08\xc09G\xdf\xf78\xe7\xf6\xda\xc8\'\xbf\xf7'
685            b'\x07\x13\x12\x18B\x17\x9fx\xa0\x00\x00\x00\x00IEND\xaeB`\x82')
686
687    def __init__(self, parent, winid, label, *args, **kwds):
688        kwds["style"] = wx.BORDER_NONE|wx.BU_EXACTFIT
689        buttons.GenBitmapTextToggleButton.__init__(self, parent, winid, None,
690                                                   label, *args, **kwds)
691        if isinstance(self.bmp0, type(b'')):
692            self.__class__.bmp0 = wx.BitmapFromImage(wx.ImageFromStream(
693                io.BytesIO(self.bmp0)))
694            self.__class__.bmp1 = wx.BitmapFromImage(wx.ImageFromStream(
695                io.BytesIO(self.bmp1)))
696
697        self.SetBitmapLabel(self.bmp0)
698        self.SetBitmapSelected(self.bmp1)
699        if not label:
700            self.SetSize(self.bmp0.GetSize())
701        else:
702            self.SetBestSize()
703        self.labelDelta = 0
704        self.useFocusInd = False
705        self.SetToolTipString('Show')
706        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
707
708    def OnEraseBackground(self, event):
709        pass
710
711    def Notify(self):
712        wx.lib.buttons.GenBitmapTextToggleButton.Notify(self)
713        self.SetToolTipString("%s" % ('Show' if self.up else 'Hide'))
714
715    def DoGetBestSize(self):
716        width, height, usemin = self._GetLabelSize()
717        return width + 5, height + 4
718
719    def OnPaint(self, event):
720        width, height = self.GetClientSizeTuple()
721        dc = wx.BufferedPaintDC(self)
722        brush = None
723        bgcol = self.GetBackgroundColour()
724        brush = wx.Brush(bgcol, wx.SOLID)
725        defattr = self.GetDefaultAttributes()
726        if self.style & wx.BORDER_NONE and bgcol == defattr.colBg:
727            defattr = self.GetParent().GetDefaultAttributes()
728            if self.GetParent().GetBackgroundColour() == defattr.colBg:
729                if wx.Platform == "__WXMSW__":
730                    if self.DoEraseBackground(dc):
731                        brush = None
732                elif wx.Platform == "__WXMAC__":
733                    brush.MacSetTheme(1)
734            else:
735                bgcol = self.GetParent().GetBackgroundColour()
736                brush = wx.Brush(bgcol, wx.SOLID)
737        if brush is not None:
738            dc.SetBackground(brush)
739            dc.Clear()
740        self.DrawLabel(dc, width, height)
741
742    def DrawLabel(self, dc, width, height, center=False):
743        bmp = self.bmpLabel
744        if bmp is not None:
745            if self.bmpDisabled and not self.IsEnabled():
746                bmp = self.bmpDisabled
747            if self.bmpFocus and self.hasFocus:
748                bmp = self.bmpFocus
749            if self.bmpSelected and not self.up:
750                bmp = self.bmpSelected
751            bmpwidth, bmpheight = bmp.GetWidth(), bmp.GetHeight()
752            hasmask = bmp.GetMask() is not None
753        else:
754            bmpwidth = bmpheight = 0
755
756        dc.SetFont(self.GetFont())
757        color = (self.GetForegroundColour() if self.IsEnabled() else
758                 wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
759        dc.SetTextForeground(color)
760
761        label = self.GetLabel()
762        txtwidth, txtheight = dc.GetTextExtent(label)
763        # center bitmap and text
764        xpos = (width - bmpwidth - txtwidth) // 2 if center else 0
765        if bmp is not None:
766            dc.DrawBitmap(bmp, xpos, (height - bmpheight) // 2, hasmask)
767            xpos += 5
768        dc.DrawText(label, xpos + bmpwidth, (height - txtheight) // 2)
769
770
771class SelectionEvent(wx.PyCommandEvent):
772    """Notification of changed element."""
773
774    def __init__(self, evtType, winid, sel):
775        wx.PyCommandEvent.__init__(self, evtType, winid)
776        self.selection = sel
777
778    def SetSelection(self, select):
779        self.selection = select
780
781    def GetSelection(self):
782        return self.selection
783
784
785class WxPeriodicTable(wx.Frame):
786    """Main application window."""
787
788    # Class used to instanciate the panel with the elements.
789    periodic_panel_class = PeriodicPanel
790
791    def __init__(self, *args, **kwds):
792        kwds["style"] = (wx.DEFAULT_DIALOG_STYLE | wx.MINIMIZE_BOX | wx.TAB_TRAVERSAL)
793        wx.Frame.__init__(self, *args, **kwds)
794        self.selected = -1
795
796        self.SetTitle(MainApp.name)
797        icon = wx.EmptyIcon()
798        #icon.CopyFromBitmap(wx.Bitmap(MainApp.icon + '.png',
799        #                              wx.BITMAP_TYPE_ANY))
800        self.SetIcon(icon)
801        self.SetBackgroundColour(wx.SystemSettings_GetColour(
802            wx.SYS_COLOUR_3DFACE))
803
804        # create menu
805        self.menu = wx.MenuBar()
806        self.SetMenuBar(self.menu)
807        menu = wx.Menu()
808        menu.Append(wx.ID_EXIT, "Exit", "Exit the application", wx.ITEM_NORMAL)
809        self.menu.Append(menu, "File")
810        menu = wx.Menu()
811        menu.Append(wx.ID_COPY,
812            "Copy\tCtrl+C", "Copy selected element to the clipboard",
813            wx.ITEM_NORMAL)
814        self.menu.Append(menu, "Edit")
815        menu = wx.Menu()
816        menu.Append(wx.ID_VIEW_DETAILS, "Details", "Show or hide details",
817            wx.ITEM_CHECK)
818        self.menu.Append(menu, "View")
819        menu = wx.Menu()
820        menu.Append(wx.ID_ABOUT, "About...",
821            "Display information about the program", wx.ITEM_NORMAL)
822        self.menu.Append(menu, "Help")
823
824        # create panels and controls
825        self.notebook = wx.Notebook(self, -1, style=0)
826        self.description = DecriptionPanel(self.notebook, -1)
827        self.details = DetailsPanel(self.notebook, -1)
828
829        self.table = self.periodic_panel_class(self, id=-1)
830
831        self.disclose = DisclosureCtrl(self.table, -1, '')
832        self.table.AddCtrl(self.disclose)
833        self.notebook.AddPage(self.description, "Description")
834        self.notebook.AddPage(self.details, "Properties")
835
836        # event bindings
837        self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)
838        self.Bind(wx.EVT_MENU, self.OnCopy, id=wx.ID_COPY)
839        self.Bind(wx.EVT_MENU, self.OnDetails, id=wx.ID_VIEW_DETAILS)
840        self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT)
841        self.Bind(EVT_ELE_CHANGED, self.OnSelect, self.table)
842        self.Bind(EVT_ELE_CHANGED, self.OnSelect, self.details)
843        self.Bind(wx.EVT_BUTTON, self.OnDetails, self.disclose)
844
845        # create sizers
846        self.sizer = wx.BoxSizer(wx.VERTICAL)
847        self.sizer.Add(self.table, 1, (wx.LEFT|wx.TOP|wx.RIGHT|wx.EXPAND|
848            wx.ALIGN_CENTER_HORIZONTAL| wx.EXPAND|wx.ADJUST_MINSIZE), BORDER-5)
849        self.sizer.Add((BORDER, BORDER))
850        self.sizer.Add(self.notebook, 0, (wx.LEFT|wx.RIGHT|wx.BOTTOM|
851            wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|
852            wx.ADJUST_MINSIZE), BORDER)
853
854        self.notebook.SetSelection(1)
855        self.SetAutoLayout(True)
856        self.SetSizerAndFit(self.sizer, True)
857        self.sizer.SetSizeHints(self)
858        self.ApplyLayout(True)
859        self.SetSelection(0)
860
861    def ApplyLayout(self, show=False):
862        self.show_info = show
863        self.sizer.Show(2, show, True)
864        self.SetAutoLayout(True)
865        self.SetSizerAndFit(self.sizer, True)
866        self.sizer.SetSizeHints(self)
867        self.Layout()
868        self.menu.Check(wx.ID_VIEW_DETAILS, show)
869        self.disclose.SetToggle(self.show_info)
870
871    def OnDetails(self, evt):
872        self.ApplyLayout(not self.show_info)
873
874    def OnEraseBackground(self, event):
875        pass
876
877    def OnCopy(self, evt):
878        dobj = wx.TextDataObject()
879        dobj.SetText(repr(ELEMENTS[self.selected + 1]))
880        if wx.TheClipboard.Open():
881            wx.TheClipboard.SetData(dobj)
882            wx.TheClipboard.Close()
883
884    def OnAbout(self, evt):
885        info = wx.AboutDialogInfo()
886        info.Name = MainApp.name
887        info.Version = MainApp.version
888        info.Copyright = MainApp.copyright
889        info.WebSite = MainApp.website
890        wx.AboutBox(info)
891
892    def OnWebsite(self, evt):
893        webbrowser.open(MainApp.website)
894
895    def OnWikipedia(self, evt):
896        webbrowser.open("http://en.wikipedia.org/wiki/%s" % (
897            ELEMENTS[self.selected].name), 1)
898
899    def OnWebElements(self, evt):
900        webbrowser.open("http://www.webelements.com/%s/" % (
901            ELEMENTS[self.selected].name.lower()))
902
903    def OnSelect(self, evt):
904        self.SetSelection(evt.GetSelection())
905
906    def SetSelection(self, select):
907        """Set active element."""
908        if self.selected != select:
909            self.selected = select
910            self.description.SetSelection(select)
911            self.table.SetSelection(select)
912            self.details.SetSelection(select)
913
914    def OnExit(self, evt):
915        self.Close()
916        if __name__ == "__main__":
917            sys.exit(0)
918        else:
919            return self.selected
920
921
922pteEVT_ELE_CHANGED = wx.NewEventType()
923EVT_ELE_CHANGED = wx.PyEventBinder(pteEVT_ELE_CHANGED, 1)
924
925SPACER = 12 if wx.Platform == "__WXMAC__" else 10
926BORDER = 22 if wx.Platform == "__WXMAC__" else 10
927
928COLORS = {
929    1: (0x99, 0xff, 0x99),  # Nonmetals
930    2: (0xc0, 0xff, 0xff),  # Noble gases
931    3: (0xff, 0x99, 0x99),  # Alkali metals
932    4: (0xff, 0xde, 0xad),  # Alkaline earth metals
933    5: (0xcc, 0xcc, 0x99),  # Metalloids
934    6: (0xff, 0xff, 0x99),  # Halogens
935    7: (0xcc, 0xcc, 0xcc),  # Poor metals
936    8: (0xff, 0xc0, 0xc0),  # Transition metals
937    9: (0xff, 0xbf, 0xff),  # Lanthanides
938    10: (0xff, 0x99, 0xcc),  # Actinides
939}
940
941_u = (lambda x: x.decode('latin-1')) if sys.version[0] == '2' else str
942
943if __name__ == "__main__":
944    MainApp(0).MainLoop()
945