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