File: frigcal-products/unzipped/searchcals_dialog.py
"""
=====================================================================================
[3.0] Provide calendar search in GUI, instead of former command-line script.
In the GUI, a Find button replaces prior and rarely (if ever) used Images toggle.
This supercedes the former (still included) searchcals.py 2.0 command-line script,
and is much faster, much more convenient, and adds "Calendar" as a search field.
To use in main frigcal.py code:
(install Find button in gui)
import searchcals_dialog
def onFindButton(self): search_dialog.FindInputsDialog(self)
=====================================================================================
"""
import frigcal as fc # mind the circular import: frigcal-main.py
from sharednames import Configs, PROGRAM, VERSION
import calendar # py stdlib: for month num -> abbr
from tkinter import *
from tkinter.messagebox import showinfo
#====================================================================================
# Find-inputs dialog [3.0]
#====================================================================================
class FindInputsDialog(fc.Dialog):
"""
[3.0] Top-level entry to search: inputs -> matches -> month, all nonmodal.
Use already-parsed-and-loaded events index, instead of reparsing ics
files like the former (but still supported) searchcals.py script; this
also ensures that any new mods not yet saved to ics files are picked up.
Master=monthwindow's root so auto-closes with Clone popup, else no-op+errors.
"""
def __init__(self, monthwindow):
self.root = monthwindow # creator's window, Dialog? [1.6]
self.modal = False # False=nonblocking, >one allowed
self.make_widgets()
self.run() # wait for user action (not) [1.4]
def make_widgets(self):
# new window specs
popup = Toplevel(self.root.root) # new dialog window, master=mw
self.dialogwin = popup # [1.4] for Dialog.run()
fc.try_set_window_icon(popup) # replace red tk window icon [1.2]
popup.title('%s %.1f - Find: Inputs' % (PROGRAM, VERSION))
fc.trybgconfig(popup, Configs.eventdialogbg)
# pack first = clip last on shrink (retain on resizes)
toolbar = Frame(popup, relief=RIDGE)
toolbar.pack(side=BOTTOM, fill=X)
fc.trybgconfig(toolbar, Configs.eventdialogbg)
# action buttons in toolbar at bottom
searchbtn = Button(toolbar, text='Search', command=self.onSearch)
cancelbtn = Button(toolbar, text='Cancel', command=self.onCancel)
for btn in searchbtn, cancelbtn:
fc.tryfontconfig(btn, Configs.controlsfont)
fc.killbtnpaddingmacos(btn) # [3.0] else padded
fc.killbtnbordermacos(btn, Configs.eventdialogbg) # [3.0] else border
searchbtn.pack(side=LEFT, expand=NO)
cancelbtn.pack(side=RIGHT, expand=NO)
# bind Enter keypress (where available) to be same as Search button
popup.bind('<Return>', lambda e: self.onSearch())
# frame for gridding inputs
inputsfrm = Frame(popup, relief=RIDGE, border=2)
inputsfrm.pack(side=TOP, expand=YES, fill=BOTH)
fc.trybgconfig(inputsfrm, Configs.eventdialogbg)
inputsfrm.grid_columnconfigure(1, weight=1) # make text grid col expandable
# label + entry for search text input
searchlbl = Label(inputsfrm, text='Text to find:', relief=RIDGE)
searchfor = Entry(inputsfrm, width=30) # 30 chars wide initially
searchlbl.grid(row=0, column=0, padx=2, sticky=NSEW)
searchfor.grid(row=0, column=1, sticky=NSEW) # focused on open ahead
self.searchfor = searchfor
# label + pull-down menu for field to search (!= MenuButton for cut/paste)
modelbl = Label(inputsfrm, text='Field to search:', relief=RIDGE)
searchmodes = 'Summary', 'Description', 'Category', 'Calendar', 'All' # +Calendar, oct30
modevar = StringVar()
modemnu = OptionMenu(inputsfrm, modevar, *searchmodes)
modelbl.grid(row=1, column=0, padx=2, sticky=NSEW)
modemnu.grid(row=1, column=1, sticky=NSEW)
modevar.set('All') # initialize to common case
self.modevar = modevar
# apply configs
"""no: match event dialogs labels, make dark/light-mode dependent
for lbl in searchlbl, modelbl:
fc.trybgconfig(lbl, Configs.eventdialogbg) # [2.0] Unicode replacement
fc.tryfgconfig(lbl, Configs.eventdialogfg) # [3.0] else white
fc.tryfontconfig(lbl, Configs.eventdialogfont)
"""
for inp in searchfor, modemnu:
fc.trybgconfig(inp, Configs.eventdialoginpbg) # [3.0] != all dlg
fc.tryfgconfig(inp, Configs.eventdialogfg)
fc.tryfontconfig(inp, Configs.eventdialogfont)
searchfor.config(insertbackground=Configs.eventdialogfg) # [3.0] else white
searchfor.focus()
def onCancel(self):
self.dialogwin.destroy()
def onSearch(self):
# assume too quick to warrant thread
searchfor = self.searchfor.get()
searchmode = self.modevar.get()
matches = []
for (date, dateEvents) in fc.EventsTable.items():
for (uid, event) in dateEvents.items():
# get event's text
if searchmode == 'All':
searchtxt = '\n'.join(
[event.summary, event.description, event.category, event.calendar])
else:
searchtxt = getattr(event, searchmode.lower(), '')
# neutralize case differences (probably a good thing)
searchkey = searchfor.lower()
searchtxt = searchtxt.lower()
if searchkey in searchtxt:
# order parts for later sorting here
(m, d, y) = date.as_tuple()
dateforsort = (y, m, d)
matches.append((dateforsort, event.calendar)) # date + calname
if not matches:
# notfound dialog, leave search dialog open
showinfo(f'{PROGRAM}: no matches found',
f'No matches found for "{searchfor}" in {searchmode}.',
parent=self.dialogwin)
self.dialogwin.focus_force()
else:
# popup nonmodal select list of ordered dates, leave search dialog open
#self.dialogwin.destroy()
FindMatchesDialog(self.root, matches, (searchfor, searchmode))
#====================================================================================
# Find-matches dialog [3.0]
#====================================================================================
class FindMatchesDialog(fc.Dialog):
"""
Display matches for click/tap to open date in monthwindow.
Master=monthwindow's root so auto-closes with Clone popup, else no-op+errors.
"""
def __init__(self, monthwindow, matches, searchinfo):
self.modal = False
self.root = monthwindow # creator's window, Dialog? [1.6]
self.make_widgets(monthwindow, matches, searchinfo)
self.run() # wait for user action (not) [1.4]
def make_widgets(self, monthwindow, matches, searchinfo):
# new window specs
popup = Toplevel(self.root.root) # new dialog window, master=mw
self.dialogwin = popup # [1.4] for Dialog.run()
fc.try_set_window_icon(popup) # replace red tk window icon [1.2]
popup.title('%s %.1f - Find: Matches' % (PROGRAM, VERSION))
fc.trybgconfig(popup, Configs.eventdialogbg)
# sort descending = newest first
matches.sort(reverse=True)
# format matches for display and opens
listtxts, gototxts = [], []
for ((y, m, d), calname) in matches:
listtxts.append(f'{y:4}-{calendar.month_abbr[m]}-{d:02} ({calname})')
gototxts.append(f'{m}/{d}/{y}') # 'mm/dd/yyyy', size+pad not needed
# label at top (default colors/font)
topmsg = Label(popup, text=f'{len(gototxts)} '
f"match{'es' if len(gototxts) > 1 else ''} " # py<3.12
f'for "{searchinfo[0]}" in {searchinfo[1]}')
topmsg.pack(side=TOP, fill=X)
# draw selection list in this dialog window
ScrolledMatchesList(popup, monthwindow, listtxts, gototxts, True, Configs.eventdialogfont)
#====================================================================================
# Scrolled-list widget [3.0]
#====================================================================================
class ScrolledMatchesList(Frame):
"""
Selections list inside matches dialog.
Taken from PP4E's Gui.Tour and adapted.
And custom version of ./scrolledlist.py.
"""
def __init__(self, parent, monthwindow, listtxts, gototxts, horizscroll, listfont):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH) # make me expandable
self.gototxts = gototxts
self.monthwindow = monthwindow
self.parent = parent
self.make_widgets(listtxts, horizscroll, listfont)
def make_widgets(self, listtxts, horizscroll, listfont):
sbar = Scrollbar(self)
list = Listbox(self, relief=RIDGE, border=2, width=30) # else width is 20
if listfont:
list.config(font=listfont)
if horizscroll:
hbar = Scrollbar(self, orient='horizontal')
sbar.config(command=list.yview) # xlink sbar and list
list.config(yscrollcommand=sbar.set) # move one moves other
if horizscroll:
hbar.config(command=list.xview)
list.config(xscrollcommand=hbar.set)
sbar.pack(side=RIGHT, fill=Y) # pack first=clip last
if horizscroll:
hbar.pack(side=BOTTOM, fill=X)
list.pack(side=LEFT, expand=YES, fill=BOTH) # list clipped first
for (pos, label) in enumerate(listtxts): # add to listbox, 0..N-1
list.insert(pos, f' {label} ') # or insert(END,label)
selector = '<Double-1>' if Configs.clickmode == 'mouse' else '<Button-1>'
list.bind(selector, self.onListSelect) # set event handler
#list.selection_set(0) # no: fix tap1 fail for 'mouse'
self.listbox = list
#use defaults
#list.config(selectmode=SINGLE, setgrid=1) # select,resize modes
def onListSelect(self, event):
# failed in single-tap mode: not yet selected
"""CUT
index = self.listbox.curselection() # on list double-click
#label = self.listbox.get(index) # fetch selection text
gototxt = self.gototxts[index[0]] # [3.0] just index here: (6,)
CUT"""
# works for both single- and double-tap modes
# but fails for double if do focus_force below...
choice = self.listbox.nearest(event.y) # on list single-click
gototxt = self.gototxts[choice] # [3.0] just index here: 6
#self.update() # doesn't help
self.monthwindow.onGoToDate(datetxt=gototxt)
self.monthwindow.root.lift()
#self.monthwindow.root.focus_force()
#====================================================================================
# Self-test [3.0]
#====================================================================================
if __name__ == '__main__':
root = Tk()
Button(root, text='Open', command=lambda: FindInputsDialog(root)).pack()
mainloop()