Source code for ascii_designer.tk_treeedit

'''``ttk.TreeView`` control augmented by editing capabilities.

For basic information, see official Tkinter (``ttk``) docs.

The following additional functionality is provided:

 * Mark column as editable using :any:`TreeEdit.editable`.
 * allow= parameter to specify legal structural operations.

``allow`` is a list of strings or a comma-separated string. It can contain any of:

 * ``add`` to allow adding new items (anywhere)
 * ``addchild`` to allow insertion of child items
 * ``remove`` to allow deletion of items.

For each allowance, the corresponding control is shown, and the keybinding is activated.

The following bindings / behaviors are built-in. Generally, value is
submitted on close, except if Escape key is used.

**Treeview**:

 * Dblclick:      open edit on col
 * Scroll:        Take focus (close edit)
 * Resize:        close edit box
 * F2:            open first edit of row
 * Ctrl+plus,
   Insert:        add item (if enabled, see below)
 * Ctrl+asterisk: add child (if enabled)
 * Ctrl+minus,
 * Delete:        remove item (if enabled)

**Edit box**:

 * Lose Focus:    close
 * Return:        close
 * Escape:        close without submitting
 * Shift+enter,
 * Down arrow:    Close + edit same column in next row
 * Tab,
 * Shift+Right arrow:   close + edit next column (or 1st col in next row)
 * Shift+Tab,
 * Shift+Left arrow:    like Tab but backwards
 * Up arrow:      Close + edit same col in prev row

**Events**:

These are properties of the TreeeEdit control. 
Use ``treeedit.<property> += handler``to bind a handler, ``-=`` to unbind it.

 * ``on_cell_edit(iid, columnname, cur_value)`` when editor is opened
 * ``on_cell_modified(iid, columname, new_value)`` when editor is closed
 * ``on_add(iid)``: before item is inserted after (iid).
 * ``on_add_child(iid)``: before child is inserted under (iid).
 * ``on_remove(iid)``: before child is deleted

.. note:: 
    ``on_cell_modified``, ``on_add``, ``on_add_child``, ``on_remove`` are fired
    immediately before the respective action takes place in the widget.

    Your handler can return ``False`` to indicate that the widget content shall
    not be modified; e.g. if the action is forbidden or you took care of
    updating the tree yourself. Note that ``None`` is counted as ``True`` result
    in this case.

# TODO:
# reorder
# custom editor types (button, checkbox, combo, ... pass own widget(s))
# copy/paste
# have a handler for after-item-insertion with the actual iid as param (we don't need it currently)
'''

__all__ = ['TreeEdit',]

import sys

import tkinter as tk
import tkinter.ttk as ttk

from .event import EventSource

[docs]class TreeEdit(ttk.Treeview): '''see module docs''' list_bindings = [ ('<Double-Button-1>', "_dblclick"), ('<F2>', 'begin_edit_row'), # Scroll wheel ('<4>', '_close_edit_refocus'), ('<5>', '_close_edit_refocus'), ('<Control-plus>', 'ins_item'), ('<Insert>', 'ins_item'), ('<Control-asterisk>', 'ins_child_item'), ('<Control-minus>', 'del_item'), ('<Delete>', 'del_item'), ] editbox_bindings = [ ('<FocusOut>', 'close_edit'), ('<Return>', '_close_edit_refocus'), ('<KP_Enter>', '_close_edit_refocus'), ('<Escape>', '_cancel_edit_refocus'), ('<Shift-Right>', 'advance_right'), ('<Tab>', 'advance_right'), ('<Shift-Left>', 'advance_left'), ('<Shift-Tab>', 'advance_left'), ('<Shift-Return>', 'advance_down'), ('<Shift-KP_Enter>', 'advance_down'), ('<Down>', 'advance_down'), ('<Up>', 'advance_up'), ('<Control-plus>', 'ins_item'), ('<Control-asterisk>', 'ins_child_item'), ('<Control-minus>', 'del_item'), ] + ( [] if not sys.platform.startswith('linux') else [ ('<Shift-ISO_Left_Tab>', 'advance_left'), ] ) def __init__(self, master, allow=None, *args, **kwargs): super().__init__(master, *args, **kwargs) self._editvar = tk.StringVar(self, '') self._editbox = ttk.Entry(self, textvariable=self._editvar) self._edit_cell = None self._editable = {} self._all_columns = ['#0'] + list(kwargs.get('columns', [])) for name in self._all_columns: self._editable[name] = False self.bind('<Configure>', self._on_configure) for trigger, handler in self.list_bindings: if isinstance(handler, str): handler = getattr(self, handler) self.bind(trigger, handler) for trigger, handler in self.editbox_bindings: if isinstance(handler, str): handler = getattr(self, handler) self._editbox.bind(trigger, handler) self.on_add = EventSource() self.on_add_child = EventSource() self.on_remove = EventSource() self.on_cell_edit = EventSource() self.on_cell_modified = EventSource() self.allow = allow self.autoedit_added = True @property def allow(self): '''Allowed structural edits (add, delete, addchild). Pass the allowed actions as list of strings or comma-separated string. Can be updated during operation. ''' return self._allow[:] @allow.setter def allow(self, allow): allow = allow or [] if isinstance(allow, str): allow = allow.split(',') allow = [item.strip() for item in allow] bad_items = [item for item in allow if item not in ['add', 'addchild', 'remove']] if bad_items: raise ValueError('Unknown allow entries: %s' % (bad_items,)) self._allow = allow self._update_controls() @property def autoedit_added(self) -> bool: '''Automatically begin editing added items yes/no''' return self._autoedit_added @autoedit_added.setter def autoedit_added(self, val: bool): self._autoedit_added = bool(val)
[docs] def editable(self, column, editable=None): '''Query or specify whether the column is editable. Only accepts Column Name or ``'#0'``. ''' if column not in self._editable: raise KeyError(column) if editable is not None: self._editable[column] = bool(editable) return self._editable[column]
@property def _ed_list(self): return [name for name in self._all_columns if self._editable[name]]
[docs] def begin_edit(self, iid, column): '''Show edit widget for the specified cell.''' self.close_edit() self.see(iid) self.focus(iid) self.update_idletasks() try: x, y, w, h = self.bbox(iid, column=column) except ValueError: # not visible return if column == '#0': val = self.item(iid, option='text') else: # self.set GETS the value! val = self.set(iid, column) self._editvar.set(val) self._editbox.place(x=x, y=y, width=w, height=h) self._edit_cell = (iid, column) self._editbox.selection_range(0, 'end') self._editbox.focus_set() self.on_cell_edit(iid, column, self._editvar.get())
def _close_edit_refocus(self, ev=None, cancel=False): self.close_edit(ev, cancel) self.focus_set() def _cancel_edit_refocus(self, ev=None): self.close_edit(ev, cancel=True) self.focus_set()
[docs] def close_edit(self, ev=None, cancel=False): '''Close the currently open editor, if any.''' if not cancel and self._edit_cell is not None: iid, column = self._edit_cell result = self.on_cell_modified(iid, column, self._editvar.get()) if result is None or result: # Modify content if column == '#0': self.item(iid, text=self._editvar.get()) else: self.set(iid, column, self._editvar.get()) self._edit_cell = None self._editbox.place_forget()
def _dblclick(self, ev): iid = self.identify_row(ev.y) column = self.identify_column(ev.x) idx = int(column[1:]) if idx > 0: dc = self['displaycolumns'] if dc == ('#all',): dc = self['columns'] colname = dc[idx-1] else: colname = '#0' if not iid or not self._editable[colname]: return self.begin_edit(iid, colname)
[docs] def begin_edit_row(self, ev): '''Start editing the first editable column of the focused row.''' iid = self.focus() columns = self._ed_list if not columns: return self.begin_edit(iid, columns[0])
[docs] def advance(self, direction='right'): '''switch to next cell. ``direction`` can be left, right, up, down. If going left/right beyond the first or last cell, edit box moves to the previous/next row. ''' if self._edit_cell is None: return iid, column = self._edit_cell if direction == 'down': iid = self.next(iid) elif direction == 'up': iid = self.prev(iid) elif direction == 'left': columns = self._ed_list idx = columns.index(column)-1 if idx < 0: idx = len(columns)-1 iid = self.prev(iid) column = columns[idx] elif direction == 'right': columns = self._ed_list idx = columns.index(column)+1 if idx >= len(columns): idx=0 iid = self.next(iid) column = columns[idx] else: raise ValueError('invalid direction %s' % direction) if iid: self.begin_edit(iid, column) else: self._close_edit_refocus() # to disable default behaviour return 'break'
[docs] def advance_left(self, ev=None): return self.advance('left')
[docs] def advance_right(self, ev=None): return self.advance('right')
[docs] def advance_up(self, ev=None): return self.advance('up')
[docs] def advance_down(self, ev=None): return self.advance('down')
def _on_configure(self, ev): self.close_edit() if self._controls: self._controls.place(relx=1, rely=1, anchor='se') def _update_controls(self): allow = self._allow if allow: ctls = self._controls = ttk.Frame(self) if 'add' in allow: addbtn = ttk.Button(ctls, text=' + ', command=lambda:self.ins_item()) addbtn.pack(side='left') if 'addchild' in allow: addcbtn = ttk.Button(ctls, text='+>', command=lambda:self.ins_item(child=True)) addcbtn.pack(side='left') if 'remove' in allow: delbtn = ttk.Button(ctls, text=' X ', command=self.del_item) delbtn.pack(side='left') else: self._controls = None
[docs] def ins_item(self, ev=None, child=False): '''Trigger insertion of a new item.''' if ('addchild' if child else 'add') not in self._allow: return f = self.focus() self.close_edit() if child: result = self.on_add_child(f) else: result = self.on_add(f) if result is None or result: new_iid = self.insert(f if child else self.parent(f), self.index(f)+1) self.focus(new_iid) if self.autoedit_added: self.begin_edit_row(None)
[docs] def ins_child_item(self, ev=None): '''Trigger insertion of new child item''' return self.ins_item(ev, child=True)
[docs] def del_item(self, ev=None): '''Trigger deletion of focused item.''' if 'remove' not in self._allow: return self.close_edit(cancel=True) iid = self.focus() if iid: result = self.on_remove(iid) if result is None or result: self.delete(iid)
def main(): import tkinter as tk from tkinter import ttk from tkinter import font tl = tk.Tk() tl.grid_rowconfigure(0, weight=1) tl.grid_columnconfigure(0, weight=1) style = ttk.Style() style.configure(".", font= font.Font(family='Helvetica')) style.configure("Treeview.Heading", font=('Helvetica', 10, 'bold')) style.configure("Treeview", rowheight=30) te = TreeEdit(tl, columns=['col1', 'col2', 'col3']) #te.pack(fill='both', expand=True) te.grid(row=0, column=0, sticky='nsew') te.allow = 'add,remove,addchild' te.on_add += lambda focus: print('inserting an item after', focus) def delete_myself(iid): print('delete', iid) te.delete(iid) return False te.on_remove += delete_myself def print_begin(iid, column, current_val): print('Edit: ', iid, column, current_val) def print_change(iid, column, new_val): print('Modified: ', iid, column, new_val) te.on_cell_edit += print_begin te.on_cell_modified += print_change te.editable('#0', True) te.editable('col1', True) te.editable('col3', True) fakedata = [ ['this', 'is', 'a', 'row'], ['another', 'row', 'is', 'here'], ['lorem', 'ipsum', 'dolor', 'sit'], ['romanes', 'eunt', 'domus', ''], ['romani', 'ite', 'domum', ''], ]*5 parent = '' for i in range(2): for data in fakedata: txt, t1, t2, t3 = data iid = te.insert(parent, 'end', text=txt) te.set(iid, 'col1', t1) te.set(iid, 'col2', t2) te.set(iid, 'col3', t3) parent = iid tl.mainloop() if __name__ == '__main__': main()