Hide keyboard shortcuts

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 

6 

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 

13 

14from ase.gui.i18n import _ 

15 

16 

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', 'set_windowtype'] 

22 

23 

24if sys.platform == 'darwin': 

25 mouse_buttons = {2: 3, 3: 2} 

26else: 

27 mouse_buttons = {} 

28 

29 

30def error(title, message=None): 

31 if message is None: 

32 message = title 

33 title = _('Error') 

34 return showerror(title, message) 

35 

36 

37def about(name, version, webpage): 

38 text = [name, 

39 '', 

40 _('Version') + ': ' + version, 

41 _('Web-page') + ': ' + webpage] 

42 win = Window(_('About')) 

43 set_windowtype(win.win, 'dialog') 

44 win.add(Text('\n'.join(text))) 

45 

46 

47def helpbutton(text): 

48 return Button(_('Help'), helpwindow, text) 

49 

50 

51def helpwindow(text): 

52 win = Window(_('Help')) 

53 set_windowtype(win.win, 'dialog') 

54 win.add(Text(text)) 

55 

56 

57def set_windowtype(win, wmtype): 

58 # only on X11 

59 # WM_TYPE, for possible settings see 

60 # https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45623487848608 

61 # you want dialog, normal or utility most likely 

62 if win._windowingsystem == "x11": 

63 win.wm_attributes('-type', wmtype) 

64 

65 

66class BaseWindow: 

67 def __init__(self, title, close=None, wmtype='normal'): 

68 self.title = title 

69 if close: 

70 self.win.protocol('WM_DELETE_WINDOW', close) 

71 else: 

72 self.win.protocol('WM_DELETE_WINDOW', self.close) 

73 

74 self.things = [] 

75 self.exists = True 

76 set_windowtype(self.win, wmtype) 

77 

78 def close(self): 

79 self.win.destroy() 

80 self.exists = False 

81 

82 def title(self, txt): 

83 self.win.title(txt) 

84 

85 title = property(None, title) 

86 

87 def add(self, stuff, anchor='w'): # 'center'): 

88 if isinstance(stuff, str): 

89 stuff = Label(stuff) 

90 elif isinstance(stuff, list): 

91 stuff = Row(stuff) 

92 stuff.pack(self.win, anchor=anchor) 

93 self.things.append(stuff) 

94 

95 

96class Window(BaseWindow): 

97 def __init__(self, title, close=None, wmtype='normal'): 

98 self.win = tk.Toplevel() 

99 BaseWindow.__init__(self, title, close, wmtype) 

100 

101 

102class Widget: 

103 def pack(self, parent, side='top', anchor='center'): 

104 widget = self.create(parent) 

105 widget.pack(side=side, anchor=anchor) 

106 if not isinstance(self, (Rows, RadioButtons)): 

107 pass 

108 

109 def grid(self, parent): 

110 widget = self.create(parent) 

111 widget.grid() 

112 

113 def create(self, parent): 

114 self.widget = self.creator(parent) 

115 return self.widget 

116 

117 @property 

118 def active(self): 

119 return self.widget['state'] == 'normal' 

120 

121 @active.setter 

122 def active(self, value): 

123 self.widget['state'] = ['disabled', 'normal'][bool(value)] 

124 

125 

126class Row(Widget): 

127 def __init__(self, things): 

128 self.things = things 

129 

130 def create(self, parent): 

131 self.widget = tk.Frame(parent) 

132 for thing in self.things: 

133 if isinstance(thing, str): 

134 thing = Label(thing) 

135 thing.pack(self.widget, 'left') 

136 return self.widget 

137 

138 def __getitem__(self, i): 

139 return self.things[i] 

140 

141 

142class Label(Widget): 

143 def __init__(self, text='', color=None): 

144 self.creator = partial(tk.Label, text=text, fg=color) 

145 

146 @property 

147 def text(self): 

148 return self.widget['text'] 

149 

150 @text.setter 

151 def text(self, new): 

152 self.widget.config(text=new) 

153 

154 

155class Text(Widget): 

156 def __init__(self, text): 

157 self.creator = partial(tk.Text, height=text.count('\n') + 1) 

158 s = re.split('<(.*?)>', text) 

159 self.text = [(s[0], ())] 

160 i = 1 

161 tags = [] 

162 while i < len(s): 

163 tag = s[i] 

164 if tag[0] != '/': 

165 tags.append(tag) 

166 else: 

167 tags.pop() 

168 self.text.append((s[i + 1], tuple(tags))) 

169 i += 2 

170 

171 def create(self, parent): 

172 widget = Widget.create(self, parent) 

173 widget.tag_configure('sub', offset=-6) 

174 widget.tag_configure('sup', offset=6) 

175 widget.tag_configure('c', foreground='blue') 

176 for text, tags in self.text: 

177 widget.insert('insert', text, tags) 

178 widget.configure(state='disabled', background=parent['bg']) 

179 widget.bind("<1>", lambda event: widget.focus_set()) 

180 return widget 

181 

182 

183class Button(Widget): 

184 def __init__(self, text, callback, *args, **kwargs): 

185 self.callback = partial(callback, *args, **kwargs) 

186 self.creator = partial(tk.Button, 

187 text=text, 

188 command=self.callback) 

189 

190 

191class CheckButton(Widget): 

192 def __init__(self, text, value=False, callback=None): 

193 self.text = text 

194 self.var = tk.BooleanVar(value=value) 

195 self.callback = callback 

196 

197 def create(self, parent): 

198 self.check = tk.Checkbutton(parent, text=self.text, 

199 var=self.var, command=self.callback) 

200 return self.check 

201 

202 @property 

203 def value(self): 

204 return self.var.get() 

205 

206 

207class SpinBox(Widget): 

208 def __init__(self, value, start, end, step, callback=None, 

209 rounding=None, width=6): 

210 self.callback = callback 

211 self.rounding = rounding 

212 self.creator = partial(tk.Spinbox, 

213 from_=start, 

214 to=end, 

215 increment=step, 

216 command=callback, 

217 width=width) 

218 self.initial = str(value) 

219 

220 def create(self, parent): 

221 self.widget = self.creator(parent) 

222 self.widget.bind('<Return>', lambda event: self.callback()) 

223 self.value = self.initial 

224 return self.widget 

225 

226 @property 

227 def value(self): 

228 x = self.widget.get().replace(',', '.') 

229 if '.' in x: 

230 return float(x) 

231 if x == 'None': 

232 return None 

233 return int(x) 

234 

235 @value.setter 

236 def value(self, x): 

237 self.widget.delete(0, 'end') 

238 if '.' in str(x) and self.rounding is not None: 

239 try: 

240 x = round(float(x), self.rounding) 

241 except (ValueError, TypeError): 

242 pass 

243 self.widget.insert(0, x) 

244 

245 

246# Entry and ComboBox use same mechanism (since ttk ComboBox 

247# is a subclass of tk Entry). 

248def _set_entry_value(widget, value): 

249 widget.delete(0, 'end') 

250 widget.insert(0, value) 

251 

252 

253class Entry(Widget): 

254 def __init__(self, value='', width=20, callback=None): 

255 self.creator = partial(tk.Entry, 

256 width=width) 

257 if callback is not None: 

258 self.callback = lambda event: callback() 

259 else: 

260 self.callback = None 

261 self.initial = value 

262 

263 def create(self, parent): 

264 self.entry = self.creator(parent) 

265 self.value = self.initial 

266 if self.callback: 

267 self.entry.bind('<Return>', self.callback) 

268 return self.entry 

269 

270 @property 

271 def value(self): 

272 return self.entry.get() 

273 

274 @value.setter 

275 def value(self, x): 

276 _set_entry_value(self.entry, x) 

277 

278 

279class Scale(Widget): 

280 def __init__(self, value, start, end, callback): 

281 def command(val): 

282 callback(int(val)) 

283 

284 self.creator = partial(tk.Scale, 

285 from_=start, 

286 to=end, 

287 orient='horizontal', 

288 command=command) 

289 self.initial = value 

290 

291 def create(self, parent): 

292 self.scale = self.creator(parent) 

293 self.value = self.initial 

294 return self.scale 

295 

296 @property 

297 def value(self): 

298 return self.scale.get() 

299 

300 @value.setter 

301 def value(self, x): 

302 self.scale.set(x) 

303 

304 

305class RadioButtons(Widget): 

306 def __init__(self, labels, values=None, callback=None, vertical=False): 

307 self.var = tk.IntVar() 

308 

309 if callback: 

310 def callback2(): 

311 callback(self.value) 

312 else: 

313 callback2 = None 

314 

315 self.values = values or list(range(len(labels))) 

316 self.buttons = [RadioButton(label, i, self.var, callback2) 

317 for i, label in enumerate(labels)] 

318 self.vertical = vertical 

319 

320 def create(self, parent): 

321 self.widget = frame = tk.Frame(parent) 

322 side = 'top' if self.vertical else 'left' 

323 for button in self.buttons: 

324 button.create(frame).pack(side=side) 

325 return frame 

326 

327 @property 

328 def value(self): 

329 return self.values[self.var.get()] 

330 

331 @value.setter 

332 def value(self, value): 

333 self.var.set(self.values.index(value)) 

334 

335 def __getitem__(self, value): 

336 return self.buttons[self.values.index(value)] 

337 

338 

339class RadioButton(Widget): 

340 def __init__(self, label, i, var, callback): 

341 self.creator = partial(tk.Radiobutton, 

342 text=label, 

343 var=var, 

344 value=i, 

345 command=callback) 

346 

347 

348if ttk is not None: 

349 class ComboBox(Widget): 

350 def __init__(self, labels, values=None, callback=None): 

351 self.values = values or list(range(len(labels))) 

352 self.callback = callback 

353 self.creator = partial(ttk.Combobox, 

354 values=labels) 

355 

356 def create(self, parent): 

357 widget = Widget.create(self, parent) 

358 widget.current(0) 

359 if self.callback: 

360 def callback(event): 

361 self.callback(self.value) 

362 widget.bind('<<ComboboxSelected>>', callback) 

363 

364 return widget 

365 

366 @property 

367 def value(self): 

368 return self.values[self.widget.current()] 

369 

370 @value.setter 

371 def value(self, val): 

372 _set_entry_value(self.widget, val) 

373else: 

374 # Use Entry object when there is no ttk: 

375 def ComboBox(labels, values, callback): 

376 return Entry(values[0], callback=callback) 

377 

378 

379class Rows(Widget): 

380 def __init__(self, rows=None): 

381 self.rows_to_be_added = rows or [] 

382 self.creator = tk.Frame 

383 self.rows = [] 

384 

385 def create(self, parent): 

386 widget = Widget.create(self, parent) 

387 for row in self.rows_to_be_added: 

388 self.add(row) 

389 self.rows_to_be_added = [] 

390 return widget 

391 

392 def add(self, row): 

393 if isinstance(row, str): 

394 row = Label(row) 

395 elif isinstance(row, list): 

396 row = Row(row) 

397 row.grid(self.widget) 

398 self.rows.append(row) 

399 

400 def clear(self): 

401 while self.rows: 

402 del self[0] 

403 

404 def __getitem__(self, i): 

405 return self.rows[i] 

406 

407 def __delitem__(self, i): 

408 widget = self.rows.pop(i).widget 

409 widget.grid_remove() 

410 widget.destroy() 

411 

412 def __len__(self): 

413 return len(self.rows) 

414 

415 

416class MenuItem: 

417 def __init__(self, label, callback=None, key=None, 

418 value=None, choices=None, submenu=None, disabled=False): 

419 self.underline = label.find('_') 

420 self.label = label.replace('_', '') 

421 

422 if key: 

423 if key[:4] == 'Ctrl': 

424 self.keyname = '<Control-{0}>'.format(key[-1].lower()) 

425 else: 

426 self.keyname = { 

427 'Home': '<Home>', 

428 'End': '<End>', 

429 'Page-Up': '<Prior>', 

430 'Page-Down': '<Next>', 

431 'Backspace': '<BackSpace>'}.get(key, key.lower()) 

432 

433 if key: 

434 def callback2(event=None): 

435 callback(key) 

436 

437 callback2.__name__ = callback.__name__ 

438 self.callback = callback2 

439 else: 

440 self.callback = callback 

441 

442 self.key = key 

443 self.value = value 

444 self.choices = choices 

445 self.submenu = submenu 

446 self.disabled = disabled 

447 

448 def addto(self, menu, window, stuff=None): 

449 callback = self.callback 

450 if self.label == '---': 

451 menu.add_separator() 

452 elif self.value is not None: 

453 var = tk.BooleanVar(value=self.value) 

454 stuff[self.callback.__name__.replace('_', '-')] = var 

455 

456 menu.add_checkbutton(label=self.label, 

457 underline=self.underline, 

458 command=self.callback, 

459 accelerator=self.key, 

460 var=var) 

461 

462 def callback(key): 

463 var.set(not var.get()) 

464 self.callback() 

465 

466 elif self.choices: 

467 submenu = tk.Menu(menu) 

468 menu.add_cascade(label=self.label, menu=submenu) 

469 var = tk.IntVar() 

470 var.set(0) 

471 stuff[self.callback.__name__.replace('_', '-')] = var 

472 for i, choice in enumerate(self.choices): 

473 submenu.add_radiobutton(label=choice.replace('_', ''), 

474 underline=choice.find('_'), 

475 command=self.callback, 

476 value=i, 

477 var=var) 

478 elif self.submenu: 

479 submenu = tk.Menu(menu) 

480 menu.add_cascade(label=self.label, 

481 menu=submenu) 

482 for thing in self.submenu: 

483 thing.addto(submenu, window) 

484 else: 

485 state = 'normal' 

486 if self.disabled: 

487 state = 'disabled' 

488 menu.add_command(label=self.label, 

489 underline=self.underline, 

490 command=self.callback, 

491 accelerator=self.key, 

492 state=state) 

493 if self.key: 

494 window.bind(self.keyname, callback) 

495 

496 

497class MainWindow(BaseWindow): 

498 def __init__(self, title, close=None, menu=[]): 

499 self.win = tk.Tk() 

500 BaseWindow.__init__(self, title, close) 

501 

502 # self.win.tk.call('tk', 'scaling', 3.0) 

503 # self.win.tk.call('tk', 'scaling', '-displayof', '.', 7) 

504 

505 self.menu = {} 

506 

507 if menu: 

508 self.create_menu(menu) 

509 

510 def create_menu(self, menu_description): 

511 menu = tk.Menu(self.win) 

512 self.win.config(menu=menu) 

513 

514 for label, things in menu_description: 

515 submenu = tk.Menu(menu) 

516 menu.add_cascade(label=label.replace('_', ''), 

517 underline=label.find('_'), 

518 menu=submenu) 

519 for thing in things: 

520 thing.addto(submenu, self.win, self.menu) 

521 

522 def resize_event(self): 

523 # self.scale *= sqrt(1.0 * self.width * self.height / (w * h)) 

524 self.draw() 

525 self.configured = True 

526 

527 def run(self): 

528 # Workaround for nasty issue with tkinter on Mac: 

529 # https://gitlab.com/ase/ase/issues/412 

530 # 

531 # It is apparently a compatibility issue between Python and Tkinter. 

532 # Some day we should remove this hack. 

533 while True: 

534 try: 

535 tk.mainloop() 

536 break 

537 except UnicodeDecodeError: 

538 pass 

539 

540 def __getitem__(self, name): 

541 return self.menu[name].get() 

542 

543 def __setitem__(self, name, value): 

544 return self.menu[name].set(value) 

545 

546 

547def bind(callback, modifier=None): 

548 def handle(event): 

549 event.button = mouse_buttons.get(event.num, event.num) 

550 event.key = event.keysym.lower() 

551 event.modifier = modifier 

552 callback(event) 

553 return handle 

554 

555 

556class ASEFileChooser(LoadFileDialog): 

557 def __init__(self, win, formatcallback=lambda event: None): 

558 from ase.io.formats import all_formats, get_ioformat 

559 LoadFileDialog.__init__(self, win, _('Open ...')) 

560 # fix tkinter not automatically setting dialog type 

561 # remove from Python3.8+ 

562 # see https://github.com/python/cpython/pull/25187 

563 # and https://bugs.python.org/issue43655 

564 # and https://github.com/python/cpython/pull/25592 

565 set_windowtype(self.top, 'dialog') 

566 labels = [_('Automatic')] 

567 values = [''] 

568 

569 def key(item): 

570 return item[1][0] 

571 

572 for format, (description, code) in sorted(all_formats.items(), 

573 key=key): 

574 io = get_ioformat(format) 

575 if io.can_read and description != '?': 

576 labels.append(_(description)) 

577 values.append(format) 

578 

579 self.format = None 

580 

581 def callback(value): 

582 self.format = value 

583 

584 Label(_('Choose parser:')).pack(self.top) 

585 formats = ComboBox(labels, values, callback) 

586 formats.pack(self.top) 

587 

588 

589def show_io_error(filename, err): 

590 showerror(_('Read error'), 

591 _('Could not read {}: {}'.format(filename, err))) 

592 

593 

594class ASEGUIWindow(MainWindow): 

595 def __init__(self, close, menu, config, 

596 scroll, scroll_event, 

597 press, move, release, resize): 

598 MainWindow.__init__(self, 'ASE-GUI', close, menu) 

599 

600 self.size = np.array([450, 450]) 

601 

602 self.fg = config['gui_foreground_color'] 

603 self.bg = config['gui_background_color'] 

604 

605 self.canvas = tk.Canvas(self.win, 

606 width=self.size[0], 

607 height=self.size[1], 

608 bg=self.bg, 

609 highlightthickness=0) 

610 self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) 

611 

612 self.status = tk.Label(self.win, text='', anchor=tk.W) 

613 self.status.pack(side=tk.BOTTOM, fill=tk.X) 

614 

615 right = mouse_buttons.get(3, 3) 

616 self.canvas.bind('<ButtonPress>', bind(press)) 

617 self.canvas.bind('<B1-Motion>', bind(move)) 

618 self.canvas.bind('<B{right}-Motion>'.format(right=right), bind(move)) 

619 self.canvas.bind('<ButtonRelease>', bind(release)) 

620 self.canvas.bind('<Control-ButtonRelease>', bind(release, 'ctrl')) 

621 self.canvas.bind('<Shift-ButtonRelease>', bind(release, 'shift')) 

622 self.canvas.bind('<Configure>', resize) 

623 if not config['swap_mouse']: 

624 self.canvas.bind('<Shift-B{right}-Motion>'.format(right=right), 

625 bind(scroll)) 

626 else: 

627 self.canvas.bind('<Shift-B1-Motion>', 

628 bind(scroll)) 

629 

630 self.win.bind('<MouseWheel>', bind(scroll_event)) 

631 self.win.bind('<Key>', bind(scroll)) 

632 self.win.bind('<Shift-Key>', bind(scroll, 'shift')) 

633 self.win.bind('<Control-Key>', bind(scroll, 'ctrl')) 

634 

635 def update_status_line(self, text): 

636 self.status.config(text=text) 

637 

638 def run(self): 

639 MainWindow.run(self) 

640 

641 def click(self, name): 

642 self.callbacks[name]() 

643 

644 def clear(self): 

645 self.canvas.delete(tk.ALL) 

646 

647 def update(self): 

648 self.canvas.update_idletasks() 

649 

650 def circle(self, color, selected, *bbox): 

651 if selected: 

652 outline = '#004500' 

653 width = 3 

654 else: 

655 outline = 'black' 

656 width = 1 

657 self.canvas.create_oval(*tuple(int(x) for x in bbox), fill=color, 

658 outline=outline, width=width) 

659 

660 def arc(self, color, selected, start, extent, *bbox): 

661 if selected: 

662 outline = '#004500' 

663 width = 3 

664 else: 

665 outline = 'black' 

666 width = 1 

667 self.canvas.create_arc(*tuple(int(x) for x in bbox), 

668 start=start, 

669 extent=extent, 

670 fill=color, 

671 outline=outline, 

672 width=width) 

673 

674 def line(self, bbox, width=1): 

675 self.canvas.create_line(*tuple(int(x) for x in bbox), width=width) 

676 

677 def text(self, x, y, txt, anchor=tk.CENTER, color='black'): 

678 anchor = {'SE': tk.SE}.get(anchor, anchor) 

679 self.canvas.create_text((x, y), text=txt, anchor=anchor, fill=color) 

680 

681 def after(self, time, callback): 

682 id = self.win.after(int(time * 1000), callback) 

683 # Quick'n'dirty object with a cancel() method: 

684 return namedtuple('Timer', 'cancel')(lambda: self.win.after_cancel(id))