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