From 81c04612339bc05ccb176e199fc189f019bd2d3e Mon Sep 17 00:00:00 2001 From: eager Date: Fri, 16 Oct 2020 11:28:04 -0700 Subject: [PATCH] Refactor ThumbnailCtrl, create stand-alone ScrolledThumbnail widget ThumbnailCtrl is more of a image browser demo application than a widget, in that it reads files from a directory, selects which files to display, deletes files, displays the source directory path in a text ctrl, etc. This makes it unlikely that it could be used in any other application, for example, to provide thumbnails of files with different file types than the ones hard-coded in the class. ThumbnailCtrl delegates most of its operations to ScrolledThumbnail which actually implements a scrolled window of thumbnails, a Thumb class, which contains information about a thumbnail, and an ImageHandler class, which manipulates images. There was poor isolation of functionality between these classes, violating object-oriented design, with one class making changes to the internal data of another class. Additionally, there was substantial non-functional code, as well as code which did not function correctly. This refactoring maintains the functionality and interfaces of ThumbnailCtrl, except for those which were unused. Existing uses of the thumbnailctrl package should work without modification. A new package, scrolledthumbnail, contains the functionality for a scrolled window containing thumbnails, an extendable Thumb class, and image manipulation classes. The scrolledthumbnail package can be used in other applications, independent of the ThumbnailCtrl class, and without the functional restrictions of that application. Detailed changes: ThumbnailCtrl.py (demo program): - Always import from wx.lib.agw - Optional code to use PIL instead of native image handling - Add setting for thumbnail width and height - Increase size of demo window thumbnailctrl.py: - Move Thumb, ScrolledThumbnail, ImageHandler to scrolledthumbnail.py - Remove EVT_THUMBNAILS_CAPTION_CHANGED (unused) - Add EVT_THUMBNAILS_CHAR to respond to keystrokes - Remove image processing code - Add scrolling dialog for delete files - Move directory processing from ScrolledThumbnail - Move file delete processing from ScrolledThumbnail - List all files to be deleted in scrolling dialog - Remove unused or unimplemented methods and options scrolledthumbnail.py: - Move Thumb, ScrolledThumbnail, ImageHander classes from thumbnailctrl.py - Add documentation for ScrolledThumbnail widget - Add example program which does not use ThumbnailCtrl - New EVT_THUMBNAILS_CHAR event for key press - Remove unused options and dead code - Add Rotate() to PILImageHandler and NativeImageHandler - Throw event EVT_THUMBNAILS_CHAR for keystroke on thumbnail - Fix failure to rotate images correctly - Redisplay window when thumb size changed - Simplify logic - Remove popup dialog when rotating images --- demo/agw/ThumbnailCtrl.py | 51 +- wx/lib/agw/scrolledthumbnail.py | 2119 +++++++++++++++++++++++++++++ wx/lib/agw/thumbnailctrl.py | 2269 ++----------------------------- 3 files changed, 2259 insertions(+), 2180 deletions(-) create mode 100644 wx/lib/agw/scrolledthumbnail.py diff --git a/demo/agw/ThumbnailCtrl.py b/demo/agw/ThumbnailCtrl.py index 7ab5f8f3..f688e670 100644 --- a/demo/agw/ThumbnailCtrl.py +++ b/demo/agw/ThumbnailCtrl.py @@ -12,13 +12,13 @@ except: sys.path.append(os.path.split(dirName)[0]) -try: - from agw import thumbnailctrl as TC -except ImportError: # if it's not there locally, try the wxPython lib. - import wx.lib.agw.thumbnailctrl as TC +import wx.lib.agw.thumbnailctrl as TC import images +from wx.lib.agw.scrolledthumbnail import EVT_THUMBNAILS_SEL_CHANGED, EVT_THUMBNAILS_POINTED, \ +EVT_THUMBNAILS_DCLICK + class ThumbnailCtrlDemo(wx.Frame): @@ -32,7 +32,7 @@ class ThumbnailCtrlDemo(wx.Frame): self.statusbar = self.CreateStatusBar(2) self.statusbar.SetStatusWidths([-2, -1]) # statusbar fields - statusbar_fields = [("ThumbnailCtrl Demo, Andrea Gavana @ 10 Dec 2005"), + statusbar_fields = [("ThumbnailCtrl Demo, Michael Eager @ 15 Oct 2020"), ("Welcome To wxPython!")] for i in range(len(statusbar_fields)): @@ -46,6 +46,7 @@ class ThumbnailCtrlDemo(wx.Frame): sizer = wx.BoxSizer(wx.HORIZONTAL) scroll = TC.ThumbnailCtrl(splitter, -1, imagehandler=TC.NativeImageHandler) + #scroll = TC.ThumbnailCtrl(splitter, -1, imagehandler=TC.PILImageHandler) scroll.ShowFileNames() if os.path.isdir("../bitmaps"): @@ -74,6 +75,9 @@ class ThumbnailCtrlDemo(wx.Frame): self.enabletooltip = wx.CheckBox(self.panel, -1, "Enable thumb tooltips") self.textzoom = wx.TextCtrl(self.panel, -1, "1.4") self.zoombutton = wx.Button(self.panel, -1, "Set zoom factor") + self.textthumbwidth = wx.TextCtrl(self.panel, -1, "96") + self.textthumbheight = wx.TextCtrl(self.panel, -1, "80") + self.thumbsizebutton = wx.Button(self.panel, -1, "Set thumbnail size (WxH)") self.fontbutton = wx.Button(self.panel, -1, "Set caption font") self.colourbutton = wx.Button(self.panel, -1, "Set selection colour") @@ -100,18 +104,19 @@ class ThumbnailCtrlDemo(wx.Frame): self.Bind(wx.EVT_CHECKBOX, self.OnShowComboBox, self.showcombo) self.Bind(wx.EVT_CHECKBOX, self.OnEnableToolTips, self.enabletooltip) self.Bind(wx.EVT_BUTTON, self.OnSetZoom, self.zoombutton) + self.Bind(wx.EVT_BUTTON, self.OnSetThumbSize, self.thumbsizebutton) self.Bind(wx.EVT_BUTTON, self.OnSetFont, self.fontbutton) self.Bind(wx.EVT_BUTTON, self.OnSetColour, self.colourbutton) self.Bind(wx.EVT_BUTTON, self.OnSetDirectory, self.dirbutton) - self.TC.Bind(TC.EVT_THUMBNAILS_SEL_CHANGED, self.OnSelChanged) - self.TC.Bind(TC.EVT_THUMBNAILS_POINTED, self.OnPointed) - self.TC.Bind(TC.EVT_THUMBNAILS_DCLICK, self.OnDClick) + self.TC.Bind(EVT_THUMBNAILS_SEL_CHANGED, self.OnSelChanged) + self.TC.Bind(EVT_THUMBNAILS_POINTED, self.OnPointed) + self.TC.Bind(EVT_THUMBNAILS_DCLICK, self.OnDClick) - splitter.SplitVertically(scroll, self.panel, 180) + splitter.SplitVertically(scroll, self.panel, 400) splitter.SetMinimumPaneSize(140) - self.SetMinSize((700, 590)) + self.SetMinSize((1000, 1000)) self.CenterOnScreen() @@ -131,6 +136,7 @@ class ThumbnailCtrlDemo(wx.Frame): splitsizer = wx.BoxSizer(wx.VERTICAL) optionsizer = wx.StaticBoxSizer(self.optionsizer_staticbox, wx.VERTICAL) zoomsizer = wx.BoxSizer(wx.HORIZONTAL) + thumbsizesizer = wx.BoxSizer(wx.HORIZONTAL) customsizer = wx.StaticBoxSizer(self.customsizer_staticbox, wx.VERTICAL) thumbsizer = wx.StaticBoxSizer(self.thumbsizer_staticbox, wx.VERTICAL) radiosizer = wx.BoxSizer(wx.VERTICAL) @@ -153,7 +159,11 @@ class ThumbnailCtrlDemo(wx.Frame): splitsizer.Add(customsizer, 0, wx.TOP|wx.EXPAND|wx.LEFT, 5) zoomsizer.Add(self.textzoom, 1, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 3) zoomsizer.Add(self.zoombutton, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 3) + thumbsizesizer.Add(self.textthumbwidth, 1, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 3) + thumbsizesizer.Add(self.textthumbheight, 1, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 3) + thumbsizesizer.Add(self.thumbsizebutton, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL|wx.ADJUST_MINSIZE, 3) optionsizer.Add(zoomsizer, 1, wx.EXPAND, 0) + optionsizer.Add(thumbsizesizer, 1, wx.EXPAND, 0) optionsizer.Add(self.fontbutton, 0, wx.ALL|wx.ADJUST_MINSIZE, 3) optionsizer.Add(self.colourbutton, 0, wx.TOP|wx.LEFT|wx.ADJUST_MINSIZE, 3) splitsizer.Add(optionsizer, 0, wx.EXPAND | wx.TOP|wx.LEFT, 5) @@ -193,9 +203,10 @@ class ThumbnailCtrlDemo(wx.Frame): msg = "This Is The About Dialog Of The ThumbnailCtrl Demo.\n\n" + \ "Author: Andrea Gavana @ 10 Dec 2005\n\n" + \ + "Modified: Michael Eager @ 15 Oct 2020\n\n" + \ "Please Report Any Bug/Requests Of Improvements\n" + \ "To Me At The Following Addresses:\n\n" + \ - "andrea.gavana@agip.it\n" + "andrea_gavana@tin.it\n\n" + \ + "eager@eagercon.com\n\n" + \ "Welcome To wxPython " + wx.VERSION_STRING + "!!" dlg = wx.MessageDialog(self, msg, "ThumbnailCtrl Demo", @@ -362,6 +373,24 @@ class ThumbnailCtrlDemo(wx.Frame): event.Skip() + def OnSetThumbSize(self, event): + try: + width = int(self.textthumbwidth.GetValue().strip()) + height = int(self.textthumbheight.GetValue().strip()) + except: + errstr = "Error: thumb size must be integers (min 50x50)." + dlg = wx.MessageDialog(self, errstr, "ThumbnailCtrlDemo Error", + wx.OK | wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + return + + width = max(width, 50) + height = max(height, 50) + self.log.write("OnSetThumbSize: (%s, %s)\n" % (width, height)) + self.TC.SetThumbSize (width, height) + + event.Skip() def OnSelChanged(self, event): diff --git a/wx/lib/agw/scrolledthumbnail.py b/wx/lib/agw/scrolledthumbnail.py new file mode 100644 index 00000000..340412d9 --- /dev/null +++ b/wx/lib/agw/scrolledthumbnail.py @@ -0,0 +1,2119 @@ +# --------------------------------------------------------------------------- # +# SCROLLEDTHUMBNAIL Control wxPython IMPLEMENTATION +# Python Code By: +# +# Michael Eager (eager@eagercon.com), 26 Sep 2020 +# +# Based on thumnailctrl.py by +# Andrea Gavana And Peter Damoc, @ 12 Dec 2005, revised 27 Dec 2012 +# andrea.gavana@gmail.com +# andrea.gavana@maerskoil.com +# +# Tags: phoenix-port, documented, unittest, py3-port +# +# End Of Comments +# --------------------------------------------------------------------------- # + +""" +:class:`ScrolledThumbnail` is a widget that can be used to display a series of +thumbnails for files in a scrollable window. + +Description +=========== + +:class:`ScrolledThumbnail` is a widget derived from :class:`wx.ScrolledWindow` +which will display the thumbnail image for a file in a scrollable, resizable +window. It supports selecting one or more thumbnails, rotating the thumbnails, +displaying information for each thumbnail, and popups for both the window and +for individual thumbnails. + +The class uses two support classes: :class:`Thumb` and :class:`ImageHandler`. + +:class:`Thumb` contains all of the information for a particular thumbnail, +including filename, bitmaped thumbnail image, caption, and other data. This +class also has methods to perform thumbnail operations such as rotation, +highlighting, setting a file name. + +:class:`ImageHandler` provides file/image handling functions, including loading +a file and creating an image from it, rotating an image, or highlighting an image. + +The implementations of these two classes included in this file support generating +thumbnails from supported image files, such as JPEG, GIF, PNG, etc., using either +WxPythons native support or the PIL image library. Additional file types can be +supported by extending these classes, for example, to provide a thumbnail image +for an MP4 file, perhaps the cover photo, an MPEG by providing the image of a +title frame, or a PDF by providing an image of the cover page. The images for +these files may be generated on the fly by a suitably extended :class:`ImageHandler`, +or may be provided with the instance of :class:`Thumb`. The list of `Thumb` +instances passed to `ScrolledThumbnail` may contain different derived classes, +as long as each contains the required funtions. + +NB: Use of :class:`ScrolledThumbnail` has not been tested with extended classes. + +:class:`ScrolledThumbnail`, :class:`Thumb`, and :class:`ImageHandler`, implemented +here are derived from the similarly named classes included in :class:`agw.ThumbnailCtrl`, +written by Andrea Gavana. That implementation was tightly integrated as a image +file browser application. The current implementation removes dependencies between +the several classes and narrows the scope of `ScrolledThumbnail` to the placement +and management of thumbnails within a window. + +An updated :class:`ThumbnailCtrl` which uses this implementation of +:class:`ScrolledThumbnail`, :class:`Thumb`, and :class:`ImageHandler` provides +all of the previous functionality, which described as 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 in the previous implementation is partly based on `wxVillaLib`, a +wxWidgets implementation of the :class:`ThumbnailCtrl` control. Andrea Gavana +notes that :class:`ThumbnailCtrl` wouldn't have been so fast and complete +without the suggestions and hints from Peter Damoc. + +Usage: +===== + +Usage example:: + + import os + import wx + from scrolledthumbnail import ScrolledThumbnail, Thumb, NativeImageHandler + + class MyFrame(wx.Frame): + + def __init__(self, parent): + wx.Frame.__init__(self, parent, -1, "ScrolledThumb Demo", size=(400,300)) + + self.scroll = ScrolledThumbnail(self, -1, size=(400,300)) + + def ShowDir(self, dir): + dir = os.getcwd() + files = os.listdir(dir) + thumbs = [] + for f in files: + if os.path.splitext(f)[1] in [".jpg", ".gif", ".png"]: + thumbs.append(Thumb(dir, f, caption=f, imagehandler=NativeImageHandler)) + self.scroll.ShowThumbs(thumbs) + + + app = wx.App(False) + frame = MyFrame(None) + frame.ShowDir(os.getcwd()) + frame.Show(True) + + app.MainLoop() + + + +Methods and Settings +==================== + +With :class:`ScrolledThumbnail` 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. + +- Drag and drop thumbnails from :class:`ScrolledThumbnail` to whatever application you want; +- Use local (when at least one thumbnail is selected) or global (no need for + thumbnail selection) popup menus; +- 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: + +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. +``EVT_THUMBNAILS_CHAR`` A character has been typed +================================== ================================================== + + +License And Version +=================== + +:class:`ScrolledThumbnail` is distributed under the wxPython license. + +Latest revision: Michael Eager @ 2 Oct 2020 + +Version 1.0 + +""" + +#---------------------------------------------------------------------- +# Beginning Of ThumbnailCtrl wxPython Code +#---------------------------------------------------------------------- + +import os +import wx +import six +import zlib +from math import radians + +from wx.lib.embeddedimage import PyEmbeddedImage + +if six.PY3: + import _thread as thread +else: + import thread + +#---------------------------------------------------------------------- +# Get Default Icon/Data +#---------------------------------------------------------------------- + +#---------------------------------------------------------------------- +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) + + return img + + + def GetBitmap(self, width, height): + """ + Returns the bitmap of the thumbnail + + :param `width`: the associated bitmap width; + :param `height`: the associated bitmap height. + """ + + if self._bitmap: + if self._bitmap.GetWidth() == width and self._bitmap.GetHeight() == height: + return self._bitmap + + img = self.GetThumbnail(width, height) + bmp = img.ConvertToBitmap() + + return bmp + + + def GetFullFileName(self): + """ Returns the full filename of the thumbnail. """ + + return os.path.join(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 in bytes associated to a thumbnail. """ + + return self._filesize + + + def GetDisplayFileSize(self): + """ Return printable file size (with bytes, Kb, Mb suffix). """ + + size = self.GetFileSize() + 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" + return size + + + 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. """ + + return self._originalsize + + + 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 GetInfo(self): + """ Returns info for thumbnain in display format. """ + thumbinfo = "Name: " + self.GetFileName() + "\n" \ + "Size: " + self.GetDisplayFileSize() + "\n" \ + "Modified: " + self.GetCreationDate() + "\n" \ + "Dimensions: " + str(self.GetOriginalSize()) + "\n" + return thumbinfo + + + def LoadImage(self): + """ Load image using imagehandler. """ + filename = self.GetFullFileName() + img, size, alpha = self._imagehandler.Load(filename) + self._image = img + self._originalsize = size + self._alpha = alpha + + + def Rotate(self, angle): + """ Rotate image using imagehandler. """ + img = self._imagehandler.Rotate(self._image, angle) + self._image = img + self._originalsize = (img.GetWidth, img.GetHeight) + self._alpha = img.HasAlpha() + # Clear _bitmap so thumbnail is recreated after rotate + self._bitmap = None + + + def GetHighlightBitmap(self, width, height, factor): + """ Returned highlighted bitmap of thumbnail. """ + + img = self.GetThumbnail(width, height) + img = self._imagehandler.HighlightImage(img, factor) + + bmp = img.ConvertToBitmap() + + return bmp + +# ---------------------------------------------------------------------------- # +# 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, + imagehandler=None): + """ + 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 `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._items = [] + self.SetThumbSize(96, 80) + self._tOutline = thumboutline + self._selected = -1 + self._pointed = -1 + self._pmenu = None + self._gpmenu = None + self._dragging = False + self._checktext = False + 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._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 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 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 SetPopupMenu(self, menu): + """ + Sets the thumbnails popup menu when at least one thumbnail is selected. + + :param `menu`: an instance of :class:`wx.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:`wx.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:`wx.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 = self._items[thumb].GetInfo() + \ + "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) + if self._items: + self.UpdateShow() + + + 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 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 + + self._items[imagecount].LoadImage() + + + def ShowThumbs(self, thumbs): + """ + Shows all the thumbnails. + + :param `thumbs`: should be a sequence with instances of :class:`Thumb`; + """ + + self._isrunning = False + + # update items + self._items = thumbs + myfiles = [thumb.GetFullFileName() for thumb in thumbs] + + self._isrunning = True + + thread.start_new_thread(self.ThreadImage, (myfiles,)) + wx.MilliSleep(20) + + self._selectedarray = [] + self.UpdateProp() + self.Refresh() + + + + 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 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 SetCaptionFont(self, font=None): + """ + Sets the font for all the thumbnail captions. + + :param `font`: a valid :class:`wx.Font` object. If defaulted to ``None``, a standard + font will be generated. + """ + + if font is None: + font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_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 GetItem(self, pos): + """ + Return thumbnail at specified position. + + :param pos: the index of the thumbnail + :return: the Thumb + """ + + return self._items[pos] + + 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 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:`wx.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.BRUSHSTYLE_SOLID)) + dc.DrawRectangle(0, 0, bmp.GetWidth(), bmp.GetHeight()) + + # image + if index == self.GetPointed() and self.GetHighlightPointed(): + factor = 1.5 + img = thumb.GetHighlightBitmap(self._tWidth, self._tHeight, factor) + else: + img = thumb.GetBitmap(self._tWidth, self._tHeight) + + ww = img.GetWidth() + hh = img.GetHeight() + 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.PENSTYLE_SOLID)) + dc.SetBrush(wx.Brush(wx.BLACK, wx.BRUSHSTYLE_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.PENSTYLE_TRANSPARENT)) + dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_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:`wx.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) + elif self._selected >= 0 and not self._pmenu and self._gpmenu: + self.PopupMenu(self._gpmenu) + elif self._selected == -1 and self._gpmenu: + self.PopupMenu(self._gpmenu) + + 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 in [wx.WXK_ADD, wx.WXK_NUMPAD_ADD]: + self.ZoomIn() + elif event.KeyCode in [wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT]: + self.ZoomOut() + + selected = [] + for ii in range(len(self._items)): + if self.IsSelected(ii): + selected.append(ii) + + eventOut = ThumbnailEvent(wxEVT_THUMBNAILS_CHAR, self.GetId(), + thumbs=selected, keycode=event.KeyCode) + self.GetEventHandler().ProcessEvent(eventOut) + + 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() + + selected = [] + for ii in range(len(self._items)): + if self.IsSelected(ii): + selected.append(self._items[ii]) + + for thumb in selected: + thumb.Rotate(angle) + + wx.EndBusyCursor() + + if self.GetSelection() != -1: + self.Refresh() + + + 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() + + +# ---------------------------------------------------------------------------- # +# Class ThumbnailEvent +# ---------------------------------------------------------------------------- # + +class ThumbnailEvent(wx.PyCommandEvent): + """ + This class is used to send events when a thumbnail is hovered, selected, + double-clicked or when its caption has been changed. + """ + def __init__(self, evtType, evtId=-1, thumbs=None, keycode=None): + """ + Default class constructor. + + :param `evtType`: the event type; + :param `evtId`: the event identifier. + """ + + wx.PyCommandEvent.__init__(self, evtType, evtId) + self.thumbs = thumbs + self.keycode = keycode diff --git a/wx/lib/agw/thumbnailctrl.py b/wx/lib/agw/thumbnailctrl.py index 0e938164..97ef39e3 100644 --- a/wx/lib/agw/thumbnailctrl.py +++ b/wx/lib/agw/thumbnailctrl.py @@ -4,27 +4,10 @@ # # 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!!! +# Refactored and reorganized: Michael Eager (eager@eagercon.com), 26 Sep 2020 # # Tags: phoenix-port, documented, unittest, py3-port # @@ -52,6 +35,11 @@ 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 ===== @@ -137,11 +125,11 @@ 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. +``EVT_THUMBNAILS_CHAR`` A key was typed in the widget ================================== ================================================== @@ -150,9 +138,9 @@ License And Version :class:`ThumbnailCtrl` is distributed under the wxPython license. -Latest revision: Andrea Gavana @ 27 Dec 2012, 21.00 GMT +Latest revision: Michael Eager @ 26 Sep 2020 -Version 0.9 +Version 1.0 """ @@ -164,170 +152,13 @@ Version 0.9 import wx import os import time -import zlib -import 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:`wx.Bitmap`. """ - - return wx.Bitmap(GetMondrianImage()) - - -def GetMondrianImage(): - """ Returns a default image placeholder as a :class:`wx.Image`. """ - - stream = six.BytesIO(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 # ---------------------------------------------------------------------------- # @@ -909,7 +244,8 @@ class ThumbnailCtrl(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, thumboutline=THUMB_OUTLINE_IMAGE, - thumbfilter=THUMB_FILTER_IMAGES, imagehandler=PILImageHandler): + thumbfilter=None, # Ignored, included for backward compatibility + imagehandler=PILImageHandler): """ Default class constructor. @@ -930,8 +266,7 @@ class ThumbnailCtrl(wx.Panel): ``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 `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. """ @@ -942,7 +277,7 @@ class ThumbnailCtrl(wx.Panel): self._combo = wx.ComboBox(self, -1, style=wx.CB_DROPDOWN | wx.CB_READONLY) self._scrolled = ScrolledThumbnail(self, -1, thumboutline=thumboutline, - thumbfilter=thumbfilter, imagehandler = imagehandler) + imagehandler = imagehandler) subsizer = wx.BoxSizer(wx.HORIZONTAL) subsizer.Add((3, 0), 0) @@ -959,13 +294,13 @@ class ThumbnailCtrl(wx.Panel): methods = ["GetSelectedItem", "GetPointed", "GetHighlightPointed", "SetHighlightPointed", "SetThumbOutline", "GetThumbOutline", "GetPointedItem", "GetItem", "GetItemCount", "GetThumbWidth", "GetThumbHeight", "GetThumbBorder", - "ShowFileNames", "SetPopupMenu", "GetPopupMenu", "SetGlobalPopupMenu", + "SetPopupMenu", "GetPopupMenu", "SetGlobalPopupMenu", "GetGlobalPopupMenu", "SetSelectionColour", "GetSelectionColour", - "EnableDragging", "SetThumbSize", "GetThumbSize", "ShowThumbs", "ShowDir", - "GetShowDir", "SetSelection", "GetSelection", "SetZoomFactor", + "EnableDragging", "SetThumbSize", "GetThumbSize", "ShowThumbs", + "SetSelection", "GetSelection", "SetZoomFactor", "GetZoomFactor", "SetCaptionFont", "GetCaptionFont", "GetItemIndex", "InsertItem", "RemoveItemAt", "IsSelected", "Rotate", "ZoomIn", "ZoomOut", - "EnableToolTips", "GetThumbInfo", "GetOriginalImage", "SetDropShadow", "GetDropShadow"] + "EnableToolTips", "GetThumbInfo", "SetDropShadow", "GetDropShadow"] for method in methods: setattr(self, method, getattr(self._scrolled, method)) @@ -975,6 +310,9 @@ class ThumbnailCtrl(wx.Panel): 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): @@ -1071,409 +409,45 @@ class ThumbnailCtrl(wx.Panel): return self.thumbnail_ctrl._scrolled.SetBackgroundColour(colour) -# ---------------------------------------------------------------------------- # -# 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): + def ShowDir(self, folder): """ - Default class constructor. + Shows thumbnails for a particular folder. - :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. + :param `folder`: a directory containing the images to thumbnail; """ - wx.ScrolledWindow.__init__(self, parent, id, pos, size) + self._dir = folder - 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.RecreateComboBox(folder) - 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 = [] + # update items + thumbs = [] - self._enabletooltip = False + filenames = self.ListDirectory(self._dir, extensions) - self._parent = parent + filenames.sort() + for files in filenames: - 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)) + caption = (self._showfilenames and [files] or [""])[0] + fullfile = opj(self._dir + "/" + files) + if not os.path.isfile(fullfile): + continue - t, b, s = getShadow() - self.shadow = wx.MemoryDC() - self.shadow.SelectObject(s) + stats = os.stat(fullfile) + size = stats[6] - self.ShowFileNames(True) + lastmod = time.strftime(TIME_FMT, time.localtime(stats[8])) - 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) + thumbs.append(Thumb(folder, files, caption, size, lastmod, + imagehandler=self._imagehandler)) - self.Bind(wx.EVT_SIZE, self.OnResize) - self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) - self.Bind(wx.EVT_PAINT, self.OnPaint) + return self.ShowThumbs(thumbs) - 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:`wx.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:`wx.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:`wx.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 GetShowDir(self): + """ Returns the working directory with images. """ + return self._dir def ListDirectory(self, directory, fileExtList): """ @@ -1487,1016 +461,41 @@ class ScrolledThumbnail(wx.ScrolledWindow): return [f for f in os.listdir(directory) if lSplitExt(f)[1].lower() in fileExtList] - def ThreadImage(self, filenames): + def ShowFileNames(self, show=True): """ - Threaded method to load images. Used internally. + Sets whether the user wants to show file names under the thumbnails or not. - :param `filenames`: a Python list of file names containing images. + :param `show`: ``True`` to show file names under the thumbnails, ``False`` otherwise. """ - count = 0 + self._showfilenames = show + self._scrolled.ShowFileNames(show) - while count < len(filenames): - if not self._isrunning: - self._isrunning = False - thread.exit() - return + def OnThumbChar(self, ev): + for th in ev.thumbs: + thumb = self._scrolled.GetItem(th) - self.LoadImages(filenames[count], count) - if count < 4: - self.Refresh() - elif count%4 == 0: - self.Refresh() + if ev.keycode == wx.WXK_DELETE: + self.DeleteFiles(ev.thumbs) - 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:`wx.Font` object. If defaulted to ``None``, a standard - font will be generated. - """ - - if font is None: - font = wx.Font(8, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_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:`wx.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.BRUSHSTYLE_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.PENSTYLE_SOLID)) - dc.SetBrush(wx.Brush(wx.BLACK, wx.BRUSHSTYLE_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.PENSTYLE_TRANSPARENT)) - dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_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:`wx.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) - elif self._selected >= 0 and not self._pmenu and self._gpmenu: - self.PopupMenu(self._gpmenu) - elif self._selected == -1 and self._gpmenu: - self.PopupMenu(self._gpmenu) - - 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()) - with Image.open(fil) as fid: - pil = fid.rotate(newangle) - img = wx.Image(pil.size[0], pil.size[1]) - img.SetData(pil.convert('RGB').tobytes()) - 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): + def DeleteFiles(self, thumbs): """ Deletes the selected thumbnails and their associated files. - .. warning:: This method deletes the original files too. + + :parem `thumbs`: List of indexs to thumbnails. + """ - dlg = wx.MessageDialog(self, 'Are you sure you want to delete the files?', - 'Confirmation', - wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) + msg = "" + for index in thumbs: + thumb = self._scrolled.GetItem(index) + msg += thumb.GetFileName() + '\n' - if dlg.ShowModal() == wx.ID_YES: + dlg = ScrolledTextDialog(self, 'Delete these files?', msg) + + if dlg.ShowModal() == wx.ID_OK: errordelete = [] count = 0 @@ -2504,16 +503,16 @@ class ScrolledThumbnail(wx.ScrolledWindow): 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) + # 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() @@ -2528,71 +527,3 @@ class ScrolledThumbnail(wx.ScrolledWindow): 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() - -