# -*- coding: utf-8 -*-
"""
tkfilebrowser - Alternative to filedialog for Tkinter
Copyright 2017-2018 Juliette Monsel <j_4321@protonmail.com>
tkfilebrowser is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
tkfilebrowser is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Main class
"""
import psutil
from re import search
from subprocess import check_output
from os import walk, mkdir, stat, access, W_OK, listdir
from os import name as OSNAME
from os.path import sep as SEP
from os.path import exists, join, getmtime, realpath, split, expanduser, \
abspath, isabs, splitext, dirname, getsize, isdir, isfile, islink
try:
from os import scandir
SCANDIR = True
except ImportError:
SCANDIR = False
import traceback
import tkfilebrowser.constants as cst
from tkfilebrowser.constants import unquote, tk, ttk, key_sort_files, \
get_modification_date, display_modification_date, display_size
from tkfilebrowser.autoscrollbar import AutoScrollbar
from tkfilebrowser.path_button import PathButton
from tkfilebrowser.tooltip import TooltipTreeWrapper
from tkfilebrowser.recent_files import RecentFiles
if OSNAME == 'nt':
from win32com.shell import shell, shellcon
_ = cst._
class Stats:
"""Fake stats class to create dummy stats for broken links."""
def __init__(self, **kwargs):
self._prop = kwargs
def __getattr__(self, attr):
if attr not in self._prop:
raise AttributeError("Stats has no attribute %s." % attr)
else:
return self._prop[attr]
[docs]class FileBrowser(tk.Toplevel):
"""Filebrowser dialog class."""
[docs] def __init__(self, parent, initialdir="", initialfile="", mode="openfile",
multiple_selection=False, defaultext="", title="Filebrowser",
filetypes=[], okbuttontext=None, cancelbuttontext=_("Cancel"),
foldercreation=True, **kw):
"""
Create a filebrowser dialog.
Arguments:
parent : Tk or Toplevel instance
parent window
title : str
the title of the filebrowser window
initialdir : str
directory whose content is initially displayed
initialfile : str
initially selected item (just the name, not the full path)
mode : str
kind of dialog: "openfile", "opendir" or "save"
multiple_selection : bool
whether to allow multiple items selection (open modes only)
defaultext : str (e.g. '.png')
extension added to filename if none is given (default is none)
filetypes : list :obj:`[("name", "*.ext1|*.ext2|.."), ...]`
only the files of given filetype will be displayed,
e.g. to allow the user to switch between displaying only PNG or JPG
pictures or dispalying all files:
:obj:`filtypes=[("Pictures", "\*.png|\*.PNG|\*.jpg|\*.JPG'), ("All files", "\*")]`
okbuttontext : str
text displayed on the validate button, default is "Open".
cancelbuttontext : str
text displayed on the button that cancels the selection, default is "Cancel".
foldercreation : bool
enable the user to create new folders if True (default)
"""
# compatibility with tkinter.filedialog arguments: the parent window is called 'master'
if 'master' in kw and parent is None:
parent = kw.pop('master')
if 'defaultextension' in kw and not defaultext:
defaultext = kw.pop('defaultextension')
tk.Toplevel.__init__(self, parent, **kw)
# python version compatibility
if SCANDIR:
self.display_folder = self._display_folder_scandir
else:
self.display_folder = self._display_folder_walk
# keep track of folders to be able to move backward/foreward in history
if initialdir:
self.history = [initialdir]
else:
self.history = [expanduser("~")]
self._hist_index = -1
self.transient(parent)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.quit)
self.title(title)
self.rowconfigure(2, weight=1)
self.columnconfigure(0, weight=1)
self.mode = mode
self.result = ""
self.foldercreation = foldercreation
# hidden files/folders visibility
self.hide = False
# hidden items
self.hidden = ()
# --- style
style = ttk.Style(self)
bg = style.lookup("TFrame", "background")
style.layout("right.tkfilebrowser.Treeview.Item",
[('Treeitem.padding',
{'children':
[('Treeitem.image', {'side': 'left', 'sticky': ''}),
('Treeitem.focus',
{'children':
[('Treeitem.text',
{'side': 'left', 'sticky': ''})],
'side': 'left',
'sticky': ''})],
'sticky': 'nswe'})])
style.layout("left.tkfilebrowser.Treeview.Item",
[('Treeitem.padding',
{'children':
[('Treeitem.image', {'side': 'left', 'sticky': ''}),
('Treeitem.focus',
{'children':
[('Treeitem.text', {'side': 'left', 'sticky': ''})],
'side': 'left',
'sticky': ''})],
'sticky': 'nswe'})])
style.configure("right.tkfilebrowser.Treeview", font="TkDefaultFont")
style.configure("right.tkfilebrowser.Treeview.Item", padding=2)
style.configure("right.tkfilebrowser.Treeview.Heading",
font="TkDefaultFont")
style.configure("left.tkfilebrowser.Treeview.Heading",
font="TkDefaultFont")
style.configure("left.tkfilebrowser.Treeview.Item", padding=2)
style.configure("listbox.tkfilebrowser.TFrame", background="white", relief="sunken")
field_bg = style.lookup("TEntry", "fieldbackground", default='white')
tree_field_bg = style.lookup("ttk.Treeview", "fieldbackground",
default='white')
fg = style.lookup('TLabel', 'foreground', default='black')
active_bg = style.lookup('TButton', 'background', ('active',))
sel_bg = style.lookup('Treeview', 'background', ('selected',))
sel_fg = style.lookup('Treeview', 'foreground', ('selected',))
self.option_add('*TCombobox*Listbox.selectBackground', sel_bg)
self.option_add('*TCombobox*Listbox.selectForeground', sel_fg)
style.map('types.tkfilebrowser.TCombobox', foreground=[], fieldbackground=[])
style.configure('types.tkfilebrowser.TCombobox', lightcolor=bg,
fieldbackground=bg)
style.configure('types.tkfilebrowser.TCombobox.Item', background='red')
style.configure("left.tkfilebrowser.Treeview", background=active_bg,
font="TkDefaultFont",
fieldbackground=active_bg)
self.configure(background=bg)
# path button style
style.configure("path.tkfilebrowser.TButton", padding=2)
selected_bg = style.lookup("TButton", "background", ("pressed",))
map_bg = style.map("TButton", "background")
map_bg.append(("selected", selected_bg))
style.map("path.tkfilebrowser.TButton",
background=map_bg,
font=[("selected", "TkDefaultFont 9 bold")])
# tooltip style
style.configure('tooltip.tkfilebrowser.TLabel', background='black',
foreground='white')
# --- images
self.im_file = cst.PhotoImage(file=cst.IM_FILE, master=self)
self.im_folder = cst.PhotoImage(file=cst.IM_FOLDER, master=self)
self.im_desktop = cst.PhotoImage(file=cst.IM_DESKTOP, master=self)
self.im_file_link = cst.PhotoImage(file=cst.IM_FILE_LINK, master=self)
self.im_link_broken = cst.PhotoImage(file=cst.IM_LINK_BROKEN, master=self)
self.im_folder_link = cst.PhotoImage(file=cst.IM_FOLDER_LINK, master=self)
self.im_new = cst.PhotoImage(file=cst.IM_NEW, master=self)
self.im_drive = cst.PhotoImage(file=cst.IM_DRIVE, master=self)
self.im_home = cst.PhotoImage(file=cst.IM_HOME, master=self)
self.im_recent = cst.PhotoImage(file=cst.IM_RECENT, master=self)
self.im_recent_24 = cst.PhotoImage(file=cst.IM_RECENT_24, master=self)
# --- filetypes
self.filetype = tk.StringVar(self)
self.filetypes = {}
if filetypes:
for name, exts in filetypes:
if name not in self.filetypes:
self.filetypes[name] = []
self.filetypes[name] = r'%s$' % exts.strip().replace('.', '\.').replace('*', '.*')
values = list(self.filetypes.keys())
w = max([len(f) for f in values] + [5])
b_filetype = ttk.Combobox(self, textvariable=self.filetype,
state='readonly',
style='types.tkfilebrowser.TCombobox',
values=values,
width=w)
b_filetype.grid(row=3, sticky="e", padx=10, pady=(4, 0))
self.filetype.set(filetypes[0][0])
try:
self.filetype.trace_add('write', lambda *args: self._change_filetype())
except AttributeError:
self.filetype.trace('w', lambda *args: self._change_filetype())
else:
self.filetypes[""] = r".*$"
# --- recent files
self._recent_files = RecentFiles(cst.RECENT_FILES, 30)
# --- path completion
self.complete = self.register(self._completion)
self.listbox_var = tk.StringVar(self)
self.listbox_frame = ttk.Frame(self, style="listbox.tkfilebrowser.TFrame", borderwidth=1)
self.listbox = tk.Listbox(self.listbox_frame,
listvariable=self.listbox_var,
highlightthickness=0,
borderwidth=0,
background=field_bg,
foreground=fg,
selectforeground=sel_fg,
selectbackground=sel_bg)
self.listbox.pack(expand=True, fill="x")
# --- path bar
self.path_var = tk.StringVar(self)
frame_bar = ttk.Frame(self)
frame_bar.columnconfigure(0, weight=1)
frame_bar.grid(row=1, sticky="ew", pady=10, padx=10)
frame_recent = ttk.Frame(frame_bar)
frame_recent.grid(row=0, column=0, sticky="w")
ttk.Label(frame_recent, image=self.im_recent_24).pack(side="left")
ttk.Label(frame_recent, text=_("Recently used"),
font="TkDefaultFont 9 bold").pack(side="left", padx=4)
self.path_bar = ttk.Frame(frame_bar)
self.path_bar.grid(row=0, column=0, sticky="ew")
self.path_bar_buttons = []
self.b_new_folder = ttk.Button(frame_bar, image=self.im_new,
command=self.create_folder)
if self.foldercreation:
self.b_new_folder.grid(row=0, column=1, sticky="e")
if mode == "save":
ttk.Label(self.path_bar, text=_("Folder: ")).grid(row=0, column=0)
self.defaultext = defaultext
frame_name = ttk.Frame(self)
frame_name.grid(row=0, pady=(10, 0), padx=10, sticky="ew")
ttk.Label(frame_name, text=_("Name: ")).pack(side="left")
self.entry = ttk.Entry(frame_name, validate="key",
validatecommand=(self.complete, "%d", "%S",
"%i", "%s"))
self.entry.pack(side="left", fill="x", expand=True)
if initialfile:
self.entry.insert(0, initialfile)
else:
self.multiple_selection = multiple_selection
self.entry = ttk.Entry(frame_bar, validate="key",
validatecommand=(self.complete, "%d", "%S",
"%i", "%s"))
self.entry.grid(row=1, column=0, columnspan=2, sticky="ew", padx=0,
pady=(10, 0))
self.entry.grid_remove()
paned = ttk.PanedWindow(self, orient="horizontal")
paned.grid(row=2, sticky="eswn", padx=10)
# --- left pane
left_pane = ttk.Frame(paned)
left_pane.columnconfigure(0, weight=1)
left_pane.rowconfigure(0, weight=1)
paned.add(left_pane, weight=0)
self.left_tree = ttk.Treeview(left_pane, selectmode="browse",
style="left.tkfilebrowser.Treeview")
wrapper = TooltipTreeWrapper(self.left_tree)
self.left_tree.column("#0", width=150)
self.left_tree.heading("#0", text=_("Shortcuts"), anchor="w")
self.left_tree.grid(row=0, column=0, sticky="sewn")
scroll_left = AutoScrollbar(left_pane, command=self.left_tree.yview)
scroll_left.grid(row=0, column=1, sticky="ns")
self.left_tree.configure(yscrollcommand=scroll_left.set)
# list devices and bookmarked locations
# -------- recent
self.left_tree.insert("", "end", iid="recent", text=_("Recent"),
image=self.im_recent)
wrapper.add_tooltip("recent", _("Recently used"))
# -------- devices
devices = psutil.disk_partitions(all=True if OSNAME == "nt" else False)
for d in devices:
m = d.mountpoint
if m == "/":
txt = "/"
else:
if OSNAME == 'nt':
txt = m
else:
txt = split(m)[-1]
self.left_tree.insert("", "end", iid=m, text=txt,
image=self.im_drive)
wrapper.add_tooltip(m, m)
# -------- home
home = expanduser("~")
self.left_tree.insert("", "end", iid=home, image=self.im_home,
text=split(home)[-1])
wrapper.add_tooltip(home, home)
# -------- desktop
if OSNAME == 'nt':
desktop = shell.SHGetFolderPath(0, shellcon.CSIDL_DESKTOP, None, 0)
else:
try:
desktop = check_output(['xdg-user-dir', 'DESKTOP']).decode().strip()
except Exception:
# FileNotFoundError in python3 if xdg-users-dir is not installed,
# but OSError in python2
desktop = join(home, 'Desktop')
if exists(desktop):
self.left_tree.insert("", "end", iid=desktop, image=self.im_desktop,
text=split(desktop)[-1])
wrapper.add_tooltip(desktop, desktop)
# -------- bookmarks
if OSNAME == 'nt':
bm = []
for folder in [shellcon.CSIDL_PERSONAL, shellcon.CSIDL_MYPICTURES,
shellcon.CSIDL_MYMUSIC, shellcon.CSIDL_MYVIDEO]:
try:
bm.append([shell.SHGetFolderPath(0, folder, None, 0)])
except Exception:
pass
else:
path_bm = join(home, ".config", "gtk-3.0", "bookmarks")
path_bm2 = join(home, ".gtk-bookmarks") # old location
if exists(path_bm):
with open(path_bm) as f:
bms = f.read().splitlines()
elif exists(path_bm2):
with open(path_bm) as f:
bms = f.read().splitlines()
else:
bms = []
bms = [ch.split() for ch in bms]
bm = []
for ch in bms:
ch[0] = unquote(ch[0]).replace("file://", "")
bm.append(ch)
for l in bm:
if len(l) == 1:
txt = split(l[0])[-1]
else:
txt = l[1]
self.left_tree.insert("", "end", iid=l[0],
text=txt,
image=self.im_folder)
wrapper.add_tooltip(l[0], l[0])
# --- right pane
right_pane = ttk.Frame(paned)
right_pane.columnconfigure(0, weight=1)
right_pane.rowconfigure(0, weight=1)
paned.add(right_pane, weight=1)
if mode != "save" and multiple_selection:
selectmode = "extended"
else:
selectmode = "browse"
self.right_tree = ttk.Treeview(right_pane, selectmode=selectmode,
style="right.tkfilebrowser.Treeview",
columns=("location", "size", "date"),
displaycolumns=("size", "date"))
# headings
self.right_tree.heading("#0", text=_("Name"), anchor="w",
command=lambda: self._sort_files_by_name(True))
self.right_tree.heading("location", text=_("Location"), anchor="w",
command=lambda: self._sort_by_location(False))
self.right_tree.heading("size", text=_("Size"), anchor="w",
command=lambda: self._sort_by_size(False))
self.right_tree.heading("date", text=_("Modified"), anchor="w",
command=lambda: self._sort_by_date(False))
# columns
self.right_tree.column("#0", width=250)
self.right_tree.column("location", width=100)
self.right_tree.column("size", stretch=False, width=85)
self.right_tree.column("date", width=120)
# tags
self.right_tree.tag_configure("0", background=tree_field_bg)
self.right_tree.tag_configure("1", background=active_bg)
self.right_tree.tag_configure("folder", image=self.im_folder)
self.right_tree.tag_configure("file", image=self.im_file)
self.right_tree.tag_configure("folder_link", image=self.im_folder_link)
self.right_tree.tag_configure("file_link", image=self.im_file_link)
self.right_tree.tag_configure("link_broken", image=self.im_link_broken)
if mode == "opendir":
self.right_tree.tag_configure("file", foreground="gray")
self.right_tree.tag_configure("file_link", foreground="gray")
self.right_tree.grid(row=0, column=0, sticky="eswn")
# scrollbar
self._scroll_h = AutoScrollbar(right_pane, orient='horizontal',
command=self.right_tree.xview)
self._scroll_h.grid(row=1, column=0, sticky='ew')
scroll_right = AutoScrollbar(right_pane, command=self.right_tree.yview)
scroll_right.grid(row=0, column=1, sticky="ns")
self.right_tree.configure(yscrollcommand=scroll_right.set,
xscrollcommand=self._scroll_h.set)
# --- buttons
frame_buttons = ttk.Frame(self)
frame_buttons.grid(row=4, sticky="ew", pady=10, padx=10)
if okbuttontext is None:
if mode == "save":
okbuttontext = _("Save")
else:
okbuttontext = _("Open")
ttk.Button(frame_buttons, text=okbuttontext,
command=self.validate).pack(side="right")
ttk.Button(frame_buttons, text=cancelbuttontext,
command=self.quit).pack(side="right", padx=4)
# --- key browsing entry
self.key_browse_var = tk.StringVar(self)
self.key_browse_entry = ttk.Entry(self, textvariable=self.key_browse_var,
width=10)
cst.add_trace(self.key_browse_var, "write", self._key_browse)
# list of folders/files beginning by the letters inserted in self.key_browse_entry
self.paths_beginning_by = []
self.paths_beginning_by_index = 0 # current index in the list
# --- initialization
if not initialdir:
initialdir = expanduser("~")
self.display_folder(initialdir)
initialpath = join(initialdir, initialfile)
if initialpath in self.right_tree.get_children(""):
self.right_tree.see(initialpath)
self.right_tree.selection_add(initialpath)
# --- bindings
# filetype combobox
self.bind_class('TCombobox', '<<ComboboxSelected>>',
lambda e: e.widget.selection_clear(),
add=True)
# left tree
self.left_tree.bind("<<TreeviewSelect>>", self._shortcut_select)
# right tree
self.right_tree.bind("<Double-1>", self._select)
self.right_tree.bind("<Return>", self._select)
self.right_tree.bind("<Left>", self._go_left)
if multiple_selection:
self.right_tree.bind("<Control-a>", self._right_tree_select_all)
if mode == "opendir":
self.right_tree.bind("<<TreeviewSelect>>",
self._file_selection_opendir)
elif mode == "openfile":
self.right_tree.bind("<<TreeviewSelect>>",
self._file_selection_openfile)
else:
self.right_tree.bind("<<TreeviewSelect>>",
self._file_selection_save)
self.right_tree.bind("<KeyPress>", self._key_browse_show)
# listbox
self.listbox.bind("<FocusOut>",
lambda e: self.listbox_frame.place_forget())
# path entry
self.entry.bind("<Escape>",
lambda e: self.listbox_frame.place_forget())
self.entry.bind("<Down>", self._down)
self.entry.bind("<Return>", self.validate)
self.entry.bind("<Right>", self._tab)
self.entry.bind("<Tab>", self._tab)
self.entry.bind("<Control-a>", self._select_all)
# key browse entry
self.key_browse_entry.bind("<FocusOut>", self._key_browse_hide)
self.key_browse_entry.bind("<Escape>", self._key_browse_hide)
self.key_browse_entry.bind("<Return>", self._key_browse_validate)
# main bindings
self.bind("<Control-h>", self.toggle_hidden)
self.bind("<Alt-Left>", self._hist_backward)
self.bind("<Alt-Right>", self._hist_forward)
self.bind("<Alt-Up>", self._go_to_parent)
self.bind("<Alt-Down>", self._go_to_child)
self.bind("<Button-1>", self._unpost, add=True)
self.bind("<FocusIn>", self._hide_listbox)
if mode != "save":
self.bind("<Control-l>", self.toggle_path_entry)
if self.foldercreation:
self.right_tree.bind("<Control-Shift-N>", self.create_folder)
self.update_idletasks()
self.lift()
if mode == 'save':
self.entry.selection_range(0, 'end')
self.entry.focus_set()
def _right_tree_select_all(self, event):
if self.mode == 'opendir':
tags = ['folder', 'folder_link']
else:
tags = ['file', 'file_link']
items = self.right_tree.tag_has(tags[0]) + self.right_tree.tag_has(tags[1])
self.right_tree.selection_clear()
self.right_tree.selection_set(items)
def _select_all(self, event):
"""Select all entry content."""
event.widget.selection_range(0, "end")
return "break" # suppress class binding
# --- key browsing
def _key_browse_hide(self, event):
"""Hide key browsing entry."""
if self.key_browse_entry.winfo_ismapped():
self.key_browse_entry.place_forget()
self.key_browse_entry.delete(0, "end")
def _key_browse_show(self, event):
"""Show key browsing entry."""
if event.char.isalnum() or event.char in [".", "_", "(", "-", "*", "$"]:
self.key_browse_entry.place(in_=self.right_tree, relx=0, rely=1,
y=4, x=1, anchor="nw")
self.key_browse_entry.focus_set()
self.key_browse_entry.insert(0, event.char)
def _key_browse_validate(self, event):
"""Hide key browsing entry and validate selection."""
self._key_browse_hide(event)
self.right_tree.focus_set()
self.validate()
def _key_browse(self, *args):
"""Use keyboard to browse tree."""
self.key_browse_entry.unbind("<Up>")
self.key_browse_entry.unbind("<Down>")
deb = self.key_browse_entry.get().lower()
if deb:
if self.mode == 'opendir':
children = list(self.right_tree.tag_has("folder"))
children.extend(self.right_tree.tag_has("folder_link"))
children.sort()
else:
children = self.right_tree.get_children("")
self.paths_beginning_by = [i for i in children if split(i)[-1][:len(deb)].lower() == deb]
sel = self.right_tree.selection()
if sel:
self.right_tree.selection_remove(*sel)
if self.paths_beginning_by:
self.paths_beginning_by_index = 0
self._browse_list(0)
self.key_browse_entry.bind("<Up>",
lambda e: self._browse_list(-1))
self.key_browse_entry.bind("<Down>",
lambda e: self._browse_list(1))
def _browse_list(self, delta):
"""
Navigate between folders/files with Up/Down keys.
Navigation between folders/files beginning by the letters in
self.key_browse_entry.
"""
self.paths_beginning_by_index += delta
self.paths_beginning_by_index %= len(self.paths_beginning_by)
sel = self.right_tree.selection()
if sel:
self.right_tree.selection_remove(*sel)
path = abspath(join(self.history[self._hist_index],
self.paths_beginning_by[self.paths_beginning_by_index]))
self.right_tree.see(path)
self.right_tree.selection_add(path)
# --- column sorting
def _sort_files_by_name(self, reverse):
"""Sort files and folders by (reversed) alphabetical order."""
files = list(self.right_tree.tag_has("file"))
files.extend(list(self.right_tree.tag_has("file_link")))
folders = list(self.right_tree.tag_has("folder"))
folders.extend(list(self.right_tree.tag_has("folder_link")))
files.sort(reverse=reverse)
folders.sort(reverse=reverse)
for index, item in enumerate(folders):
self.move_item(item, index)
l = len(folders)
for index, item in enumerate(files):
self.move_item(item, index + l)
self.right_tree.heading("#0",
command=lambda: self._sort_files_by_name(not reverse))
def _sort_by_location(self, reverse):
"""Sort files by location."""
l = [(self.right_tree.set(k, "location"), k) for k in self.right_tree.get_children('')]
l.sort(reverse=reverse)
for index, (val, k) in enumerate(l):
self.move_item(k, index)
self.right_tree.heading("location",
command=lambda: self._sort_by_location(not reverse))
def _sort_by_size(self, reverse):
"""Sort files by size."""
files = list(self.right_tree.tag_has("file"))
files.extend(list(self.right_tree.tag_has("file_link")))
nb_folders = len(self.right_tree.tag_has("folder"))
nb_folders += len(list(self.right_tree.tag_has("folder_link")))
files.sort(reverse=reverse, key=getsize)
for index, item in enumerate(files):
self.move_item(item, index + nb_folders)
self.right_tree.heading("size",
command=lambda: self._sort_by_size(not reverse))
def _sort_by_date(self, reverse):
"""Sort files and folders by modification date."""
files = list(self.right_tree.tag_has("file"))
files.extend(list(self.right_tree.tag_has("file_link")))
folders = list(self.right_tree.tag_has("folder"))
folders.extend(list(self.right_tree.tag_has("folder_link")))
l = len(folders)
folders.sort(reverse=reverse, key=getmtime)
files.sort(reverse=reverse, key=getmtime)
for index, item in enumerate(folders):
self.move_item(item, index)
for index, item in enumerate(files):
self.move_item(item, index + l)
self.right_tree.heading("date",
command=lambda: self._sort_by_date(not reverse))
# --- file selection
def _file_selection_save(self, event):
"""Save mode only: put selected file name in name_entry."""
sel = self.right_tree.selection()
if sel:
sel = sel[0]
tags = self.right_tree.item(sel, "tags")
if ("file" in tags) or ("file_link" in tags):
self.entry.delete(0, "end")
if self.path_bar.winfo_ismapped():
self.entry.insert(0, self.right_tree.item(sel, "text"))
else:
# recently used files
self.entry.insert(0, sel)
self.entry.selection_clear()
self.entry.icursor("end")
def _file_selection_openfile(self, event):
"""Put selected file name in path_entry if visible."""
sel = self.right_tree.selection()
if sel and self.entry.winfo_ismapped():
self.entry.delete(0, 'end')
self.entry.insert("end", self.right_tree.item(sel[0], "text"))
self.entry.selection_clear()
self.entry.icursor("end")
def _file_selection_opendir(self, event):
"""
Prevent selection of files in opendir mode and put selected folder
name in path_entry if visible.
"""
sel = self.right_tree.selection()
if sel:
for s in sel:
tags = self.right_tree.item(s, "tags")
if ("file" in tags) or ("file_link" in tags):
self.right_tree.selection_remove(s)
sel = self.right_tree.selection()
if len(sel) == 1 and self.entry.winfo_ismapped():
self.entry.delete(0, 'end')
self.entry.insert("end", self.right_tree.item(sel[0], "text"))
self.entry.selection_clear()
self.entry.icursor("end")
def _shortcut_select(self, event):
"""Selection of a shortcut (left pane)."""
sel = self.left_tree.selection()
if sel:
sel = sel[0]
if sel != "recent":
self.display_folder(sel)
else:
self._display_recents()
def _display_recents(self):
"""Display recently used files/folders."""
self.path_bar.grid_remove()
self.right_tree.configure(displaycolumns=("location", "size", "date"))
w = self.right_tree.winfo_width() - 305
if w < 0:
w = 250
self.right_tree.column("#0", width=w)
self.right_tree.column("location", stretch=False, width=100)
self.right_tree.column("size", stretch=False, width=85)
self.right_tree.column("date", width=120)
if self.foldercreation:
self.b_new_folder.grid_remove()
extension = self.filetypes[self.filetype.get()]
files = self._recent_files.get()
self.right_tree.delete(*self.right_tree.get_children(""))
i = 0
if self.mode == "opendir":
paths = []
for p in files:
if isfile(p):
p = dirname(p)
d, f = split(p)
tags = [str(i % 2)]
vals = ()
if f:
if f[0] == ".":
tags.append("hidden")
else:
f = "/"
if isdir(p):
if islink(p):
tags.append("folder_link")
else:
tags.append("folder")
vals = (p, "", get_modification_date(p))
if vals and p not in paths:
i += 1
paths.append(p)
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=vals)
else:
for p in files:
d, f = split(p)
tags = [str(i % 2)]
vals = ()
if f:
if f[0] == ".":
tags.append("hidden")
else:
f = "/"
if islink(p):
if isfile(p):
if extension == r".*$" or search(extension, f):
tags.append("file_link")
stats = stat(p)
vals = (p, display_size(stats.st_size),
display_modification_date(stats.st_mtime))
elif isdir(p):
tags.append("folder_link")
vals = (p, "", get_modification_date(p))
elif isfile(p):
if extension == r".*$" or search(extension, f):
tags.append("file")
stats = stat(p)
vals = (p, display_size(stats.st_size),
display_modification_date(stats.st_mtime))
elif isdir(p):
tags.append("folder")
vals = (p, "", get_modification_date(p))
if vals:
i += 1
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=vals)
def _select(self, event):
"""display folder content on double click / Enter, validate if file."""
sel = self.right_tree.selection()
if sel:
sel = sel[0]
tags = self.right_tree.item(sel, "tags")
if ("folder" in tags) or ("folder_link" in tags):
self.display_folder(sel)
elif self.mode != "opendir":
self.validate(event)
elif self.mode == "opendir":
self.validate(event)
def _unpost(self, event):
"""Hide self.key_browse_entry."""
if event.widget != self.key_browse_entry:
self._key_browse_hide(event)
def _hide_listbox(self, event):
"""Hide the path proposition listbox."""
if event.widget not in [self.listbox, self.entry, self.listbox_frame]:
self.listbox_frame.place_forget()
def _change_filetype(self):
"""Update view on filetype change."""
if self.path_bar.winfo_ismapped():
self.display_folder(self.history[self._hist_index])
else:
self._display_recents()
if self.mode == 'save':
filename = self.entry.get()
new_ext = self.filetypes[self.filetype.get()]
if filename and not search(new_ext, filename):
old_ext = search(r'\..+$', filename).group()
exts = [e[2:].replace('\.', '.') for e in new_ext[:-1].split('|')]
exts = [e for e in exts if search(r'\.[^\*]+$', e)]
if exts:
filename = filename.replace(old_ext, exts[0])
self.entry.delete(0, 'end')
self.entry.insert(0, filename)
# --- path completion in entries: key bindings
def _down(self, event):
"""Focus listbox on Down arrow press in entry."""
self.listbox.focus_set()
self.listbox.selection_set(0)
def _tab(self, event):
"""Go to the end of selected text and remove selection on tab press."""
self.entry = event.widget
self.entry.selection_clear()
self.entry.icursor("end")
return "break"
def _select_enter(self, event, d):
"""Change entry content on Return key press in listbox."""
self.entry.delete(0, "end")
self.entry.insert(0, join(d, self.listbox.selection_get()))
self.entry.selection_clear()
self.entry.focus_set()
self.entry.icursor("end")
def _select_mouse(self, event, d):
"""Change entry content on click in listbox."""
self.entry.delete(0, "end")
self.entry.insert(0, join(d, self.listbox.get("@%i,%i" % (event.x, event.y))))
self.entry.selection_clear()
self.entry.focus_set()
self.entry.icursor("end")
def _completion(self, action, modif, pos, prev_txt):
"""Complete the text in the path entry with existing folder/file names."""
if self.entry.selection_present():
sel = self.entry.selection_get()
txt = prev_txt.replace(sel, '')
else:
txt = prev_txt
if action == "0":
self.listbox_frame.place_forget()
txt = txt[:int(pos)] + txt[int(pos) + 1:]
elif isabs(txt) or self.path_bar.winfo_ismapped():
txt = txt[:int(pos)] + modif + txt[int(pos):]
d, f = split(txt)
if f and not (f[0] == "." and self.hide):
if not isabs(txt):
d2 = join(self.history[self._hist_index], d)
else:
d2 = d
try:
root, dirs, files = walk(d2).send(None)
dirs.sort(key=lambda n: n.lower())
l2 = []
if self.mode != "opendir":
files.sort(key=lambda n: n.lower())
extension = self.filetypes[self.filetype.get()]
if extension == r".*$":
l2.extend([i.replace(" ", "\ ") for i in files if i[:len(f)] == f])
else:
for i in files:
if search(extension, i) and i[:len(f)] == f:
l2.append(i.replace(" ", "\ "))
l2.extend([i.replace(" ", "\ ") + "/" for i in dirs if i[:len(f)] == f])
except StopIteration:
# invalid content
l2 = []
if len(l2) == 1:
self.listbox_frame.place_forget()
i = self.entry.index("insert")
self.entry.delete(0, "end")
self.entry.insert(0, join(d, l2[0]))
self.entry.selection_range(i + 1, "end")
self.entry.icursor(i + 1)
elif len(l2) > 1:
self.listbox.bind("<Return>", lambda e, arg=d: self._select_enter(e, arg))
self.listbox.bind("<Button-1>", lambda e, arg=d: self._select_mouse(e, arg))
self.listbox_var.set(" ".join(l2))
self.listbox_frame.lift()
self.listbox.configure(height=len(l2))
self.listbox_frame.place(in_=self.entry, relx=0, rely=1,
anchor="nw", relwidth=1)
else:
self.listbox_frame.place_forget()
return True
def _go_left(self, event):
"""Move focus to left pane."""
sel = self.left_tree.selection()
if not sel:
sel = expanduser("~")
else:
sel = sel[0]
self.left_tree.focus_set()
self.left_tree.focus(sel)
# --- go to parent/children folder with Alt+Up/Down
def _go_to_parent(self, event):
"""Go to parent directory."""
parent = dirname(self.path_var.get())
self.display_folder(parent, update_bar=False)
def _go_to_child(self, event):
"""Go to child directory."""
lb = [b.get_value() for b in self.path_bar_buttons]
i = lb.index(self.path_var.get())
if i < len(lb) - 1:
self.display_folder(lb[i + 1], update_bar=False)
# --- navigate in history with Alt+Left/ Right keys
def _hist_backward(self, event):
"""Navigate backward in folder selection history."""
if self._hist_index > -len(self.history):
self._hist_index -= 1
self.display_folder(self.history[self._hist_index], reset=False)
def _hist_forward(self, event):
"""Navigate forward in folder selection history."""
try:
self.left_tree.selection_remove(*self.left_tree.selection())
except TypeError:
# error raised in python 2 by empty selection
pass
if self._hist_index < -1:
self._hist_index += 1
self.display_folder(self.history[self._hist_index], reset=False)
def _update_path_bar(self, path):
"""Update the buttons in path bar."""
for b in self.path_bar_buttons:
b.destroy()
self.path_bar_buttons = []
if path == "/":
folders = []
else:
folders = path.split(SEP)
while '' in folders:
folders.remove('')
if OSNAME == 'nt':
p = folders.pop(0) + '\\'
b = PathButton(self.path_bar, self.path_var, p, text=p,
command=lambda path=p: self.display_folder(path, update_bar=False))
else:
p = "/"
b = PathButton(self.path_bar, self.path_var, p, image=self.im_drive,
command=lambda path=p: self.display_folder(path, update_bar=False))
self.path_bar_buttons.append(b)
b.grid(row=0, column=1, sticky="ns")
for i, folder in enumerate(folders):
p = join(p, folder)
b = PathButton(self.path_bar, self.path_var, p, text=folder,
command=lambda f=p: self.display_folder(f, update_bar=False),
style="path.tkfilebrowser.TButton")
self.path_bar_buttons.append(b)
b.grid(row=0, column=i + 2, sticky="ns")
def _display_folder_listdir(self, folder, reset=True, update_bar=True):
"""
Display the content of folder in self.right_tree.
Arguments:
* reset (boolean): forget all the part of the history right of self._hist_index
* update_bar (boolean): update the buttons in path bar
"""
# remove trailing / if any
folder = abspath(folder)
# reorganize display if previous was 'recent'
if not self.path_bar.winfo_ismapped():
self.path_bar.grid()
self.right_tree.configure(displaycolumns=("size", "date"))
w = self.right_tree.winfo_width() - 205
if w < 0:
w = 250
self.right_tree.column("#0", width=w)
self.right_tree.column("size", stretch=False, width=85)
self.right_tree.column("date", width=120)
if self.foldercreation:
self.b_new_folder.grid()
# reset history
if reset:
if not self._hist_index == -1:
self.history = self.history[:self._hist_index + 1]
self._hist_index = -1
self.history.append(folder)
# update path bar
if update_bar:
self._update_path_bar(folder)
self.path_var.set(folder)
# disable new folder creation if no write access
if self.foldercreation:
if access(folder, W_OK):
self.b_new_folder.state(('!disabled',))
else:
self.b_new_folder.state(('disabled',))
# clear self.right_tree
self.right_tree.delete(*self.right_tree.get_children(""))
self.right_tree.delete(*self.hidden)
self.hidden = ()
root = folder
extension = self.filetypes[self.filetype.get()]
content = listdir(folder)
i = 0
for f in content:
p = join(root, f)
if f[0] == ".":
tags = ("hidden",)
if not self.hide:
tags = (str(i % 2),)
i += 1
else:
tags = (str(i % 2),)
i += 1
if isfile(p):
if extension == r".*$" or search(extension, f):
if islink(p):
tags = tags + ("file_link",)
else:
tags = tags + ("file",)
try:
stats = stat(p)
except OSError:
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=("", "??", "??"))
else:
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=("",
display_size(stats.st_size),
display_modification_date(stats.st_mtime)))
elif isdir(p):
if islink(p):
tags = tags + ("folder_link",)
else:
tags = tags + ("folder",)
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=("", "", get_modification_date(p)))
else: # broken link
tags = tags + ("link_broken",)
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=("", "??", "??"))
items = self.right_tree.get_children("")
if items:
self.right_tree.focus_set()
self.right_tree.focus(items[0])
if self.hide:
self.hidden = self.right_tree.tag_has("hidden")
self.right_tree.detach(*self.right_tree.tag_has("hidden"))
self._sort_files_by_name(False)
def _display_folder_walk(self, folder, reset=True, update_bar=True):
"""
Display the content of folder in self.right_tree.
Arguments:
* reset (boolean): forget all the part of the history right of self._hist_index
* update_bar (boolean): update the buttons in path bar
"""
# remove trailing / if any
folder = abspath(folder)
# reorganize display if previous was 'recent'
if not self.path_bar.winfo_ismapped():
self.path_bar.grid()
self.right_tree.configure(displaycolumns=("size", "date"))
w = self.right_tree.winfo_width() - 205
if w < 0:
w = 250
self.right_tree.column("#0", width=w)
self.right_tree.column("size", stretch=False, width=85)
self.right_tree.column("date", width=120)
if self.foldercreation:
self.b_new_folder.grid()
# reset history
if reset:
if not self._hist_index == -1:
self.history = self.history[:self._hist_index + 1]
self._hist_index = -1
self.history.append(folder)
# update path bar
if update_bar:
self._update_path_bar(folder)
self.path_var.set(folder)
# disable new folder creation if no write access
if self.foldercreation:
if access(folder, W_OK):
self.b_new_folder.state(('!disabled',))
else:
self.b_new_folder.state(('disabled',))
# clear self.right_tree
self.right_tree.delete(*self.right_tree.get_children(""))
self.right_tree.delete(*self.hidden)
self.hidden = ()
try:
root, dirs, files = walk(folder).send(None)
# display folders first
dirs.sort(key=lambda n: n.lower())
i = 0
for d in dirs:
p = join(root, d)
if islink(p):
tags = ("folder_link",)
else:
tags = ("folder",)
if d[0] == ".":
tags = tags + ("hidden",)
if not self.hide:
tags = tags + (str(i % 2),)
i += 1
else:
tags = tags + (str(i % 2),)
i += 1
self.right_tree.insert("", "end", p, text=d, tags=tags,
values=("", "", get_modification_date(p)))
# display files
files.sort(key=lambda n: n.lower())
extension = self.filetypes[self.filetype.get()]
for f in files:
if extension == r".*$" or search(extension, f):
p = join(root, f)
if islink(p):
tags = ("file_link",)
else:
tags = ("file",)
try:
stats = stat(p)
except FileNotFoundError:
stats = Stats(st_size="??", st_mtime="??")
tags = ("link_broken",)
if f[0] == ".":
tags = tags + ("hidden",)
if not self.hide:
tags = tags + (str(i % 2),)
i += 1
else:
tags = tags + (str(i % 2),)
i += 1
self.right_tree.insert("", "end", p, text=f, tags=tags,
values=("",
display_size(stats.st_size),
display_modification_date(stats.st_mtime)))
items = self.right_tree.get_children("")
if items:
self.right_tree.focus_set()
self.right_tree.focus(items[0])
if self.hide:
self.hidden = self.right_tree.tag_has("hidden")
self.right_tree.detach(*self.right_tree.tag_has("hidden"))
except StopIteration:
self._display_folder_listdir(folder, reset, update_bar)
except PermissionError as e:
cst.showerror('PermissionError', str(e), master=self)
def _display_folder_scandir(self, folder, reset=True, update_bar=True):
"""
Display the content of folder in self.right_tree.
Arguments:
* reset (boolean): forget all the part of the history right of self._hist_index
* update_bar (boolean): update the buttons in path bar
"""
# remove trailing / if any
folder = abspath(folder)
# reorganize display if previous was 'recent'
if not self.path_bar.winfo_ismapped():
self.path_bar.grid()
self.right_tree.configure(displaycolumns=("size", "date"))
w = self.right_tree.winfo_width() - 205
if w < 0:
w = 250
self.right_tree.column("#0", width=w)
self.right_tree.column("size", stretch=False, width=85)
self.right_tree.column("date", width=120)
if self.foldercreation:
self.b_new_folder.grid()
# reset history
if reset:
if not self._hist_index == -1:
self.history = self.history[:self._hist_index + 1]
self._hist_index = -1
self.history.append(folder)
# update path bar
if update_bar:
self._update_path_bar(folder)
self.path_var.set(folder)
# disable new folder creation if no write access
if self.foldercreation:
if access(folder, W_OK):
self.b_new_folder.state(('!disabled',))
else:
self.b_new_folder.state(('disabled',))
# clear self.right_tree
self.right_tree.delete(*self.right_tree.get_children(""))
self.right_tree.delete(*self.hidden)
self.hidden = ()
extension = self.filetypes[self.filetype.get()]
try:
content = sorted(scandir(folder), key=key_sort_files)
i = 0
tags_array = [["folder", "folder_link"],
["file", "file_link"]]
for f in content:
b_file = f.is_file()
name = f.name
try:
stats = f.stat()
tags = (tags_array[b_file][f.is_symlink()],)
except FileNotFoundError:
stats = Stats(st_size="??", st_mtime="??")
tags = ("link_broken",)
if name[0] == '.':
tags = tags + ("hidden",)
if not self.hide:
tags = tags + (str(i % 2),)
i += 1
else:
tags = tags + (str(i % 2),)
i += 1
if b_file:
if extension == r".*$" or search(extension, name):
self.right_tree.insert("", "end", f.path, text=name, tags=tags,
values=("",
display_size(stats.st_size),
display_modification_date(stats.st_mtime)))
else:
self.right_tree.insert("", "end", f.path, text=name, tags=tags,
values=("", "",
display_modification_date(stats.st_mtime)))
items = self.right_tree.get_children("")
if items:
self.right_tree.focus_set()
self.right_tree.focus(items[0])
if self.hide:
self.hidden = self.right_tree.tag_has("hidden")
self.right_tree.detach(*self.right_tree.tag_has("hidden"))
except FileNotFoundError:
self._display_folder_scandir(expanduser('~'), reset=True, update_bar=True)
except PermissionError as e:
cst.showerror('PermissionError', str(e), master=self)
[docs] def create_folder(self, event=None):
"""Create new folder in current location."""
def ok(event):
name = e.get()
e.destroy()
if name:
folder = join(path, name)
try:
mkdir(folder)
except Exception:
# show exception to the user (typically PermissionError or FileExistsError)
cst.showerror(_("Error"), traceback.format_exc())
self.display_folder(path)
def cancel(event):
e.destroy()
self.right_tree.delete("tmp")
path = self.path_var.get()
if self.path_bar.winfo_ismapped() and access(path, W_OK):
self.right_tree.insert("", 0, "tmp", tags=("folder", "1"))
self.right_tree.see("tmp")
e = ttk.Entry(self)
x, y, w, h = self.right_tree.bbox("tmp", column="#0")
e.place(in_=self.right_tree, x=x + 24, y=y,
width=w - x - 4)
e.bind("<Return>", ok)
e.bind("<Escape>", cancel)
e.bind("<FocusOut>", cancel)
e.focus_set()
[docs] def move_item(self, item, index):
"""Move item to index and update dark/light line alternance."""
self.right_tree.move(item, "", index)
tags = [t for t in self.right_tree.item(item, 'tags')
if t not in ['1', '0']]
tags.append(str(index % 2))
self.right_tree.item(item, tags=tags)
[docs] def toggle_path_entry(self, event):
"""Toggle visibility of path entry."""
if self.entry.winfo_ismapped():
self.entry.grid_remove()
self.entry.delete(0, "end")
else:
self.entry.grid()
self.entry.focus_set()
[docs] def toggle_hidden(self, event=None):
"""Toggle the visibility of hidden files/folders."""
if self.hide:
self.hide = False
for item in reversed(self.hidden):
self.right_tree.move(item, "", 0)
self.hidden = ()
else:
self.hide = True
self.hidden = self.right_tree.tag_has("hidden")
self.right_tree.detach(*self.right_tree.tag_has("hidden"))
# restore color alternance
for i, item in enumerate(self.right_tree.get_children("")):
tags = [t for t in self.right_tree.item(item, 'tags')
if t not in ['1', '0']]
tags.append(str(i % 2))
self.right_tree.item(item, tags=tags)
[docs] def get_result(self):
"""Return selection."""
return self.result
[docs] def quit(self):
"""Destroy dialog."""
self.destroy()
if self.result:
if isinstance(self.result, tuple):
for path in self.result:
self._recent_files.add(path)
else:
self._recent_files.add(self.result)
def _validate_save(self):
"""Validate selection in save mode."""
name = self.entry.get()
if name:
ext = splitext(name)[-1]
if not ext and not name[-1] == "/":
# append default extension if none given
name += self.defaultext
if isabs(name):
# name is an absolute path
if exists(dirname(name)):
rep = True
if isfile(name):
rep = cst.askyesnocancel(_("Confirmation"),
_("The file {file} already exists, do you want to replace it?").format(file=name),
icon="warning")
elif isdir(name):
# it's a directory
rep = False
self.display_folder(name)
path = name
else:
# the path is invalid
rep = False
elif self.path_bar.winfo_ismapped():
# we are not in the "recent files"
path = join(self.history[self._hist_index], name)
rep = True
if exists(path):
if isfile(path):
rep = cst.askyesnocancel(_("Confirmation"),
_("The file {file} already exists, do you want to replace it?").format(file=name),
icon="warning")
else:
# it's a directory
rep = False
self.display_folder(path)
elif not exists(dirname(path)):
# the path is invalid
rep = False
else:
# recently used file
sel = self.right_tree.selection()
if len(sel) == 1:
path = sel[0]
tags = self.right_tree.item(sel, "tags")
if ("folder" in tags) or ("folder_link" in tags):
rep = False
self.display_folder(path)
elif isfile(path):
rep = cst.askyesnocancel(_("Confirmation"),
_("The file {file} already exists, do you want to replace it?").format(file=name),
icon="warning")
else:
rep = True
else:
rep = False
if rep:
self.result = realpath(path)
self.quit()
elif rep is None:
self.quit()
else:
self.entry.delete(0, "end")
self.entry.focus_set()
def _validate_from_entry(self):
"""
Validate selection from path entry in open mode.
Return False if the entry is empty, True otherwise.
"""
name = self.entry.get()
if name: # get file/folder from entry
if not isabs(name) and self.path_bar.winfo_ismapped():
# we are not in the "recent files"
name = join(self.history[self._hist_index], name)
if not exists(name):
self.entry.delete(0, "end")
elif self.mode == "openfile":
if isfile(name):
if self.multiple_selection:
self.result = (realpath(name),)
else:
self.result = realpath(name)
self.quit()
else:
self.display_folder(name)
self.entry.grid_remove()
self.entry.delete(0, "end")
else:
if self.multiple_selection:
self.result = (realpath(name),)
else:
self.result = realpath(name)
self.quit()
return True
else:
return False
def _validate_multiple_sel(self):
"""Validate selection in open mode with multiple selection."""
sel = self.right_tree.selection()
if self.mode == "opendir":
if sel:
self.result = tuple(realpath(s) for s in sel)
else:
self.result = (realpath(self.history[self._hist_index]),)
self.quit()
else: # mode == openfile
if len(sel) == 1:
sel = sel[0]
tags = self.right_tree.item(sel, "tags")
if ("folder" in tags) or ("folder_link" in tags):
self.display_folder(sel)
else:
self.result = (realpath(sel),)
self.quit()
elif len(sel) > 1:
files = tuple(s for s in sel if "file" in self.right_tree.item(s, "tags"))
files = files + tuple(realpath(s) for s in sel if "file_link" in self.right_tree.item(s, "tags"))
if files:
self.result = files
self.quit()
else:
self.right_tree.selection_remove(*sel)
def _validate_single_sel(self):
"""Validate selection in open mode without multiple selection."""
sel = self.right_tree.selection()
if self.mode == "openfile":
if len(sel) == 1:
sel = sel[0]
tags = self.right_tree.item(sel, "tags")
if ("folder" in tags) or ("folder_link" in tags):
self.display_folder(sel)
else:
self.result = realpath(sel)
self.quit()
else: # mode is "opendir"
if len(sel) == 1:
self.result = realpath(sel[0])
else:
self.result = realpath(self.history[self._hist_index])
self.quit()
[docs] def validate(self, event=None):
"""Validate selection and store it in self.results if valid."""
if self.mode == "save":
self._validate_save()
else:
validation = self._validate_from_entry()
if not validation:
# the entry is empty
if self.multiple_selection:
self._validate_multiple_sel()
else:
self._validate_single_sel()