1#!/usr/local/bin/python3.8 2 3try: 4 from gi.repository import Gio, Gtk, GObject, Gdk, GdkPixbuf, GLib 5 import tempfile 6 import os 7 import sys 8 import zipfile 9 import shutil 10 import html 11 import subprocess 12 import threading 13 import dbus 14 from PIL import Image 15 import datetime 16 import proxygsettings 17 import time 18except Exception as detail: 19 print(detail) 20 sys.exit(1) 21 22from http.client import HTTPSConnection 23from urllib.parse import urlparse 24 25try: 26 import json 27except ImportError: 28 import simplejson as json 29 30home = os.path.expanduser("~") 31locale_inst = '%s/.local/share/locale' % home 32settings_dir = '%s/.cinnamon/configs/' % home 33 34URL_SPICES_HOME = "https://cinnamon-spices.linuxmint.com" 35URL_MAP = { 36 'applet': URL_SPICES_HOME + "/json/applets.json", 37 'theme': URL_SPICES_HOME + "/json/themes.json", 38 'desklet': URL_SPICES_HOME + "/json/desklets.json", 39 'extension': URL_SPICES_HOME + "/json/extensions.json" 40} 41 42ABORT_NONE = 0 43ABORT_ERROR = 1 44ABORT_USER = 2 45 46def ui_thread_do(callback, *args): 47 GLib.idle_add (callback, *args, priority=GLib.PRIORITY_DEFAULT) 48 49def removeEmptyFolders(path): 50 if not os.path.isdir(path): 51 return 52 53 # remove empty subfolders 54 files = os.listdir(path) 55 if len(files): 56 for f in files: 57 fullpath = os.path.join(path, f) 58 if os.path.isdir(fullpath): 59 removeEmptyFolders(fullpath) 60 61 # if folder empty, delete it 62 files = os.listdir(path) 63 if len(files) == 0: 64 print("Removing empty folder:", path) 65 os.rmdir(path) 66 67class ThreadedTaskManager(GObject.GObject): 68 def __init__(self, max_threads): 69 super(ThreadedTaskManager, self).__init__() 70 self.max_threads = max_threads 71 self.abort_status = False 72 self.jobs = [] 73 self.threads = [] 74 self.lock = threading.Lock() 75 self.start_id = 0 76 77 def get_n_jobs(self): 78 return len(self.jobs) + len(self.threads) 79 80 def busy(self): 81 return len(self.jobs) > 0 or len(self.threads) > 0 82 83 def push(self, func, callback, data): 84 with self.lock: 85 self.jobs.insert(0, (func, callback, data)) 86 87 if self.start_id == 0: 88 self.start_id = GLib.idle_add(self.check_start_job) 89 90 def check_start_job(self): 91 self.start_id = 0 92 if len(self.jobs) > 0: 93 if len(self.threads) == self.max_threads: 94 return 95 96 with self.lock: 97 job = self.jobs.pop() 98 newthread = threading.Thread(target=self.thread_function_wrapper, args=job) 99 self.threads.append(newthread) 100 101 newthread.start() 102 103 self.check_start_job() 104 105 def thread_function_wrapper(self, func, callback, data): 106 result = func(*data) 107 108 with self.lock: 109 try: 110 self.threads.remove(threading.current_thread()) 111 except: 112 pass 113 114 if self.abort_status and not self.busy(): 115 self.abort_status = False 116 117 self.check_start_job() 118 119 if callback is not None: 120 ui_thread_do(callback, result) 121 122 def abort(self): 123 if self.busy(): 124 with self.lock: 125 self.abort_status = True 126 del self.jobs[:] 127 128class Spice_Harvester(GObject.Object): 129 __gsignals__ = { 130 'installed-changed': (GObject.SignalFlags.RUN_FIRST, None, ()), 131 'status-changed': (GObject.SignalFlags.RUN_FIRST, None, ()), 132 'cache-loaded': (GObject.SignalFlags.RUN_FIRST, None, ()) 133 } 134 135 def __init__(self, collection_type, window=None): 136 super(Spice_Harvester, self).__init__() 137 self.collection_type = collection_type 138 self.window = window 139 140 self.themes = collection_type == 'theme' 141 self.index_cache = {} 142 self.meta_map = {} 143 self.download_manager = ThreadedTaskManager(10) 144 self._proxy = None 145 self._proxy_deferred_actions = [] 146 self._proxy_signals = [] 147 self.running_uuids = [] 148 self.jobs = [] 149 self.progressbars = [] 150 self.updates_available = [] 151 self.processing_jobs = False 152 self.is_downloading_image_cache = False 153 self.current_job = None 154 self.total_jobs = 0 155 self.download_total_files = 0 156 self.download_current_file = 0 157 self.cache_folder = '%s/.cinnamon/spices.cache/%s/' % (home, self.collection_type) 158 159 if self.themes: 160 self.settings = Gio.Settings.new('org.cinnamon.theme') 161 self.enabled_key = 'name' 162 else: 163 self.settings = Gio.Settings.new('org.cinnamon') 164 self.enabled_key = 'enabled-%ss' % self.collection_type 165 self.settings.connect('changed::%s' % self.enabled_key, self._update_status) 166 167 if self.themes: 168 self.install_folder = '%s/.themes/' % (home) 169 self.spices_directories = (self.install_folder, ) 170 else: 171 self.install_folder = '%s/.local/share/cinnamon/%ss/' % (home, self.collection_type) 172 self.spices_directories = ('/usr/local/share/cinnamon/%ss/' % self.collection_type, self.install_folder) 173 174 self._load_metadata() 175 176 self._load_cache() 177 178 self.abort_download = ABORT_NONE 179 self._sigLoadFinished = None 180 181 self.monitorId = 0 182 self.monitor = None 183 try: 184 self.monitor = Gio.File.new_for_path(self.install_folder).monitor_directory(0, None) 185 self.monitorId = self.monitor.connect('changed', self._directory_changed) 186 except Exception as e: 187 # File monitors can fail when the OS runs out of file handles 188 print(e) 189 190 try: 191 Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, 192 'org.Cinnamon', '/org/Cinnamon', 'org.Cinnamon', None, self._on_proxy_ready, None) 193 except dbus.exceptions.DBusException as e: 194 print(e) 195 196 def _on_proxy_ready (self, object, result, data=None): 197 try: 198 self._proxy = Gio.DBusProxy.new_for_bus_finish(result) 199 self._proxy.connect('g-signal', self._on_signal) 200 201 if self._proxy.get_name_owner(): 202 self.send_deferred_proxy_calls() 203 else: 204 print("org.Cinnamon proxy created, but no owner - is Cinnamon running?") 205 except GLib.Error as e: 206 print("Could not establish proxy for org.Cinnamon: %s" % e.message) 207 208 self.connect_proxy('XletAddedComplete', self._update_status) 209 self._update_status() 210 211 def _on_signal(self, proxy, sender_name, signal_name, params): 212 if signal_name == "RunStateChanged": 213 if self._proxy.GetRunState() == 2: 214 self.send_deferred_proxy_calls() 215 return 216 217 for name, callback in self._proxy_signals: 218 if signal_name == name: 219 callback(*params) 220 221 def send_deferred_proxy_calls(self): 222 if self._proxy.get_name_owner() and self._proxy_deferred_actions: 223 for command, args in self._proxy_deferred_actions: 224 getattr(self._proxy, command)(*args) 225 226 self._proxy_deferred_actions = [] 227 228 def connect_proxy(self, name, callback): 229 """ connects a callback to a dbus signal""" 230 self._proxy_signals.append((name, callback)) 231 232 def disconnect_proxy(self, name): 233 """ disconnects a previously connected dbus signal""" 234 for signal in self._proxy_signals: 235 if name in signal: 236 self._proxy_signals.remove(signal) 237 break 238 239 def send_proxy_signal(self, command, *args): 240 """ sends a command over dbus""" 241 if self._proxy is None or not self._proxy.get_name_owner(): 242 self._proxy_deferred_actions.append((command, args)) 243 else: 244 getattr(self._proxy, command)(*args) 245 246 def _update_status(self, *args): 247 try: 248 if self._proxy and self._proxy.get_name_owner(): 249 self.running_uuids = self._proxy.GetRunningXletUUIDs('(s)', self.collection_type) 250 else: 251 self.running_uuids = [] 252 except: 253 self.running_uuids = [] 254 self.emit('status-changed') 255 256 def open_spice_page(self, uuid): 257 """ opens to the web page of the given uuid""" 258 id = self.index_cache[uuid]['spices-id'] 259 os.system('xdg-open "%s/%ss/view/%s"' % (URL_SPICES_HOME, self.collection_type, id)) 260 261 def get_progressbar(self): 262 """ returns a Gtk.Widget that can be added to the application. This widget will show the 263 progress of any asynchronous actions taking place (ie. refreshing the cache or 264 downloading an applet)""" 265 progressbar = Gtk.ProgressBar() 266 progressbar.set_show_text(True) 267 progressbar.set_text('') 268 progressbar.set_fraction(0) 269 270 revealer = Gtk.Revealer() 271 revealer.add(progressbar) 272 revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN) 273 revealer.set_transition_duration(150) 274 progressbar.revealer = revealer 275 276 self.progressbars.append(progressbar) 277 278 return revealer 279 280 def _set_progressbar_text(self, text): 281 for progressbar in self.progressbars: 282 progressbar.set_text(text) 283 284 def _set_progressbar_fraction(self, fraction): 285 for progressbar in self.progressbars: 286 progressbar.set_fraction(fraction) 287 if self.window: 288 self.window.set_progress(int(fraction*100)) 289 290 def _set_progressbar_visible(self, visible): 291 for progressbar in self.progressbars: 292 progressbar.revealer.set_reveal_child(visible) 293 294 # updates any progress bars with the download progress 295 def _update_progress(self, count, blockSize, totalSize): 296 if self.download_manager.busy() and self.download_total_files > 1: 297 total = self.download_total_files 298 current = total - self.download_manager.get_n_jobs() 299 fraction = float(current) / float(total) 300 text = "%s %i/%i" % (_("Downloading images:"), current, total) 301 self._set_progressbar_text(text) 302 else: 303 fraction = count * blockSize / float((totalSize / blockSize + 1) * (blockSize)) 304 305 self._set_progressbar_fraction(fraction) 306 307 while Gtk.events_pending(): 308 Gtk.main_iteration() 309 310 # Jobs are added by calling _push_job. _process_job and _advance_queue form a wrapper that runs the job in it's own thread. 311 def _push_job(self, job): 312 self.total_jobs += 1 313 job['job_number'] = self.total_jobs 314 self.jobs.append(job) 315 if not self.processing_jobs: 316 self._advance_queue() 317 318 def _process_job(self, job): 319 job['result'] = job['func'](job) 320 if 'callback' in job: 321 GLib.idle_add(job['callback'], job) 322 GLib.idle_add(self._advance_queue) 323 324 def _advance_queue(self): 325 if self.monitorId > 0: 326 self.monitor.disconnect(self.monitorId) 327 self.monitorId = 0 328 329 self.processing_jobs = True 330 if self.is_downloading_image_cache: 331 return 332 333 self._set_progressbar_fraction(0) 334 335 if len(self.jobs) > 0: 336 self._set_progressbar_visible(True) 337 job = self.jobs.pop(0) 338 self.current_job = job 339 text = job['progress_text'] 340 if self.total_jobs > 1: 341 text += " (%i/%i)" % (job['job_number'], self.total_jobs) 342 self._set_progressbar_text(text) 343 job_thread = threading.Thread(target=self._process_job, args=(job,)) 344 job_thread.start() 345 else: 346 self.processing_jobs = False 347 self.current_job = None 348 self.total_jobs = 0 349 self._set_progressbar_visible(False) 350 self._set_progressbar_text('') 351 if self.monitor is not None: 352 try: 353 self.monitorId = self.monitor.connect('changed', self._directory_changed) 354 except Exception as e: 355 # File monitors can fail when the OS runs out of file handles 356 print(e) 357 self._directory_changed() 358 359 def _download(self, out_file, url, binary=True): 360 timestamp = round(time.time()) 361 url = "%s?time=%d" % (url, timestamp) 362 print("Downloading from %s" % url) 363 try: 364 open_args = 'wb' if binary else 'w' 365 with open(out_file, open_args) as outfd: 366 self._url_retrieve(url, outfd, self._update_progress, binary) 367 except Exception as e: 368 try: 369 os.remove(out_file) 370 except OSError: 371 pass 372 if not isinstance(e, KeyboardInterrupt) and not self.download_manager.abort_status: 373 self.errorMessage(_("An error occurred while trying to access the server. Please try again in a little while."), e) 374 self.abort() 375 return None 376 377 return out_file 378 379 def _url_retrieve(self, url, outfd, reporthook, binary): 380 #Like the one in urllib. Unlike urllib.retrieve url_retrieve 381 #can be interrupted. KeyboardInterrupt exception is raised when 382 #interrupted. 383 count = 0 384 blockSize = 1024 * 8 385 parsed_url = urlparse(url) 386 host = parsed_url.netloc 387 try: 388 proxy = proxygsettings.get_proxy_settings() 389 if proxy and proxy.get('https'): 390 connection = HTTPSConnection(proxy.get('https'), timeout=15) 391 connection.set_tunnel(host) 392 else: 393 connection = HTTPSConnection(host, timeout=15) 394 headers = { "Accept-Encoding": "identity", "Host": host, "User-Agent": "Python/3" } 395 full_path = "%s?%s" % (parsed_url.path, parsed_url.query) 396 connection.request("GET", full_path, headers=headers) 397 urlobj = connection.getresponse() 398 assert urlobj.getcode() == 200 399 400 totalSize = int(urlobj.info()['content-length']) 401 402 while not self._is_aborted(): 403 data = urlobj.read(blockSize) 404 count += 1 405 if not data: 406 break 407 if not binary: 408 data = data.decode("utf-8") 409 outfd.write(data) 410 ui_thread_do(reporthook, count, blockSize, totalSize) 411 except Exception as e: 412 raise e 413 414 def _load_metadata(self): 415 self.meta_map = {} 416 417 for directory in self.spices_directories: 418 if os.path.exists(directory): 419 extensions = os.listdir(directory) 420 421 for uuid in extensions: 422 subdirectory = os.path.join(directory, uuid) 423 try: 424 json_data = open(os.path.join(subdirectory, 'metadata.json')).read() 425 metadata = json.loads(json_data) 426 metadata['path'] = subdirectory 427 metadata['writable'] = os.access(subdirectory, os.W_OK) 428 self.meta_map[uuid] = metadata 429 except Exception as detail: 430 print(detail) 431 print("Skipping %s: there was a problem trying to read metadata.json" % uuid) 432 else: 433 print("%s does not exist! Creating it now." % directory) 434 subprocess.call(["mkdir", "-p", directory]) 435 436 def _directory_changed(self, *args): 437 self._load_metadata() 438 self._generate_update_list() 439 self.emit("installed-changed") 440 441 def get_installed(self): 442 """ returns a dictionary of the metadata by uuid of all installed spices""" 443 return self.meta_map 444 445 def get_is_installed(self, uuid): 446 """ returns a boolean specifying whether the given spice is installed or not""" 447 return uuid in self.meta_map 448 449 def get_has_update(self, uuid): 450 """ returns a boolean indicating whether the given spice has an update available""" 451 if uuid not in self.index_cache: 452 return False 453 454 try: 455 return int(self.meta_map[uuid]["last-edited"]) < self.index_cache[uuid]["last_edited"] 456 except Exception as e: 457 return False 458 459 def get_enabled(self, uuid): 460 """ returns the number of instances currently enabled""" 461 enabled_count = 0 462 if not self.themes: 463 enabled_list = self.settings.get_strv(self.enabled_key) 464 for item in enabled_list: 465 item = item.replace("!", "") 466 if uuid in item.split(":"): 467 enabled_count += 1 468 elif self.settings.get_string(self.enabled_key) == uuid: 469 enabled_count = 1 470 471 return enabled_count 472 473 def get_is_running(self, uuid): 474 """ checks whether the spice is currently running (it may be enabled but not running if 475 there was an error in initialization)""" 476 return uuid in self.running_uuids 477 478 def are_updates_available(self): 479 """ returns True if there are updates available or False otherwise""" 480 return len(self.updates_available) > 0 481 482 def get_n_updates(self): 483 """ returns the number of available updates""" 484 return len(self.updates_available) 485 486 def get_cache(self): 487 """ retrieves a copy of the index cache """ 488 return self.index_cache 489 490 def _load_cache(self): 491 filename = os.path.join(self.cache_folder, 'index.json') 492 if not os.path.exists(self.cache_folder): 493 os.makedirs(self.cache_folder, mode=0o755, exist_ok=True) 494 495 if not os.path.exists(filename): 496 self.has_cache = False 497 return 498 else: 499 self.has_cache = True 500 501 f = open(filename, 'r') 502 try: 503 self.index_cache = json.load(f) 504 except ValueError as detail: 505 try: 506 os.remove(filename) 507 except: 508 pass 509 self.errorMessage(_("Something went wrong with the spices download. Please try refreshing the list again."), str(detail)) 510 511 self._generate_update_list() 512 513 def _generate_update_list(self): 514 self.updates_available = [] 515 for uuid in self.index_cache: 516 if self.get_is_installed(uuid) and self.get_has_update(uuid): 517 self.updates_available.append(uuid) 518 519 def refresh_cache(self): 520 """ downloads an updated version of the index and assets""" 521 self.old_cache = self.index_cache 522 523 job = {'func': self._download_cache} 524 job['progress_text'] = _("Refreshing the cache") 525 self._push_job(job) 526 527 def _download_cache(self, load_assets=True): 528 download_url = URL_MAP[self.collection_type] 529 530 filename = os.path.join(self.cache_folder, "index.json") 531 if self._download(filename, download_url, binary=False) is None: 532 return 533 534 self._load_cache() 535 self._download_image_cache() 536 537 def _download_image_cache(self): 538 self.is_downloading_image_cache = True 539 540 self.used_thumbs = [] 541 542 self.download_total_files = 0 543 self.download_current_file = 0 544 545 for uuid, info in self.index_cache.items(): 546 if self.themes: 547 icon_basename = self._sanitize_thumb(os.path.basename(self.index_cache[uuid]['screenshot'])) 548 download_url = URL_SPICES_HOME + "/uploads/themes/thumbs/" + icon_basename 549 else: 550 icon_basename = os.path.basename(self.index_cache[uuid]['icon']) 551 download_url = URL_SPICES_HOME + self.index_cache[uuid]['icon'] 552 self.used_thumbs.append(icon_basename) 553 554 icon_path = os.path.join(self.cache_folder, icon_basename) 555 556 # if the image doesn't exist, is corrupt, or may have changed we want to download it 557 if not os.path.isfile(icon_path) or self._is_bad_image(icon_path) or self.old_cache[uuid]["last_edited"] != self.index_cache[uuid]["last_edited"]: 558 self.download_manager.push(self._download, self._check_download_image_cache_complete, (icon_path, download_url)) 559 self.download_total_files += 1 560 561 ui_thread_do(self._check_download_image_cache_complete) 562 563 def _check_download_image_cache_complete(self, *args): 564 # we're using multiple threads to download image assets, so we only clean up when all the downloads are done 565 if self.download_manager.busy(): 566 return 567 568 # Cleanup obsolete thumbs 569 trash = [] 570 flist = os.listdir(self.cache_folder) 571 for f in flist: 572 if f not in self.used_thumbs and f != "index.json": 573 trash.append(f) 574 for t in trash: 575 try: 576 os.remove(os.path.join(self.cache_folder, t)) 577 except: 578 pass 579 580 self.download_total_files = 0 581 self.download_current_file = 0 582 self.is_downloading_image_cache = False 583 self.settings.set_int('%s-cache-updated' % self.collection_type, time.time()) 584 self._advance_queue() 585 self.emit('cache-loaded') 586 587 def get_cache_age(self): 588 return (time.time() - self.settings.get_int('%s-cache-updated' % self.collection_type)) / 86400 589 590 # checks for corrupt images in the cache so we can redownload them the next time we refresh 591 def _is_bad_image(self, path): 592 try: 593 Image.open(path) 594 except IOError as detail: 595 return True 596 return False 597 598 # make sure the thumbnail fits the correct format (we are expecting it to be <uuid>.png 599 def _sanitize_thumb(self, basename): 600 return basename.replace("jpg", "png").replace("JPG", "png").replace("PNG", "png") 601 602 def install(self, uuid): 603 """ downloads and installs the given extension""" 604 job = {'uuid': uuid, 'func': self._install, 'callback': self._install_finished} 605 job['progress_text'] = _("Installing %s") % uuid 606 self._push_job(job) 607 608 def _install(self, job): 609 uuid = job['uuid'] 610 611 download_url = URL_SPICES_HOME + self.index_cache[uuid]['file'] 612 self.current_uuid = uuid 613 614 fd, ziptempfile = tempfile.mkstemp() 615 616 if self._download(ziptempfile, download_url) is None: 617 return 618 619 try: 620 zip = zipfile.ZipFile(ziptempfile) 621 622 tempfolder = tempfile.mkdtemp() 623 zip.extractall(tempfolder) 624 625 uuidfolder = os.path.join(tempfolder, uuid) 626 627 self.install_from_folder(uuidfolder, uuid, True) 628 except Exception as detail: 629 if not self.abort_download: 630 self.errorMessage(_("An error occurred during the installation of %s. Please report this incident to its developer.") % uuid, str(detail)) 631 return 632 633 try: 634 shutil.rmtree(tempfolder) 635 os.remove(ziptempfile) 636 except Exception: 637 pass 638 639 def install_from_folder(self, folder, uuid, from_spices=False): 640 """ installs a spice from a specified folder""" 641 contents = os.listdir(folder) 642 643 if not self.themes: 644 # Install spice localization files, if any 645 if 'po' in contents: 646 po_dir = os.path.join(folder, 'po') 647 for file in os.listdir(po_dir): 648 if file.endswith('.po'): 649 lang = file.split(".")[0] 650 locale_dir = os.path.join(locale_inst, lang, 'LC_MESSAGES') 651 os.makedirs(locale_dir, mode=0o755, exist_ok=True) 652 subprocess.call(['msgfmt', '-c', os.path.join(po_dir, file), '-o', os.path.join(locale_dir, '%s.mo' % uuid)]) 653 654 dest = os.path.join(self.install_folder, uuid) 655 if os.path.exists(dest): 656 shutil.rmtree(dest) 657 shutil.copytree(folder, dest) 658 659 if not self.themes: 660 # ensure proper file permissions 661 for root, dirs, files in os.walk(dest): 662 for file in files: 663 os.chmod(os.path.join(root, file), 0o755) 664 665 meta_path = os.path.join(dest, 'metadata.json') 666 if self.themes and not os.path.exists(meta_path): 667 md = {} 668 else: 669 file = open(meta_path, 'r') 670 raw_meta = file.read() 671 file.close() 672 md = json.loads(raw_meta) 673 674 if from_spices and uuid in self.index_cache: 675 md['last-edited'] = self.index_cache[uuid]['last_edited'] 676 else: 677 md['last-edited'] = int(datetime.datetime.utcnow().timestamp()) 678 679 raw_meta = json.dumps(md, indent=4) 680 file = open(meta_path, 'w+') 681 file.write(raw_meta) 682 file.close() 683 684 def _install_finished(self, job): 685 uuid = job['uuid'] 686 if self.get_enabled(uuid): 687 self.send_proxy_signal('ReloadXlet', '(ss)', uuid, self.collection_type.upper()) 688 689 def uninstall(self, uuid): 690 """ uninstalls and removes the given extension""" 691 job = {'uuid': uuid, 'func': self._uninstall} 692 job['progress_text'] = _("Uninstalling %s") % uuid 693 self._push_job(job) 694 695 def _uninstall(self, job): 696 try: 697 uuid = job['uuid'] 698 if not self.themes: 699 # Uninstall spice localization files, if any 700 if (os.path.exists(locale_inst)): 701 i19_folders = os.listdir(locale_inst) 702 for i19_folder in i19_folders: 703 if os.path.isfile(os.path.join(locale_inst, i19_folder, 'LC_MESSAGES', '%s.mo' % uuid)): 704 os.remove(os.path.join(locale_inst, i19_folder, 'LC_MESSAGES', '%s.mo' % uuid)) 705 # Clean-up this locale folder 706 removeEmptyFolders(os.path.join(locale_inst, i19_folder)) 707 708 # Uninstall settings file, if any 709 if (os.path.exists(os.path.join(settings_dir, uuid))): 710 shutil.rmtree(os.path.join(settings_dir, uuid)) 711 shutil.rmtree(os.path.join(self.install_folder, uuid)) 712 except Exception as detail: 713 self.errorMessage(_("A problem occurred while removing %s.") % job['uuid'], str(detail)) 714 715 def update_all(self): 716 """ applies all available updates""" 717 for uuid in self.updates_available: 718 self.install(uuid) 719 720 def abort(self, abort_type=ABORT_USER): 721 """ trigger in-progress download to halt""" 722 self.abort_download = abort_type 723 self.download_manager.abort() 724 725 def _is_aborted(self): 726 return self.download_manager.abort_status 727 728 def _ui_error_message(self, msg, detail = None): 729 dialog = Gtk.MessageDialog(transient_for = self.window, 730 modal = True, 731 message_type = Gtk.MessageType.ERROR, 732 buttons = Gtk.ButtonsType.OK) 733 markup = msg 734 if detail is not None: 735 markup += _("\n\nDetails: %s") % (str(detail)) 736 esc = html.escape(markup) 737 dialog.set_markup(esc) 738 dialog.show_all() 739 response = dialog.run() 740 dialog.destroy() 741 742 def errorMessage(self, msg, detail=None): 743 ui_thread_do(self._ui_error_message, msg, detail) 744 745 def enable_extension(self, uuid, panel=1, box='right', position=0): 746 if self.collection_type == 'applet': 747 entries = [] 748 applet_id = self.settings.get_int('next-applet-id') 749 self.settings.set_int('next-applet-id', (applet_id+1)) 750 751 for entry in self.settings.get_strv(self.enabled_key): 752 info = entry.split(':') 753 pos = int(info[2]) 754 if info[0] == 'panel%d' % panel and info[1] == box and position <= pos: 755 info[2] = str(pos+1) 756 entries.append(':'.join(info)) 757 else: 758 entries.append(entry) 759 760 entries.append('panel%d:%s:%d:%s:%d' % (panel, box, position, uuid, applet_id)) 761 762 self.settings.set_strv(self.enabled_key, entries) 763 elif self.collection_type == 'desklet': 764 desklet_id = self.settings.get_int('next-desklet-id') 765 self.settings.set_int('next-desklet-id', (desklet_id+1)) 766 enabled = self.settings.get_strv(self.enabled_key) 767 768 screen = Gdk.Screen.get_default() 769 primary = screen.get_primary_monitor() 770 primary_rect = screen.get_monitor_geometry(primary) 771 enabled.append(('%s:%d:%d:%d') % (uuid, desklet_id, primary_rect.x + 100, primary_rect.y + 100)) 772 773 self.settings.set_strv(self.enabled_key, enabled) 774 775 else: 776 enabled = self.settings.get_strv(self.enabled_key) 777 enabled.append(uuid) 778 self.settings.set_strv(self.enabled_key, enabled) 779 780 def disable_extension(self, uuid): 781 enabled_extensions = self.settings.get_strv(self.enabled_key) 782 new_list = [] 783 for enabled_extension in enabled_extensions: 784 if self.collection_type == 'applet': 785 enabled_uuid = enabled_extension.split(':')[3].strip('!') 786 elif self.collection_type == 'desklet': 787 enabled_uuid = enabled_extension.split(':')[0].strip('!') 788 else: 789 enabled_uuid = enabled_extension 790 791 if enabled_uuid != uuid: 792 new_list.append(enabled_extension) 793 self.settings.set_strv(self.enabled_key, new_list) 794 795 def get_icon(self, uuid): 796 """ gets the icon for a given uuid""" 797 try: 798 if self.themes: 799 file_path = os.path.join(self.cache_folder, os.path.basename(self.index_cache[uuid]['screenshot'])) 800 pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file_path, 100, -1, True) 801 else: 802 file_path = os.path.join(self.cache_folder, os.path.basename(self.index_cache[uuid]['icon'])) 803 pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file_path, 24, 24, True) 804 805 return Gtk.Image.new_from_pixbuf(pixbuf) 806 except Exception as e: 807 print("There was an error processing one of the images. Try refreshing the cache.") 808 return Gtk.Image.new_from_icon_name('image-missing', 2) 809