# --------------------------------------------------------------------------- # # THUMBNAILCTRL Control wxPython IMPLEMENTATION # Python Code By: # # Andrea Gavana And Peter Damoc, @ 12 Dec 2005 # Latest Revision: 27 Dec 2012, 21.00 GMT # # # TODO List/Caveats # # 1. Thumbnail Creation/Display May Be Somewhat Improved From The Execution # Speed Point Of View; # # 2. The Implementation For wx.HORIZONTAL Style Is Still To Be Written; # # 3. I Have No Idea On How To Implement Thumbnails For Audio, Video And Other Files. # # 4. Other Ideas? # # # For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please # Write To Me At: # # andrea.gavana@gmail.com # andrea.gavana@maerskoil.com # # Or, Obviously, To The wxPython Mailing List!!! # # Tags: phoenix-port, documented, unittest, py3-port # # End Of Comments # --------------------------------------------------------------------------- # """ :class:`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. 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_CAPTION_CHANGED`` The thumbnail caption has been changed. Not used at present. ``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. ================================== ================================================== License And Version =================== :class:`ThumbnailCtrl` is distributed under the wxPython license. Latest revision: Andrea Gavana @ 27 Dec 2012, 21.00 GMT Version 0.9 """ #---------------------------------------------------------------------- # Beginning Of ThumbnailCtrl wxPython Code #---------------------------------------------------------------------- import wx import os import time import zlib import wx.lib.six as six from math import pi from wx.lib.embeddedimage import PyEmbeddedImage if six.PY3: import _thread as thread else: import thread #---------------------------------------------------------------------- # Get Default Icon/Data #---------------------------------------------------------------------- def GetMondrianData(): """ Returns a default image placeholder as a decompressed stream of characters. """ return \ b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\ \x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00qID\ ATX\x85\xed\xd6;\n\x800\x10E\xd1{\xc5\x8d\xb9r\x97\x16\x0b\xad$\x8a\x82:\x16\ o\xda\x84pB2\x1f\x81Fa\x8c\x9c\x08\x04Z{\xcf\xa72\xbcv\xfa\xc5\x08 \x80r\x80\ \xfc\xa2\x0e\x1c\xe4\xba\xfaX\x1d\xd0\xde]S\x07\x02\xd8>\xe1wa-`\x9fQ\xe9\ \x86\x01\x04\x10\x00\\(Dk\x1b-\x04\xdc\x1d\x07\x14\x98;\x0bS\x7f\x7f\xf9\x13\ \x04\x10@\xf9X\xbe\x00\xc9 \x14K\xc1<={\x00\x00\x00\x00IEND\xaeB`\x82' def GetMondrianBitmap(): """ Returns a default image placeholder as a :class:`Bitmap`. """ return wx.Bitmap(GetMondrianImage()) def GetMondrianImage(): """ Returns a default image placeholder as a :class:`Image`. """ stream = six.StringIO(GetMondrianData()) return wx.Image(stream) #---------------------------------------------------------------------- file_broken = PyEmbeddedImage( b"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAAK/INwWK6QAADU9J" b"REFUeJzNWn1QE2cefja7yRI+lAKiAgKCVIqegUYMFMsM4kfvzk7nmFrnvHGcOT3bq3P/OJ1p" b"b+zYf+r0HGxt6VVFqpYqWq9TW23RVnp6R7FXiq0UPAGhyFeQSCQJISHZ7Mf9gZuGkE02IeA9" b"M5k3u/vuu+/z/H7v7/297y4BPzh//vxfSJIsIgjC5a/eTIPn+UnHgiDIqisIAsUwTNfmzZtf" b"A8D7qk/5e3B0dPS6wsLCp/09MFTIbdO7ntR9vs5HRESgvr6+E0AlAD2AKZX8CsDzvEKtVsvq" b"aKjw7LggCO7jcJQqlQoMw6gBpAIYRAgCECGxCgDPTsohEopI4n+e5xXww1MRHkry4d1Zf9fk" b"uv90MOsCeMKfGN51pO6ZrmizKsBsWt976EjhoQ+BYMQItpSDWRNAjptPZ4xLCfl/PQTCaX2p" b"+wNhVgSYbet7tumdRXpj1ofAbFhfqn1fmHEBHpb1AYAgAudxMy4AQRB+LReuSB/MGsETMyqA" b"2WyG1WqVZQl/CDUZeuge0NvbKwwODk7bC7zryYFI/qF6QGNj42BnZ6dZ6rocQtNNhQNhxgQw" b"Go24fPnyDy0tLTcFQfDpBZ7/w2198RcIMyZAW1vb+KVLlxoGBwfr7927B4Vi8qO8CfnqbCjW" b"D4Y8MEMCCIKA77777me73d7a1tb25dDQkNXzmud/giDgdDrBsqzkuA1l7CuVSgCA0+n0y3FG" b"BBgYGMCVK1eub926dRFN04aBgYH/el73JKRUKjE4OIi+vj5QFDWtaVG0fGRkJHiex7lz5xyn" b"T59uBMBI9TWsAoidaW1tNTc1Nd1cv379zuzs7F91dHT8x+l0TooDnuju7h779ttvjdOdLlUq" b"FaKjo9HV1YX9+/d379q16+QXX3xxFsAAMHU7DAgggNw1tQiCIOByuXDt2rWbK1asUOfk5KxM" b"SUlZW19f39LX18eTJDmpbbG8cePGndra2mtDQ0NQKpUhJUPR0dHgeR6ffvqpbdeuXV+9/vrr" b"R4eGhs4A+ArA3ZAECAXd3d24dOnSD2VlZavy8vKQmppaUFdXN2gwGO561yVJEmazGR0dHV3n" b"z5//+NatW0MU5XebcgooikJMTAy6urqwb9++zp07d1ZfvXq1GsAn8+bNa6qsrEyGxJY4EGBT" b"lCAIhVR0ZhgGTqcTTqcTDocDDocDgiDgm2++0Y+MjNxbuXLl7wmCQFpaWtbcuXMje3p6fly9" b"enWyeL8gCCBJEj09PUJLS0s7z/PXWlpavigoKNhBURRYlnU/S6qMjIwEwzD47LPPbIcOHWq4" b"cuXKPwF8D+DHF154QV1cXFxpsVjiAGwGYIUPL/ArgNFoNNbV1dmtVqvdarW6LBaLY2xszOFw" b"OBiLxWKz2WxOs9lsHx0ddZhMJpvZbB7v7u7+effu3elZWVmJAJCUlBS1bt26nNbW1kaj0fj0" b"I4884iYHAHfu3Lnf2dnZDWC4oaHhH08++eRWrVZLe9bxJk9RFNRqNTo6OvDhhx92Hj16tG5k" b"ZKQBwHUAneXl5cVarfaQVqtddurUqesAUgC0BysAsWfPngOjo6ONY2NjUQzDOACMA7ADcGAi" b"sjoBuB6U4jFTWlp6Kj4+HjzPIzExEbm5uSuPHz9+csuWLfaEhIRIl8sFgiDA8zxu3rz585Yt" b"W3SpqanX9u7d+93GjRuv5eXlrRGvT+oQQUCtVsPhcODcuXO2w4cPi1ZvAnADgP3EiRN7NBrN" b"ntzcXDXHcXA6nREAJF9u+BNA6OnpuQ1gGAD5gCjrUXIPFOUflAIA7siRI8Xp6ekaYOI1lVKp" b"RGZmpqatre2QXq/v1mg0y4EJKxoMBrS1tQ2UlJQ8abPZjAD23Lhx46Pi4uI1aWlp7mEl1qdp" b"Grdu3UJNTU1nVVVV3cjIyDUAPwJof+WVVzJ0Ol1NQUHBbxMTEyHOOoEQKOKMP/jJxoIFC/6Q" b"mZlJidYTBAHp6emp2dnZCV1dXY0MwywnCAIkSUKv1zu7u7uNu3fvTh8aGtoE4Pj7779/ec2a" b"NZ0ZGRlZwC9Wt9vt+Pzzz22VlZUNV69eFa3+EwDTkSNHynJyct5ZtWpVikKhgMPhgEKhkJx2" b"gxFgSv1t27ZRUVFRFMuyZGZmZmJcXNwcpVIZT9N0tMvlWpiSklKmVConBbGkpCRq3bp1Kxsb" b"G69v3Lhxe3p6OgRBQHt7u2Hx4sVRycnJ9Ny5czO3bdv2VHV19XvNzc2frF69+pXY2FgoFAop" b"q3ds2rQp6plnnnkzLy9v99KlS8EwDDiOC2r5LSnAiRMnIubNm/dHkiSzaZqOIUkygabpGIqi" b"4iiKmkuSZDRJkiqVSkWRJKmkaZqMiIhAZGQkOI5zk+c4DnFxccjOztZWVVV9+eKLLw5nZGTM" b"YxgGzc3Ndx577LGF8fHxiI2NhU6n+111dfWZixcvnispKfmzTqebe+HCBW+rtwK4/8Ybb+Rp" b"NJojBQUFq2JiYuB0OgH8kgqLpdiXoAVobW2NfvbZZ/cWFhbOly3ngwf6yvczMzNzWJZV9Pb2" b"/lRUVLR2cHAQt2/fvrt9+3YtSZJQKBRYtmxZYXFx8ar6+vqrTU1NF7/++mtdZWXll0ajsQET" b"Qa4TE3HmBY1Gsz8/P3+Oy+Vyj3dP8nK9QFKA/v5+Cn7GfzBZmiAISE5OTiwqKsq4fft2k91u" b"X9vf3283GAyWtLS0ZNFTsrOzI0pKSsrq6+v//c477/xNr9evwMRr7ZsAhl966aUFOp3uzfz8" b"/C2LFi3C+Pj4JMLeAsjJYiUFYFk2oIRyReA4DikpKSgqKlpZW1t7saysjOvo6NAvXbo0NjEx" b"MZLneXAchzlz5kCj0TwVGxu7RK/Xt2Mihx8DwBw4cGBDbm5uRWFh4aNKpRJ2u30S8VCsD8hY" b"CwRzXqouz/OIiopCVlaWtrm5+X57e/tAS0uLPicnJzEhIcE9TnmeR05OTvJzzz33a0xMtyNa" b"rVY4fvz4a6WlpRdKSkoeFZfPYpSXIi93SyzYWWASMTmlZ/2MjIys+Pj4mI6Ojoa+vj5h/fr1" b"xaKrikIlJyfj8ccfLwNw7NVXX43TarV/LyoqWhsXF4fx8XF3TPFF2Pv/tPIAu93ul7g/+BJD" b"EAQsXLgwqrS0NLexsfE8RVHLFi1atNn7PpVKhdzc3OUvv/zyvg0bNvwmPz8/WeyPt8sHEkLO" b"anZaQyCYDUmO47BgwQIsX758VW1t7bc6nU5ISkpSeuYLHMeBYRhkZWWpd+zY8afCwsJklmXB" b"MMwUlw9EXjwOhIDL4dHRUQwMDMBms4Hn+YCuJSUKz/NQqVRYsmTJcgDzlixZsnzOnDlu1xfj" b"AMdxoGkaqampsNvtEATBvZ8olzxBEO57whIDxsbGMD4+DrVajaioKERGRoKiKMmdXn9IT09P" b"Li4ufiIxMVEHACzLThGW47gp54IhHwz8CiCSEt3P4XDA6XTCYrGApmnQNA2VSuUWwzvyescA" b"l8uF+fPnK59//vm/zp8/f6FoebF98VlSBOWS99WXkATwfrhnw+JmiEKhgEKhAEmSEDM6KTEI" b"gkBcXBwRExOTkpCQAJfLNSPkg4EsD/Algncs4Hl+ktv6+onjOS0tDTRNT1q4hJN8MCKE5AFS" b"nQx0DQAYhkFqaqpbrJkmH5YPJMJFXqFQTBmTM0E+LB5gt9slXTpU8t4E5JKXSnsDkfV+HReU" b"AJ6YLnlf1pNLXhAEmM1msCw7KSZ598/7PEEQ4DgOLMv6VSHQt8JhIS/HG/y5vV6vh9FohFqt" b"ducN3r8pxCgKJpMJvb29o/hlzzI4AUSEk7yUtfyJwTAMMjMzsXjxYr/zuuf7wbt376K8vLz/" b"7NmzlzCxpA5NgHCTl1vHFzkAPjNE774aDAaUl5f3V1RU1HAc9y9MbKr4RMA8QIr4TJP3LEU3" b"5zjOnTtItWUwGLB//36R/CVMbKXZQhbAV2dnk7zYD3G1KI537/oURbndvqKi4hTHcV9iYvd4" b"zB/HkFPh6ZL3JurvHIBJXuB9XfzG4MCBA/3vvvtujVzysgTwR2I65KVmAl/3iUtmlmUnDQGR" b"/P3793H48GH9A/Ki2wckH1AAzxjgS4yZJO/dD899A7EORVEYGRlBVVWV8b333vs4GMvLEsAT" b"oQ4Ff+Q92/YniGcMEAUQ5/ljx44Nv/XWWx9ZrdaLAJqDIR9QAO/9gHCTlxsERRFEAZRKJUwm" b"E6qrq4cPHjx4xmq11mLi1bglGPIBBfBF8mGQFyHmACaTCSdPnhx+++23z4yOjtZi4pWZKVjy" b"sgTwFiIU8v7GuVzyIsxmM06fPj188ODBjx5Y/nsAkl+jBoLsPECEryDl7ziQMHLJkyQJi8WC" b"mpqa4YqKCtHtmzAN8oCM9wJKpRIRERGyyAQi6CvwyeokRcFsNqOurm64oqLijMVimZbbT2pb" b"6gJN04TNZlNZLBYwDOOeEgNtMsqpF+y1sbEx1NbWDn/wwQdhJQ8AkmZYsWJFVEZGxtZ79+49" b"wbIsDa/VlBQJqS0of6QD3UMQBHp7e++YTKYrCIPbe8KfHyoALACwGIAqXA8MEQImPnO7A2Ak" b"nA3/D+/OyD/Ur3BPAAAAAElFTkSuQmCC") def getDataSH(): """ Return the first part of the shadow dropped behind thumbnails. """ return zlib.decompress( b'x\xda\xeb\x0c\xf0s\xe7\xe5\x92\xe2b``\xe0\xf5\xf4p\t\x02\xd2_A\x98\x83\rHvl\ \xdc\x9c\n\xa4X\x8a\x9d float(height)/imgheight: scale = float(height)/imgheight newW, newH = int(imgwidth*scale), int(imgheight*scale) if newW < 1: newW = 1 if newH < 1: newH = 1 img = img.Scale(newW, newH) bmp = img.ConvertToBitmap() self._image = img return bmp def GetOriginalImage(self): """ Returns the bitmap associated to a thumbnail, as a file name. """ original = opj((self._dir + "/" + self._filename)) return original def GetFullFileName(self): """ Returns the full filename of the thumbnail. """ return self._dir + "/" + self._filename def GetCaption(self, line): """ Returns the caption associated to a thumbnail. :param `line`: the caption line we wish to retrieve (useful for multilines caption strings). """ if line + 1 >= len(self._captionbreaks): return "" strs = self._caption return strs def GetFileSize(self): """ Returns the file size associated to a thumbnail. """ return self._filesize def GetCreationDate(self): """ Returns the file last modification date associated to a thumbnail. """ return self._lastmod def GetOriginalSize(self): """ Returns a tuple containing the original image width and height, in pixels. """ if hasattr(self, "_threadedimage"): img = self._threadedimage else: img = GetMondrianImage() if hasattr(self, "_originalsize"): imgwidth, imgheight = self._originalsize else: imgwidth, imgheight = (img.GetWidth(), img.GetHeight()) return imgwidth, imgheight def GetCaptionLinesCount(self, width): """ Returns the number of lines for the caption. :param `width`: the maximum width, in pixels, available for the caption text. """ self.BreakCaption(width) return len(self._captionbreaks) - 1 def BreakCaption(self, width): """ Breaks the caption in several lines of text (if needed). :param `width`: the maximum width, in pixels, available for the caption text. """ if len(self._captionbreaks) > 0 or width < 16: return self._captionbreaks.append(0) if len(self._caption) == 0: return pos = width//16 beg = 0 end = 0 dc = wx.MemoryDC() bmp = wx.Bitmap(10, 10) dc.SelectObject(bmp) while 1: if pos >= len(self._caption): self._captionbreaks.append(len(self._caption)) break sw, sh = dc.GetTextExtent(self._caption[beg:pos-beg]) if sw > width: if end > 0: self._captionbreaks.append(end) beg = end else: self._captionbreaks.append(pos) beg = pos pos = beg + width//16 end = 0 if pos < len(self._caption) and self._caption[pos] in [" ", "-", ",", ".", "_"]: end = pos + 1 pos = pos + 1 dc.SelectObject(wx.NullBitmap) def SetRotation(self, angle=0): """ Sets the thumbnail rotation. :param `angle`: the thumbnail rotation, in radians. """ self._rotation = angle def GetRotation(self): """ Returns the thumbnail rotation, in radians. """ return self._rotation # ---------------------------------------------------------------------------- # # 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=THUMB_FILTER_IMAGES, 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. Actually only ``THUMB_FILTER_IMAGES`` is implemented; :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, thumbfilter=thumbfilter, 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", "ShowFileNames", "SetPopupMenu", "GetPopupMenu", "SetGlobalPopupMenu", "GetGlobalPopupMenu", "SetSelectionColour", "GetSelectionColour", "EnableDragging", "SetThumbSize", "GetThumbSize", "ShowThumbs", "ShowDir", "GetShowDir", "SetSelection", "GetSelection", "SetZoomFactor", "GetZoomFactor", "SetCaptionFont", "GetCaptionFont", "GetItemIndex", "InsertItem", "RemoveItemAt", "IsSelected", "Rotate", "ZoomIn", "ZoomOut", "EnableToolTips", "GetThumbInfo", "GetOriginalImage", "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) 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() # ---------------------------------------------------------------------------- # # Class ScrolledThumbnail # This Is The Main Class Implementation # ---------------------------------------------------------------------------- # class ScrolledThumbnail(wx.ScrolledWindow): """ This is the main class implementation of :class:`ThumbnailCtrl`. """ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, thumboutline=THUMB_OUTLINE_IMAGE, thumbfilter=THUMB_FILTER_IMAGES, 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:`ScrolledThumbnail`, 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. Actually only ``THUMB_FILTER_IMAGES`` is implemented; :param `imagehandler`: can be :class:`PILImageHandler` if PIL is installed (faster), or :class:`NativeImageHandler` which only uses wxPython image methods. """ wx.ScrolledWindow.__init__(self, parent, id, pos, size) self.SetThumbSize(96, 80) self._tOutline = thumboutline self._filter = thumbfilter self._imageHandler = imagehandler() self._selected = -1 self._pointed = -1 self._labelcontrol = None self._pmenu = None self._gpmenu = None self._dragging = False self._checktext = False self._orient = THUMB_VERTICAL self._dropShadow = True self._tCaptionHeight = [] self._selectedarray = [] self._tTextHeight = 16 self._tCaptionBorder = 8 self._tOutlineNotSelected = True self._mouseeventhandled = False self._highlight = False self._zoomfactor = 1.4 self.SetCaptionFont() self._items = [] self._enabletooltip = False self._parent = parent self._selectioncolour = "#009EFF" self.grayPen = wx.Pen("#A2A2D2", 1, wx.SHORT_DASH) self.grayPen.SetJoin(wx.JOIN_MITER) self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)) t, b, s = getShadow() self.shadow = wx.MemoryDC() self.shadow.SelectObject(s) self.ShowFileNames(True) self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDClick) self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseDown) self.Bind(wx.EVT_RIGHT_UP, self.OnMouseUp) self.Bind(wx.EVT_MOTION, self.OnMouseMove) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) self.Bind(EVT_THUMBNAILS_THUMB_CHANGED, self.OnThumbChanged) self.Bind(wx.EVT_CHAR, self.OnChar) self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) self.Bind(wx.EVT_SIZE, self.OnResize) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) self.Bind(wx.EVT_PAINT, self.OnPaint) def GetSelectedItem(self, index): """ Returns the selected thumbnail. :param `index`: the thumbnail index (i.e., the selection). """ return self.GetItem(self.GetSelection(index)) def GetPointed(self): """ Returns the pointed thumbnail index. """ return self._pointed def GetHighlightPointed(self): """ Returns whether the thumbnail pointed should be highlighted or not. :note: Please be aware that this functionality may be slow on slower computers. """ return self._highlight def SetHighlightPointed(self, highlight=True): """ Sets whether the thumbnail pointed should be highlighted or not. :param `highlight`: ``True`` to enable highlight-on-point with the mouse, ``False`` otherwise. :note: Please be aware that this functionality may be slow on slower computers. """ self._highlight = highlight def SetThumbOutline(self, outline): """ Sets the thumbnail outline style on selection. :param `outline`: the outline to use on selection. This can be one of the following bits: =========================== ======= ================================== 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. =========================== ======= ================================== """ if outline not in [THUMB_OUTLINE_NONE, THUMB_OUTLINE_FULL, THUMB_OUTLINE_RECT, THUMB_OUTLINE_IMAGE]: return self._tOutline = outline def GetThumbOutline(self): """ Returns the thumbnail outline style on selection. :see: :meth:`~ScrolledThumbnail.SetThumbOutline` for a list of possible return values. """ return self._tOutline def SetDropShadow(self, drop): """ Sets whether to drop a shadow behind thumbnails or not. :param `drop`: ``True`` to drop a shadow behind each thumbnail, ``False`` otheriwise. """ self._dropShadow = drop self.Refresh() def GetDropShadow(self): """ Returns whether to drop a shadow behind thumbnails or not. """ return self._dropShadow def GetPointedItem(self): """ Returns the pointed thumbnail. """ return self.GetItem(self._pointed) def GetItem(self, index): """ Returns the item at position `index`. :param `index`: the thumbnail index position. """ return index >= 0 and (index < len(self._items) and [self._items[index]] or [None])[0] def GetItemCount(self): """ Returns the number of thumbnails. """ return len(self._items) def SortItems(self): """ Sorts the items accordingly to the :func:`~CmpThumb` function. """ self._items.sort(key=KeyThumb) def GetThumbWidth(self): """ Returns the thumbnail width. """ return self._tWidth def GetThumbHeight(self): """ Returns the thumbnail height. """ return self._tHeight def GetThumbBorder(self): """ Returns the thumbnail border. """ return self._tBorder def GetCaption(self): """ Returns the thumbnail caption. """ return self._caption def SetLabelControl(self, statictext): """ Sets the thumbnail label as :class:`StaticText`. :param `statictext`: an instance of :class:`StaticText`. """ self._labelcontrol = statictext 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.Refresh() def SetOrientation(self, orient=THUMB_VERTICAL): """ Set the :class:`ThumbnailCtrl` orientation (partially implemented). :param `orient`: one of ``THUMB_VERTICAL``, ``THUMB_HORIZONTAL``. .. todo:: Correctly implement the ``THUMB_HORIZONTAL`` orientation. """ self._orient = orient def SetPopupMenu(self, menu): """ Sets the thumbnails popup menu when at least one thumbnail is selected. :param `menu`: an instance of :class:`Menu`. """ self._pmenu = menu def GetPopupMenu(self): """ Returns the thumbnails popup menu when at least one thumbnail is selected. """ return self._pmenu def SetGlobalPopupMenu(self, gpmenu): """ Sets the global thumbnails popup menu (no need of thumbnail selection). :param `gpmenu`: an instance of :class:`Menu`. """ self._gpmenu = gpmenu def GetGlobalPopupMenu(self): """ Returns the global thumbnailss popup menu (no need of thumbnail selection). """ return self._gpmenu def GetSelectionColour(self): """ Returns the colour used to indicate a selected thumbnail. """ return self._selectioncolour def SetSelectionColour(self, colour=None): """ Sets the colour used to indicate a selected thumbnail. :param `colour`: a valid :class:`Colour` object. If defaulted to ``None``, it will be taken from the system settings. """ if colour is None: colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT) self._selectioncolour = colour def EnableDragging(self, enable=True): """ Enables/disables thumbnails drag and drop. :param `enable`: ``True`` to enable drag and drop, ``False`` to disable it. """ self._dragging = enable def EnableToolTips(self, enable=True): """ Globally enables/disables thumbnail file information. :param `enable`: ``True`` to enable thumbnail file information, ``False`` to disable it. """ self._enabletooltip = enable if not enable and hasattr(self, "_tipwindow"): self._tipwindow.Enable(False) def GetThumbInfo(self, thumb=-1): """ Returns the thumbnail information. :param `thumb`: the index of the thumbnail for which we are collecting information. """ thumbinfo = None if thumb >= 0: thumbinfo = "Name: " + self._items[thumb].GetFileName() + "\n" \ "Size: " + self._items[thumb].GetFileSize() + "\n" \ "Modified: " + self._items[thumb].GetCreationDate() + "\n" \ "Dimensions: " + str(self._items[thumb].GetOriginalSize()) + "\n" \ "Thumb: " + str(self.GetThumbSize()[0:2]) return thumbinfo def SetThumbSize(self, width, height, border=6): """ Sets the thumbnail size as width, height and border. :param `width`: the desired thumbnail width; :param `height`: the desired thumbnail height; :param `border`: the spacing between thumbnails. """ if width > 350 or height > 280: return self._tWidth = width self._tHeight = height self._tBorder = border self.SetScrollRate((self._tWidth + self._tBorder)/4, (self._tHeight + self._tBorder)/4) self.SetSizeHints(self._tWidth + self._tBorder*2 + 16, self._tHeight + self._tBorder*2 + 8) def GetThumbSize(self): """ Returns the thumbnail size as width, height and border. """ return self._tWidth, self._tHeight, self._tBorder def Clear(self): """ Clears :class:`ThumbnailCtrl`. """ self._items = [] self._selected = -1 self._selectedarray = [] self.UpdateProp() self.Refresh() 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. """ fileList = [os.path.normcase(f) for f in os.listdir(directory)] fileList = [f for f in fileList \ if os.path.splitext(f)[1] in fileExtList] return fileList def ThreadImage(self, filenames): """ Threaded method to load images. Used internally. :param `filenames`: a Python list of file names containing images. """ count = 0 while count < len(filenames): if not self._isrunning: self._isrunning = False thread.exit() return self.LoadImages(filenames[count], count) if count < 4: self.Refresh() elif count%4 == 0: self.Refresh() count = count + 1 self._isrunning = False thread.exit() def LoadImages(self, newfile, imagecount): """ Threaded method to load images. Used internally. :param `newfile`: a file name containing an image to thumbnail; :param `imagecount`: the number of images loaded until now. """ if not self._isrunning: thread.exit() return img, originalsize, alpha = self._imageHandler.LoadThumbnail(newfile, (300, 240)) try: self._items[imagecount]._threadedimage = img self._items[imagecount]._originalsize = originalsize self._items[imagecount]._bitmap = img self._items[imagecount]._alpha = alpha except: return def ShowThumbs(self, thumbs, caption): """ Shows all the thumbnails. :param `thumbs`: should be a sequence with instances of :class:`Thumb`; :param `caption`: the caption text for the current selected thumbnail. """ self.SetCaption(caption) self._isrunning = False # update items self._items = thumbs myfiles = [thumb.GetFullFileName() for thumb in thumbs] items = self._items[:] self._items.sort(key=KeyThumb) newfiles = SortFiles(items, self._items, myfiles) self._isrunning = True thread.start_new_thread(self.ThreadImage, (newfiles,)) wx.MilliSleep(20) self._selectedarray = [] self.UpdateProp() self.Refresh() def ShowDir(self, folder, filter=THUMB_FILTER_IMAGES): """ Shows thumbnails for a particular folder. :param `folder`: a directory containing the images to thumbnail; :param `filter`: filter images, video audio (currently implemented only for images). .. todo:: Find a way to create thumbnails of video, audio and other formats. """ self._dir = folder if filter >= 0: self._filter = filter self._parent.RecreateComboBox(folder) # update items thumbs = [] filenames = self.ListDirectory(self._dir, extensions) 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] if size < 1000: size = str(size) + " bytes" elif size < 1000000: size = str(int(round(size/1000.0))) + " Kb" else: size = str(round(size/1000000.0, 2)) + " Mb" lastmod = time.strftime(TIME_FMT, time.localtime(stats[8])) if self._filter & THUMB_FILTER_IMAGES: thumbs.append(Thumb(self, folder, files, caption, size, lastmod)) return self.ShowThumbs(thumbs, caption=self._dir) def GetShowDir(self): """ Returns the working directory with images. """ return self._dir def SetSelection(self, value=-1): """ Sets thumbnail selection. :param `value`: the thumbnail index to select. """ self._selected = value if value != -1: self._selectedarray = [value] eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_SEL_CHANGED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) self.ScrollToSelected() self.Refresh() def SetZoomFactor(self, zoom=1.4): """ Sets the zoom factor. :param `zoom`: a floating point number representing the zoom factor. Must be greater than or equal to 1.0. """ if zoom <= 1.0: raise Exception("\nERROR: Zoom Factor Must Be Greater Than 1.0") self._zoomfactor = zoom def GetZoomFactor(self): """ Returns the zoom factor. """ return self._zoomfactor def IsAudioVideo(self, fname): """ Returns ``True`` if a file contains either audio or video data. Currently unused as :class:`ThumbnailCtrl` recognizes only image files. :param `fname`: a file name. .. todo:: Find a way to create thumbnails of video, audio and other formats. """ return os.path.splitext(fname)[1].lower() in \ [".mpg", ".mpeg", ".vob"] def IsVideo(self, fname): """ Returns ``True`` if a file contains video data. Currently unused as :class:`ThumbnailCtrl` recognizes only image files. :param `fname`: a file name. .. todo:: Find a way to create thumbnails of video, audio and other formats. """ return os.path.splitext(fname)[1].lower() in \ [".m1v", ".m2v"] def IsAudio(self, fname): """ Returns ``True`` if a file contains audio data. Currently unused as :class:`ThumbnailCtrl` recognizes only image files. :param `fname`: a file name. .. todo:: Find a way to create thumbnails of video, audio and other formats. """ return os.path.splitext(fname)[1].lower() in \ [".mpa", ".mp2", ".mp3", ".ac3", ".dts", ".pcm"] def UpdateItems(self): """ Updates thumbnail items. """ selected = self._selectedarray selectedfname = [] selecteditemid = [] for ii in range(len(self._selectedarray)): selectedfname.append(self.GetSelectedItem(ii).GetFileName()) selecteditemid.append(self.GetSelectedItem(ii).GetId()) self.UpdateShow() if len(selected) > 0: self._selectedarray = [] for ii in range(len(self._items)): for jj in range(len(selected)): if self._items[ii].GetFileName() == selectedfname[jj] and \ self._items[ii].GetId() == selecteditemid[jj]: self._selectedarray.append(ii) if len(self._selectedarray) == 1: self.ScrollToSelected() if len(self._selectedarray) > 0: self.Refresh() eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_SEL_CHANGED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) def SetCaption(self, caption=""): """ Sets the current caption string. :param `caption`: the current caption string. """ self._caption = caption if self._labelcontrol: maxWidth = self._labelcontrol.GetSize().GetWidth()/8 if len(caption) > maxWidth: caption = "..." + caption[len(caption) + 3 - maxWidth] self._labelcontrol.SetLabel(caption) eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_CAPTION_CHANGED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) def SetCaptionFont(self, font=None): """ Sets the font for all the thumbnail captions. :param `font`: a valid :class:`Font` object. If defaulted to ``None``, a standard font will be generated. """ if font is None: font = wx.Font(8, wx.SWISS, wx.NORMAL, wx.BOLD, False) self._captionfont = font def GetCaptionFont(self): """ Returns the font for all the thumbnail captions. """ return self._captionfont def UpdateShow(self): """ Updates thumbnail items. """ self.ShowThumbs(self._items) def GetCaptionHeight(self, begRow, count=1): """ Returns the height for the file name caption. :param `begRow`: the caption line at which we start measuring the height; :param `count`: the number of lines to measure. """ capHeight = 0 for ii in range(begRow, begRow + count): if ii < len(self._tCaptionHeight): capHeight = capHeight + self._tCaptionHeight[ii] return capHeight*self._tTextHeight def GetItemIndex(self, x, y): """ Returns the thumbnail index at position (x, y). :param `x`: the mouse `x` position; :param `y`: the mouse `y` position. """ col = (x - self._tBorder)/(self._tWidth + self._tBorder) if col >= self._cols: col = self._cols - 1 row = -1 y = y - self._tBorder while y > 0: row = row + 1 y = y - (self._tHeight + self._tBorder + self.GetCaptionHeight(row)) if row < 0: row = 0 index = row*self._cols + col if index >= len(self._items): index = -1 return index def UpdateProp(self, checkSize=True): """ Updates :class:`ThumbnailCtrl` and its visible thumbnails. :param `checkSize`: ``True`` to update the items visibility if the window size has changed. """ width = self.GetClientSize().GetWidth() self._cols = (width - self._tBorder)//(self._tWidth + self._tBorder) if self._cols <= 0: self._cols = 1 tmpvar = (len(self._items)%self._cols and [1] or [0])[0] self._rows = len(self._items)//self._cols + tmpvar self._tCaptionHeight = [] for row in range(self._rows): capHeight = 0 for col in range(self._cols): ii = row*self._cols + col if len(self._items) > ii and \ self._items[ii].GetCaptionLinesCount(self._tWidth - self._tCaptionBorder) > capHeight: capHeight = self._items[ii].GetCaptionLinesCount(self._tWidth - self._tCaptionBorder) self._tCaptionHeight.append(capHeight) self.SetVirtualSize((self._cols*(self._tWidth + self._tBorder) + self._tBorder, self._rows*(self._tHeight + self._tBorder) + \ self.GetCaptionHeight(0, self._rows) + self._tBorder)) self.SetSizeHints(self._tWidth + 2*self._tBorder + 16, self._tHeight + 2*self._tBorder + 8 + \ (self._rows and [self.GetCaptionHeight(0)] or [0])[0]) if checkSize and width != self.GetClientSize().GetWidth(): self.UpdateProp(False) def InsertItem(self, thumb, pos): """ Inserts a thumbnail in the specified position. :param `pos`: the index at which we wish to insert the new thumbnail. """ if pos < 0 or pos > len(self._items): self._items.append(thumb) else: self._items.insert(pos, thumb) self.UpdateProp() def RemoveItemAt(self, pos): """ Removes a thumbnail at the specified position. :param `pos`: the index at which we wish to remove the thumbnail. """ del self._items[pos] self.UpdateProp() def GetPaintRect(self): """ Returns the paint bounding rect for the :meth:`~ScrolledThumbnail.OnPaint` method. """ size = self.GetClientSize() paintRect = wx.Rect(0, 0, size.GetWidth(), size.GetHeight()) paintRect.x, paintRect.y = self.GetViewStart() xu, yu = self.GetScrollPixelsPerUnit() paintRect.x = paintRect.x*xu paintRect.y = paintRect.y*yu return paintRect def IsSelected(self, indx): """ Returns whether a thumbnail is selected or not. :param `indx`: the index of the thumbnail to check for selection. """ return self._selectedarray.count(indx) != 0 def GetSelection(self, selIndex=-1): """ Returns the selected thumbnail. :param `selIndex`: if not equal to -1, the index of the selected thumbnail. """ return (selIndex == -1 and [self._selected] or \ [self._selectedarray[selIndex]])[0] def GetOriginalImage(self, index=None): """ Returns the original image associated to a thumbnail. :param `index`: the index of the thumbnail. If defaulted to ``None``, the current selection is used. """ if index is None: index = self.GetSelection() return self._items[index].GetOriginalImage() def ScrollToSelected(self): """ Scrolls the :class:`ScrolledWindow` to the selected thumbnail. """ if self.GetSelection() == -1: return # get row row = self.GetSelection()/self._cols # calc position to scroll view paintRect = self.GetPaintRect() y1 = row*(self._tHeight + self._tBorder) + self.GetCaptionHeight(0, row) y2 = y1 + self._tBorder + self._tHeight + self.GetCaptionHeight(row) if y1 < paintRect.GetTop(): sy = y1 # scroll top elif y2 > paintRect.GetBottom(): sy = y2 - paintRect.height # scroll bottom else: return # scroll view xu, yu = self.GetScrollPixelsPerUnit() sy = sy/yu + (sy%yu and [1] or [0])[0] # convert sy to scroll units x, y = self.GetViewStart() self.Scroll(x,sy) def CalculateBestCaption(self, dc, caption, sw, width): """ Calculates the best caption string to show based on the actual zoom factor. :param `dc`: an instance of :class:`DC`; :param `caption`: the original caption string; :param `sw`: the maximum width allowed for the caption string, in pixels; :param `width`: the caption string width, in pixels. """ caption = caption + "..." while sw > width: caption = caption[1:] sw, sh = dc.GetTextExtent(caption) return "..." + caption[0:-3] def DrawThumbnail(self, bmp, thumb, index): """ Draws a visible thumbnail. :param `bmp`: the thumbnail version of the original image; :param `thumb`: an instance of :class:`Thumb`; :param `index`: the index of the thumbnail to draw. """ dc = wx.MemoryDC() dc.SelectObject(bmp) x = self._tBorder/2 y = self._tBorder/2 # background dc.SetPen(wx.Pen(wx.BLACK, 0, wx.TRANSPARENT)) dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.SOLID)) dc.DrawRectangle(0, 0, bmp.GetWidth(), bmp.GetHeight()) # image img = thumb.GetBitmap(self._tWidth, self._tHeight) ww = img.GetWidth() hh = img.GetHeight() if index == self.GetPointed() and self.GetHighlightPointed(): factor = 1.5 img = self._imageHandler.HighlightImage(img.ConvertToImage(), factor).ConvertToBitmap() imgRect = wx.Rect(x + (self._tWidth - img.GetWidth())/2, y + (self._tHeight - img.GetHeight())/2, img.GetWidth(), img.GetHeight()) if not thumb._alpha and self._dropShadow: dc.Blit(imgRect.x+5, imgRect.y+5, imgRect.width, imgRect.height, self.shadow, 500-ww, 500-hh) dc.DrawBitmap(img, imgRect.x, imgRect.y, True) colour = self.GetSelectionColour() selected = self.IsSelected(index) colour = self.GetSelectionColour() # draw caption sw, sh = 0, 0 if self._showfilenames: textWidth = 0 dc.SetFont(self.GetCaptionFont()) mycaption = thumb.GetCaption(0) sw, sh = dc.GetTextExtent(mycaption) if sw > self._tWidth: mycaption = self.CalculateBestCaption(dc, mycaption, sw, self._tWidth) sw = self._tWidth textWidth = sw + 8 tx = x + (self._tWidth - textWidth)/2 ty = y + self._tHeight txtcolour = "#7D7D7D" dc.SetTextForeground(txtcolour) tx = x + (self._tWidth - sw)/2 if hh >= self._tHeight: ty = y + self._tHeight + (self._tTextHeight - sh)/2 + 3 else: ty = y + hh + (self._tHeight-hh)/2 + (self._tTextHeight - sh)/2 + 3 dc.DrawText(mycaption, tx, ty) # outline if self._tOutline != THUMB_OUTLINE_NONE and (self._tOutlineNotSelected or self.IsSelected(index)): dotrect = wx.Rect() dotrect.x = x - 2 dotrect.y = y - 2 dotrect.width = bmp.GetWidth() - self._tBorder + 4 dotrect.height = bmp.GetHeight() - self._tBorder + 4 dc.SetPen(wx.Pen((self.IsSelected(index) and [colour] or [wx.LIGHT_GREY])[0], 0, wx.SOLID)) dc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT)) if self._tOutline == THUMB_OUTLINE_FULL or self._tOutline == THUMB_OUTLINE_RECT: imgRect.x = x imgRect.y = y imgRect.width = bmp.GetWidth() - self._tBorder imgRect.height = bmp.GetHeight() - self._tBorder if self._tOutline == THUMB_OUTLINE_RECT: imgRect.height = self._tHeight dc.SetBrush(wx.TRANSPARENT_BRUSH) if selected: dc.SetPen(self.grayPen) dc.DrawRoundedRectangle(dotrect, 2) dc.SetPen(wx.Pen(wx.WHITE)) dc.DrawRectangle(imgRect.x, imgRect.y, imgRect.width, imgRect.height) pen = wx.Pen((selected and [colour] or [wx.LIGHT_GREY])[0], 2) pen.SetJoin(wx.JOIN_MITER) dc.SetPen(pen) if self._tOutline == THUMB_OUTLINE_FULL: dc.DrawRoundedRectangle(imgRect.x - 1, imgRect.y - 1, imgRect.width + 3, imgRect.height + 3, 2) else: dc.DrawRectangle(imgRect.x - 1, imgRect.y - 1, imgRect.width + 3, imgRect.height + 3) else: dc.SetPen(wx.Pen(wx.LIGHT_GREY)) dc.DrawRectangle(imgRect.x - 1, imgRect.y - 1, imgRect.width + 2, imgRect.height + 2) dc.SelectObject(wx.NullBitmap) def OnPaint(self, event): """ Handles the ``wx.EVT_PAINT`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`PaintEvent` event to be processed. """ paintRect = self.GetPaintRect() dc = wx.BufferedPaintDC(self) self.PrepareDC(dc) dc.SetPen(wx.Pen(wx.BLACK, 0, wx.TRANSPARENT)) dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.SOLID)) w, h = self.GetClientSize() # items row = -1 xwhite = self._tBorder for ii in range(len(self._items)): col = ii%self._cols if col == 0: row = row + 1 xwhite = ((w - self._cols*(self._tWidth + self._tBorder)))/(self._cols+1) tx = xwhite + col*(self._tWidth + self._tBorder) ty = self._tBorder/2 + row*(self._tHeight + self._tBorder) + \ self.GetCaptionHeight(0, row) tw = self._tWidth + self._tBorder th = self._tHeight + self.GetCaptionHeight(row) + self._tBorder # visible? if not paintRect.Intersects(wx.Rect(tx, ty, tw, th)): continue thmb = wx.Bitmap(tw, th) self.DrawThumbnail(thmb, self._items[ii], ii) dc.DrawBitmap(thmb, tx, ty) rect = wx.Rect(xwhite, self._tBorder/2, self._cols*(self._tWidth + self._tBorder), self._rows*(self._tHeight + self._tBorder) + \ self.GetCaptionHeight(0, self._rows)) w = max(self.GetClientSize().GetWidth(), rect.width) h = max(self.GetClientSize().GetHeight(), rect.height) dc.DrawRectangle(0, 0, w, rect.y) dc.DrawRectangle(0, 0, rect.x, h) dc.DrawRectangle(rect.GetRight(), 0, w - rect.GetRight(), h + 50) dc.DrawRectangle(0, rect.GetBottom(), w, h - rect.GetBottom() + 50) col = len(self._items)%self._cols if col > 0: rect.x = rect.x + col*(self._tWidth + self._tBorder) rect.y = rect.y + (self._rows - 1)*(self._tHeight + self._tBorder) + \ self.GetCaptionHeight(0, self._rows - 1) dc.DrawRectangle(rect) def OnResize(self, event): """ Handles the ``wx.EVT_SIZE`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`SizeEvent` event to be processed. """ self.UpdateProp() self.ScrollToSelected() self.Refresh() def OnMouseDown(self, event): """ Handles the ``wx.EVT_LEFT_DOWN`` and ``wx.EVT_RIGHT_DOWN`` events for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. """ x = event.GetX() y = event.GetY() x, y = self.CalcUnscrolledPosition(x, y) # get item number to select lastselected = self._selected self._selected = self.GetItemIndex(x, y) self._mouseeventhandled = False update = False if event.ControlDown(): if self._selected == -1: self._mouseeventhandled = True elif not self.IsSelected(self._selected): self._selectedarray.append(self._selected) update = True self._mouseeventhandled = True elif event.ShiftDown(): if self._selected != -1: begindex = self._selected endindex = lastselected if lastselected < self._selected: begindex = lastselected endindex = self._selected self._selectedarray = [] for ii in range(begindex, endindex+1): self._selectedarray.append(ii) update = True self._selected = lastselected self._mouseeventhandled = True else: if self._selected == -1: update = len(self._selectedarray) > 0 self._selectedarray = [] self._mouseeventhandled = True elif len(self._selectedarray) <= 1: try: update = len(self._selectedarray)== 0 or self._selectedarray[0] != self._selected except: update = True self._selectedarray = [] self._selectedarray.append(self._selected) self._mouseeventhandled = True if update: self.ScrollToSelected() self.Refresh() eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_SEL_CHANGED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) self.SetFocus() def OnMouseUp(self, event): """ Handles the ``wx.EVT_LEFT_UP`` and ``wx.EVT_RIGHT_UP`` events for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. """ # get item number to select x = event.GetX() y = event.GetY() x, y = self.CalcUnscrolledPosition(x, y) lastselected = self._selected self._selected = self.GetItemIndex(x,y) if not self._mouseeventhandled: # set new selection if event.ControlDown(): if self._selected in self._selectedarray: self._selectedarray.remove(self._selected) self._selected = -1 else: self._selectedarray = [] self._selectedarray.append(self._selected) self.ScrollToSelected() self.Refresh() eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_SEL_CHANGED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) # Popup menu if event.RightUp(): if self._selected >= 0 and self._pmenu: self.PopupMenu(self._pmenu, event.GetPosition()) elif self._selected >= 0 and not self._pmenu and self._gpmenu: self.PopupMenu(self._gpmenu, event.GetPosition()) elif self._selected == -1 and self._gpmenu: self.PopupMenu(self._gpmenu, event.GetPosition()) if event.ShiftDown(): self._selected = lastselected def OnMouseDClick(self, event): """ Handles the ``wx.EVT_LEFT_DCLICK`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. """ eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_DCLICK, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) def OnMouseMove(self, event): """ Handles the ``wx.EVT_MOTION`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. """ # -- drag & drop -- if self._dragging and event.Dragging() and len(self._selectedarray) > 0: files = wx.FileDataObject() for ii in range(len(self._selectedarray)): files.AddFile(opj(self.GetSelectedItem(ii).GetFullFileName())) source = wx.DropSource(self) source.SetData(files) source.DoDragDrop(wx.Drag_DefaultMove) # -- light-effect -- x = event.GetX() y = event.GetY() x, y = self.CalcUnscrolledPosition(x, y) # get item number sel = self.GetItemIndex(x, y) if sel == self._pointed: if self._enabletooltip and sel >= 0: if not hasattr(self, "_tipwindow"): self._tipwindow = wx.ToolTip(self.GetThumbInfo(sel)) self._tipwindow.SetDelay(1000) self.SetToolTip(self._tipwindow) else: self._tipwindow.SetDelay(1000) self._tipwindow.SetTip(self.GetThumbInfo(sel)) event.Skip() return if self._enabletooltip: if hasattr(self, "_tipwindow"): self._tipwindow.Enable(False) # update thumbnail self._pointed = sel if self._enabletooltip and sel >= 0: if not hasattr(self, "_tipwindow"): self._tipwindow = wx.ToolTip(self.GetThumbInfo(sel)) self._tipwindow.SetDelay(1000) self._tipwindow.Enable(True) self.SetToolTip(self._tipwindow) else: self._tipwindow.SetDelay(1000) self._tipwindow.Enable(True) self._tipwindow.SetTip(self.GetThumbInfo(sel)) self.Refresh() eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_POINTED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) event.Skip() def OnMouseLeave(self, event): """ Handles the ``wx.EVT_LEAVE_WINDOW`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. """ if self._pointed != -1: self._pointed = -1 self.Refresh() eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_POINTED, self.GetId()) self.GetEventHandler().ProcessEvent(eventOut) def OnThumbChanged(self, event): """ Handles the ``EVT_THUMBNAILS_THUMB_CHANGED`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`ThumbnailEvent` event to be processed. """ for ii in range(len(self._items)): if self._items[ii].GetFileName() == event.GetString(): self._items[ii].SetFilename(self._items[ii].GetFileName()) if event.GetClientData(): img = wx.Image(event.GetClientData()) self._items[ii].SetImage(img) self.Refresh() def OnChar(self, event): """ Handles the ``wx.EVT_CHAR`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`KeyEvent` event to be processed. :note: You have these choices: (1) ``d`` key rotates 90 degrees clockwise the selected thumbnails; (2) ``s`` key rotates 90 degrees counter-clockwise the selected thumbnails; (3) ``a`` key rotates 180 degrees the selected thumbnails; (4) ``Del`` key deletes the selected thumbnails; (5) ``+`` key zooms in; (6) ``-`` key zooms out. """ if event.KeyCode == ord("s"): self.Rotate() elif event.KeyCode == ord("d"): self.Rotate(270) elif event.KeyCode == ord("a"): self.Rotate(180) elif event.KeyCode == wx.WXK_DELETE: self.DeleteFiles() elif event.KeyCode in [wx.WXK_ADD, wx.WXK_NUMPAD_ADD]: self.ZoomIn() elif event.KeyCode in [wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT]: self.ZoomOut() event.Skip() def Rotate(self, angle=90): """ Rotates the selected thumbnails by the angle specified by `angle`. :param `angle`: the rotation angle for the thumbnail, in degrees. """ wx.BeginBusyCursor() count = 0 selected = [] for ii in range(len(self._items)): if self.IsSelected(ii): selected.append(self._items[ii]) dlg = wx.ProgressDialog("Thumbnail Rotation", "Rotating Thumbnail... Please Wait", maximum = len(selected)+1, parent=None) for thumb in selected: count = count + 1 if TN_USE_PIL: newangle = thumb.GetRotation()*180/pi + angle fil = opj(thumb.GetFullFileName()) pil = Image.open(fil).rotate(newangle) img = wx.Image(pil.size[0], pil.size[1]) img.SetData(pil.convert('RGB').tostring()) thumb.SetRotation(newangle*pi/180) else: img = thumb._threadedimage newangle = thumb.GetRotation() + angle*pi/180 thumb.SetRotation(newangle) img = img.Rotate(newangle, (img.GetWidth()/2, img.GetHeight()/2), True) thumb.SetRotatedImage(img) dlg.Update(count) wx.EndBusyCursor() dlg.Destroy() if self.GetSelection() != -1: self.Refresh() def DeleteFiles(self): """ Deletes the selected thumbnails and their associated files. .. warning:: This method deletes the original files too. """ dlg = wx.MessageDialog(self, 'Are you sure you want to delete the files?', 'Confirmation', wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) if dlg.ShowModal() == wx.ID_YES: errordelete = [] count = 0 dlg.Destroy() wx.BeginBusyCursor() for ii in range(len(self._items)): if self.IsSelected(ii): thumb = self._items[ii] files = self._items[ii].GetFullFileName() filename = opj(files) try: os.remove(filename) count = count + 1 except: errordelete.append(files) 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() if count: self.UpdateShow() def OnMouseWheel(self, event): """ Handles the ``wx.EVT_MOUSEWHEEL`` event for :class:`ThumbnailCtrl`. :param `event`: a :class:`MouseEvent` event to be processed. :note: If you hold down the ``Ctrl`` key, you can zoom in/out with the mouse wheel. """ if event.ControlDown(): if event.GetWheelRotation() > 0: self.ZoomIn() else: self.ZoomOut() else: event.Skip() def ZoomOut(self): """ Zooms the thumbnails out. """ w, h, b = self.GetThumbSize() if w < 40 or h < 40: return zoom = self.GetZoomFactor() neww = float(w)/zoom newh = float(h)/zoom self.SetThumbSize(int(neww), int(newh)) self.OnResize(None) self._checktext = True self.Refresh() def ZoomIn(self): """ Zooms the thumbnails in. """ size = self.GetClientSize() w, h, b = self.GetThumbSize() zoom = self.GetZoomFactor() if w*zoom + b > size.GetWidth() or h*zoom + b > size.GetHeight(): if w*zoom + b > size.GetWidth(): neww = size.GetWidth() - 2*self._tBorder newh = (float(h)/w)*neww else: newh = size.GetHeight() - 2*self._tBorder neww = (float(w)/h)*newh else: neww = float(w)*zoom newh = float(h)*zoom self.SetThumbSize(int(neww), int(newh)) self.OnResize(None) self._checktext = True self.Refresh()