Skip to main content
`Ctrl`+`K`

#

# Source code for ase.gui.gui

    # fmt: off

    import functools
    import pickle
    import platform
    import subprocess
    import sys
    from functools import partial
    from time import time

    import numpy as np

    import ase.gui.ui as ui
    from ase import Atoms, __version__
    from ase.gui.defaults import read_defaults
    from ase.gui.i18n import _
    from ase.gui.images import Images
    from ase.gui.nanoparticle import SetupNanoparticle
    from ase.gui.nanotube import SetupNanotube
    from ase.gui.observer import Observers
    from ase.gui.save import save_dialog
    from ase.gui.settings import Settings
    from ase.gui.status import Status
    from ase.gui.surfaceslab import SetupSurfaceSlab
    from ase.gui.view import View

    class GUIObservers:
        def __init__(self):
            self.new_atoms = Observers()
            self.set_atoms = Observers()
            self.change_atoms = Observers()

    class GUI(View):
        ARROWKEY_SCAN = 0
        ARROWKEY_MOVE = 1
        ARROWKEY_ROTATE = 2

        def __init__(self, images=None,
                     rotations='',
                     show_bonds=False, expr=None):

            if not isinstance(images, Images):
                images = Images(images)
            self.images = images
            self.images.history.initialize_history()

            self.system = platform.system()

            # Ordinary observers seem unused now, delete?
            self.observers = []
            self.obs = GUIObservers()

            self.config = read_defaults()
            if show_bonds:
                self.config['show_bonds'] = True

            menu = self.get_menu_data()

            self.window = ui.ASEGUIWindow(close=self.exit, menu=menu,
                                          config=self.config, scroll=self.scroll,
                                          scroll_event=self.scroll_event,
                                          press=self.press, move=self.move,
                                          release=self.release,
                                          resize=self.resize)

            super().__init__(rotations)
            self.status = Status(self)

            self.subprocesses = []  # list of external processes
            self.movie_window = None
            self.simulation = {}  # Used by modules on Calculate menu.
            self.module_state = {}  # Used by modules to store their state.

            self.arrowkey_mode = self.ARROWKEY_SCAN
            self.move_atoms_mask = None

            self.set_frame(len(self.images) - 1, focus=True)

            # Used to move the structure with the mouse
            self.prev_pos = None
            self.last_scroll_time = time()
            self.orig_scale = self.scale

            if len(self.images) > 1:
                self.movie()

            if expr is None:
                expr = self.config['gui_graphs_string']

            if expr is not None and expr != '' and len(self.images) > 1:
                self.plot_graphs(expr=expr, ignore_if_nan=True)

        def redo_history(self, key=None):
            self.images.history.redo_history(self.frame)
            self.set_frame()
            self.draw()

        def undo_history(self, key=None):
            # Let's end any rotate/move mode that may still be active so the
            # current positions are saved to the history:
            if self.moving:
                self.toggle_arrowkey_mode(self.arrowkey_mode)
            self.images.history.undo_history(self.frame)
            self.set_frame()
            self.draw()

        def update_history(self, mask=None):
            if mask is not None:
                for i, update in enumerate(mask):
                    if update:
                        self.images.history.update_history(i)
            else:
                self.images.history.update_history(self.frame)

        def clear_history(self):
            self.images.history.initialize_history()

        @property
        def moving(self):
            return self.arrowkey_mode != self.ARROWKEY_SCAN

        def run(self):
            self.window.run()

        def toggle_move_mode(self, key=None):
            self.toggle_arrowkey_mode(self.ARROWKEY_MOVE)

        def toggle_rotate_mode(self, key=None):
            self.toggle_arrowkey_mode(self.ARROWKEY_ROTATE)

        def toggle_arrowkey_mode(self, mode):
            # If not currently in given mode, activate it.
            # Else, deactivate it (go back to SCAN mode)
            assert mode != self.ARROWKEY_SCAN

            if self.arrowkey_mode == mode:
                self.arrowkey_mode = self.ARROWKEY_SCAN
                self.move_atoms_mask = None
                self.update_history()
            elif np.any(self.images.selected):
                self.arrowkey_mode = mode
                self.move_atoms_mask = self.images.selected.copy()
            else:
                from ase.gui.ui import showwarning
                showwarning(
                    _('No atoms selected!'),
                    _('You need to select one or more atoms to do this')
                )

            self.draw()

        @functools.cached_property
        def arrowkey_hint(self):
            hint = ui.tk.Frame(
                self.window.canvas, bg='#ffffff'
            )
            hint.label = ui.tk.Label(hint)
            hint.qm = ui.tk.Label(
                hint, text='(?)', padx=3,
                bg='#ffffff', activeforeground="#ffb617"
            )
            hint.qm.grid(row=0, column=0)
            hint.label.grid(row=0, column=1)
            hint.tooltip = ui.Tooltip()
            hint.qm.bind(
                '<Enter>', hint.tooltip.show
            )
            hint.qm.bind(
                '<Leave>', hint.tooltip.hide
            )
            hint.exists = False
            return hint

        def step(self, key):
            d = {'Home': -10000000,
                 'PageUp': -1,
                 'PageDown': 1,
                 'End': 10000000}[key]
            i = max(0, min(len(self.images) - 1, self.frame + d))
            self.set_frame(i)

        def copy_image(self, key=None):
            self.images._images.append(self.atoms.copy())
            self.images.history.append_image(self.images._images[-1])
            self.images.filenames.append(None)

            if self.movie_window is not None:
                self.movie_window.frame_number.scale.configure(to=len(self.images))
            self.step('End')

        def _do_zoom(self, x):
            """Utility method for zooming"""
            self.scale *= x
            self.draw()

        def zoom(self, key):
            """Zoom in/out on keypress or clicking menu item"""
            x = {'+': 1.2, '-': 1 / 1.2}[key]
            self._do_zoom(x)

        def scroll_event(self, event):
            """Zoom in/out when using mouse wheel"""
            SHIFT = event.modifier == 'shift'
            x = 1.0
            if event.button == 4 or event.delta > 0:
                x = 1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01
            elif event.button == 5 or event.delta < 0:
                x = 1.0 / (1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01)
            self._do_zoom(x)

        def settings(self):
            return Settings(self)

        def scroll(self, event):
            shift = 0x1
            ctrl = 0x4
            alt_l = 0x8  # Also Mac Command Key
            mac_option_key = 0x10

            self.remove_bothersome_key_states(event)

            use_small_step = bool(event.state & shift)
            rotate_into_plane = bool(event.state & (ctrl | alt_l | mac_option_key))

            dxdydz = {'up': (0, 1 - rotate_into_plane, rotate_into_plane),
                      'down': (0, -1 + rotate_into_plane, -rotate_into_plane),
                      'right': (1, 0, 0),
                      'left': (-1, 0, 0)}.get(event.key, None)

            # Get scroll direction using shift + right mouse button
            # event.type == '6' is mouse motion, see:
            # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-types.html
            if event.type == '6':
                cur_pos = np.array([event.x, -event.y])
                # Continue scroll if button has not been released
                if self.prev_pos is None or time() - self.last_scroll_time > .5:
                    self.prev_pos = cur_pos
                    self.last_scroll_time = time()
                else:
                    dxdy = cur_pos - self.prev_pos
                    dxdydz = np.append(dxdy, [0])
                    self.prev_pos = cur_pos
                    self.last_scroll_time = time()

            if dxdydz is None:
                return

            if self.arrowkey_mode == self.ARROWKEY_ROTATE:
                # A little tweak to make rotation more intuitive for users:
                mod_m = np.array([[0, -1, 0], [1, 0, 0], [0, 0, -1]])
                dxdydz = np.dot(mod_m, dxdydz)

            vec = 0.1 * np.dot(self.axes, dxdydz)
            if use_small_step:
                vec *= 0.1

            if self.arrowkey_mode == self.ARROWKEY_MOVE:
                self.atoms.positions[self.move_atoms_mask[:len(self.atoms)]] += vec
                self.set_frame()
            elif self.arrowkey_mode == self.ARROWKEY_ROTATE:
                # For now we use atoms.rotate having the simplest interface.
                # (Better to use something more minimalistic, obviously.)
                mask = self.move_atoms_mask[:len(self.atoms)]
                center = self.atoms.positions[mask].mean(axis=0)
                tmp_atoms = self.atoms[mask]
                tmp_atoms.positions -= center
                tmp_atoms.rotate(50 * np.linalg.norm(vec), vec)
                self.atoms.positions[mask] = tmp_atoms.positions + center
                self.set_frame()
            else:
                # The displacement vector is scaled
                # so that the cursor follows the structure
                # Scale by a third works for some reason
                scale = self.orig_scale / (3 * self.scale)
                self.center -= vec * scale

                # dx * 0.1 * self.axes[:, 0] - dy * 0.1 * self.axes[:, 1])

                self.draw()

        def remove_bothersome_key_states(self, event):
            """Modify event.state so that Num Lock doesn't get caught by
            bitmasks"""
            # We need to strip away Num Lock (and other bothersome key
            # events) that interfere with bitmasking or else the scrolling
            # will behave as if ctrl is always pressed down.
            nl_windows = 0x0008
            nl_linux = 0x0010

            if self.system == 'Linux':
                if event.state & nl_linux:
                    event.state -= nl_linux
            elif self.system == 'Windows':
                # Not completely sure what 0x40000 is but it seems to haunt Windows
                if event.state & 0x40000:
                    event.state -= 0x40000
                if event.state & nl_windows:
                    event.state -= nl_windows

        def delete_selected_atoms(self, widget=None, data=None):
            import ase.gui.ui as ui
            nselected = sum(self.images.selected)
            if nselected and ui.ask_question(_('Delete atoms'),
                                             _('Delete selected atoms?')):
                self.really_delete_selected_atoms()

        def really_delete_selected_atoms(self):
            mask = self.images.selected[:len(self.atoms)]
            del self.atoms[mask]

            # Will remove selection in other images, too
            self.images.selected[:] = False
            self.set_frame()
            self.draw()
            self.update_history()

        def constraints_window(self):
            from ase.gui.constraints import Constraints
            return Constraints(self)

        def set_selected_atoms(self, selected):
            newmask = np.zeros(len(self.images.selected), bool)
            newmask[selected] = True

            if np.array_equal(newmask, self.images.selected):
                return

            # (By creating newmask, we can avoid resetting the selection in
            # case the selected indices are invalid)
            self.images.selected[:] = newmask
            self.draw()

        def select_all(self, key=None):
            self.images.selected[:] = True
            self.draw()

        def invert_selection(self, key=None):
            self.images.selected[:] = ~self.images.selected
            self.draw()

        def select_constrained_atoms(self, key=None):
            self.images.selected[:] = ~self.images.get_dynamic(self.atoms)
            self.draw()

        def select_immobile_atoms(self, key=None):
            if len(self.images) > 1:
                R0 = self.images[0].positions
                for atoms in self.images[1:]:
                    R = atoms.positions
                    self.images.selected[:] = ~(np.abs(R - R0) > 1.0e-10).any(1)
            self.draw()

        def movie(self):
            from ase.gui.movie import Movie
            self.movie_window = Movie(self)

        def plot_graphs(self, key=None, expr=None, ignore_if_nan=False):
            from ase.gui.graphs import Graphs
            g = Graphs(self)
            if expr is not None:
                g.plot(expr=expr, ignore_if_nan=ignore_if_nan)

        def pipe(self, task, data):
            process = subprocess.Popen([sys.executable, '-m', 'ase.gui.pipe'],
                                       stdout=subprocess.PIPE,
                                       stdin=subprocess.PIPE)
            pickle.dump((task, data), process.stdin)
            process.stdin.close()
            # Either process writes a line, or it crashes and line becomes ''
            line = process.stdout.readline().decode('utf8').strip()

            if line != 'GUI:OK':
                if line == '':  # Subprocess probably crashed
                    line = _('Failure in subprocess')
                self.bad_plot(line)
            else:
                self.subprocesses.append(process)
            return process

        def bad_plot(self, err, msg=''):
            ui.error(_('Plotting failed'), '\n'.join([str(err), msg]).strip())

        def neb(self):
            from ase.utils.forcecurve import fit_images
            try:
                forcefit = fit_images(self.images)
            except Exception as err:
                self.bad_plot(err, _('Images must have energies and forces, '
                                     'and atoms must not be stationary.'))
            else:
                self.pipe('neb', forcefit)

        def bulk_modulus(self):
            try:
                v = [abs(np.linalg.det(atoms.cell)) for atoms in self.images]
                e = [self.images.get_energy(a) for a in self.images]
                from ase.eos import EquationOfState
                eos = EquationOfState(v, e)
                plotdata = eos.getplotdata()
            except Exception as err:
                self.bad_plot(err, _('Images must have energies '
                                     'and varying cell.'))
            else:
                self.pipe('eos', plotdata)

        def reciprocal(self):
            if self.atoms.cell.rank != 3:
                self.bad_plot(_('Requires 3D cell.'))
                return None

            cell = self.atoms.cell.uncomplete(self.atoms.pbc)
            bandpath = cell.bandpath(npoints=0)
            return self.pipe('reciprocal', bandpath)

        def open(self, button=None, filename=None):
            chooser = ui.ASEFileChooser(self.window.win)

            filename = filename or chooser.go()
            format = chooser.format
            if filename:
                try:
                    self.images.read([filename], slice(None), format)
                except Exception as err:
                    ui.show_io_error(filename, err)
                    return  # Hmm.  Is self.images in a consistent state?
                self.set_frame(len(self.images) - 1, focus=True)

        def modify_atoms(self, key=None):
            from ase.gui.modify import ModifyAtoms
            return ModifyAtoms(self)

        def add_atoms(self, key=None):
            from ase.gui.add import AddAtoms
            return AddAtoms(self)

        def cell_editor(self, key=None):
            from ase.gui.celleditor import CellEditor
            return CellEditor(self)

        def atoms_editor(self, key=None):
            from ase.gui.atomseditor import AtomsEditor
            return AtomsEditor(self)

        def quick_info_window(self, key=None):
            from ase.gui.quickinfo import info
            info_win = ui.Window(_('Quick Info'))
            info_win.add(info(self))

            # Update quickinfo window when we change frame
            def update(window):
                exists = window.exists
                if exists:
                    # Only update if we exist
                    window.things[0].text = info(self)
                return exists
            self.attach(update, info_win)
            return info_win

        def surface_window(self):
            return SetupSurfaceSlab(self)

        def nanoparticle_window(self):
            return SetupNanoparticle(self)

        def nanotube_window(self):
            return SetupNanotube(self)

        def new_atoms(self, atoms):
            "Set a new atoms object."
            self.images.history.isolate_history(self.frame)
            rpt = getattr(self.images, 'repeat', None)
            self.images.repeat_images(np.ones(3, int))
            self.images.initialize([atoms])
            self.frame = 0  # Prevent crashes
            self.images.repeat_images(rpt)
            self.set_frame(frame=0, focus=True)
            self.obs.new_atoms.notify()
            self.images.history.update_history(self.frame)

        def exit(self, event=None):
            for process in self.subprocesses:
                process.terminate()
            self.window.close()

        def new(self, key=None):
            subprocess.Popen([sys.executable, '-m', 'ase', 'gui'])

        def save(self, key=None):
            return save_dialog(self)

        def external_viewer(self, name):
            from ase.visualize import view
            return view(list(self.images), viewer=name)

        def selected_atoms(self):
            selection_mask = self.images.selected[:len(self.atoms)]
            return self.atoms[selection_mask]

        def wrap_atoms(self, key=None):
            """Wrap atoms around the unit cell."""
            for atoms in self.images:
                atoms.wrap()
            self.set_frame()
            self.update_history()

        @property
        def clipboard(self):
            from ase.gui.clipboard import AtomsClipboard
            return AtomsClipboard(self.window.win)

        def cut_atoms_to_clipboard(self, event=None):
            self.copy_atoms_to_clipboard(event)
            self.really_delete_selected_atoms()

        def copy_atoms_to_clipboard(self, event=None):
            atoms = self.selected_atoms()
            self.clipboard.set_atoms(atoms)

        def paste_atoms_from_clipboard(self, event=None):
            try:
                atoms = self.clipboard.get_atoms()
            except Exception as err:
                ui.error(
                    'Cannot paste atoms',
                    'Pasting currently works only with the ASE JSON format.\n\n'
                    f'Original error:\n\n{err}')
                return

            if self.atoms == Atoms():
                self.atoms.cell = atoms.cell
                self.atoms.pbc = atoms.pbc
            self.paste_atoms_onto_existing(atoms)

        def paste_atoms_onto_existing(self, atoms):
            selection = self.selected_atoms()
            if len(selection):
                paste_center = selection.positions.sum(axis=0) / len(selection)
                # atoms.center() is a no-op in directions without a cell vector.
                # But we actually want the thing centered nevertheless!
                # Therefore we have to set the cell.
                atoms = atoms.copy()
                atoms.cell = (1, 1, 1)  # arrrgh.
                atoms.center(about=paste_center)

            self.add_atoms_and_select(atoms)
            self.move_atoms_mask = self.images.selected.copy()
            self.arrowkey_mode = self.ARROWKEY_MOVE
            self.draw()

        def add_atoms_and_select(self, new_atoms):
            atoms = self.atoms
            atoms += new_atoms

            if len(atoms) > self.images.maxnatoms:
                self.images.initialize(list(self.images),
                                       self.images.filenames)

            selected = self.images.selected
            selected[:] = False
            # 'selected' array may be longer than current atoms
            selected[len(atoms) - len(new_atoms):len(atoms)] = True

            self.set_frame()
            self.draw()

        def get_menu_data(self):
            M = ui.MenuItem
            return [
                (_('_File'),
                 [M(_('_Open'), self.open, 'Ctrl+O'),
                  M(_('_New'), self.new, 'Ctrl+N'),
                  M(_('_Save'), self.save, 'Ctrl+S'),
                  M('---'),
                  M(_('_Quit'), self.exit, 'Ctrl+Q')]),

                (_('_Edit'),
                 [M(_('Undo'), self.undo_history, 'Ctrl+Z'),
                  M(_('Redo'), self.redo_history, 'Ctrl+Shift+Z'),
                  M('---'),
                  M(_('Select _all'), self.select_all),
                  M(_('_Invert selection'), self.invert_selection),
                  M(_('Select _constrained atoms'), self.select_constrained_atoms),
                  M(_('Select _immobile atoms'), self.select_immobile_atoms),
                  # M('---'),
                  M(_('_Cut'), self.cut_atoms_to_clipboard, 'Ctrl+X'),
                  M(_('_Copy'), self.copy_atoms_to_clipboard, 'Ctrl+C'),
                  M(_('_Paste'), self.paste_atoms_from_clipboard, 'Ctrl+V'),
                  M('---'),
                  M(_('Hide selected atoms'), self.hide_selected),
                  M(_('Show selected atoms'), self.show_selected),
                  M('---'),
                  M(_('_Modify'), self.modify_atoms, 'Ctrl+Y'),
                  M(_('_Add atoms'), self.add_atoms, 'Ctrl+A'),
                  M(_('_Delete selected atoms'), self.delete_selected_atoms,
                    'Backspace'),
                  M(_('Edit _cell …'), self.cell_editor, 'Ctrl+E'),
                  M(_('Edit _atoms …'), self.atoms_editor, 'A'),
                  M('---'),
                  M(_('_First image'), self.step, 'Home'),
                  M(_('_Previous image'), self.step, 'PageUp'),
                  M(_('_Next image'), self.step, 'PageDown'),
                  M(_('_Last image'), self.step, 'End'),
                  M(_('Append image copy'), self.copy_image)]),

                (_('_View'),
                 [M(_('Show _unit cell'), self.toggle_show_unit_cell, 'Ctrl+U',
                    value=self.config['show_unit_cell']),
                  M(_('Show _axes'), self.toggle_show_axes,
                    value=self.config['show_axes']),
                  M(_('Show _bonds'), self.toggle_show_bonds, 'Ctrl+B',
                    value=self.config['show_bonds']),
                  M(_('Show _velocities'), self.toggle_show_velocities, 'Ctrl+G',
                    value=False),
                  M(_('Show _forces'), self.toggle_show_forces, 'Ctrl+F',
                    value=False),
                  M(_('Show _magmoms'), self.toggle_show_magmoms,
                    value=False),
                  M(_('Show _Labels'), self.show_labels,
                    choices=[_('_None'),
                             _('Atom _Index'),
                             _('_Magnetic Moments'),  # XXX check if exist
                             _('_Element Symbol'),
                             _('_Initial Charges'),  # XXX check if exist
                             ]),
                  M('---'),
                  M(_('Quick Info ...'), self.quick_info_window, 'Ctrl+I'),
                  M(_('Repeat ...'), self.repeat_window, 'R'),
                  M(_('Rotate ...'), self.rotate_window),
                  M(_('Colors ...'), self.colors_window, 'C'),
                  # TRANSLATORS: verb
                  M(_('Focus'), self.focus, 'F'),
                  M(_('Zoom in'), self.zoom, '+'),
                  M(_('Zoom out'), self.zoom, '-'),
                  M(_('Change View'),
                    submenu=[
                        M(_('Reset View'), self.reset_view, '='),
                        M(_('xy-plane'), self.set_view, 'Z'),
                        M(_('yz-plane'), self.set_view, 'X'),
                        M(_('zx-plane'), self.set_view, 'Y'),
                        M(_('yx-plane'), self.set_view, 'Shift+Z'),
                        M(_('zy-plane'), self.set_view, 'Shift+X'),
                        M(_('xz-plane'), self.set_view, 'Shift+Y'),
                        M(_('a2,a3-plane'), self.set_view, 'I'),
                        M(_('a3,a1-plane'), self.set_view, 'J'),
                        M(_('a1,a2-plane'), self.set_view, 'K'),
                        M(_('a3,a2-plane'), self.set_view, 'Shift+I'),
                        M(_('a1,a3-plane'), self.set_view, 'Shift+J'),
                        M(_('a2,a1-plane'), self.set_view, 'Shift+K')]),
                  M(_('Settings ...'), self.settings),
                  M('---'),
                  M(_('VMD'), partial(self.external_viewer, 'vmd')),
                  M(_('RasMol'), partial(self.external_viewer, 'rasmol')),
                  M(_('xmakemol'), partial(self.external_viewer, 'xmakemol')),
                  M(_('avogadro'), partial(self.external_viewer, 'avogadro'))]),

                (_('_Tools'),
                 [M(_('Graphs ...'), self.plot_graphs),
                  M(_('Movie ...'), self.movie),
                  M(_('Constraints ...'), self.constraints_window),
                  M(_('Render scene ...'), self.render_window),
                  M(_('_Move selected atoms'), self.toggle_move_mode, 'Ctrl+M'),
                  M(_('_Rotate selected atoms'), self.toggle_rotate_mode,
                    'Ctrl+R'),
                  M(_('NE_B plot'), self.neb),
                  M(_('B_ulk Modulus'), self.bulk_modulus),
                  M(_('Reciprocal space ...'), self.reciprocal),
                  M(_('Wrap atoms'), self.wrap_atoms, 'Ctrl+W')]),

                # TRANSLATORS: Set up (i.e. build) surfaces, nanoparticles, ...
                (_('_Setup'),
                 [M(_('_Surface slab'), self.surface_window, disabled=False),
                  M(_('_Nanoparticle'),
                    self.nanoparticle_window),
                  M(_('Nano_tube'), self.nanotube_window)]),

                # (_('_Calculate'),
                # [M(_('Set _Calculator'), self.calculator_window, disabled=True),
                #  M(_('_Energy and Forces'), self.energy_window, disabled=True),
                #  M(_('Energy Minimization'), self.energy_minimize_window,
                #    disabled=True)]),

                (_('_Help'),
                 [M(_('_About'), partial(
                     ui.about, 'ASE-GUI',
                     version=__version__,
                     webpage='https://ase-lib.org/ase/gui/gui.html')),
                  M(_('Webpage ...'), webpage)])]

        def attach(self, function, *args, **kwargs):
            self.observers.append((function, args, kwargs))

        def call_observers(self):
            # Use function return value to determine if we keep observer
            self.observers = [(function, args, kwargs) for (function, args, kwargs)
                              in self.observers if function(*args, **kwargs)]

        def repeat_poll(self, callback, ms, ensure_update=True):
            """Invoke callback(gui=self) every ms milliseconds.

            This is useful for polling a resource for updates to load them
            into the GUI.  The GUI display will be hence be updated after
            each call; pass ensure_update=False to circumvent this.

            Polling stops if the callback function raises StopIteration.

            Example to run a movie manually, then quit::

                from ase.collections import g2
                from ase.gui.gui import GUI

                names = iter(g2.names)

                def main(gui):
                    try:
                        name = next(names)
                    except StopIteration:
                        gui.window.win.quit()
                    else:
                        atoms = g2[name]
                        gui.images.initialize([atoms])

                gui = GUI()
                gui.repeat_poll(main, 30)
                gui.run()"""

            def callbackwrapper():
                try:
                    callback(gui=self)
                except StopIteration:
                    pass
                finally:
                    # Reinsert self so we get called again:
                    self.window.win.after(ms, callbackwrapper)

                if ensure_update:
                    self.set_frame()
                    self.draw()

            self.window.win.after(ms, callbackwrapper)

    def webpage():
        import webbrowser
        webbrowser.open('https://ase-lib.org/ase/gui/gui.html')