Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# type: ignore
2import re
3import sys
4from collections import namedtuple
5from functools import partial
7import numpy as np
8import tkinter as tk
9import tkinter.ttk as ttk
10from tkinter.messagebox import askokcancel as ask_question
11from tkinter.messagebox import showerror, showwarning, showinfo
12from tkinter.filedialog import LoadFileDialog, SaveFileDialog
14from ase.gui.i18n import _
17__all__ = [
18 'error', 'ask_question', 'MainWindow', 'LoadFileDialog', 'SaveFileDialog',
19 'ASEGUIWindow', 'Button', 'CheckButton', 'ComboBox', 'Entry', 'Label',
20 'Window', 'MenuItem', 'RadioButton', 'RadioButtons', 'Rows', 'Scale',
21 'showinfo', 'showwarning', 'SpinBox', 'Text']
24if sys.platform == 'darwin':
25 mouse_buttons = {2: 3, 3: 2}
26else:
27 mouse_buttons = {}
30def error(title, message=None):
31 if message is None:
32 message = title
33 title = _('Error')
34 return showerror(title, message)
37def about(name, version, webpage):
38 text = [name,
39 '',
40 _('Version') + ': ' + version,
41 _('Web-page') + ': ' + webpage]
42 win = Window(_('About'))
43 win.add(Text('\n'.join(text)))
46def helpbutton(text):
47 return Button(_('Help'), helpwindow, text)
50def helpwindow(text):
51 win = Window(_('Help'))
52 win.add(Text(text))
55class BaseWindow:
56 def __init__(self, title, close=None):
57 self.title = title
58 if close:
59 self.win.protocol('WM_DELETE_WINDOW', close)
60 else:
61 self.win.protocol('WM_DELETE_WINDOW', self.close)
63 self.things = []
64 self.exists = True
66 def close(self):
67 self.win.destroy()
68 self.exists = False
70 def title(self, txt):
71 self.win.title(txt)
73 title = property(None, title)
75 def add(self, stuff, anchor='w'): # 'center'):
76 if isinstance(stuff, str):
77 stuff = Label(stuff)
78 elif isinstance(stuff, list):
79 stuff = Row(stuff)
80 stuff.pack(self.win, anchor=anchor)
81 self.things.append(stuff)
84class Window(BaseWindow):
85 def __init__(self, title, close=None):
86 self.win = tk.Toplevel()
87 BaseWindow.__init__(self, title, close)
90class Widget:
91 def pack(self, parent, side='top', anchor='center'):
92 widget = self.create(parent)
93 widget.pack(side=side, anchor=anchor)
94 if not isinstance(self, (Rows, RadioButtons)):
95 pass
97 def grid(self, parent):
98 widget = self.create(parent)
99 widget.grid()
101 def create(self, parent):
102 self.widget = self.creator(parent)
103 return self.widget
105 @property
106 def active(self):
107 return self.widget['state'] == 'normal'
109 @active.setter
110 def active(self, value):
111 self.widget['state'] = ['disabled', 'normal'][bool(value)]
114class Row(Widget):
115 def __init__(self, things):
116 self.things = things
118 def create(self, parent):
119 self.widget = tk.Frame(parent)
120 for thing in self.things:
121 if isinstance(thing, str):
122 thing = Label(thing)
123 thing.pack(self.widget, 'left')
124 return self.widget
126 def __getitem__(self, i):
127 return self.things[i]
130class Label(Widget):
131 def __init__(self, text='', color=None):
132 self.creator = partial(tk.Label, text=text, fg=color)
134 @property
135 def text(self):
136 return self.widget['text']
138 @text.setter
139 def text(self, new):
140 self.widget.config(text=new)
143class Text(Widget):
144 def __init__(self, text):
145 self.creator = partial(tk.Text, height=text.count('\n') + 1)
146 s = re.split('<(.*?)>', text)
147 self.text = [(s[0], ())]
148 i = 1
149 tags = []
150 while i < len(s):
151 tag = s[i]
152 if tag[0] != '/':
153 tags.append(tag)
154 else:
155 tags.pop()
156 self.text.append((s[i + 1], tuple(tags)))
157 i += 2
159 def create(self, parent):
160 widget = Widget.create(self, parent)
161 widget.tag_configure('sub', offset=-6)
162 widget.tag_configure('sup', offset=6)
163 widget.tag_configure('c', foreground='blue')
164 for text, tags in self.text:
165 widget.insert('insert', text, tags)
166 widget.configure(state='disabled', background=parent['bg'])
167 widget.bind("<1>", lambda event: widget.focus_set())
168 return widget
171class Button(Widget):
172 def __init__(self, text, callback, *args, **kwargs):
173 self.callback = partial(callback, *args, **kwargs)
174 self.creator = partial(tk.Button,
175 text=text,
176 command=self.callback)
179class CheckButton(Widget):
180 def __init__(self, text, value=False, callback=None):
181 self.text = text
182 self.var = tk.BooleanVar(value=value)
183 self.callback = callback
185 def create(self, parent):
186 self.check = tk.Checkbutton(parent, text=self.text,
187 var=self.var, command=self.callback)
188 return self.check
190 @property
191 def value(self):
192 return self.var.get()
195class SpinBox(Widget):
196 def __init__(self, value, start, end, step, callback=None,
197 rounding=None, width=6):
198 self.callback = callback
199 self.rounding = rounding
200 self.creator = partial(tk.Spinbox,
201 from_=start,
202 to=end,
203 increment=step,
204 command=callback,
205 width=width)
206 self.initial = str(value)
208 def create(self, parent):
209 self.widget = self.creator(parent)
210 self.widget.bind('<Return>', lambda event: self.callback())
211 self.value = self.initial
212 return self.widget
214 @property
215 def value(self):
216 x = self.widget.get().replace(',', '.')
217 if '.' in x:
218 return float(x)
219 if x == 'None':
220 return None
221 return int(x)
223 @value.setter
224 def value(self, x):
225 self.widget.delete(0, 'end')
226 if '.' in str(x) and self.rounding is not None:
227 try:
228 x = round(float(x), self.rounding)
229 except (ValueError, TypeError):
230 pass
231 self.widget.insert(0, x)
234# Entry and ComboBox use same mechanism (since ttk ComboBox
235# is a subclass of tk Entry).
236def _set_entry_value(widget, value):
237 widget.delete(0, 'end')
238 widget.insert(0, value)
241class Entry(Widget):
242 def __init__(self, value='', width=20, callback=None):
243 self.creator = partial(tk.Entry,
244 width=width)
245 if callback is not None:
246 self.callback = lambda event: callback()
247 else:
248 self.callback = None
249 self.initial = value
251 def create(self, parent):
252 self.entry = self.creator(parent)
253 self.value = self.initial
254 if self.callback:
255 self.entry.bind('<Return>', self.callback)
256 return self.entry
258 @property
259 def value(self):
260 return self.entry.get()
262 @value.setter
263 def value(self, x):
264 _set_entry_value(self.entry, x)
267class Scale(Widget):
268 def __init__(self, value, start, end, callback):
269 def command(val):
270 callback(int(val))
272 self.creator = partial(tk.Scale,
273 from_=start,
274 to=end,
275 orient='horizontal',
276 command=command)
277 self.initial = value
279 def create(self, parent):
280 self.scale = self.creator(parent)
281 self.value = self.initial
282 return self.scale
284 @property
285 def value(self):
286 return self.scale.get()
288 @value.setter
289 def value(self, x):
290 self.scale.set(x)
293class RadioButtons(Widget):
294 def __init__(self, labels, values=None, callback=None, vertical=False):
295 self.var = tk.IntVar()
297 if callback:
298 def callback2():
299 callback(self.value)
300 else:
301 callback2 = None
303 self.values = values or list(range(len(labels)))
304 self.buttons = [RadioButton(label, i, self.var, callback2)
305 for i, label in enumerate(labels)]
306 self.vertical = vertical
308 def create(self, parent):
309 self.widget = frame = tk.Frame(parent)
310 side = 'top' if self.vertical else 'left'
311 for button in self.buttons:
312 button.create(frame).pack(side=side)
313 return frame
315 @property
316 def value(self):
317 return self.values[self.var.get()]
319 @value.setter
320 def value(self, value):
321 self.var.set(self.values.index(value))
323 def __getitem__(self, value):
324 return self.buttons[self.values.index(value)]
327class RadioButton(Widget):
328 def __init__(self, label, i, var, callback):
329 self.creator = partial(tk.Radiobutton,
330 text=label,
331 var=var,
332 value=i,
333 command=callback)
336if ttk is not None:
337 class ComboBox(Widget):
338 def __init__(self, labels, values=None, callback=None):
339 self.values = values or list(range(len(labels)))
340 self.callback = callback
341 self.creator = partial(ttk.Combobox,
342 values=labels)
344 def create(self, parent):
345 widget = Widget.create(self, parent)
346 widget.current(0)
347 if self.callback:
348 def callback(event):
349 self.callback(self.value)
350 widget.bind('<<ComboboxSelected>>', callback)
352 return widget
354 @property
355 def value(self):
356 return self.values[self.widget.current()]
358 @value.setter
359 def value(self, val):
360 _set_entry_value(self.widget, val)
361else:
362 # Use Entry object when there is no ttk:
363 def ComboBox(labels, values, callback):
364 return Entry(values[0], callback=callback)
367class Rows(Widget):
368 def __init__(self, rows=None):
369 self.rows_to_be_added = rows or []
370 self.creator = tk.Frame
371 self.rows = []
373 def create(self, parent):
374 widget = Widget.create(self, parent)
375 for row in self.rows_to_be_added:
376 self.add(row)
377 self.rows_to_be_added = []
378 return widget
380 def add(self, row):
381 if isinstance(row, str):
382 row = Label(row)
383 elif isinstance(row, list):
384 row = Row(row)
385 row.grid(self.widget)
386 self.rows.append(row)
388 def clear(self):
389 while self.rows:
390 del self[0]
392 def __getitem__(self, i):
393 return self.rows[i]
395 def __delitem__(self, i):
396 widget = self.rows.pop(i).widget
397 widget.grid_remove()
398 widget.destroy()
400 def __len__(self):
401 return len(self.rows)
404class MenuItem:
405 def __init__(self, label, callback=None, key=None,
406 value=None, choices=None, submenu=None, disabled=False):
407 self.underline = label.find('_')
408 self.label = label.replace('_', '')
410 if key:
411 if key[:4] == 'Ctrl':
412 self.keyname = '<Control-{0}>'.format(key[-1].lower())
413 else:
414 self.keyname = {
415 'Home': '<Home>',
416 'End': '<End>',
417 'Page-Up': '<Prior>',
418 'Page-Down': '<Next>',
419 'Backspace': '<BackSpace>'}.get(key, key.lower())
421 if key:
422 def callback2(event=None):
423 callback(key)
425 callback2.__name__ = callback.__name__
426 self.callback = callback2
427 else:
428 self.callback = callback
430 self.key = key
431 self.value = value
432 self.choices = choices
433 self.submenu = submenu
434 self.disabled = disabled
436 def addto(self, menu, window, stuff=None):
437 callback = self.callback
438 if self.label == '---':
439 menu.add_separator()
440 elif self.value is not None:
441 var = tk.BooleanVar(value=self.value)
442 stuff[self.callback.__name__.replace('_', '-')] = var
444 menu.add_checkbutton(label=self.label,
445 underline=self.underline,
446 command=self.callback,
447 accelerator=self.key,
448 var=var)
450 def callback(key):
451 var.set(not var.get())
452 self.callback()
454 elif self.choices:
455 submenu = tk.Menu(menu)
456 menu.add_cascade(label=self.label, menu=submenu)
457 var = tk.IntVar()
458 var.set(0)
459 stuff[self.callback.__name__.replace('_', '-')] = var
460 for i, choice in enumerate(self.choices):
461 submenu.add_radiobutton(label=choice.replace('_', ''),
462 underline=choice.find('_'),
463 command=self.callback,
464 value=i,
465 var=var)
466 elif self.submenu:
467 submenu = tk.Menu(menu)
468 menu.add_cascade(label=self.label,
469 menu=submenu)
470 for thing in self.submenu:
471 thing.addto(submenu, window)
472 else:
473 state = 'normal'
474 if self.disabled:
475 state = 'disabled'
476 menu.add_command(label=self.label,
477 underline=self.underline,
478 command=self.callback,
479 accelerator=self.key,
480 state=state)
481 if self.key:
482 window.bind(self.keyname, callback)
485class MainWindow(BaseWindow):
486 def __init__(self, title, close=None, menu=[]):
487 self.win = tk.Tk()
488 BaseWindow.__init__(self, title, close)
490 # self.win.tk.call('tk', 'scaling', 3.0)
491 # self.win.tk.call('tk', 'scaling', '-displayof', '.', 7)
493 self.menu = {}
495 if menu:
496 self.create_menu(menu)
498 def create_menu(self, menu_description):
499 menu = tk.Menu(self.win)
500 self.win.config(menu=menu)
502 for label, things in menu_description:
503 submenu = tk.Menu(menu)
504 menu.add_cascade(label=label.replace('_', ''),
505 underline=label.find('_'),
506 menu=submenu)
507 for thing in things:
508 thing.addto(submenu, self.win, self.menu)
510 def resize_event(self):
511 # self.scale *= sqrt(1.0 * self.width * self.height / (w * h))
512 self.draw()
513 self.configured = True
515 def run(self):
516 # Workaround for nasty issue with tkinter on Mac:
517 # https://gitlab.com/ase/ase/issues/412
518 #
519 # It is apparently a compatibility issue between Python and Tkinter.
520 # Some day we should remove this hack.
521 while True:
522 try:
523 tk.mainloop()
524 break
525 except UnicodeDecodeError:
526 pass
528 def __getitem__(self, name):
529 return self.menu[name].get()
531 def __setitem__(self, name, value):
532 return self.menu[name].set(value)
535def bind(callback, modifier=None):
536 def handle(event):
537 event.button = mouse_buttons.get(event.num, event.num)
538 event.key = event.keysym.lower()
539 event.modifier = modifier
540 callback(event)
541 return handle
544class ASEFileChooser(LoadFileDialog):
545 def __init__(self, win, formatcallback=lambda event: None):
546 from ase.io.formats import all_formats, get_ioformat
547 LoadFileDialog.__init__(self, win, _('Open ...'))
548 labels = [_('Automatic')]
549 values = ['']
551 def key(item):
552 return item[1][0]
554 for format, (description, code) in sorted(all_formats.items(),
555 key=key):
556 io = get_ioformat(format)
557 if io.can_read and description != '?':
558 labels.append(_(description))
559 values.append(format)
561 self.format = None
563 def callback(value):
564 self.format = value
566 Label(_('Choose parser:')).pack(self.top)
567 formats = ComboBox(labels, values, callback)
568 formats.pack(self.top)
571def show_io_error(filename, err):
572 showerror(_('Read error'),
573 _('Could not read {}: {}'.format(filename, err)))
576class ASEGUIWindow(MainWindow):
577 def __init__(self, close, menu, config,
578 scroll, scroll_event,
579 press, move, release, resize):
580 MainWindow.__init__(self, 'ASE-GUI', close, menu)
582 self.size = np.array([450, 450])
584 self.fg = config['gui_foreground_color']
585 self.bg = config['gui_background_color']
587 self.canvas = tk.Canvas(self.win,
588 width=self.size[0],
589 height=self.size[1],
590 bg=self.bg,
591 highlightthickness=0)
592 self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
594 self.status = tk.Label(self.win, text='', anchor=tk.W)
595 self.status.pack(side=tk.BOTTOM, fill=tk.X)
597 right = mouse_buttons.get(3, 3)
598 self.canvas.bind('<ButtonPress>', bind(press))
599 self.canvas.bind('<B1-Motion>', bind(move))
600 self.canvas.bind('<B{right}-Motion>'.format(right=right), bind(move))
601 self.canvas.bind('<ButtonRelease>', bind(release))
602 self.canvas.bind('<Control-ButtonRelease>', bind(release, 'ctrl'))
603 self.canvas.bind('<Shift-ButtonRelease>', bind(release, 'shift'))
604 self.canvas.bind('<Configure>', resize)
605 if not config['swap_mouse']:
606 self.canvas.bind('<Shift-B{right}-Motion>'.format(right=right),
607 bind(scroll))
608 else:
609 self.canvas.bind('<Shift-B1-Motion>',
610 bind(scroll))
612 self.win.bind('<MouseWheel>', bind(scroll_event))
613 self.win.bind('<Key>', bind(scroll))
614 self.win.bind('<Shift-Key>', bind(scroll, 'shift'))
615 self.win.bind('<Control-Key>', bind(scroll, 'ctrl'))
617 def update_status_line(self, text):
618 self.status.config(text=text)
620 def run(self):
621 MainWindow.run(self)
623 def click(self, name):
624 self.callbacks[name]()
626 def clear(self):
627 self.canvas.delete(tk.ALL)
629 def update(self):
630 self.canvas.update_idletasks()
632 def circle(self, color, selected, *bbox):
633 if selected:
634 outline = '#004500'
635 width = 3
636 else:
637 outline = 'black'
638 width = 1
639 self.canvas.create_oval(*tuple(int(x) for x in bbox), fill=color,
640 outline=outline, width=width)
642 def arc(self, color, selected, start, extent, *bbox):
643 if selected:
644 outline = '#004500'
645 width = 3
646 else:
647 outline = 'black'
648 width = 1
649 self.canvas.create_arc(*tuple(int(x) for x in bbox),
650 start=start,
651 extent=extent,
652 fill=color,
653 outline=outline,
654 width=width)
656 def line(self, bbox, width=1):
657 self.canvas.create_line(*tuple(int(x) for x in bbox), width=width)
659 def text(self, x, y, txt, anchor=tk.CENTER, color='black'):
660 anchor = {'SE': tk.SE}.get(anchor, anchor)
661 self.canvas.create_text((x, y), text=txt, anchor=anchor, fill=color)
663 def after(self, time, callback):
664 id = self.win.after(int(time * 1000), callback)
665 # Quick'n'dirty object with a cancel() method:
666 return namedtuple('Timer', 'cancel')(lambda: self.win.after_cancel(id))