Coverage for ase / gui / gui.py: 66.58%
404 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 15:52 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 15:52 +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=self.window.bg
158 )
159 hint.label = ui.tk.Label(hint)
160 hint.qm = ui.tk.Label(
161 hint, text='(?)', padx=3,
162 bg=self.window.bg,
163 fg=self.window.fg
164 )
165 hint.qm.grid(row=0, column=0)
166 hint.label.grid(row=0, column=1)
167 hint.tooltip = ui.Tooltip()
168 hint.qm.bind(
169 '<Enter>', hint.tooltip.show
170 )
171 hint.qm.bind(
172 '<Leave>', hint.tooltip.hide
173 )
174 hint.exists = False
175 return hint
177 def step(self, key):
178 d = {'Home': -10000000,
179 'PageUp': -1,
180 'PageDown': 1,
181 'End': 10000000}[key]
182 i = max(0, min(len(self.images) - 1, self.frame + d))
183 self.set_frame(i)
185 def copy_image(self, key=None):
186 self.images._images.append(self.atoms.copy())
187 self.images.history.append_image(self.images._images[-1])
188 self.images.filenames.append(None)
190 if self.movie_window is not None:
191 self.movie_window.frame_number.scale.configure(to=len(self.images))
192 self.step('End')
194 def _do_zoom(self, x):
195 """Utility method for zooming"""
196 self.scale *= x
197 self.draw()
199 def zoom(self, key):
200 """Zoom in/out on keypress or clicking menu item"""
201 x = {'+': 1.2, '-': 1 / 1.2}[key]
202 self._do_zoom(x)
204 def scroll_event(self, event):
205 """Zoom in/out when using mouse wheel"""
206 SHIFT = event.modifier == 'shift'
207 x = 1.0
208 if event.button == 4 or event.delta > 0:
209 x = 1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01
210 elif event.button == 5 or event.delta < 0:
211 x = 1.0 / (1.0 + (1 - SHIFT) * 0.2 + SHIFT * 0.01)
212 self._do_zoom(x)
214 def settings(self):
215 return Settings(self)
217 def scroll(self, event):
218 shift = 0x1
219 ctrl = 0x4
220 alt_l = 0x8 # Also Mac Command Key
221 mac_option_key = 0x10
223 self.remove_bothersome_key_states(event)
225 use_small_step = bool(event.state & shift)
226 rotate_into_plane = bool(event.state & (ctrl | alt_l | mac_option_key))
228 dxdydz = {'up': (0, 1 - rotate_into_plane, rotate_into_plane),
229 'down': (0, -1 + rotate_into_plane, -rotate_into_plane),
230 'right': (1, 0, 0),
231 'left': (-1, 0, 0)}.get(event.key, None)
233 # Get scroll direction using shift + right mouse button
234 # event.type == '6' is mouse motion, see:
235 # http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/event-types.html
236 if event.type == '6':
237 cur_pos = np.array([event.x, -event.y])
238 # Continue scroll if button has not been released
239 if self.prev_pos is None or time() - self.last_scroll_time > .5:
240 self.prev_pos = cur_pos
241 self.last_scroll_time = time()
242 else:
243 dxdy = cur_pos - self.prev_pos
244 dxdydz = np.append(dxdy, [0])
245 self.prev_pos = cur_pos
246 self.last_scroll_time = time()
248 if dxdydz is None:
249 return
251 if self.arrowkey_mode == self.ARROWKEY_ROTATE:
252 # A little tweak to make rotation more intuitive for users:
253 mod_m = np.array([[0, -1, 0], [1, 0, 0], [0, 0, -1]])
254 dxdydz = np.dot(mod_m, dxdydz)
256 vec = 0.1 * np.dot(self.axes, dxdydz)
257 if use_small_step:
258 vec *= 0.1
260 if self.arrowkey_mode == self.ARROWKEY_MOVE:
261 self.atoms.positions[self.move_atoms_mask[:len(self.atoms)]] += vec
262 self.set_frame()
263 elif self.arrowkey_mode == self.ARROWKEY_ROTATE:
264 # For now we use atoms.rotate having the simplest interface.
265 # (Better to use something more minimalistic, obviously.)
266 mask = self.move_atoms_mask[:len(self.atoms)]
267 center = self.atoms.positions[mask].mean(axis=0)
268 tmp_atoms = self.atoms[mask]
269 tmp_atoms.positions -= center
270 tmp_atoms.rotate(50 * np.linalg.norm(vec), vec)
271 self.atoms.positions[mask] = tmp_atoms.positions + center
272 self.set_frame()
273 else:
274 # The displacement vector is scaled
275 # so that the cursor follows the structure
276 # Scale by a third works for some reason
277 scale = self.orig_scale / (3 * self.scale)
278 self.center -= vec * scale
280 # dx * 0.1 * self.axes[:, 0] - dy * 0.1 * self.axes[:, 1])
282 self.draw()
284 def remove_bothersome_key_states(self, event):
285 """Modify event.state so that Num Lock doesn't get caught by
286 bitmasks"""
287 # We need to strip away Num Lock (and other bothersome key
288 # events) that interfere with bitmasking or else the scrolling
289 # will behave as if ctrl is always pressed down.
290 nl_windows = 0x0008
291 nl_linux = 0x0010
293 if self.system == 'Linux':
294 if event.state & nl_linux:
295 event.state -= nl_linux
296 elif self.system == 'Windows':
297 # Not completely sure what 0x40000 is but it seems to haunt Windows
298 if event.state & 0x40000:
299 event.state -= 0x40000
300 if event.state & nl_windows:
301 event.state -= nl_windows
303 def delete_selected_atoms(self, widget=None, data=None):
304 import ase.gui.ui as ui
305 nselected = sum(self.images.selected)
306 if nselected and ui.ask_question(_('Delete atoms'),
307 _('Delete selected atoms?')):
308 self.really_delete_selected_atoms()
310 def really_delete_selected_atoms(self):
311 mask = self.images.selected[:len(self.atoms)]
312 del self.atoms[mask]
314 # Will remove selection in other images, too
315 self.images.selected[:] = False
316 self.set_frame()
317 self.draw()
318 self.update_history()
320 def constraints_window(self):
321 from ase.gui.constraints import Constraints
322 return Constraints(self)
324 def set_selected_atoms(self, selected):
325 newmask = np.zeros(len(self.images.selected), bool)
326 newmask[selected] = True
328 if np.array_equal(newmask, self.images.selected):
329 return
331 # (By creating newmask, we can avoid resetting the selection in
332 # case the selected indices are invalid)
333 self.images.selected[:] = newmask
334 self.draw()
336 def select_all(self, key=None):
337 self.images.selected[:] = True
338 self.draw()
340 def invert_selection(self, key=None):
341 self.images.selected[:] = ~self.images.selected
342 self.draw()
344 def select_constrained_atoms(self, key=None):
345 self.images.selected[:] = ~self.images.get_dynamic(self.atoms)
346 self.draw()
348 def select_immobile_atoms(self, key=None):
349 if len(self.images) > 1:
350 R0 = self.images[0].positions
351 for atoms in self.images[1:]:
352 R = atoms.positions
353 self.images.selected[:] = ~(np.abs(R - R0) > 1.0e-10).any(1)
354 self.draw()
356 def movie(self):
357 from ase.gui.movie import Movie
358 self.movie_window = Movie(self)
360 def plot_graphs(self, key=None, expr=None, ignore_if_nan=False):
361 from ase.gui.graphs import Graphs
362 g = Graphs(self)
363 if expr is not None:
364 g.plot(expr=expr, ignore_if_nan=ignore_if_nan)
366 def pipe(self, task, data):
367 process = subprocess.Popen([sys.executable, '-m', 'ase.gui.pipe'],
368 stdout=subprocess.PIPE,
369 stdin=subprocess.PIPE)
370 pickle.dump((task, data), process.stdin)
371 process.stdin.close()
372 # Either process writes a line, or it crashes and line becomes ''
373 line = process.stdout.readline().decode('utf8').strip()
375 if line != 'GUI:OK':
376 if line == '': # Subprocess probably crashed
377 line = _('Failure in subprocess')
378 self.bad_plot(line)
379 else:
380 self.subprocesses.append(process)
381 return process
383 def bad_plot(self, err, msg=''):
384 ui.error(_('Plotting failed'), '\n'.join([str(err), msg]).strip())
386 def neb(self):
387 from ase.utils.forcecurve import fit_images
388 try:
389 forcefit = fit_images(self.images)
390 except Exception as err:
391 self.bad_plot(err, _('Images must have energies and forces, '
392 'and atoms must not be stationary.'))
393 else:
394 self.pipe('neb', forcefit)
396 def bulk_modulus(self):
397 try:
398 v = [abs(np.linalg.det(atoms.cell)) for atoms in self.images]
399 e = [self.images.get_energy(a) for a in self.images]
400 from ase.eos import EquationOfState
401 eos = EquationOfState(v, e)
402 plotdata = eos.getplotdata()
403 except Exception as err:
404 self.bad_plot(err, _('Images must have energies '
405 'and varying cell.'))
406 else:
407 self.pipe('eos', plotdata)
409 def reciprocal(self):
410 if self.atoms.cell.rank != 3:
411 self.bad_plot(_('Requires 3D cell.'))
412 return None
414 cell = self.atoms.cell.uncomplete(self.atoms.pbc)
415 bandpath = cell.bandpath(npoints=0)
416 return self.pipe('reciprocal', bandpath)
418 def open(self, button=None, filename=None):
419 chooser = ui.ASEFileChooser(self.window.win)
421 filename = filename or chooser.go()
422 format = chooser.format
423 if filename:
424 try:
425 self.images.read([filename], slice(None), format)
426 except Exception as err:
427 ui.show_io_error(filename, err)
428 return # Hmm. Is self.images in a consistent state?
429 self.set_frame(len(self.images) - 1, focus=True)
431 def modify_atoms(self, key=None):
432 from ase.gui.modify import ModifyAtoms
433 return ModifyAtoms(self)
435 def add_atoms(self, key=None):
436 from ase.gui.add import AddAtoms
437 return AddAtoms(self)
439 def cell_editor(self, key=None):
440 from ase.gui.celleditor import CellEditor
441 return CellEditor(self)
443 def atoms_editor(self, key=None):
444 from ase.gui.atomseditor import AtomsEditor
445 return AtomsEditor(self)
447 def quick_info_window(self, key=None):
448 from ase.gui.quickinfo import info
449 info_win = ui.Window(_('Quick Info'))
450 info_win.add(info(self))
452 # Update quickinfo window when we change frame
453 def update(window):
454 exists = window.exists
455 if exists:
456 # Only update if we exist
457 window.things[0].text = info(self)
458 return exists
459 self.attach(update, info_win)
460 return info_win
462 def surface_window(self):
463 return SetupSurfaceSlab(self)
465 def nanoparticle_window(self):
466 return SetupNanoparticle(self)
468 def nanotube_window(self):
469 return SetupNanotube(self)
471 def new_atoms(self, atoms):
472 "Set a new atoms object."
473 self.images.history.isolate_history(self.frame)
474 rpt = getattr(self.images, 'repeat', None)
475 self.images.repeat_images(np.ones(3, int))
476 self.images.initialize([atoms])
477 self.frame = 0 # Prevent crashes
478 self.images.repeat_images(rpt)
479 self.set_frame(frame=0, focus=True)
480 self.obs.new_atoms.notify()
481 self.images.history.update_history(self.frame)
483 def exit(self, event=None):
484 for process in self.subprocesses:
485 process.terminate()
486 self.window.close()
488 def new(self, key=None):
489 subprocess.Popen([sys.executable, '-m', 'ase', 'gui'])
491 def save(self, key=None):
492 return save_dialog(self)
494 def external_viewer(self, name):
495 from ase.visualize import view
496 return view(list(self.images), viewer=name)
498 def selected_atoms(self):
499 selection_mask = self.images.selected[:len(self.atoms)]
500 return self.atoms[selection_mask]
502 def wrap_atoms(self, key=None):
503 """Wrap atoms around the unit cell."""
504 for atoms in self.images:
505 atoms.wrap()
506 self.set_frame()
507 self.update_history()
509 @property
510 def clipboard(self):
511 from ase.gui.clipboard import AtomsClipboard
512 return AtomsClipboard(self.window.win)
514 def cut_atoms_to_clipboard(self, event=None):
515 self.copy_atoms_to_clipboard(event)
516 self.really_delete_selected_atoms()
518 def copy_atoms_to_clipboard(self, event=None):
519 atoms = self.selected_atoms()
520 self.clipboard.set_atoms(atoms)
522 def paste_atoms_from_clipboard(self, event=None):
523 try:
524 atoms = self.clipboard.get_atoms()
525 except Exception as err:
526 ui.error(
527 'Cannot paste atoms',
528 'Pasting currently works only with the ASE JSON format.\n\n'
529 f'Original error:\n\n{err}')
530 return
532 if self.atoms == Atoms():
533 self.atoms.cell = atoms.cell
534 self.atoms.pbc = atoms.pbc
535 self.paste_atoms_onto_existing(atoms)
537 def paste_atoms_onto_existing(self, atoms):
538 selection = self.selected_atoms()
539 if len(selection):
540 paste_center = selection.positions.sum(axis=0) / len(selection)
541 # atoms.center() is a no-op in directions without a cell vector.
542 # But we actually want the thing centered nevertheless!
543 # Therefore we have to set the cell.
544 atoms = atoms.copy()
545 atoms.cell = (1, 1, 1) # arrrgh.
546 atoms.center(about=paste_center)
548 self.add_atoms_and_select(atoms)
549 self.move_atoms_mask = self.images.selected.copy()
550 self.arrowkey_mode = self.ARROWKEY_MOVE
551 self.draw()
553 def add_atoms_and_select(self, new_atoms):
554 atoms = self.atoms
555 atoms += new_atoms
557 if len(atoms) > self.images.maxnatoms:
558 self.images.initialize(list(self.images),
559 self.images.filenames)
561 selected = self.images.selected
562 selected[:] = False
563 # 'selected' array may be longer than current atoms
564 selected[len(atoms) - len(new_atoms):len(atoms)] = True
566 self.set_frame()
567 self.draw()
569 def get_menu_data(self):
570 M = ui.MenuItem
571 return [
572 (_('_File'),
573 [M(_('_Open'), self.open, 'Ctrl+O'),
574 M(_('_New'), self.new, 'Ctrl+N'),
575 M(_('_Save'), self.save, 'Ctrl+S'),
576 M('---'),
577 M(_('_Quit'), self.exit, 'Ctrl+Q')]),
579 (_('_Edit'),
580 [M(_('Undo'), self.undo_history, 'Ctrl+Z'),
581 M(_('Redo'), self.redo_history, 'Ctrl+Shift+Z'),
582 M('---'),
583 M(_('Select _all'), self.select_all),
584 M(_('_Invert selection'), self.invert_selection),
585 M(_('Select _constrained atoms'), self.select_constrained_atoms),
586 M(_('Select _immobile atoms'), self.select_immobile_atoms),
587 # M('---'),
588 M(_('_Cut'), self.cut_atoms_to_clipboard, 'Ctrl+X'),
589 M(_('_Copy'), self.copy_atoms_to_clipboard, 'Ctrl+C'),
590 M(_('_Paste'), self.paste_atoms_from_clipboard, 'Ctrl+V'),
591 M('---'),
592 M(_('Hide selected atoms'), self.hide_selected),
593 M(_('Show selected atoms'), self.show_selected),
594 M('---'),
595 M(_('_Modify'), self.modify_atoms, 'Ctrl+Y'),
596 M(_('_Add atoms'), self.add_atoms, 'Ctrl+A'),
597 M(_('_Delete selected atoms'), self.delete_selected_atoms,
598 'Backspace'),
599 M(_('Edit _cell …'), self.cell_editor, 'Ctrl+E'),
600 M(_('Edit _atoms …'), self.atoms_editor, 'A'),
601 M('---'),
602 M(_('_First image'), self.step, 'Home'),
603 M(_('_Previous image'), self.step, 'PageUp'),
604 M(_('_Next image'), self.step, 'PageDown'),
605 M(_('_Last image'), self.step, 'End'),
606 M(_('Append image copy'), self.copy_image)]),
608 (_('_View'),
609 [M(_('Show _unit cell'), self.toggle_show_unit_cell, 'Ctrl+U',
610 value=self.config['show_unit_cell']),
611 M(_('Show _axes'), self.toggle_show_axes,
612 value=self.config['show_axes']),
613 M(_('Show _bonds'), self.toggle_show_bonds, 'Ctrl+B',
614 value=self.config['show_bonds']),
615 M(_('Show _velocities'), self.toggle_show_velocities, 'Ctrl+G',
616 value=False),
617 M(_('Show _forces'), self.toggle_show_forces, 'Ctrl+F',
618 value=False),
619 M(_('Show _magmoms'), self.toggle_show_magmoms,
620 value=False),
621 M(_('Show _labels'), self.show_labels,
622 choices=[_('_None'),
623 _('Atom _index'),
624 _('_Magnetic moments'), # XXX check if exist
625 _('_Element symbol'),
626 _('_Initial charges'), # XXX check if exist
627 ]),
628 M('---'),
629 M(_('Quick info ...'), self.quick_info_window, 'Ctrl+I'),
630 M(_('Repeat ...'), self.repeat_window, 'R'),
631 M(_('Rotate ...'), self.rotate_window),
632 M(_('Colors ...'), self.colors_window, 'C'),
633 # TRANSLATORS: verb
634 M(_('Focus'), self.focus, 'F'),
635 M(_('Zoom in'), self.zoom, '+'),
636 M(_('Zoom out'), self.zoom, '-'),
637 M(_('Change view'),
638 submenu=[
639 M(_('Reset view'), self.reset_view, '='),
640 M(_('xy-plane'), self.set_view, 'Z'),
641 M(_('yz-plane'), self.set_view, 'X'),
642 M(_('zx-plane'), self.set_view, 'Y'),
643 M(_('yx-plane'), self.set_view, 'Shift+Z'),
644 M(_('zy-plane'), self.set_view, 'Shift+X'),
645 M(_('xz-plane'), self.set_view, 'Shift+Y'),
646 M(_('a2,a3-plane'), self.set_view, 'I'),
647 M(_('a3,a1-plane'), self.set_view, 'J'),
648 M(_('a1,a2-plane'), self.set_view, 'K'),
649 M(_('a3,a2-plane'), self.set_view, 'Shift+I'),
650 M(_('a1,a3-plane'), self.set_view, 'Shift+J'),
651 M(_('a2,a1-plane'), self.set_view, 'Shift+K')]),
652 M(_('Settings ...'), self.settings),
653 M('---'),
654 M(_('VMD'), partial(self.external_viewer, 'vmd')),
655 M(_('RasMol'), partial(self.external_viewer, 'rasmol')),
656 M(_('xmakemol'), partial(self.external_viewer, 'xmakemol')),
657 M(_('avogadro'), partial(self.external_viewer, 'avogadro'))]),
659 (_('_Tools'),
660 [M(_('Graphs ...'), self.plot_graphs),
661 M(_('Movie ...'), self.movie),
662 M(_('Constraints ...'), self.constraints_window),
663 M(_('Render scene ...'), self.render_window),
664 M(_('_Move selected atoms'), self.toggle_move_mode, 'Ctrl+M'),
665 M(_('_Rotate selected atoms'), self.toggle_rotate_mode,
666 'Ctrl+R'),
667 M(_('NE_B plot'), self.neb),
668 M(_('B_ulk modulus'), self.bulk_modulus),
669 M(_('Reciprocal space ...'), self.reciprocal),
670 M(_('Wrap atoms'), self.wrap_atoms, 'Ctrl+W')]),
672 # TRANSLATORS: Set up (i.e. build) surfaces, nanoparticles, ...
673 (_('_Setup'),
674 [M(_('_Surface slab'), self.surface_window, disabled=False),
675 M(_('_Nanoparticle'),
676 self.nanoparticle_window),
677 M(_('Nano_tube'), self.nanotube_window)]),
679 # (_('_Calculate'),
680 # [M(_('Set _Calculator'), self.calculator_window, disabled=True),
681 # M(_('_Energy and Forces'), self.energy_window, disabled=True),
682 # M(_('Energy Minimization'), self.energy_minimize_window,
683 # disabled=True)]),
685 (_('_Help'),
686 [M(_('_About'), partial(
687 ui.about, 'ASE-GUI',
688 version=__version__,
689 webpage='https://ase-lib.org/ase/gui/gui.html')),
690 M(_('Webpage ...'), webpage)])]
692 def attach(self, function, *args, **kwargs):
693 self.observers.append((function, args, kwargs))
695 def call_observers(self):
696 # Use function return value to determine if we keep observer
697 self.observers = [(function, args, kwargs) for (function, args, kwargs)
698 in self.observers if function(*args, **kwargs)]
700 def repeat_poll(self, callback, ms, ensure_update=True):
701 """Invoke callback(gui=self) every ms milliseconds.
703 This is useful for polling a resource for updates to load them
704 into the GUI. The GUI display will be hence be updated after
705 each call; pass ensure_update=False to circumvent this.
707 Polling stops if the callback function raises StopIteration.
709 Example to run a movie manually, then quit::
711 from ase.collections import g2
712 from ase.gui.gui import GUI
714 names = iter(g2.names)
716 def main(gui):
717 try:
718 name = next(names)
719 except StopIteration:
720 gui.window.win.quit()
721 else:
722 atoms = g2[name]
723 gui.images.initialize([atoms])
725 gui = GUI()
726 gui.repeat_poll(main, 30)
727 gui.run()"""
729 def callbackwrapper():
730 try:
731 callback(gui=self)
732 except StopIteration:
733 pass
734 finally:
735 # Reinsert self so we get called again:
736 self.window.win.after(ms, callbackwrapper)
738 if ensure_update:
739 self.set_frame()
740 self.draw()
742 self.window.win.after(ms, callbackwrapper)
745def webpage():
746 import webbrowser
747 webbrowser.open('https://ase-lib.org/ase/gui/gui.html')