Files
Phoenix/wx/lib/agw/thumbnailctrl.py
2021-08-07 18:55:49 +02:00

530 lines
18 KiB
Python

# --------------------------------------------------------------------------- #
# THUMBNAILCTRL Control wxPython IMPLEMENTATION
# Python Code By:
#
# Andrea Gavana And Peter Damoc, @ 12 Dec 2005
# Latest Revision: 27 Dec 2012, 21.00 GMT
# andrea.gavana@gmail.com
# andrea.gavana@maerskoil.com
#
# Refactored and reorganized: Michael Eager (eager@eagercon.com), 26 Sep 2020
#
# Tags: phoenix-port, documented, unittest, py3-port
#
# End Of Comments
# --------------------------------------------------------------------------- #
"""
:class:`~wx.lib.agw.thumbnailctrl.ThumbnailCtrl` is a widget that can be used to display a series of images in
a "thumbnail" format.
Description
===========
:class:`ThumbnailCtrl` is a widget that can be used to display a series of images in
a "thumbnail" format; it mimics, for example, the windows explorer behavior
when you select the "view thumbnails" option.
Basically, by specifying a folder that contains some image files, the files
in the folder are displayed as miniature versions of the actual images in
a :class:`ScrolledWindow`.
The code is partly based on `wxVillaLib`, a wxWidgets implementation of this
control. However, :class:`ThumbnailCtrl` wouldn't have been so fast and complete
without the suggestions and hints from Peter Damoc. So, if he accepts the
mention, this control is his as much as mine.
:class:`ThumbnailCtrl` is more of a demo application than a widget.
The :class:`ScrolledThumbnail` is a freestanding widget, accepting a list of
:class:`Thumb` objects which represent files and which returns a thumbnail
on request. :class:`Thumb` can be extended by the user to provide thumbnails
for other data types, such as text, audio, video, or PDF files.
Usage
=====
Usage example::
import os
import wx
import wx.lib.agw.thumbnailctrl as TC
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "ThumbnailCtrl Demo")
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
thumbnail = TC.ThumbnailCtrl(panel, imagehandler=TC.NativeImageHandler)
sizer.Add(thumbnail, 1, wx.EXPAND | wx.ALL, 10)
thumbnail.ShowDir(os.getcwd())
panel.SetSizer(sizer)
# our normal wxApp-derived class, as usual
app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
Methods and Settings
====================
With :class:`ThumbnailCtrl` you can:
- Create different thumbnail outlines (none, images only, full, etc...);
- Highlight thumbnails on mouse hovering;
- Show/hide file names below thumbnails;
- Change thumbnail caption font;
- Zoom in/out thumbnails (done via ``Ctrl`` key + mouse wheel or with ``+`` and ``-`` chars,
with zoom factor value customizable);
- Rotate thumbnails with these specifications:
a) ``d`` key rotates 90 degrees clockwise;
b) ``s`` key rotates 90 degrees counter-clockwise;
c) ``a`` key rotates 180 degrees.
- Delete files/thumbnails (via the ``del`` key);
- Drag and drop thumbnails from :class:`ThumbnailCtrl` to whatever application you want;
- Use local (when at least one thumbnail is selected) or global (no need for
thumbnail selection) popup menus;
- Show/hide a :class:`ComboBox` at the top of :class:`ThumbnailCtrl`: this combobox contains
working directory information and it has history entries;
- possibility to show tooltips on thumbnails, which display file information
(like file name, size, last modification date and thumbnail size).
:note: Using highlight thumbnails on mouse hovering may be slow on slower
computers.
Window Styles
=============
`No particular window styles are available for this class.`
Events Processing
=================
This class processes the following events:
================================== ==================================================
Event Name Description
================================== ==================================================
``EVT_THUMBNAILS_DCLICK`` The user has double-clicked on a thumbnail.
``EVT_THUMBNAILS_POINTED`` The mouse cursor is hovering over a thumbnail.
``EVT_THUMBNAILS_SEL_CHANGED`` The user has changed the selected thumbnail.
``EVT_THUMBNAILS_THUMB_CHANGED`` The thumbnail of an image has changed. Used internally.
``EVT_THUMBNAILS_CHAR`` A key was typed in the widget
================================== ==================================================
License And Version
===================
:class:`ThumbnailCtrl` is distributed under the wxPython license.
Latest revision: Michael Eager @ 26 Sep 2020
Version 1.0
"""
#----------------------------------------------------------------------
# Beginning Of ThumbnailCtrl wxPython Code
#----------------------------------------------------------------------
import wx
import os
import time
from wx.lib.agw.scrolledthumbnail import (ScrolledThumbnail, EVT_THUMBNAILS_CHAR,
PILImageHandler, NativeImageHandler, Thumb)
# Image File Name Extensions: Am I Missing Some Extensions Here?
extensions = [".jpeg", ".jpg", ".bmp", ".png", ".ico", ".tiff", ".ani", ".cur", ".gif",
".iff", ".icon", ".pcx", ".tif", ".xpm", ".xbm", ".mpeg", ".mpg", ".mov"]
#-----------------------------------------------------------------------------
# PATH & FILE FILLING (OS INDEPENDENT)
#-----------------------------------------------------------------------------
def opj(path):
"""
Convert paths to the platform-specific separator.
:param `path`: the path to convert.
"""
strs = os.path.join(*tuple(path.split('/')))
# HACK: on Linux, a leading / gets lost...
if path.startswith('/'):
strs = '/' + strs
return strs
#-----------------------------------------------------------------------------
# Different Outline On Thumb Selection:
# THUMB_OUTLINE_NONE: No Outline Drawn On Selection
# THUMB_OUTLINE_FULL: Full Outline Drawn On Selection
# THUMB_OUTLINE_RECT: Only Maximum Image Rect Outlined On Selection
# THUMB_OUTLINE_IMAGE: Only Image Rect Outlined On Selection
THUMB_OUTLINE_NONE = 0
""" No outline drawn on selection. """
THUMB_OUTLINE_FULL = 1
""" Full outline drawn on selection. """
THUMB_OUTLINE_RECT = 2
""" Only the maximum image rectangle outlined on selection. """
THUMB_OUTLINE_IMAGE = 4
""" Only the image rectangle outlined on selection. """
# Options For Filtering Files -- Not used, provided for backward-compatiblity
THUMB_FILTER_IMAGES = 1
TIME_FMT = '%d %b %Y, %H:%M:%S'
""" Time format string for the :class:`Thumb` representation on screen. """
# ---------------------------------------------------------------------------- #
# Class ScrolledTextDialog
# Create dialog with scrolling text box
# ---------------------------------------------------------------------------- #
class ScrolledTextDialog(wx.Dialog):
"""
:class:`ScrolledTextDialog` is a dialog widget which displays a scrolled
text box and OK/CANCEL buttons.
"""
def __init__(self, parent, title="Title", msg="Message"):
wx.Dialog.__init__(self, parent, title=title)
text = wx.TextCtrl(self, -1, msg, size=(600,240),
style=wx.TE_MULTILINE|wx.TE_DONTWRAP|wx.TE_READONLY)
sizer = wx.BoxSizer(wx.VERTICAL)
buttonsizer = wx.BoxSizer()
OKbutton = wx.Button(self, wx.ID_OK)
buttonsizer.Add(OKbutton, 0, wx.ALL, 5)
buttonsizer.Add((5,-1), 0, wx.ALL, 5)
CANCELbutton = wx.Button(self, wx.ID_CANCEL)
buttonsizer.Add(CANCELbutton, 0, wx.ALL, 5)
sizer.Add(text, 0, wx.EXPAND|wx.ALL, 5)
sizer.Add(buttonsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
self.SetSizerAndFit(sizer)
# ---------------------------------------------------------------------------- #
# Class ThumbnailCtrl
# Auxiliary Class, All Useful Methods Are Defined On ScrolledThumbnail Class.
# ---------------------------------------------------------------------------- #
class ThumbnailCtrl(wx.Panel):
"""
:class:`ThumbnailCtrl` is a widget that can be used to display a series of images in
a "thumbnail" format.
"""
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
size=wx.DefaultSize, thumboutline=THUMB_OUTLINE_IMAGE,
thumbfilter=None, # Ignored, included for backward compatibility
imagehandler=PILImageHandler):
"""
Default class constructor.
:param `parent`: parent window. Must not be ``None``;
:param `id`: window identifier. A value of -1 indicates a default value;
:param `pos`: the control position. A value of (-1, -1) indicates a default position,
chosen by either the windowing system or wxPython, depending on platform;
:param `size`: the control size. A value of (-1, -1) indicates a default size,
chosen by either the windowing system or wxPython, depending on platform;
:param `thumboutline`: outline style for :class:`ThumbnailCtrl`, which may be:
=========================== ======= ==================================
Outline Flag Value Description
=========================== ======= ==================================
``THUMB_OUTLINE_NONE`` 0 No outline is drawn on selection
``THUMB_OUTLINE_FULL`` 1 Full outline (image+caption) is drawn on selection
``THUMB_OUTLINE_RECT`` 2 Only thumbnail bounding rectangle is drawn on selection (default)
``THUMB_OUTLINE_IMAGE`` 4 Only image bounding rectangle is drawn.
=========================== ======= ==================================
:param `thumbfilter`: filter for image/video/audio files. Ignored.
:param `imagehandler`: can be :class:`PILImageHandler` if PIL is installed (faster), or
:class:`NativeImageHandler` which only uses wxPython image methods.
"""
wx.Panel.__init__(self, parent, id, pos, size)
self._sizer = wx.BoxSizer(wx.VERTICAL)
self._combo = wx.ComboBox(self, -1, style=wx.CB_DROPDOWN | wx.CB_READONLY)
self._scrolled = ScrolledThumbnail(self, -1, thumboutline=thumboutline,
imagehandler = imagehandler)
subsizer = wx.BoxSizer(wx.HORIZONTAL)
subsizer.Add((3, 0), 0)
subsizer.Add(self._combo, 0, wx.EXPAND | wx.TOP, 3)
subsizer.Add((3, 0), 0)
self._sizer.Add(subsizer, 0, wx.EXPAND | wx.ALL, 3)
self._sizer.Add(self._scrolled, 1, wx.EXPAND)
self.SetSizer(self._sizer)
self._sizer.Show(0, False)
self._sizer.Layout()
methods = ["GetSelectedItem", "GetPointed", "GetHighlightPointed", "SetHighlightPointed",
"SetThumbOutline", "GetThumbOutline", "GetPointedItem", "GetItem",
"GetItemCount", "GetThumbWidth", "GetThumbHeight", "GetThumbBorder",
"SetPopupMenu", "GetPopupMenu", "SetGlobalPopupMenu",
"GetGlobalPopupMenu", "SetSelectionColour", "GetSelectionColour",
"EnableDragging", "SetThumbSize", "GetThumbSize", "ShowThumbs",
"SetSelection", "GetSelection", "SetZoomFactor",
"GetZoomFactor", "SetCaptionFont", "GetCaptionFont", "GetItemIndex",
"InsertItem", "RemoveItemAt", "IsSelected", "Rotate", "ZoomIn", "ZoomOut",
"EnableToolTips", "GetThumbInfo", "SetDropShadow", "GetDropShadow"]
for method in methods:
setattr(self, method, getattr(self._scrolled, method))
self._combochoices = []
self._showcombo = False
self._subsizer = subsizer
self._combo.Bind(wx.EVT_COMBOBOX, self.OnComboBox)
self.Bind(EVT_THUMBNAILS_CHAR, self.OnThumbChar)
self._imagehandler = imagehandler
def ShowComboBox(self, show=True):
"""
Shows/Hide the top folder :class:`ComboBox`.
:param `show`: ``True`` to show the combobox, ``False`` otherwise.
"""
if show:
self._showcombo = True
self._sizer.Show(0, True)
self._sizer.Layout()
else:
self._showcombo = False
self._sizer.Show(0, False)
self._sizer.Layout()
self._scrolled.Refresh()
def GetShowComboBox(self):
""" Returns whether the folder combobox is shown. """
return self._showcombo
def OnComboBox(self, event):
"""
Handles the ``wx.EVT_COMBOBOX`` for the folder combobox.
:param `event`: a :class:`CommandEvent` event to be processed.
"""
dirs = self._combo.GetValue()
if os.path.isdir(opj(dirs)):
self._scrolled.ShowDir(opj(dirs))
event.Skip()
def RecreateComboBox(self, newdir):
"""
Recreates the folder combobox every time a new directory is explored.
:param `newdir`: the new folder to be explored.
"""
newdir = newdir.strip()
if opj(newdir) in self._combochoices:
return
self.Freeze()
self._sizer.Detach(0)
self._subsizer.Detach(1)
self._subsizer.Destroy()
self._combo.Destroy()
subsizer = wx.BoxSizer(wx.HORIZONTAL)
self._combochoices.insert(0, opj(newdir))
self._combo = wx.ComboBox(self, -1, value=newdir, choices=self._combochoices,
style=wx.CB_DROPDOWN | wx.CB_READONLY)
subsizer.Add((3, 0), 0)
subsizer.Add(self._combo, 1, wx.EXPAND | wx.TOP, 3)
subsizer.Add((3, 0), 0)
self._sizer.Insert(0, subsizer, 0, wx.EXPAND | wx.ALL, 3)
self._subsizer = subsizer
self._subsizer.Layout()
if not self.GetShowComboBox():
self._sizer.Show(0, False)
self._sizer.Layout()
self._combo.Bind(wx.EVT_COMBOBOX, self.OnComboBox)
self.Thaw()
def SetBackgroundColour(self, colour):
"""
Set the background color of the widget.
:param `colour`: the color to which the background is set.
"""
return self.thumbnail_ctrl._scrolled.SetBackgroundColour(colour)
def ShowDir(self, folder):
"""
Shows thumbnails for a particular folder.
:param `folder`: a directory containing the images to thumbnail;
"""
self._dir = folder
self.RecreateComboBox(folder)
# update items
thumbs = []
filenames = self.ListDirectory(self._dir, extensions)
filenames.sort()
for files in filenames:
caption = (self._showfilenames and [files] or [""])[0]
fullfile = opj(self._dir + "/" + files)
if not os.path.isfile(fullfile):
continue
stats = os.stat(fullfile)
size = stats[6]
lastmod = time.strftime(TIME_FMT, time.localtime(stats[8]))
thumbs.append(Thumb(folder, files, caption, size, lastmod,
imagehandler=self._imagehandler))
return self.ShowThumbs(thumbs)
def GetShowDir(self):
""" Returns the working directory with images. """
return self._dir
def ListDirectory(self, directory, fileExtList):
"""
Returns list of file info objects for files of particular extensions.
:param `directory`: the folder containing the images to thumbnail;
:param `fileExtList`: a Python list of file extensions to consider.
"""
lSplitExt = os.path.splitext
return [f for f in os.listdir(directory) if lSplitExt(f)[1].lower() in fileExtList]
def ShowFileNames(self, show=True):
"""
Sets whether the user wants to show file names under the thumbnails or not.
:param `show`: ``True`` to show file names under the thumbnails, ``False`` otherwise.
"""
self._showfilenames = show
self._scrolled.ShowFileNames(show)
def OnThumbChar(self, ev):
for th in ev.thumbs:
thumb = self._scrolled.GetItem(th)
if ev.keycode == wx.WXK_DELETE:
self.DeleteFiles(ev.thumbs)
def DeleteFiles(self, thumbs):
"""
Deletes the selected thumbnails and their associated files.
.. warning:: This method deletes the original files too.
:param `thumbs`: List of indexes to thumbnails.
"""
msg = ""
for index in thumbs:
thumb = self._scrolled.GetItem(index)
msg += thumb.GetFileName() + '\n'
dlg = ScrolledTextDialog(self, 'Delete these files?', msg)
if dlg.ShowModal() == wx.ID_OK:
errordelete = []
count = 0
dlg.Destroy()
wx.BeginBusyCursor()
# Delete thumbnails & files from highest index to lowest
for index in reversed(thumbs):
thumb = self._scrolled.GetItem(index)
filename = thumb.GetFullFileName()
try:
os.remove(filename)
self._scrolled.RemoveItemAt(index)
count = count + 1
except:
errordelete.append(self._scrolled.GetFileName(index))
wx.EndBusyCursor()
if errordelete:
strs = "Unable to remove the following files:\n\n"
for fil in errordelete:
strs = strs + fil + "\n"
strs = strs + "\n"
strs = strs + "Please check your privileges and file permissions."
dlg = wx.MessageDialog(self, strs,
'Error in removing files',
wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()