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

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 win.add(Text('\n'.join(text))) 

44 

45 

46def helpbutton(text): 

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

48 

49 

50def helpwindow(text): 

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

52 win.add(Text(text)) 

53 

54 

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) 

62 

63 self.things = [] 

64 self.exists = True 

65 

66 def close(self): 

67 self.win.destroy() 

68 self.exists = False 

69 

70 def title(self, txt): 

71 self.win.title(txt) 

72 

73 title = property(None, title) 

74 

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) 

82 

83 

84class Window(BaseWindow): 

85 def __init__(self, title, close=None): 

86 self.win = tk.Toplevel() 

87 BaseWindow.__init__(self, title, close) 

88 

89 

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 

96 

97 def grid(self, parent): 

98 widget = self.create(parent) 

99 widget.grid() 

100 

101 def create(self, parent): 

102 self.widget = self.creator(parent) 

103 return self.widget 

104 

105 @property 

106 def active(self): 

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

108 

109 @active.setter 

110 def active(self, value): 

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

112 

113 

114class Row(Widget): 

115 def __init__(self, things): 

116 self.things = things 

117 

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 

125 

126 def __getitem__(self, i): 

127 return self.things[i] 

128 

129 

130class Label(Widget): 

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

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

133 

134 @property 

135 def text(self): 

136 return self.widget['text'] 

137 

138 @text.setter 

139 def text(self, new): 

140 self.widget.config(text=new) 

141 

142 

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 

158 

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 

169 

170 

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) 

177 

178 

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 

184 

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 

189 

190 @property 

191 def value(self): 

192 return self.var.get() 

193 

194 

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) 

207 

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 

213 

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) 

222 

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) 

232 

233 

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) 

239 

240 

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 

250 

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 

257 

258 @property 

259 def value(self): 

260 return self.entry.get() 

261 

262 @value.setter 

263 def value(self, x): 

264 _set_entry_value(self.entry, x) 

265 

266 

267class Scale(Widget): 

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

269 def command(val): 

270 callback(int(val)) 

271 

272 self.creator = partial(tk.Scale, 

273 from_=start, 

274 to=end, 

275 orient='horizontal', 

276 command=command) 

277 self.initial = value 

278 

279 def create(self, parent): 

280 self.scale = self.creator(parent) 

281 self.value = self.initial 

282 return self.scale 

283 

284 @property 

285 def value(self): 

286 return self.scale.get() 

287 

288 @value.setter 

289 def value(self, x): 

290 self.scale.set(x) 

291 

292 

293class RadioButtons(Widget): 

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

295 self.var = tk.IntVar() 

296 

297 if callback: 

298 def callback2(): 

299 callback(self.value) 

300 else: 

301 callback2 = None 

302 

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 

307 

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 

314 

315 @property 

316 def value(self): 

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

318 

319 @value.setter 

320 def value(self, value): 

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

322 

323 def __getitem__(self, value): 

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

325 

326 

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) 

334 

335 

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) 

343 

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) 

351 

352 return widget 

353 

354 @property 

355 def value(self): 

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

357 

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) 

365 

366 

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 = [] 

372 

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 

379 

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) 

387 

388 def clear(self): 

389 while self.rows: 

390 del self[0] 

391 

392 def __getitem__(self, i): 

393 return self.rows[i] 

394 

395 def __delitem__(self, i): 

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

397 widget.grid_remove() 

398 widget.destroy() 

399 

400 def __len__(self): 

401 return len(self.rows) 

402 

403 

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('_', '') 

409 

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()) 

420 

421 if key: 

422 def callback2(event=None): 

423 callback(key) 

424 

425 callback2.__name__ = callback.__name__ 

426 self.callback = callback2 

427 else: 

428 self.callback = callback 

429 

430 self.key = key 

431 self.value = value 

432 self.choices = choices 

433 self.submenu = submenu 

434 self.disabled = disabled 

435 

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 

443 

444 menu.add_checkbutton(label=self.label, 

445 underline=self.underline, 

446 command=self.callback, 

447 accelerator=self.key, 

448 var=var) 

449 

450 def callback(key): 

451 var.set(not var.get()) 

452 self.callback() 

453 

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) 

483 

484 

485class MainWindow(BaseWindow): 

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

487 self.win = tk.Tk() 

488 BaseWindow.__init__(self, title, close) 

489 

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

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

492 

493 self.menu = {} 

494 

495 if menu: 

496 self.create_menu(menu) 

497 

498 def create_menu(self, menu_description): 

499 menu = tk.Menu(self.win) 

500 self.win.config(menu=menu) 

501 

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) 

509 

510 def resize_event(self): 

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

512 self.draw() 

513 self.configured = True 

514 

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 

527 

528 def __getitem__(self, name): 

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

530 

531 def __setitem__(self, name, value): 

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

533 

534 

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 

542 

543 

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 = [''] 

550 

551 def key(item): 

552 return item[1][0] 

553 

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) 

560 

561 self.format = None 

562 

563 def callback(value): 

564 self.format = value 

565 

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

567 formats = ComboBox(labels, values, callback) 

568 formats.pack(self.top) 

569 

570 

571def show_io_error(filename, err): 

572 showerror(_('Read error'), 

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

574 

575 

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) 

581 

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

583 

584 self.fg = config['gui_foreground_color'] 

585 self.bg = config['gui_background_color'] 

586 

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) 

593 

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

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

596 

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)) 

611 

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')) 

616 

617 def update_status_line(self, text): 

618 self.status.config(text=text) 

619 

620 def run(self): 

621 MainWindow.run(self) 

622 

623 def click(self, name): 

624 self.callbacks[name]() 

625 

626 def clear(self): 

627 self.canvas.delete(tk.ALL) 

628 

629 def update(self): 

630 self.canvas.update_idletasks() 

631 

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) 

641 

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) 

655 

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

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

658 

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) 

662 

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))