File: android-tkinter/CODE/scrolledlist.py (original) (raw)
#!/usr/bin/python3 """
A simple, customizable, and attachable scrolled listbox component, adopted from the book Programming Python, 4th Edition.
ANDROID version, Jan 2019 (see "# ANDROID" for changes)
Modified here to support: (1) separate label/action tables (and thus allow duplicate labels); (2) single and double left-click modes; and (3) right-click events and actions that use the selected or nearest item. Left and right click handlers also receive the click's tk event object for pixel position, if needed.
Inputs: labels is [strings], both actions are [one-argument-callables]. In the listbox, left and right clicks run actions in leftactions and rightactions, respectively, which correspond to clicked labels by position. See comments ahead for more on left-click and right-item modes. The caller must sanitize non-BMP Unicode characters in labels for Tk versions through 8.6 if required.
"""
import sys from tkinter import *
class ScrolledList(Frame):
#----------------------------------------------------------------------
# activate item's left-click callback via single or double left-click;
# a single-click in left-double mode just highlights the item, which
# may seem pointless, but might be used for right-click item choice;
# note: a click in left-single mode still highlights the item (per tk
# built-in code), which may matter only if the listbox is persistent;
#----------------------------------------------------------------------
leftclickmode = 'single' # or 'double' (single requires right nearest)
#----------------------------------------------------------------------
# right-click item choice: selected item, or item nearest to click;
# Selected assumes item selected by left-single and requires left-double
# mode, but may use either nearest or selected in left-double mode;
# right-clicks never highlight the item, per tk's standard behavior;
#----------------------------------------------------------------------
rightclickitem = 'nearest' # or 'selected' (selected requires left double)
def __init__(self, labels, leftactions, rightactions, parent=None, side=TOP):
# sanity checks
assert len(labels) == len(leftactions)
assert len(labels) == len(rightactions)
# these settings can be overridden in subclasses
assert self.leftclickmode in ('single', 'double')
assert self.rightclickitem in ('nearest', 'selected')
# invalid case: single + selected
if self.leftclickmode == 'single':
assert self.rightclickitem == 'nearest' # but double => nearest or selected
if self.rightclickitem == 'selected':
assert self.leftclickmode == 'double' # but nearest => double or single
# build the widget
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH, side=side) # make me expandable
self.makeWidgets(labels) # caller: self.config(bg=x, bd=y,...)
self.leftactions = leftactions # caller: self.listbox.config/itemconfig()
self.rightactions = rightactions
def handleListLeft(self, tkevent):
"""
on list single- or double-left-click
single mode: use item nearest to click
double mode: use item selected (single left-click just selects an item)
"""
if self.leftclickmode == 'single': # activate item nearest click:
index = self.listbox.nearest(tkevent.y) # index of nearest item (rel to widget)
elif self.leftclickmode == 'double': # activate item selected by single-left:
index = self.listbox.curselection() # get selected item index
index = int(index[0]) # index= (digitstring,) tuple, 0..N-1
else:
assert False, 'invalid leftclickmode setting'
self.leftactions[index](tkevent) # call corresponding action with event
def handleListRight(self, tkevent):
"""
on list single-right-click
use item nearest to click, or formerly selected by left-click
"""
if self.rightclickitem == 'nearest': # activate item nearest click:
index = self.listbox.nearest(tkevent.y) # index of nearest item (rel to widget)
elif self.rightclickitem == 'selected': # activate item selected by single-left:
index = self.listbox.curselection() # get selected item index
index = int(index[0]) # index= (digitstring,) tuple, 0..N-1
else:
assert False, 'invalid rightclickitem setting'
self.rightactions[index](tkevent) # call corresponding action with event
def makeWidgets(self, labels):
"""
build the GUI: listbox, scroll, callbacks;
always uses default single selection and resize modes,
as in: list.config(selectmode=SINGLE, setgrid=1)
"""
# crosslink listbox, vertical scrollbar
sbar = Scrollbar(self)
lbox = Listbox(self, relief=SUNKEN)
sbar.config(command=lbox.yview) # xlink sbar and list
lbox.config(yscrollcommand=sbar.set) # move one moves other
sbar.pack(side=RIGHT, fill=Y) # pack first=clip last
lbox.pack(side=LEFT, expand=YES, fill=BOTH) # list clipped first
# fill listbox with labels
for (pos, label) in enumerate(labels): # add to listbox
lbox.insert(pos, label) # or insert(END,label)
# set left/right click handlers
if self.leftclickmode == 'single': # set left-click event handler
lbox.bind('<Button-1>', self.handleListLeft) # single-left activates item
else:
lbox.bind('<Double-1>', self.handleListLeft) # single-left only selects item
lbox.bind('<Button-3>', self.handleListRight) # set right-click event handler
# [2.0] on Mac OS X, also allow Control-click as an equivalent for right-click,
# and support Mac mice that trigger Button-2 on right button click (on Macs,
# right=Button-2 and middle=Button-3; it's the opposite on Windows and Linux!)
# ANDROID - +True: enables rightclick = drive-by swipe (else can't copy hidden events)
if True or sys.platform == 'darwin':
lbox.bind('<Control-Button-1>', self.handleListRight)
lbox.bind('<Button-2>', self.handleListRight)
self.listbox = lbox
if name == 'main':
# run to test
labels = ('spam', 'toast', 'spam', 'eggs') # duplicates: map by index
lactions = [lambda e, i=i: print(i) for i in range(4)] # print(0), print(1), ... (last i!)
ractions = [lambda e, i=i: print(i) for i in range(4, 8)] # print(4)...print(7)
ScrolledList(labels, lactions, ractions).mainloop()