1#!/usr/local/bin/python3.8
2
3import os
4import imtools
5import gettext
6import _thread as thread
7import subprocess
8import locale
9import time
10import hashlib
11import mimetypes
12import pickle
13from io import BytesIO
14from xml.etree import ElementTree
15
16from PIL import Image
17import gi
18gi.require_version("Gtk", "3.0")
19from gi.repository import Gio, Gtk, Gdk, GdkPixbuf, Pango, GLib
20
21from SettingsWidgets import SidePage
22from xapp.GSettingsWidgets import *
23
24gettext.install("cinnamon", "/usr/local/share/locale")
25
26BACKGROUND_COLOR_SHADING_TYPES = [
27    ("solid", _("Solid color")),
28    ("horizontal", _("Horizontal gradient")),
29    ("vertical", _("Vertical gradient"))
30]
31
32BACKGROUND_PICTURE_OPTIONS = [
33    ("none", _("No picture")),
34    ("wallpaper", _("Mosaic")),
35    ("centered", _("Centered")),
36    ("scaled", _("Scaled")),
37    ("stretched", _("Stretched")),
38    ("zoom", _("Zoom")),
39    ("spanned", _("Spanned"))
40]
41
42BACKGROUND_ICONS_SIZE = 100
43
44BACKGROUND_COLLECTION_TYPE_DIRECTORY = "directory"
45BACKGROUND_COLLECTION_TYPE_XML = "xml"
46
47# even though pickle supports higher protocol versions, we want to version 2 because it's the latest
48# version supported by python2 which (at this time) is still used by older versions of Cinnamon.
49# When those versions are no longer supported, we can consider using a newer version.
50PICKLE_PROTOCOL_VERSION = 2
51
52(STORE_IS_SEPARATOR, STORE_ICON, STORE_NAME, STORE_PATH, STORE_TYPE) = range(5)
53
54# EXIF utility functions (source: http://stackoverflow.com/questions/4228530/pil-thumbnail-is-rotating-my-image)
55def flip_horizontal(im): return im.transpose(Image.FLIP_LEFT_RIGHT)
56def flip_vertical(im): return im.transpose(Image.FLIP_TOP_BOTTOM)
57def rotate_180(im): return im.transpose(Image.ROTATE_180)
58def rotate_90(im): return im.transpose(Image.ROTATE_90)
59def rotate_270(im): return im.transpose(Image.ROTATE_270)
60def transpose(im): return rotate_90(flip_horizontal(im))
61def transverse(im): return rotate_90(flip_vertical(im))
62orientation_funcs = [None,
63                     lambda x: x,
64                     flip_horizontal,
65                     rotate_180,
66                     flip_vertical,
67                     transpose,
68                     rotate_270,
69                     transverse,
70                     rotate_90
71                     ]
72def apply_orientation(im):
73    """
74    Extract the oritentation EXIF tag from the image, which should be a PIL Image instance,
75    and if there is an orientation tag that would rotate the image, apply that rotation to
76    the Image instance given to do an in-place rotation.
77
78    :param Image im: Image instance to inspect
79    :return: A possibly transposed image instance
80    """
81
82    try:
83        kOrientationEXIFTag = 0x0112
84        if hasattr(im, '_getexif'): # only present in JPEGs
85            e = im._getexif()       # returns None if no EXIF data
86            if e is not None:
87                #log.info('EXIF data found: %r', e)
88                orientation = e[kOrientationEXIFTag]
89                f = orientation_funcs[orientation]
90                return f(im)
91    except:
92        # We'd be here with an invalid orientation value or some random error?
93        pass # log.exception("Error applying EXIF Orientation tag")
94    return im
95
96
97class ColorsWidget(SettingsWidget):
98    def __init__(self, size_group):
99        super(ColorsWidget, self).__init__(dep_key=None)
100
101        #gsettings
102        self.settings = Gio.Settings("org.cinnamon.desktop.background")
103
104        # settings widgets
105        combo = Gtk.ComboBox()
106        key = 'color-shading-type'
107        value = self.settings.get_string(key)
108        renderer_text = Gtk.CellRendererText()
109        combo.pack_start(renderer_text, True)
110        combo.add_attribute(renderer_text, "text", 1)
111        model = Gtk.ListStore(str, str)
112        combo.set_model(model)
113        combo.set_id_column(0)
114        for option in BACKGROUND_COLOR_SHADING_TYPES:
115            iter = model.append([option[0], option[1]])
116            if value == option[0]:
117                combo.set_active_iter(iter)
118        combo.connect('changed', self.on_combo_changed, key)
119
120        self.content_widget = Gtk.Box(valign=Gtk.Align.CENTER)
121        self.content_widget.pack_start(combo, False, False, 2)
122
123        # Primary color
124        for key in ['primary-color', 'secondary-color']:
125            color_button = Gtk.ColorButton()
126            color_button.set_use_alpha(True)
127            rgba = Gdk.RGBA()
128            rgba.parse(self.settings.get_string(key))
129            color_button.set_rgba(rgba)
130            color_button.connect('color-set', self.on_color_changed, key)
131            self.content_widget.pack_start(color_button, False, False, 2)
132
133        # Keep a ref on the second color button (so we can hide/show it when appropriate)
134        self.color2_button = color_button
135        self.color2_button.set_no_show_all(True)
136        self.show_or_hide_color2(value)
137        self.add_to_size_group(size_group)
138        self.label = SettingsLabel(_("Background color"))
139        self.pack_start(self.label, False, False, 0)
140        self.pack_end(self.content_widget, False, False, 0)
141
142    def on_color_changed(self, widget, key):
143        color_string = widget.get_color().to_string()
144        self.settings.set_string(key, color_string)
145
146    def on_combo_changed(self, widget, key):
147        tree_iter = widget.get_active_iter()
148        if tree_iter != None:
149            value = widget.get_model()[tree_iter][0]
150            self.settings.set_string(key, value)
151            self.show_or_hide_color2(value)
152
153    def show_or_hide_color2(self, value):
154        if (value == 'solid'):
155            self.color2_button.hide()
156        else:
157            self.color2_button.show()
158
159class Module:
160    name = "backgrounds"
161    category = "appear"
162    comment = _("Change your desktop's background")
163
164    def __init__(self, content_box):
165        keywords = _("background, picture, slideshow")
166        self.sidePage = SidePage(_("Backgrounds"), "cs-backgrounds", keywords, content_box, module=self)
167
168    def on_module_selected(self):
169        if not self.loaded:
170            print("Loading Backgrounds module")
171
172            self.sidePage.stack = SettingsStack()
173            self.sidePage.add_widget(self.sidePage.stack)
174
175            self.shown_collection = None  # Which collection is displayed in the UI
176
177            self._background_schema = Gio.Settings(schema="org.cinnamon.desktop.background")
178            self._slideshow_schema = Gio.Settings(schema="org.cinnamon.desktop.background.slideshow")
179            self._slideshow_schema.connect("changed::slideshow-enabled", self.on_slideshow_enabled_changed)
180            self.add_folder_dialog = Gtk.FileChooserDialog(title=_("Add Folder"),
181                                                           action=Gtk.FileChooserAction.SELECT_FOLDER,
182                                                           buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
183                                                                    Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
184
185            self.xdg_pictures_directory = os.path.expanduser("~/Pictures")
186            xdg_config = os.path.expanduser("~/.config/user-dirs.dirs")
187            if os.path.exists(xdg_config) and os.path.exists("/usr/local/bin/xdg-user-dir"):
188                path = subprocess.check_output(["xdg-user-dir", "PICTURES"]).decode("utf-8").rstrip("\n")
189                if os.path.exists(path):
190                    self.xdg_pictures_directory = path
191
192            self.get_user_backgrounds()
193
194            # Images
195
196            mainbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 2)
197            mainbox.expand = True
198            mainbox.set_border_width(8)
199
200            self.sidePage.stack.add_titled(mainbox, "images", _("Images"))
201
202            left_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
203            right_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
204
205            folder_scroller = Gtk.ScrolledWindow.new(None, None)
206            folder_scroller.set_shadow_type(Gtk.ShadowType.IN)
207            folder_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
208            folder_scroller.set_property("min-content-width", 150)
209
210            self.folder_tree = Gtk.TreeView.new()
211            self.folder_tree.set_headers_visible(False)
212            folder_scroller.add(self.folder_tree)
213
214            button_toolbar = Gtk.Toolbar.new()
215            button_toolbar.set_icon_size(1)
216            Gtk.StyleContext.add_class(Gtk.Widget.get_style_context(button_toolbar), "inline-toolbar")
217            self.add_folder_button = Gtk.ToolButton.new(None, None)
218            self.add_folder_button.set_icon_name("list-add-symbolic")
219            self.add_folder_button.set_tooltip_text(_("Add new folder"))
220            self.add_folder_button.connect("clicked", lambda w: self.add_new_folder())
221            self.remove_folder_button = Gtk.ToolButton.new(None, None)
222            self.remove_folder_button.set_icon_name("list-remove-symbolic")
223            self.remove_folder_button.set_tooltip_text(_("Remove selected folder"))
224            self.remove_folder_button.connect("clicked", lambda w: self.remove_folder())
225            button_toolbar.insert(self.add_folder_button, 0)
226            button_toolbar.insert(self.remove_folder_button, 1)
227
228            image_scroller = Gtk.ScrolledWindow.new(None, None)
229            image_scroller.set_shadow_type(Gtk.ShadowType.IN)
230            image_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
231
232            self.icon_view = ThreadedIconView()
233            image_scroller.add(self.icon_view)
234            self.icon_view.connect("selection-changed", self.on_wallpaper_selection_changed)
235
236            right_vbox.pack_start(image_scroller, True, True, 0)
237            left_vbox.pack_start(folder_scroller, True, True, 0)
238            left_vbox.pack_start(button_toolbar, False, False, 0)
239
240            mainbox.pack_start(left_vbox, False, False, 2)
241            mainbox.pack_start(right_vbox, True, True, 2)
242
243            left_vbox.set_border_width(2)
244            right_vbox.set_border_width(2)
245
246            self.collection_store = Gtk.ListStore(bool,    # is separator
247                                                  str,     # Icon name
248                                                  str,     # Display name
249                                                  str,     # Path
250                                                  str)     # Type of collection
251            cell = Gtk.CellRendererText()
252            cell.set_alignment(0, 0)
253            pb_cell = Gtk.CellRendererPixbuf()
254            self.folder_column = Gtk.TreeViewColumn()
255            self.folder_column.pack_start(pb_cell, False)
256            self.folder_column.pack_start(cell, True)
257            self.folder_column.add_attribute(pb_cell, "icon-name", 1)
258            self.folder_column.add_attribute(cell, "text", 2)
259
260            self.folder_column.set_alignment(0)
261
262            self.folder_tree.append_column(self.folder_column)
263            self.folder_tree.connect("cursor-changed", self.on_folder_source_changed)
264
265            self.get_system_backgrounds()
266
267            tree_separator = [True, None, None, None, None]
268            self.collection_store.append(tree_separator)
269
270            if len(self.user_backgrounds) > 0:
271                for item in self.user_backgrounds:
272                    self.collection_store.append(item)
273
274            self.folder_tree.set_model(self.collection_store)
275            self.folder_tree.set_row_separator_func(self.is_row_separator, None)
276
277            self.get_initial_path()
278
279            # Settings
280
281            page = SettingsPage()
282
283            settings = page.add_section(_("Background Settings"))
284
285            size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
286
287            self.sidePage.stack.add_titled(page, "settings", _("Settings"))
288
289            widget = GSettingsSwitch(_("Play backgrounds as a slideshow"), "org.cinnamon.desktop.background.slideshow", "slideshow-enabled")
290            settings.add_row(widget)
291
292            widget = GSettingsSpinButton(_("Delay"), "org.cinnamon.desktop.background.slideshow", "delay", _("minutes"), 1, 1440)
293            settings.add_reveal_row(widget, "org.cinnamon.desktop.background.slideshow", "slideshow-enabled")
294
295            widget = GSettingsSwitch(_("Play images in random order"), "org.cinnamon.desktop.background.slideshow", "random-order")
296            settings.add_reveal_row(widget, "org.cinnamon.desktop.background.slideshow", "slideshow-enabled")
297
298            widget = GSettingsComboBox(_("Picture aspect"), "org.cinnamon.desktop.background", "picture-options", BACKGROUND_PICTURE_OPTIONS, size_group=size_group)
299            settings.add_row(widget)
300
301            widget = ColorsWidget(size_group)
302            settings.add_row(widget)
303
304    def is_row_separator(self, model, iter, data):
305        return model.get_value(iter, 0)
306
307    def on_slideshow_enabled_changed(self, settings, key):
308        if self._slideshow_schema.get_boolean("slideshow-enabled"):
309            self.icon_view.set_sensitive(False)
310            self.icon_view.set_selection_mode(Gtk.SelectionMode.NONE)
311        else:
312            self.icon_view.set_sensitive(True)
313            self.icon_view.set_selection_mode(Gtk.SelectionMode.SINGLE)
314
315    def get_system_backgrounds(self):
316        picture_list = []
317        folder_list = []
318        properties_dir = "/usr/local/share/cinnamon-background-properties"
319        backgrounds = []
320        if os.path.exists(properties_dir):
321            for i in os.listdir(properties_dir):
322                if i.endswith(".xml"):
323                    xml_path = os.path.join(properties_dir, i)
324                    display_name = i.replace(".xml", "").replace("-", " ").replace("_", " ").split(" ")[-1].capitalize()
325                    icon = "preferences-desktop-wallpaper-symbolic"
326                    order = 10
327                    # Special case for Linux Mint. We don't want to use 'start-here' here as it wouldn't work depending on the theme.
328                    # Also, other distros should get equal treatment. If they define cinnamon-backgrounds and use their own distro name, we should add support for it.
329                    if display_name == "Retro":
330                        icon = "document-open-recent-symbolic"
331                        order = 20 # place retro bgs at the end
332                    if display_name == "Linuxmint":
333                        display_name = "Linux Mint"
334                        icon = "linuxmint-logo-badge-symbolic"
335                        order = 0
336                    backgrounds.append([[False, icon, display_name, xml_path, BACKGROUND_COLLECTION_TYPE_XML], display_name, order])
337
338        backgrounds.sort(key=lambda x: (x[2], x[1]))
339        for background in backgrounds:
340            self.collection_store.append(background[0])
341
342    def get_user_backgrounds(self):
343        self.user_backgrounds = []
344        path = os.path.expanduser("~/.cinnamon/backgrounds/user-folders.lst")
345        if os.path.exists(path):
346            with open(path) as f:
347                folders = f.readlines()
348            for line in folders:
349                folder_path = line.strip("\n")
350                folder_name = folder_path.split("/")[-1]
351                if folder_path == self.xdg_pictures_directory:
352                    icon = "folder-pictures-symbolic"
353                else:
354                    icon = "folder-symbolic"
355                self.user_backgrounds.append([False, icon, folder_name, folder_path, BACKGROUND_COLLECTION_TYPE_DIRECTORY])
356        else:
357            # Add XDG PICTURE DIR
358            self.user_backgrounds.append([False, "folder-pictures-symbolic", self.xdg_pictures_directory.split("/")[-1], self.xdg_pictures_directory, BACKGROUND_COLLECTION_TYPE_DIRECTORY])
359            self.update_folder_list()
360
361    def format_source(self, type, path):
362        # returns 'type://path'
363        return ("%s://%s" % (type, path))
364
365    def get_initial_path(self):
366        try:
367            image_source = self._slideshow_schema.get_string("image-source")
368            tree_iter = self.collection_store.get_iter_first()
369            collection = self.collection_store[tree_iter]
370            collection_type = collection[STORE_TYPE]
371            collection_path = collection[STORE_PATH]
372            collection_source = self.format_source(collection_type, collection_path)
373            self.remove_folder_button.set_sensitive(True)
374
375            if image_source != "" and "://" in image_source:
376                while tree_iter != None:
377                    if collection_source == image_source:
378                        tree_path = self.collection_store.get_path(tree_iter)
379                        self.folder_tree.set_cursor(tree_path)
380                        if collection_type == BACKGROUND_COLLECTION_TYPE_XML:
381                            self.remove_folder_button.set_sensitive(False)
382                        self.update_icon_view(collection_path, collection_type)
383                        return
384                    tree_iter = self.collection_store.iter_next(tree_iter)
385                    collection = self.collection_store[tree_iter]
386                    collection_type = collection[STORE_TYPE]
387                    collection_path = collection[STORE_PATH]
388                    collection_source = self.format_source(collection_type, collection_path)
389            else:
390                self._slideshow_schema.set_string("image-source", collection_source)
391                tree_path = self.collection_store.get_path(tree_iter)
392                self.folder_tree.get_selection().select_path(tree_path)
393                if collection_type == BACKGROUND_COLLECTION_TYPE_XML:
394                    self.remove_folder_button.set_sensitive(False)
395                self.update_icon_view(collection_path, collection_type)
396        except Exception as detail:
397            print(detail)
398
399    def on_row_activated(self, tree, path, column):
400        self.folder_tree.set_selection(path)
401
402    def on_folder_source_changed(self, tree):
403        self.remove_folder_button.set_sensitive(True)
404        if tree.get_selection() is not None:
405            folder_paths, iter = tree.get_selection().get_selected()
406            if iter:
407                collection_path = folder_paths[iter][STORE_PATH]
408                collection_type = folder_paths[iter][STORE_TYPE]
409                collection_source = self.format_source(collection_type, collection_path)
410                if os.path.exists(collection_path):
411                    if collection_source != self._slideshow_schema.get_string("image-source"):
412                        self._slideshow_schema.set_string("image-source", collection_source)
413                    if collection_type == BACKGROUND_COLLECTION_TYPE_XML:
414                        self.remove_folder_button.set_sensitive(False)
415                    self.update_icon_view(collection_path, collection_type)
416
417    def get_selected_wallpaper(self):
418        selected_items = self.icon_view.get_selected_items()
419        if len(selected_items) == 1:
420            path = selected_items[0]
421            iter = self.icon_view.get_model().get_iter(path)
422            return self.icon_view.get_model().get(iter, 0)[0]
423        return None
424
425    def on_wallpaper_selection_changed(self, iconview):
426        wallpaper = self.get_selected_wallpaper()
427        if wallpaper:
428            for key in wallpaper:
429                if key == "filename":
430                    self._background_schema.set_string("picture-uri", "file://" + wallpaper[key])
431                elif key == "options":
432                    self._background_schema.set_string("picture-options", wallpaper[key])
433
434    def add_new_folder(self):
435        res = self.add_folder_dialog.run()
436        if res == Gtk.ResponseType.OK:
437            folder_path = self.add_folder_dialog.get_filename()
438            folder_name = folder_path.split("/")[-1]
439            # Make sure it's not already added..
440            for background in self.user_backgrounds:
441                if background[STORE_PATH] == folder_path:
442                    self.add_folder_dialog.hide()
443                    return
444            if folder_path == self.xdg_pictures_directory:
445                icon = "folder-pictures-symbolic"
446            else:
447                icon = "folder-symbolic"
448            self.user_backgrounds.append([False, icon, folder_name, folder_path, BACKGROUND_COLLECTION_TYPE_DIRECTORY])
449            self.collection_store.append([False, icon, folder_name, folder_path, BACKGROUND_COLLECTION_TYPE_DIRECTORY])
450            self.update_folder_list()
451        self.add_folder_dialog.hide()
452
453    def remove_folder(self):
454        if self.folder_tree.get_selection() is not None:
455            self.icon_view.clear()
456            folder_paths, iter = self.folder_tree.get_selection().get_selected()
457            if iter:
458                path = folder_paths[iter][STORE_PATH]
459                self.collection_store.remove(iter)
460                for item in self.user_backgrounds:
461                    if item[STORE_PATH] == path:
462                        self.user_backgrounds.remove(item)
463                        self.update_folder_list()
464                        break
465
466    def update_folder_list(self):
467        path = os.path.expanduser("~/.cinnamon/backgrounds")
468        if not os.path.exists(path):
469            os.makedirs(path, mode=0o755, exist_ok=True)
470        path = os.path.expanduser("~/.cinnamon/backgrounds/user-folders.lst")
471        if len(self.user_backgrounds) == 0:
472            file_data = ""
473        else:
474            first_path = self.user_backgrounds[0][STORE_PATH]
475            file_data = first_path + "\n"
476            for folder in self.user_backgrounds:
477                if folder[STORE_PATH] == first_path:
478                    continue
479                else:
480                    file_data += "%s\n" % folder[STORE_PATH]
481
482        with open(path, "w") as f:
483            f.write(file_data)
484
485    def update_icon_view(self, path=None, type=None):
486        if path != self.shown_collection:
487            self.shown_collection = path
488            picture_list = []
489            if os.path.exists(path):
490                if type == BACKGROUND_COLLECTION_TYPE_DIRECTORY:
491                    files = os.listdir(path)
492                    files.sort()
493                    for i in files:
494                        filename = os.path.join(path, i)
495                        picture_list.append({"filename": filename})
496                elif type == BACKGROUND_COLLECTION_TYPE_XML:
497                    picture_list += self.parse_xml_backgrounds_list(path)
498
499            self.icon_view.set_pictures_list(picture_list, path)
500            if self._slideshow_schema.get_boolean("slideshow-enabled"):
501                self.icon_view.set_sensitive(False)
502            else:
503                self.icon_view.set_sensitive(True)
504
505    def splitLocaleCode(self, localeCode):
506        try:
507            loc = localeCode.partition("_")
508            loc = (loc[0], loc[2])
509        except:
510            loc = ("en", "US")
511        return loc
512
513    def getLocalWallpaperName(self, names, loc):
514        result = ""
515        mainLocFound = False
516        for wp in names:
517            wpLoc = wp[0]
518            wpName = wp[1]
519            if wpLoc == ("", ""):
520                if not mainLocFound:
521                    result = wpName
522            elif wpLoc[0] == loc[0]:
523                if wpLoc[1] == loc[1]:
524                    return wpName
525                elif wpLoc[1] == "":
526                    result = wpName
527                    mainLocFound = True
528        return result
529
530    def parse_xml_backgrounds_list(self, filename):
531        try:
532            locAttrName = "{http://www.w3.org/XML/1998/namespace}lang"
533            loc = self.splitLocaleCode(locale.getdefaultlocale()[0])
534            res = []
535            subLocaleFound = False
536            f = open(filename)
537            rootNode = ElementTree.fromstring(f.read())
538            f.close()
539            if rootNode.tag == "wallpapers":
540                for wallpaperNode in rootNode:
541                    if wallpaperNode.tag == "wallpaper" and wallpaperNode.get("deleted") != "true":
542                        wallpaperData = {"metadataFile": filename}
543                        names = []
544                        for prop in wallpaperNode:
545                            if type(prop.tag) == str:
546                                if prop.tag != "name":
547                                    wallpaperData[prop.tag] = prop.text
548                                else:
549                                    propAttr = prop.attrib
550                                    wpName = prop.text
551                                    locName = self.splitLocaleCode(propAttr.get(locAttrName)) if locAttrName in propAttr else ("", "")
552                                    names.append((locName, wpName))
553                        wallpaperData["name"] = self.getLocalWallpaperName(names, loc)
554
555                        if "filename" in wallpaperData and wallpaperData["filename"] != "" and os.path.exists(wallpaperData["filename"]) and os.access(wallpaperData["filename"], os.R_OK):
556                            if wallpaperData["name"] == "":
557                                wallpaperData["name"] = os.path.basename(wallpaperData["filename"])
558                            res.append(wallpaperData)
559            return res
560        except Exception as detail:
561            print("Could not parse %s!" % filename)
562            print(detail)
563            return []
564
565class PixCache(object):
566
567    def __init__(self):
568        self._data = {}
569
570    def get_pix(self, filename, size=None):
571        if filename is None:
572            return None
573        mimetype = mimetypes.guess_type(filename)[0]
574        if mimetype is None or not mimetype.startswith("image/"):
575            return None
576
577        if filename not in self._data:
578            self._data[filename] = {}
579        if size in self._data[filename]:
580            pix = self._data[filename][size]
581        else:
582            try:
583                h = hashlib.sha1(('%f%s' % (os.path.getmtime(filename), filename)).encode()).hexdigest()
584                tmp_cache_path = GLib.get_user_cache_dir() + '/cs_backgrounds/'
585                if not os.path.exists(tmp_cache_path):
586                    os.mkdir(tmp_cache_path)
587                cache_filename = tmp_cache_path + h + "v2"
588
589                loaded = False
590                if os.path.exists(cache_filename):
591                    # load from disk cache
592                    try:
593                        with open(cache_filename, "rb") as cache_file:
594                            pix = pickle.load(cache_file)
595                        tmp_img = Image.open(BytesIO(pix[0]))
596                        pix[0] = self._image_to_pixbuf(tmp_img)
597                        loaded = True
598                    except Exception as detail:
599                        # most likely either the file is corrupted, or the file was pickled using the
600                        # python2 version of cinnamon settings. Either way, we want to ditch the current
601                        # cache file and generate a new one. This is still backward compatible with older
602                        # Cinnamon versions
603                        os.remove(cache_filename)
604
605                if not loaded:
606                    if mimetype == "image/svg+xml":
607                        # rasterize svg with Gdk-Pixbuf and convert to PIL Image
608                        tmp_pix = GdkPixbuf.Pixbuf.new_from_file(filename)
609                        mode = "RGBA" if tmp_pix.props.has_alpha else "RGB"
610                        img = Image.frombytes(mode, (tmp_pix.props.width, tmp_pix.props.height),
611                                              tmp_pix.read_pixel_bytes().get_data(), "raw",
612                                              mode, tmp_pix.props.rowstride)
613                    else:
614                        img = Image.open(filename)
615                        img = apply_orientation(img)
616
617                    # generate thumbnail
618                    (width, height) = img.size
619                    if img.mode != "RGB":
620                        if img.mode == "RGBA":
621                            bg_img = Image.new("RGBA", img.size, (255,255,255,255))
622                            img = Image.alpha_composite(bg_img, img)
623                        img = img.convert("RGB")
624                    if size:
625                        img.thumbnail((size, size), Image.ANTIALIAS)
626                    img = imtools.round_image(img, {}, False, None, 3, 255)
627                    img = imtools.drop_shadow(img, 4, 4, background_color=(255, 255, 255, 0),
628                                              shadow_color=0x444444, border=8, shadow_blur=3,
629                                              force_background_color=False, cache=None)
630
631                    # save to disk cache
632                    try:
633                        png_bytes = BytesIO()
634                        img.save(png_bytes, "png")
635                        with open(cache_filename, "wb") as cache_file:
636                            pickle.dump([png_bytes.getvalue(), width, height], cache_file, PICKLE_PROTOCOL_VERSION)
637                    except Exception as detail:
638                        print("Failed to save cache file: %s: %s" % (cache_filename, detail))
639
640                    pix = [self._image_to_pixbuf(img), width, height]
641            except Exception as detail:
642                print("Failed to convert %s: %s" % (filename, detail))
643                pix = None
644            if pix:
645                self._data[filename][size] = pix
646        return pix
647
648    # Convert RGBA PIL Image to Pixbuf
649    def _image_to_pixbuf(self, img):
650        [w, h] = img.size
651        return GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(img.tobytes()),
652                                               GdkPixbuf.Colorspace.RGB,
653                                               True, 8, w, h,
654                                               w * 4)
655
656PIX_CACHE = PixCache()
657
658
659class ThreadedIconView(Gtk.IconView):
660
661    def __init__(self):
662        Gtk.IconView.__init__(self)
663        self.set_item_width(BACKGROUND_ICONS_SIZE * 1.1)
664        self._model = Gtk.ListStore(object, GdkPixbuf.Pixbuf, str, str)
665        self._model_filter = self._model.filter_new()
666        self._model_filter.set_visible_func(self.visible_func)
667        self.set_model(self._model_filter)
668
669        area = self.get_area()
670
671        self.current_path = None
672
673        pixbuf_renderer = Gtk.CellRendererPixbuf()
674        text_renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
675
676        text_renderer.set_alignment(.5, .5)
677        area.pack_start(pixbuf_renderer, True, False, False)
678        area.pack_start(text_renderer, True, False, False)
679        self.add_attribute(pixbuf_renderer, "pixbuf", 1)
680        self.add_attribute(text_renderer, "markup", 2)
681        text_renderer.set_property("alignment", Pango.Alignment.CENTER)
682
683        self._loading_queue = []
684        self._loading_queue_lock = thread.allocate_lock()
685
686        self._loading_lock = thread.allocate_lock()
687        self._loading = False
688
689        self._loaded_data = []
690        self._loaded_data_lock = thread.allocate_lock()
691
692    def visible_func(self, model, iter, data=None):
693        item_path = model.get_value(iter, 3)
694        return item_path == self.current_path
695
696    def set_pictures_list(self, pictures_list, path=None):
697        self.clear()
698        self.current_path = path
699        for i in pictures_list:
700            self.add_picture(i, path)
701
702    def clear(self):
703        self._loading_queue_lock.acquire()
704        self._loading_queue = []
705        self._loading_queue_lock.release()
706
707        self._loading_lock.acquire()
708        is_loading = self._loading
709        self._loading_lock.release()
710        while is_loading:
711            time.sleep(0.1)
712            self._loading_lock.acquire()
713            is_loading = self._loading
714            self._loading_lock.release()
715
716        self._model.clear()
717
718    def add_picture(self, picture, path):
719        self._loading_queue_lock.acquire()
720        self._loading_queue.append(picture)
721        self._loading_queue_lock.release()
722
723        start_loading = False
724        self._loading_lock.acquire()
725        if not self._loading:
726            self._loading = True
727            start_loading = True
728        self._loading_lock.release()
729
730        if start_loading:
731            GLib.timeout_add(100, self._check_loading_progress)
732            thread.start_new_thread(self._do_load, (path,))
733
734    def _check_loading_progress(self):
735        self._loading_lock.acquire()
736        self._loaded_data_lock.acquire()
737        res = self._loading
738        to_load = []
739        while len(self._loaded_data) > 0:
740            to_load.append(self._loaded_data[0])
741            self._loaded_data = self._loaded_data[1:]
742        self._loading_lock.release()
743        self._loaded_data_lock.release()
744
745        for i in to_load:
746            self._model.append(i)
747
748        return res
749
750    def _do_load(self, path):
751        finished = False
752        while not finished:
753            self._loading_queue_lock.acquire()
754            if len(self._loading_queue) == 0:
755                finished = True
756            else:
757                to_load = self._loading_queue[0]
758                self._loading_queue = self._loading_queue[1:]
759            self._loading_queue_lock.release()
760            if not finished:
761                filename = to_load["filename"]
762                if filename.endswith(".xml"):
763                    filename = self.getFirstFileFromBackgroundXml(filename)
764                pix = PIX_CACHE.get_pix(filename, BACKGROUND_ICONS_SIZE)
765                if pix != None:
766                    if "name" in to_load:
767                        label = to_load["name"]
768                    else:
769                        label = os.path.split(to_load["filename"])[1]
770                    if "artist" in to_load:
771                        artist = "%s\n" % to_load["artist"]
772                    else:
773                        artist = ""
774                    dimensions = "%dx%d" % (pix[1], pix[2])
775
776                    self._loaded_data_lock.acquire()
777                    self._loaded_data.append((to_load, pix[0], "<b>%s</b>\n<sub>%s%s</sub>" % (label, artist, dimensions), path))
778                    self._loaded_data_lock.release()
779
780        self._loading_lock.acquire()
781        self._loading = False
782        self._loading_lock.release()
783
784    def getFirstFileFromBackgroundXml(self, filename):
785        try:
786            f = open(filename)
787            rootNode = ElementTree.fromstring(f.read())
788            f.close()
789            if rootNode.tag == "background":
790                for backgroundNode in rootNode:
791                    if backgroundNode.tag == "static":
792                        for staticNode in backgroundNode:
793                            if staticNode.tag == "file":
794                                if len(staticNode) > 0 and staticNode[-1].tag == "size":
795                                    return staticNode[-1].text
796                                return staticNode.text
797            print("Could not find filename in %s" % filename)
798            return None
799        except Exception as detail:
800            print("Failed to read filename from %s: %s" % (filename, detail))
801            return None
802