1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
|
from dataclasses import dataclass
from typing import Callable
import numpy as np
import ase.gui.ui as ui
from ase.gui.i18n import _
@dataclass
class Column:
name: str
displayname: str
widget_width: int
getvalue: Callable
setvalue: Callable
format_value: Callable = lambda obj: str(obj)
class AtomsEditor:
# We subscribe to gui.draw() calls in order to track changes,
# but we should have an actual "atoms changed" event instead.
def __init__(self, gui):
gui.obs.change_atoms.register(self.update_table_from_atoms)
win = ui.Window(_('Edit atoms'))
treeview = ui.ttk.Treeview(win.win, selectmode='extended')
edit_entry = ui.ttk.Entry(win.win)
edit_entry.pack(side='bottom', fill='x')
treeview.pack(side='left', fill='y')
bar = ui.ttk.Scrollbar(
win.win, orient='vertical', command=self.scroll_via_scrollbar
)
treeview.configure(yscrollcommand=self.scroll_via_treeview)
treeview.column('#0', width=40)
treeview.heading('#0', text=_('id'))
bar.pack(side='right', fill='y')
self.scrollbar = bar
def get_symbol(atoms, i):
return atoms.symbols[i]
def set_symbol(atoms, i, value):
from ase.data import atomic_numbers
if value not in atomic_numbers:
return # Display error?
atoms.symbols[i] = value
self.gui = gui
self.treeview = treeview
self._current_entry = None
columns = []
symbols_column = Column(
'symbol', _('symbol'), 60, get_symbol, set_symbol
)
columns.append(symbols_column)
class GetSetPos:
def __init__(self, c):
self.c = c
def set_position(self, atoms, i, value):
try:
value = float(value)
except ValueError:
return
atoms.positions[i, self.c] = value
def get_position(self, atoms, i):
return atoms.positions[i, self.c]
for c, axisname in enumerate('xyz'):
column = Column(
axisname,
axisname,
92,
GetSetPos(c).get_position,
GetSetPos(c).set_position,
format_value=lambda val: f'{val:.4f}',
)
columns.append(column)
self.columns = columns
treeview.bind('<Double-1>', self.doubleclick)
treeview.bind('<<TreeviewSelect>>', self.treeview_selection_changed)
self.define_columns_on_widget()
self.update_table_from_atoms()
self.edit_entry = edit_entry
def treeview_selection_changed(self, event):
selected_items = self.treeview.selection()
indices = [self.rownumber(item) for item in selected_items]
self.gui.set_selected_atoms(indices)
def scroll_via_scrollbar(self, *args, **kwargs):
self.leave_edit_mode()
return self.treeview.yview(*args, **kwargs)
def scroll_via_treeview(self, *args, **kwargs):
# Here it is important to leave edit mode since scrolling
# invalidates the widget location. Alternatively we could keep
# it open as long as we move it but that sounds like work
self.leave_edit_mode()
return self.scrollbar.set(*args, **kwargs)
def leave_edit_mode(self):
if self._current_entry is not None:
self._current_entry.destroy()
self._current_entry = None
self.treeview.focus_force()
@property
def atoms(self):
return self.gui.atoms
def update_table_from_atoms(self):
self.treeview.delete(*self.treeview.get_children())
for i in range(len(self.atoms)):
values = self.get_row_values(i)
self.treeview.insert(
'', 'end', text=i, values=values, iid=self.rowid(i)
)
mask = self.gui.images.selected[: len(self.atoms)]
selection = np.arange(len(self.atoms))[mask]
rowids = [self.rowid(index) for index in selection]
# Note: selection_set() does *not* fire an event, and therefore
# we do not need to worry about infinite recursion.
# However the event listening is wonky now because we need
# better GUI change listeners.
self.treeview.selection_set(*rowids)
def get_row_values(self, i):
return [
column.format_value(column.getvalue(self.atoms, i))
for column in self.columns
]
def define_columns_on_widget(self):
self.treeview['columns'] = [column.name for column in self.columns]
for column in self.columns:
self.treeview.heading(column.name, text=column.displayname)
self.treeview.column(
column.name,
width=column.widget_width,
anchor='e',
)
def rowid(self, rownumber: int) -> str:
return f'R{rownumber}'
def rownumber(self, rowid: str) -> int:
assert rowid.startswith('R'), repr(rowid)
return int(rowid[1:])
def set_value(self, column_no: int, row_no: int, value: object) -> None:
column = self.columns[column_no]
column.setvalue(self.atoms, row_no, value)
text = column.format_value(column.getvalue(self.atoms, row_no))
# The text that we set here is not what matters: It may be rounded.
# It was column.setvalue() which did the actual change.
self.treeview.set(self.rowid(row_no), column.name, value=text)
# (Maybe it is not always necessary to redraw everything.)
self.gui.set_frame()
def doubleclick(self, event):
row_id = self.treeview.identify_row(event.y)
column_id = self.treeview.identify_column(event.x)
if not row_id or not column_id:
return # clicked outside actual rows/columns
self.edit_field(row_id, column_id)
def edit_field(self, row_id, column_id):
assert column_id.startswith('#'), repr(column_id)
column_no = int(column_id[1:]) - 1
if column_no == -1:
return # This is the ID column.
row_no = self.rownumber(row_id)
assert 0 <= column_no < len(self.columns)
assert 0 <= row_no < len(self.atoms)
content = self.columns[column_no].getvalue(self.atoms, row_no)
assert self._current_entry is None
entry = ui.ttk.Entry(self.treeview)
entry.insert(0, content)
entry.focus_force()
entry.selection_range(0, 'end')
def apply_change(_event=None):
value = entry.get()
try:
self.set_value(column_no, row_no, value)
finally:
# Focus was given to the text field, now return it:
self.treeview.focus_force()
self.leave_edit_mode()
entry.bind('<FocusOut>', apply_change)
ui.bind_enter(entry, apply_change)
entry.bind('<Escape>', lambda *args: self.leave_edit_mode())
bbox = self.treeview.bbox(row_id, column_id)
if bbox: # (bbox is '' when testing without display)
x, y, width, height = bbox
entry.place(x=x, y=y, height=height)
self._current_entry = entry
return entry, apply_change
|