Coverage for ase / gui / gui.py: 66.58%
404 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1# fmt: off
3import functools
4import pickle
5import platform
6import subprocess
7import sys
8from functools import partial
9from time import time
11import numpy as np
13import ase.gui.ui as ui
14from ase import Atoms, __version__
15from ase.gui.defaults import read_defaults
16from ase.gui.i18n import _
17from ase.gui.images import Images
18from ase.gui.nanoparticle import SetupNanoparticle
19from ase.gui.nanotube import SetupNanotube
20from ase.gui.observer import Observers
21from ase.gui.save import save_dialog
22from ase.gui.settings import Settings
23from ase.gui.status import Status
24from ase.gui.surfaceslab import SetupSurfaceSlab
25from ase.gui.view import View
28class GUIObservers:
29 def __init__(self):
30 self.new_atoms = Observers()
31 self.set_atoms = Observers()
32 self.change_atoms = Observers()
35class GUI(View):
36 ARROWKEY_SCAN = 0
37 ARROWKEY_MOVE = 1
38 ARROWKEY_ROTATE = 2
40 def __init__(self, images=None,
41 rotations='',
42 show_bonds=False, expr=None):
44 if not isinstance(images, Images):
45 images = Images(images)
46 self.images = images
47 self.images.history.initialize_history()
49 self.system = platform.system()
51 # Ordinary observers seem unused now, delete?
52 self.observers = []
53 self.obs = GUIObservers()
55 self.config = read_defaults()
56 if show_bonds:
57 self.config['show_bonds'] = True
59 menu = self.get_menu_data()
61 self.window = ui.ASEGUIWindow(close=self.exit, menu=menu,
62 config=self.config, scroll=self.scroll,
63 scroll_event=self.scroll_event,
64 press=self.press, move=self.move,
65 release=self.release,
66 resize=self.resize)
68 super().__init__(rotations)
69 self.status = Status(self)
71 self.subprocesses = [] # list of external processes
72 self.movie_window = None
73 self.simulation = {} # Used by modules on Calculate menu.
74 self.module_state = {} # Used by modules to store their state.
76 self.arrowkey_mode = self.ARROWKEY_SCAN
77 self.move_atoms_mask = None
79 self.set_frame(len(self.images) - 1, focus=True)
81 # Used to move the structure with the mouse
82 self.prev_pos = None
83 self.last_scroll_time = time()
84 self.orig_scale = self.scale
86 if len(self.images) > 1:
87 self.movie()
89 if expr is None:
90 expr = self.config['gui_graphs_string']
92 if expr is not None and expr != '' and len(self.images) > 1:
93 self.plot_graphs(expr=expr, ignore_if_nan=True)
95 def redo_history(self, key=None):
96 self.images.history.redo_history(self.frame)
97 self.set_frame()
98 self.draw()
100 def undo_history(self, key=None):
101 # Let's end any rotate/move mode that may still be active so the
102 # current positions are saved to the history:
103 if self.moving:
104 self.toggle_arrowkey_mode(self.arrowkey_mode)
105 self.images.history.undo_history(self.frame)
106 self.set_frame()
107 self.draw()
109 def update_history(self, mask=None):
110 if mask is not None:
111 for i, update in enumerate(mask):
112 if update:
113 self.images.history.update_history(i)
114 else:
115 self.images.history.update_history(self.frame)
117 def clear_history(self):
118 self.images.history.initialize_history()
120 @property
121 def moving(self):
122 return self.arrowkey_mode != self.ARROWKEY_SCAN
124 def run(self):
125 self.window.run()
127 def toggle_move_mode(self, key=None):
128 self.toggle_arrowkey_mode(self.ARROWKEY_MOVE)
130 def toggle_rotate_mode(self, key=None):
131 self.toggle_arrowkey_mode(self.ARROWKEY_ROTATE)
133 def toggle_arrowkey_mode(self, mode):
134 # If not currently in given mode, activate it.
135 # Else, deactivate it (go back to SCAN mode)
136 assert mode != self.ARROWKEY_SCAN
138 if self.arrowkey_mode == mode:
139 self.arrowkey_mode = self.ARROWKEY_SCAN
140 self.move_atoms_mask = None
141 self.update_history()
142 elif np.any(self.images.selected):
143 self.arrowkey_mode = mode
144 self.move_atoms_mask = self.images.selected.copy()
145 else:
146 from ase.gui.ui import showwarning
147 showwarning(
148 _('No atoms selected!'),
149 _('You need to select one or more atoms to do this')
150 )
152 self.draw()
154 @functools.cached_property
155 def arrowkey_hint(self):
156 hint = ui.tk.Frame(
157 self.window.canvas, bg='#ffffff'
158 )
159 hint.label = ui.tk.Label(hint)
160 hint.qm = ui.tk.Label(
161 hint, text='(?)', padx=3,
162 bg='#ffffff', activeforeground="#ffb617"
163 )
164 hint.qm.grid(row=0, column=0)
165 hint.label.grid(row=0, column=1)
166 hint.tooltip = ui.Tooltip()
167 hint.qm.bind(
168 '<Enter>', hint.tooltip.show
169 )
170 hint.qm.bind(
171 '<Leave>', hint.tooltip.hide
172 )
173 hint.exists = False
174 return hint
176 def step(self, key):
177 d = {'Home': -10000000,
178 'PageUp': -1,
179 'PageDown': 1,
180 'End': 10000000}[key]
181 i = max(0, min(len(self.images) - 1, self.frame + d))
182 self.set_frame(i)
184 def copy_image(self, key=None):
185 self.images._images.append(self.atoms.copy())
186 self.images.history.append_image(self.images._images[-1])
187 self.images.filenames.append(None)
189 if self.movie_window is not None:
190 self.movie_window.frame_number.scale.configure(to=len(self.images))
191 self.step('End')
193 def _do_zoom(self, x):
194 """Utility method for zooming"""
195 self.scale *= x
196 self.draw()
198 def zoom(self, key):
199 """Zoom in/out on keypress or clicking menu item"""
200 x = {'+': 1.2, '-': 1 / 1.2}[key]
201 self._do_zoom(x)
203 def scroll_event(self, event):
204 """Zoom in/out when using mouse wheel"""
205 SHIFT = event.modifier == 'shift'
206 x = 1.0
207 if event.button == 4 or event.delta > 0:
208 x = 1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01
209 elif event.button == 5 or event.delta < 0:
210 x = 1.0 / (1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01)
211 self._do_zoom(x)
213 def settings(self):
214 return Settings(self)
216 def scroll(self, event):
217 shift = 0x1
218 ctrl = 0x4
219 alt_l = 0x8 # Also Mac Command Key
220 mac_option_key = 0x10
222 self.remove_bothersome_key_states(event)
224 use_small_step = bool(event.state & shift)
225 rotate_into_plane = bool(event.state & (ctrl | alt_l | mac_option_key))
227 dxdydz = {'up': (0, 1 - rotate_into_plane, rotate_into_plane),
228 'down': (0, -1 + rotate_into_plane, -rotate_into_plane),
229 'right': (1, 0, 0),
230 'left': (-1, 0, 0)}.get(event.key, None)
232 # Get scroll direction using shift + right mouse button
233 # event.type == '6' is mouse motion, see:
234 # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-types.html
235 if event.type == '6':
236 cur_pos = np.array([event.x, -event.y])
237 # Continue scroll if button has not been released
238 if self.prev_pos is None or time() - self.last_scroll_time > .5:
239 self.prev_pos = cur_pos
240 self.last_scroll_time = time()
241 else:
242 dxdy = cur_pos - self.prev_pos
243 dxdydz = np.append(dxdy, [0])
244 self.prev_pos = cur_pos
245 self.last_scroll_time = time()
247 if dxdydz is None:
248 return
250 if self.arrowkey_mode == self.ARROWKEY_ROTATE:
251 # A little tweak to make rotation more intuitive for users:
252 mod_m = np.array([[0, -1, 0], [1, 0, 0], [0, 0, -1]])
253 dxdydz = np.dot(mod_m, dxdydz)
255 vec = 0.1 * np.dot(self.axes, dxdydz)
256 if use_small_step:
257 vec *= 0.1
259 if self.arrowkey_mode == self.ARROWKEY_MOVE:
260 self.atoms.positions[self.move_atoms_mask[:len(self.atoms)]] += vec
261 self.set_frame()
262 elif self.arrowkey_mode == self.ARROWKEY_ROTATE:
263 # For now we use atoms.rotate having the simplest interface.
264 # (Better to use something more minimalistic, obviously.)
265 mask = self.move_atoms_mask[:len(self.atoms)]
266 center = self.atoms.positions[mask].mean(axis=0)
267 tmp_atoms = self.atoms[mask]
268 tmp_atoms.positions -= center
269 tmp_atoms.rotate(50 * np.linalg.norm(vec), vec)
270 self.atoms.positions[mask] = tmp_atoms.positions + center
271 self.set_frame()
272 else:
273 # The displacement vector is scaled
274 # so that the cursor follows the structure
275 # Scale by a third works for some reason
276 scale = self.orig_scale / (3 * self.scale)
277 self.center -= vec * scale
279 # dx * 0.1 * self.axes[:, 0] - dy * 0.1 * self.axes[:, 1])
281 self.draw()
283 def remove_bothersome_key_states(self, event):
284 """Modify event.state so that Num Lock doesn't get caught by
285 bitmasks"""
286 # We need to strip away Num Lock (and other bothersome key
287 # events) that interfere with bitmasking or else the scrolling
288 # will behave as if ctrl is always pressed down.
289 nl_windows = 0x0008
290 nl_linux = 0x0010
292 if self.system == 'Linux':
293 if event.state & nl_linux:
294 event.state -= nl_linux
295 elif self.system == 'Windows':
296 # Not completely sure what 0x40000 is but it seems to haunt Windows
297 if event.state & 0x40000:
298 event.state -= 0x40000
299 if event.state & nl_windows:
300 event.state -= nl_windows
302 def delete_selected_atoms(self, widget=None, data=None):
303 import ase.gui.ui as ui
304 nselected = sum(self.images.selected)
305 if nselected and ui.ask_question(_('Delete atoms'),
306 _('Delete selected atoms?')):
307 self.really_delete_selected_atoms()
309 def really_delete_selected_atoms(self):
310 mask = self.images.selected[:len(self.atoms)]
311 del self.atoms[mask]
313 # Will remove selection in other images, too
314 self.images.selected[:] = False
315 self.set_frame()
316 self.draw()
317 self.update_history()
319 def constraints_window(self):
320 from ase.gui.constraints import Constraints
321 return Constraints(self)
323 def set_selected_atoms(self, selected):
324 newmask = np.zeros(len(self.images.selected), bool)
325 newmask[selected] = True
327 if np.array_equal(newmask, self.images.selected):
328 return
330 # (By creating newmask, we can avoid resetting the selection in
331 # case the selected indices are invalid)
332 self.images.selected[:] = newmask
333 self.draw()
335 def select_all(self, key=None):
336 self.images.selected[:] = True
337 self.draw()
339 def invert_selection(self, key=None):
340 self.images.selected[:] = ~self.images.selected
341 self.draw()
343 def select_constrained_atoms(self, key=None):
344 self.images.selected[:] = ~self.images.get_dynamic(self.atoms)
345 self.draw()
347 def select_immobile_atoms(self, key=None):
348 if len(self.images) > 1:
349 R0 = self.images[0].positions
350 for atoms in self.images[1:]:
351 R = atoms.positions
352 self.images.selected[:] = ~(np.abs(R - R0) > 1.0e-10).any(1)
353 self.draw()
355 def movie(self):
356 from ase.gui.movie import Movie
357 self.movie_window = Movie(self)
359 def plot_graphs(self, key=None, expr=None, ignore_if_nan=False):
360 from ase.gui.graphs import Graphs
361 g = Graphs(self)
362 if expr is not None:
363 g.plot(expr=expr, ignore_if_nan=ignore_if_nan)
365 def pipe(self, task, data):
366 process = subprocess.Popen([sys.executable, '-m', 'ase.gui.pipe'],
367 stdout=subprocess.PIPE,
368 stdin=subprocess.PIPE)
369 pickle.dump((task, data), process.stdin)
370 process.stdin.close()
371 # Either process writes a line, or it crashes and line becomes ''
372 line = process.stdout.readline().decode('utf8').strip()
374 if line != 'GUI:OK':
375 if line == '': # Subprocess probably crashed
376 line = _('Failure in subprocess')
377 self.bad_plot(line)
378 else:
379 self.subprocesses.append(process)
380 return process
382 def bad_plot(self, err, msg=''):
383 ui.error(_('Plotting failed'), '\n'.join([str(err), msg]).strip())
385 def neb(self):
386 from ase.utils.forcecurve import fit_images
387 try:
388 forcefit = fit_images(self.images)
389 except Exception as err:
390 self.bad_plot(err, _('Images must have energies and forces, '
391 'and atoms must not be stationary.'))
392 else:
393 self.pipe('neb', forcefit)
395 def bulk_modulus(self):
396 try:
397 v = [abs(np.linalg.det(atoms.cell)) for atoms in self.images]
398 e = [self.images.get_energy(a) for a in self.images]
399 from ase.eos import EquationOfState
400 eos = EquationOfState(v, e)
401 plotdata = eos.getplotdata()
402 except Exception as err:
403 self.bad_plot(err, _('Images must have energies '
404 'and varying cell.'))
405 else:
406 self.pipe('eos', plotdata)
408 def reciprocal(self):
409 if self.atoms.cell.rank != 3:
410 self.bad_plot(_('Requires 3D cell.'))
411 return None
413 cell = self.atoms.cell.uncomplete(self.atoms.pbc)
414 bandpath = cell.bandpath(npoints=0)
415 return self.pipe('reciprocal', bandpath)
417 def open(self, button=None, filename=None):
418 chooser = ui.ASEFileChooser(self.window.win)
420 filename = filename or chooser.go()
421 format = chooser.format
422 if filename:
423 try:
424 self.images.read([filename], slice(None), format)
425 except Exception as err:
426 ui.show_io_error(filename, err)
427 return # Hmm. Is self.images in a consistent state?
428 self.set_frame(len(self.images) - 1, focus=True)
430 def modify_atoms(self, key=None):
431 from ase.gui.modify import ModifyAtoms
432 return ModifyAtoms(self)
434 def add_atoms(self, key=None):
435 from ase.gui.add import AddAtoms
436 return AddAtoms(self)
438 def cell_editor(self, key=None):
439 from ase.gui.celleditor import CellEditor
440 return CellEditor(self)
442 def atoms_editor(self, key=None):
443 from ase.gui.atomseditor import AtomsEditor
444 return AtomsEditor(self)
446 def quick_info_window(self, key=None):
447 from ase.gui.quickinfo import info
448 info_win = ui.Window(_('Quick Info'))
449 info_win.add(info(self))
451 # Update quickinfo window when we change frame
452 def update(window):
453 exists = window.exists
454 if exists:
455 # Only update if we exist
456 window.things[0].text = info(self)
457 return exists
458 self.attach(update, info_win)
459 return info_win
461 def surface_window(self):
462 return SetupSurfaceSlab(self)
464 def nanoparticle_window(self):
465 return SetupNanoparticle(self)
467 def nanotube_window(self):
468 return SetupNanotube(self)
470 def new_atoms(self, atoms):
471 "Set a new atoms object."
472 self.images.history.isolate_history(self.frame)
473 rpt = getattr(self.images, 'repeat', None)
474 self.images.repeat_images(np.ones(3, int))
475 self.images.initialize([atoms])
476 self.frame = 0 # Prevent crashes
477 self.images.repeat_images(rpt)
478 self.set_frame(frame=0, focus=True)
479 self.obs.new_atoms.notify()
480 self.images.history.update_history(self.frame)
482 def exit(self, event=None):
483 for process in self.subprocesses:
484 process.terminate()
485 self.window.close()
487 def new(self, key=None):
488 subprocess.Popen([sys.executable, '-m', 'ase', 'gui'])
490 def save(self, key=None):
491 return save_dialog(self)
493 def external_viewer(self, name):
494 from ase.visualize import view
495 return view(list(self.images), viewer=name)
497 def selected_atoms(self):
498 selection_mask = self.images.selected[:len(self.atoms)]
499 return self.atoms[selection_mask]
501 def wrap_atoms(self, key=None):
502 """Wrap atoms around the unit cell."""
503 for atoms in self.images:
504 atoms.wrap()
505 self.set_frame()
506 self.update_history()
508 @property
509 def clipboard(self):
510 from ase.gui.clipboard import AtomsClipboard
511 return AtomsClipboard(self.window.win)
513 def cut_atoms_to_clipboard(self, event=None):
514 self.copy_atoms_to_clipboard(event)
515 self.really_delete_selected_atoms()
517 def copy_atoms_to_clipboard(self, event=None):
518 atoms = self.selected_atoms()
519 self.clipboard.set_atoms(atoms)
521 def paste_atoms_from_clipboard(self, event=None):
522 try:
523 atoms = self.clipboard.get_atoms()
524 except Exception as err:
525 ui.error(
526 'Cannot paste atoms',
527 'Pasting currently works only with the ASE JSON format.\n\n'
528 f'Original error:\n\n{err}')
529 return
531 if self.atoms == Atoms():
532 self.atoms.cell = atoms.cell
533 self.atoms.pbc = atoms.pbc
534 self.paste_atoms_onto_existing(atoms)
536 def paste_atoms_onto_existing(self, atoms):
537 selection = self.selected_atoms()
538 if len(selection):
539 paste_center = selection.positions.sum(axis=0) / len(selection)
540 # atoms.center() is a no-op in directions without a cell vector.
541 # But we actually want the thing centered nevertheless!
542 # Therefore we have to set the cell.
543 atoms = atoms.copy()
544 atoms.cell = (1, 1, 1) # arrrgh.
545 atoms.center(about=paste_center)
547 self.add_atoms_and_select(atoms)
548 self.move_atoms_mask = self.images.selected.copy()
549 self.arrowkey_mode = self.ARROWKEY_MOVE
550 self.draw()
552 def add_atoms_and_select(self, new_atoms):
553 atoms = self.atoms
554 atoms += new_atoms
556 if len(atoms) > self.images.maxnatoms:
557 self.images.initialize(list(self.images),
558 self.images.filenames)
560 selected = self.images.selected
561 selected[:] = False
562 # 'selected' array may be longer than current atoms
563 selected[len(atoms) - len(new_atoms):len(atoms)] = True
565 self.set_frame()
566 self.draw()
568 def get_menu_data(self):
569 M = ui.MenuItem
570 return [
571 (_('_File'),
572 [M(_('_Open'), self.open, 'Ctrl+O'),
573 M(_('_New'), self.new, 'Ctrl+N'),
574 M(_('_Save'), self.save, 'Ctrl+S'),
575 M('---'),
576 M(_('_Quit'), self.exit, 'Ctrl+Q')]),
578 (_('_Edit'),
579 [M(_('Undo'), self.undo_history, 'Ctrl+Z'),
580 M(_('Redo'), self.redo_history, 'Ctrl+Shift+Z'),
581 M('---'),
582 M(_('Select _all'), self.select_all),
583 M(_('_Invert selection'), self.invert_selection),
584 M(_('Select _constrained atoms'), self.select_constrained_atoms),
585 M(_('Select _immobile atoms'), self.select_immobile_atoms),
586 # M('---'),
587 M(_('_Cut'), self.cut_atoms_to_clipboard, 'Ctrl+X'),
588 M(_('_Copy'), self.copy_atoms_to_clipboard, 'Ctrl+C'),
589 M(_('_Paste'), self.paste_atoms_from_clipboard, 'Ctrl+V'),
590 M('---'),
591 M(_('Hide selected atoms'), self.hide_selected),
592 M(_('Show selected atoms'), self.show_selected),
593 M('---'),
594 M(_('_Modify'), self.modify_atoms, 'Ctrl+Y'),
595 M(_('_Add atoms'), self.add_atoms, 'Ctrl+A'),
596 M(_('_Delete selected atoms'), self.delete_selected_atoms,
597 'Backspace'),
598 M(_('Edit _cell …'), self.cell_editor, 'Ctrl+E'),
599 M(_('Edit _atoms …'), self.atoms_editor, 'A'),
600 M('---'),
601 M(_('_First image'), self.step, 'Home'),
602 M(_('_Previous image'), self.step, 'PageUp'),
603 M(_('_Next image'), self.step, 'PageDown'),
604 M(_('_Last image'), self.step, 'End'),
605 M(_('Append image copy'), self.copy_image)]),
607 (_('_View'),
608 [M(_('Show _unit cell'), self.toggle_show_unit_cell, 'Ctrl+U',
609 value=self.config['show_unit_cell']),
610 M(_('Show _axes'), self.toggle_show_axes,
611 value=self.config['show_axes']),
612 M(_('Show _bonds'), self.toggle_show_bonds, 'Ctrl+B',
613 value=self.config['show_bonds']),
614 M(_('Show _velocities'), self.toggle_show_velocities, 'Ctrl+G',
615 value=False),
616 M(_('Show _forces'), self.toggle_show_forces, 'Ctrl+F',
617 value=False),
618 M(_('Show _magmoms'), self.toggle_show_magmoms,
619 value=False),
620 M(_('Show _Labels'), self.show_labels,
621 choices=[_('_None'),
622 _('Atom _Index'),
623 _('_Magnetic Moments'), # XXX check if exist
624 _('_Element Symbol'),
625 _('_Initial Charges'), # XXX check if exist
626 ]),
627 M('---'),
628 M(_('Quick Info ...'), self.quick_info_window, 'Ctrl+I'),
629 M(_('Repeat ...'), self.repeat_window, 'R'),
630 M(_('Rotate ...'), self.rotate_window),
631 M(_('Colors ...'), self.colors_window, 'C'),
632 # TRANSLATORS: verb
633 M(_('Focus'), self.focus, 'F'),
634 M(_('Zoom in'), self.zoom, '+'),
635 M(_('Zoom out'), self.zoom, '-'),
636 M(_('Change View'),
637 submenu=[
638 M(_('Reset View'), self.reset_view, '='),
639 M(_('xy-plane'), self.set_view, 'Z'),
640 M(_('yz-plane'), self.set_view, 'X'),
641 M(_('zx-plane'), self.set_view, 'Y'),
642 M(_('yx-plane'), self.set_view, 'Shift+Z'),
643 M(_('zy-plane'), self.set_view, 'Shift+X'),
644 M(_('xz-plane'), self.set_view, 'Shift+Y'),
645 M(_('a2,a3-plane'), self.set_view, 'I'),
646 M(_('a3,a1-plane'), self.set_view, 'J'),
647 M(_('a1,a2-plane'), self.set_view, 'K'),
648 M(_('a3,a2-plane'), self.set_view, 'Shift+I'),
649 M(_('a1,a3-plane'), self.set_view, 'Shift+J'),
650 M(_('a2,a1-plane'), self.set_view, 'Shift+K')]),
651 M(_('Settings ...'), self.settings),
652 M('---'),
653 M(_('VMD'), partial(self.external_viewer, 'vmd')),
654 M(_('RasMol'), partial(self.external_viewer, 'rasmol')),
655 M(_('xmakemol'), partial(self.external_viewer, 'xmakemol')),
656 M(_('avogadro'), partial(self.external_viewer, 'avogadro'))]),
658 (_('_Tools'),
659 [M(_('Graphs ...'), self.plot_graphs),
660 M(_('Movie ...'), self.movie),
661 M(_('Constraints ...'), self.constraints_window),
662 M(_('Render scene ...'), self.render_window),
663 M(_('_Move selected atoms'), self.toggle_move_mode, 'Ctrl+M'),
664 M(_('_Rotate selected atoms'), self.toggle_rotate_mode,
665 'Ctrl+R'),
666 M(_('NE_B plot'), self.neb),
667 M(_('B_ulk Modulus'), self.bulk_modulus),
668 M(_('Reciprocal space ...'), self.reciprocal),
669 M(_('Wrap atoms'), self.wrap_atoms, 'Ctrl+W')]),
671 # TRANSLATORS: Set up (i.e. build) surfaces, nanoparticles, ...
672 (_('_Setup'),
673 [M(_('_Surface slab'), self.surface_window, disabled=False),
674 M(_('_Nanoparticle'),
675 self.nanoparticle_window),
676 M(_('Nano_tube'), self.nanotube_window)]),
678 # (_('_Calculate'),
679 # [M(_('Set _Calculator'), self.calculator_window, disabled=True),
680 # M(_('_Energy and Forces'), self.energy_window, disabled=True),
681 # M(_('Energy Minimization'), self.energy_minimize_window,
682 # disabled=True)]),
684 (_('_Help'),
685 [M(_('_About'), partial(
686 ui.about, 'ASE-GUI',
687 version=__version__,
688 webpage='https://ase-lib.org/ase/gui/gui.html')),
689 M(_('Webpage ...'), webpage)])]
691 def attach(self, function, *args, **kwargs):
692 self.observers.append((function, args, kwargs))
694 def call_observers(self):
695 # Use function return value to determine if we keep observer
696 self.observers = [(function, args, kwargs) for (function, args, kwargs)
697 in self.observers if function(*args, **kwargs)]
699 def repeat_poll(self, callback, ms, ensure_update=True):
700 """Invoke callback(gui=self) every ms milliseconds.
702 This is useful for polling a resource for updates to load them
703 into the GUI. The GUI display will be hence be updated after
704 each call; pass ensure_update=False to circumvent this.
706 Polling stops if the callback function raises StopIteration.
708 Example to run a movie manually, then quit::
710 from ase.collections import g2
711 from ase.gui.gui import GUI
713 names = iter(g2.names)
715 def main(gui):
716 try:
717 name = next(names)
718 except StopIteration:
719 gui.window.win.quit()
720 else:
721 atoms = g2[name]
722 gui.images.initialize([atoms])
724 gui = GUI()
725 gui.repeat_poll(main, 30)
726 gui.run()"""
728 def callbackwrapper():
729 try:
730 callback(gui=self)
731 except StopIteration:
732 pass
733 finally:
734 # Reinsert self so we get called again:
735 self.window.win.after(ms, callbackwrapper)
737 if ensure_update:
738 self.set_frame()
739 self.draw()
741 self.window.win.after(ms, callbackwrapper)
744def webpage():
745 import webbrowser
746 webbrowser.open('https://ase-lib.org/ase/gui/gui.html')