1#!/usr/local/bin/python3.8
2"""Provide functionality relating to an app in a dock.
3
4    Allow info relating to a running app (e.g. icon, command line,
5    .desktop file location, running processes, open windows etc) to be
6    obtained from the information that libWnck provides
7
8    Provide a surface on which the application's icon and the running indicator
9    can be drawn
10
11    Ensure that the app's icon and indicator are always drawn correctly
12    according to the size and orientation of the panel
13
14    Provide visual feedback to the user when an app is launched by pulsating
15    the app's icon
16
17    Draw a highlight around the app's icon if it is the foreground app
18
19    Maintain a list of all of the app's running processes and their windows
20
21    Ensure that the application's windows visually minimise to the
22    application's icon on the dock
23"""
24
25#
26# Copyright (C) 1997-2003 Free Software Foundation, Inc.
27#
28# This program is free software; you can redistribute it and/or
29# modify it under the terms of the GNU General Public License as
30# published by the Free Software Foundation; either version 2 of the
31# License, or (at your option) any later version.
32#
33# This program is distributed in the hope that it will be useful, but
34# WITHOUT ANY WARRANTY; without even the implied warranty of
35# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
36# General Public License for more details.
37#
38# You should have received a copy of the GNU General Public License
39# along with this program; if not, write to the Free Software
40# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
41# 02110-1301, USA.
42#
43# Author:
44#     Robin Thompson
45
46# do not change the value of this variable - it will be set during build
47# according to the value of the --with-gtk3 option used with .configure
48build_gtk2 = False
49
50import gi
51
52if build_gtk2:
53    gi.require_version("Gtk", "2.0")
54    gi.require_version("Wnck", "1.0")
55else:
56    gi.require_version("Gtk", "3.0")
57    gi.require_version("Wnck", "3.0")
58
59gi.require_version("MatePanelApplet", "4.0")
60gi.require_version("Bamf", "3")
61
62from gi.repository import Gtk
63from gi.repository import MatePanelApplet
64from gi.repository import Gdk
65from gi.repository import GdkPixbuf
66from gi.repository import Wnck
67from gi.repository import Gio
68from gi.repository import GObject
69from gi.repository import GLib
70from gi.repository import Bamf
71
72import cairo
73import math
74import xdg.DesktopEntry as DesktopEntry
75import xdg.BaseDirectory as BaseDirectory
76import os
77import os.path
78import subprocess
79import re
80import colorsys
81
82from collections import namedtuple
83
84import dock_prefs
85from docked_app_helpers import *
86import window_control
87
88from log_it import log_it as log_it
89
90ColorTup = namedtuple('ColorTup', ['r', 'g', 'b'])
91
92
93def get_backlight_color(pixbuf):
94    """
95
96    Read all of the pixel values in a pixbuf and calculate an appropriate
97    colour to use as an icon backlight
98
99    Code adapated from Unity desktop (https://code.launchpad.net/~unity-team/unity/trunk)
100    specifically from LauncherIcon::ColorForIcon in LauncherIcon.cpp
101
102    Args:
103        pixbuf  : a pixbuf object containing the image
104
105    Returns:
106        a tuple of r,g,b value (0-255)
107    """
108
109    width = pixbuf.props.width
110    rowstride = pixbuf.props.rowstride
111    height = pixbuf.props.height
112
113    num_channels = pixbuf.get_n_channels()
114    has_alpha = pixbuf.get_has_alpha()
115
116    img = pixbuf.get_pixels()
117    r_total = g_total = b_total = 0
118    total = 0.0
119
120    for w_count in range(width):
121        for h_count in range(height):
122            pix_index = (h_count * rowstride + w_count * num_channels)
123            pix_r = img[pix_index]
124            pix_g = img[pix_index + 1]
125            pix_b = img[pix_index + 2]
126            if has_alpha:
127                pix_a = img[pix_index + 3]
128            else:
129                pix_a = 255
130
131            saturation = float(max(pix_r, max(pix_g, pix_b)) - min(pix_r, min(pix_g, pix_b))) / 255.0
132            relevance = .1 + .9 * (float(pix_a) / 255) * saturation
133
134            r_total += (pix_r * relevance) % 256
135            g_total += (pix_g * relevance) % 256
136            b_total += (pix_b * relevance) % 256
137            total += relevance * 255.0
138
139    r = r_total / total
140    g = g_total / total
141    b = b_total / total
142
143    h, s, v = colorsys.rgb_to_hsv(r, g, b)
144    if s > 0.15:
145        s = 0.65
146
147    v = 0.6666
148    # Note: Unty uses v = 0.9, but this produces a very bright value which
149    # is reduced elsewhere. We use 0.6666 to reduce it here
150    br, bg, bb = colorsys.hsv_to_rgb(h, s, v)
151
152    v = 1.0
153    gr, gg, gb = colorsys.hsv_to_rgb(h, s, v)
154
155    return int(br * 255), int(bg * 255), int(bb * 255)
156
157
158def get_avg_color(pixbuf):
159    """calculate the average colour of a pixbuf.
160
161    Read all of the pixel values in a pixbuf (excluding those which are below a
162    certain alpha value) and calculate the average colour of all the contained
163    colours
164
165    Args:
166        pixbuf  : a pixbuf object containing the image
167
168    Returns:
169        a tuple of r,g,b values (0-255)
170    """
171
172    width = pixbuf.props.width
173    rowstride = pixbuf.props.rowstride
174    height = pixbuf.props.height
175    has_alpha = pixbuf.get_has_alpha()
176    pixels = pixbuf.get_pixels()
177    nchannels = pixbuf.get_n_channels()
178    # convert the pixels into an rgb array with alpha channel
179    data = []
180    for y_count in range(height - 1):
181        x_count = 0
182        while x_count < (width * nchannels):
183            pix_red = pixels[x_count + (rowstride * y_count)]
184            pix_green = pixels[x_count + 1 + (rowstride * y_count)]
185            pix_blue = pixels[x_count + 2 + (rowstride * y_count)]
186            if has_alpha:
187                pix_alpha = pixels[x_count + 3 + (rowstride * y_count)]
188            else:
189                pix_alpha = 255
190
191            data.append([pix_red, pix_green, pix_blue, pix_alpha])
192
193            x_count += nchannels
194
195    red = 0
196    green = 0
197    blue = 0
198    num_counted = 0
199
200    for pixels in range(len(data)):
201        if data[pixels][3] > 200:       # only count pixel if alpha above this
202                                        # level
203            red += data[pixels][0]
204            green += data[pixels][1]
205            blue += data[pixels][2]
206            num_counted += 1
207
208    if num_counted > 0:
209        ravg = int(red / num_counted)
210        gavg = int(green / num_counted)
211        bavg = int(blue / num_counted)
212    else:
213        # in case of a bad icon assume a grey average colour
214        # this should fix a division by zero error that occurred at this point
215        # in the code, but which I've only seen once and never been able to
216        # duplicate
217        ravg = gavg = bavg = 128
218
219    return ravg, gavg, bavg
220
221
222CONST_PULSE_STEPS = 20
223CONST_PULSE_DELAY = 40
224
225
226class ScrollType:
227    """ Class to define the ways in which the docked apps may scroll in the dock"""
228    SCROLL_NONE = 0
229    SCROLL_UP = 1    # for horizontal applets 'up' equates to 'left'
230    SCROLL_DOWN = 2  # or, for horizontal applets, right...
231
232
233class PulseTimer(object):
234    """Class to help provide feedback when a user launches an app from the dock.
235
236    Instantiates a timer which periodically redraws an application in the dock
237    at various transparency levels until the timer has been run a certain
238    number of times
239
240    Attributes:
241        app = the DockedApp object which we want to pulsate
242        timer_id = the id of the timer that is instantiated
243
244    """
245
246    def __init__(self, app, once_only=False):
247        """Init for the PulseTimer class.
248
249        Sets everything up by creating the timer, setting a reference to the
250        DockedApp and telling the app that it is pulsing
251
252        Arguments:
253            app : the DockedApp object
254            once_only : do only one iteration of pulsing, then stop
255        """
256
257        self.timer_count = 0
258        self.app = app
259        self.app.pulse_step = 0
260        self.app.is_pulsing = True
261        self.once_only = once_only
262        self.timer_id = GObject.timeout_add(CONST_PULSE_DELAY, self.do_timer)
263
264    def do_timer(self):
265        """The timer function.
266
267        Increments the number of times the time function has been called. If it
268        hasn't reached the maximum number, increment the app's pulse counter.
269        If the maximum number has been reached, stop the app pulsing and
270        delete the timer.
271
272        Redraw the app's icon
273        """
274
275        # the docked app may indicate it no longer wants to pulse...
276        if not self.app.is_pulsing:
277            self.remove_timer()
278            self.app.queue_draw()
279            return False
280
281        self.timer_count += 1
282        if self.timer_count / int(1000 / CONST_PULSE_DELAY) == 45:
283            # we've been pulsing for long enough, the user will be getting a headache
284            self.remove_timer()
285            self.app.queue_draw()
286            return False
287
288        if self.app.pulse_step != CONST_PULSE_STEPS:
289            self.app.pulse_step += 1
290        else:
291            # if we're starting the app for the first time (with startup notification)
292            # and it still hasn't finished loading, carry on pulsing
293            if (self.app.startup_id is not None) and not self.once_only:
294                self.app.pulse_step = 0
295            else:
296                # we only want the icon to pulse once
297                # if we have a startup_id the notification needs to be cancelled
298                if self.app.startup_id is not None:
299                    self.app.cancel_startup_notification()
300
301                self.remove_timer()
302
303        self.app.queue_draw()
304        return True
305
306    def remove_timer(self):
307        """
308            Cancel the timer and stop the app icon pulsing...
309
310        """
311
312        self.app.is_pulsing = False
313        GObject.source_remove(self.timer_id)
314
315
316CONST_BLINK_DELAY = 330
317
318
319class AttentionTimer(object):
320    """Class to help provide visual feedback when an app requries user attention.
321
322    Instantiates a timer which periodically checks whether or not the app
323    still needs attention. If the app is blinking, it toggles the
324    blink state
325    on and off until the app no longer needs attention
326
327    Attributes:
328        app = the DockedApp object that needs attentions
329        timer_id = the id of the timer that is instantiated
330
331    """
332
333    def __init__(self, app):
334        """Init for the AttentionTimer class.
335
336        Sets everything up by creating the timer, setting a reference to the
337        DockedApp and setting the inital flash state to off
338
339        Arguments:
340            app : the DockedApp object
341        """
342
343        self.app = app
344        self.app.needs_attention = True
345        self.app.attention_blink_on = False
346        self.timer_id = GObject.timeout_add(CONST_BLINK_DELAY, self.do_timer)
347
348        # make the app redraw itself
349        app.queue_draw()
350
351    def do_timer(self):
352        """The timer function.
353
354        If the app no longer needs attention, stop it flashing and delete
355        the timer. Otherwise, invert the flash.
356
357        Finally,Redraw the app's icon
358        """
359
360        if self.app.needs_attention:
361            self.app.attention_blink_on = not self.app.attention_blink_on
362        else:
363            GObject.source_remove(self.timer_id)
364
365        self.app.queue_draw()
366
367        return True
368
369
370class DockedApp(object):
371    """Provide a docked app class
372
373    Attributes:
374
375        bamf_app    : the Bamf.Applications related to the running app
376        app_name    : e.g. Google Chrome, used for tooltips and the applet
377                      right click menu etc
378        rc_actions  : A list of strings containing the names of the additional application
379                      actions suppported by the app
380        cmd_line    : the command line and arguments used to start the app
381        icon_name   : name of the app icon
382        icon_filename : the filename of the app icon
383        desktop_file : the filename of the app's .desktop file
384        desktop_ai   : a Gio.GDesktopAppInfo read from the .desktop file
385        startup_id   : id used for startup notifications
386        applet_win  : the Gdk.Window of the panel applet
387        applet      : the panel applet
388        applet_orient : the applet orientation
389
390        drawing_area: Gtk.Label- provides a surface on which the app icon can
391                              be drawn
392        drawing_area_size : the base size in pixels (height AND width) that we have
393                            to draw in - note that some indicators require more
394                            and must specify this...
395        is_pulsing  : boolean - True if the app is pulsing
396        pulse_step  : a count of how far we are through the pulse animation
397        app_pb      : a pixbuf of the app's icon
398        app_surface : a surface of the app's icon
399        highlight_colour : ColorTup of the colours used to highlight the app
400                           when it is foreground
401        is_active   : boolean - True = the app is the foreground app
402        has_mouse   : boolean - True = the mouse is over the app's icon
403        is_pinned   : boolean - Whether or not the app is pinned to the dock
404        indicator   : the type of indictor (e.g. light or dark) to draw under
405                      running apps
406        active_bg   : the type of background (e.g. gradient or solid fill) to
407                      be drawn when the app is the active app
408        ind_ws      : wnck_workspace or None - if set, indicators are to be
409                      drawn for windows on the specified workspace
410        last_active_win : the Bamf.Window of the app's last active window
411
412        is_dragee  : boolean - indicates whether or not the app's icon is
413                     being dragged to a new position on the dock
414
415        show_progress : boolean - indicates whether or not to display a
416                        progress indicator on the app's icon
417        progress_val  : the progress value( 0 to 1.0)
418        show_count   : boolean - indicates whether or not to display a
419                        count value on the app's icon
420        count_val   : the value of the count
421        needs_attention: whether or not the app needs the user's attention
422        attention_type : how the docked app indicates to the user that the app
423                         needs attention
424        attention_blink_on : when an app blinks when it needs attention, this specfies
425                             the state
426        scroll_dir : indicates the way that the dock may be scrolled (if any)
427                     if the mouse hovers over this app. Also used to draw the
428                     app icon in such a way as to indicate that scrolling is available
429    """
430
431    def __init__(self):
432        """ Init for the DockApplet class.
433
434            Create a surface to draw the app icon on
435            Set detault values
436        """
437
438        super().__init__()
439
440        self.bamf_app = None
441        self.app_info = []
442        self.app_name = ""
443        self.rc_actions = []
444        self.cmd_line = ""
445        self.icon_name = ""
446        self.icon_filename = ""
447        self.desktop_file = ""
448        self.desktop_ai = None
449        self.icon_geometry_set = False
450        self.applet_win = None
451        self.applet_orient = None
452        self.ind_ws = None
453        self.startup_id = None
454        self.applet = None
455
456        # all drawing is done to a Gtk.Label rather than e.g. a drawing area
457        # or event box this allows panel transparency/custom backgrounds to be
458        # honoured
459        # However, the downside of this is that mouse events cannot be handled
460        # by this class and instead have to be done by the applet itself
461
462        self.drawing_area = Gtk.Label()
463        self.drawing_area.set_app_paintable(True)
464        self.drawing_area_size = 0
465
466        self.is_pulsing = False
467        self.pulse_step = 0
468
469        self.needs_attention = False
470        self.attention_blink_on = False
471
472        self.app_pb = None
473        self.app_surface = None
474        self.highlight_color = ColorTup(r=0.0, g=0.0, b=0.0)
475
476        self.is_active = False
477        self.has_mouse = False
478
479        self.is_pinned = False
480
481        # set defaults
482        self.indicator = IndicatorType.LIGHT     # light indicator
483        self.multi_ind = False                   # single indicator
484        self.active_bg = IconBgType.GRADIENT
485
486        self.attention_type = dock_prefs.AttentionType.BLINK
487
488        self.last_active_win = None
489
490        # set up event handler for the draw/expose event
491        if build_gtk2:
492            self.drawing_area.connect("expose-event", self.do_expose_event)
493        else:
494            self.drawing_area.connect("draw", self.do_expose_event)
495
496        self.is_dragee = False
497
498        self.show_progress = False
499        self.progress_val = 0.0
500        self.show_count = False
501        self.count_val = 0
502
503        self.scroll_dir = ScrollType.SCROLL_NONE
504
505    def set_bamf_app(self, b_app):
506        """ Sets the Bamf.Application related to this docked app
507
508        Params: b_app : the Bamf.Application to be added
509        """
510
511        self.bamf_app = b_app
512
513    def clear_bamf_app(self):
514        """ Unsets the Bamf.Application related to this docked app
515
516        Params: b_app : the Bamf.Application to removed """
517
518        self.bamf_app = None
519
520    def has_bamf_app(self, b_app):
521        """ Returns True if b_app is associated with this docked_app, False otherwise
522
523            Params: b_app - a Bamf.Application
524        """
525
526        return b_app == self.bamf_app
527
528    def get_windows(self):
529        """ Convenience function to return a list of the app's Bamf.Windows
530
531        Returns : an empty list if the app is not running or if self.bamf_app is None,
532                  otherwise the window list
533
534        """
535
536        ret_val = []
537        if (self.bamf_app is not None) and (self.bamf_app.is_running() or self.bamf_app.is_starting()):
538            ret_val = self.bamf_app.get_windows()
539
540        return ret_val
541
542    def get_first_normal_win(self):
543        """ Returns the app's first 'normal' window i.e. a window or dialog
544
545            Returns:
546                a Bamf.Window, or None
547        """
548
549        if (self.bamf_app is not None) and (self.bamf_app.is_running()):
550            for win in self.get_windows():
551                win_type = win.get_window_type()
552                if win_type in [Bamf.WindowType.NORMAL, Bamf.WindowType.DIALOG] or win.is_user_visible():
553                    return win
554
555        return None
556
557    def has_wnck_app(self, wnck_app):
558        """ see if this app has a process with the specified wnck_app
559
560        Returns True if the wnck_app is found, False otherwise
561        """
562
563        ret_val = False
564        for aai in self.app_info:
565            if aai.app == wnck_app:
566                ret_val = True
567                break
568
569        return ret_val
570
571    def has_bamf_window(self, win):
572        """
573            Checks to see if a window belongs to the app
574
575        Params:
576            win : the Bamf.Window we're interested in
577
578        Returns:
579            True if the window belongs to the app, False otherwise
580
581        """
582
583        windows = self.get_windows()
584        return win in windows
585
586    def setup_from_bamf(self, app_match_list):
587        """ Setup an already running app using info from self.bamf_app
588
589        This is only called when bamf cannot match an app with it's .desktop file,
590        so we can also do some extra checking from the list of hard to match
591        .desktop files
592
593        """
594
595        # get a list of all the possible locations of .desktop files
596        data_dirs = BaseDirectory.xdg_data_dirs
597        app_dirs = []
598        for dir in data_dirs:
599            app_dirs.append(os.path.join(dir, "applications/"))
600
601        # search for a match in
602        for app in app_match_list:
603            if self.bamf_app.get_name() == app[0]:
604
605                for dir in app_dirs:
606                    desktop_file = os.path.join(dir, app[2])
607                    if os.path.exists(desktop_file):
608                        self.desktop_file = desktop_file
609                        if self.read_info_from_desktop_file():
610                            return
611
612        # no match, so just get basic info
613        self.app_name = self.bamf_app.get_name()
614        self.icon_name = "wnck"  # indicate we want to get the app icon from wnck
615
616    def set_app_name(self, app_name):
617        """sets the app name.
618
619        Stores the entire name, which may or may not also contain a
620        document title or other app specific info. This will need to
621        be parsed when necessary to obtain the actual app name
622
623        Args: The app name
624
625        """
626
627        self.app_name = app_name
628
629    def set_urgency(self, urgent):
630        """ Sets the app's urgency state
631
632        Params : urgent - bool, whether or not the app is signalling urgency
633        """
634
635        if urgent:
636            if not self.needs_attention:
637                self.needs_attention = True
638                self.attention_blink_on = False  # initial blink state = off
639                timer = AttentionTimer(self)
640
641                if not self.is_visible():
642                    self.show_icon()
643        else:
644            if self.needs_attention:
645                # we need to turn flashing off
646                self.needs_attention = False
647                self.queue_draw()
648
649                # the timer will handle the rest ....
650                # hiding the icon (if necessary) will be taken care of next
651                # time the user changes workspace
652
653    def get_cmdline_from_pid(self, pid):
654        """ Find the command line and arguments used to launch the app
655
656        Use the ps command to return the command line and arguments
657        for the specified pid
658
659        Set self.path to the full command line
660
661        Args:
662            pid - a process id
663
664        """
665
666        cmdstr = "xargs -0 < /proc/%d/cmdline" % pid
667
668        cmd = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE)
669
670        for line in cmd.stdout:
671            pass
672
673        if line is not None:
674            self.cmd_line = line.decode("utf-8")
675
676    def has_windows_on_workspace(self, wnck_workspace):
677        """ test whether the app has at least one window open on a specified
678            workspace
679
680        Args:
681            wnck_workspace - the workspace to check for
682
683        Returns:
684            boolean
685        """
686
687        for win in self.get_windows():
688            wnck_win = Wnck.Window.get(win.get_xid())
689            if wnck_win is not None:
690                win_ws = wnck_win.get_workspace()
691                if win_ws == wnck_workspace:
692                    return True
693
694        return False
695
696    def has_unminimized_windows(self):
697        """ test whether the app has at least one unminimized window
698
699        Returns:
700            boolean
701        """
702
703        win_list = self.get_windows()
704        for win in win_list:
705            wnck_win = Wnck.Window.get(win.get_xid())
706            if (wnck_win is not None) and (not wnck_win.is_minimized()):
707                return True
708
709        return False
710
711    def hide_icon(self):
712        """ Hides the app's icon"""
713
714        self.drawing_area.set_visible(False)
715
716    def show_icon(self):
717        """ Shows the app's icon"""
718
719        self.drawing_area.set_visible(True)
720
721    def is_visible(self):
722        """ Method which returns whether or not the app's icon is visible
723
724        Returns:
725            boolean
726        """
727        return self.drawing_area.get_visible()
728
729    def get_desktop_from_custom_launcher(self, srch_dir):
730        """ Search the custom launchers in a specified directory for
731            one where the Exec field is found within self.cmd_line
732
733        If a match is found found, self.desktop_file is set accordingly
734
735        Note: All custom launchers  .desktop filenames start
736              with "mda_"
737
738        Args:
739            srch_dir : the directory to search
740
741        Returns:
742            True if a match was found, False otherwise
743        """
744
745        # TODO: replace DesktopEntry with Gio.DesktopAppInfo... and then
746
747        # if the search dir doesn't exist, don't do anything
748        if os.path.isdir(srch_dir) is False:
749            return False
750
751        for the_file in os.listdir(srch_dir):
752            if (the_file.startswith("mda_")) and \
753               (the_file.endswith(".desktop")):
754                the_de = DesktopEntry.DesktopEntry(srch_dir + the_file)
755
756                # remove command line args from the Exec field of the .desktop
757                de_exec = the_de.getExec().split(None, 1)[0]
758
759                if self.cmd_line.find(de_exec) != -1:
760                    self.desktop_file = srch_dir + the_file
761                    return True
762
763    def set_all_windows_icon_geometry(self, x, y, width, height):
764        """Set the location on screen where all of the app's windows will be
765           minimised to.
766
767        Args:
768            x : The X position in root window coordinates
769            y : The Y position in root window coordinates
770            width: the width of the minimise location
771            height:  the height of the minimise location
772
773        """
774
775        for win in self.get_windows():
776            window_control.set_minimise_target(win, x, y, width, height)
777
778        return True
779
780    def get_allocation(self):
781        """ Returns the allocated position and size of the app's icon within the applet
782        """
783
784        alloc = self.drawing_area.get_allocation()
785        return alloc.x, alloc.y, alloc.width, alloc.height
786
787    def set_drawing_area_size(self, size):
788        """Set the size request of the app's drawing area.
789
790        Args :
791            size : the size in pixels we need, although extra can be applied if required by indicators
792
793        """
794        self.drawing_area_size = size
795
796        extra_s = ind_extra_s(self.indicator)
797        if extra_s == 0:
798            self.drawing_area.set_size_request(size, size)
799        else:
800            # we need to allocate extra space in the x or y dimension depending on applet orientation
801            if (self.applet_orient == MatePanelApplet.AppletOrient.DOWN) or \
802               (self.applet_orient == MatePanelApplet.AppletOrient.UP):
803                self.drawing_area.set_size_request(size + extra_s, size)
804            else:
805                self.drawing_area.set_size_request(size, size + extra_s)
806
807    def queue_draw(self):
808        """Queue the app's icon to be redrawn.
809        """
810        self.drawing_area.queue_draw()
811
812    def set_indicator(self, indicator):
813        """Set the running indicator type to the value specified
814
815        Args:
816            indicator - the indicator type
817        """
818        self.indicator = indicator
819
820    def set_active_bg(self, active_bg):
821        """Set the active background type to the value specified
822
823        Args:
824            active_bg - the background type
825
826        """
827
828        self.active_bg = active_bg
829
830    def set_multi_ind(self, multi_ind):
831        """ Set whether to use an indicator for each open window
832
833        Args:
834            multi_ind - boolean
835        """
836        self.multi_ind = multi_ind
837
838    def set_attention_type(self, attention_type):
839        """Set the attention type to the value specified
840
841        Args:
842            indicator - the indicator type
843        """
844        self.attention_type = attention_type
845
846    def is_running(self):
847        """
848            Is the app running ?
849
850        Returns: True if the app is running, False if not
851
852        """
853
854        if self.bamf_app is None:
855            return False
856
857        return self.bamf_app.is_running()
858
859    def has_desktop_file(self):
860        """ Does the app have a .desktop file?
861
862        Returns: True if there is a desktop file, False otherwise
863        """
864
865        return self.desktop_file is not None
866
867    def read_info_from_desktop_file(self):
868        """Attempt to read from read the app's desktop file.
869
870        Will try to read the icon name and app name from the desktop file
871        Will also get the executeable path if we don't already have this
872        Will read the details of any right click menu options the .desktop
873        file defines
874
875        Returns:
876            True if successful, False otherwise
877        """
878
879        if self.desktop_file:
880            if os.path.isabs(self.desktop_file):
881                self.desktop_ai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file)
882            else:
883                self.desktop_ai = Gio.DesktopAppInfo.new(self.desktop_file)
884
885            self.app_name = self.desktop_ai.get_locale_string("Name")
886            self.icon_name = self.desktop_ai.get_string("Icon")
887
888            # if the desktop file does not specify an icon name, use the app
889            # name instead
890            if (self.icon_name is None) or (self.icon_name == ""):
891                self.icon_name = self.app_name.lower()
892
893                # hack for the MATE application browser app, where the
894                # .desktop file on Ubuntu does not specify an icon
895                if self.icon_name == "application browser":
896                    self.icon_name = "computer"
897
898            # get the command specified in the .desktop file used to launch the app
899            self.cmd_line = self.desktop_ai.get_string("Exec")
900
901            # get the list of addtional application actions (to be activated by right
902            # clicking the app's dock icon)
903            self.rc_actions = self.desktop_ai.list_actions()
904
905            return True
906
907        return False
908
909    def app_has_custom_launcher(self):
910        """ Determines whether the docked app has a custom launcher
911
912        Examine the .desktop filename. If it starts with
913        "~/.local/share/applications/mda_" the app has a custom launcher
914
915        Returns : True if the app has a custom launcher, False otherwise
916        """
917
918        cl_start = os.path.expanduser("~/.local/share/applications/mda_")
919        return os.path.expanduser(self.desktop_file).beginswith(cl_start)
920
921    def win_state_changed(self, wnck_win, changed_mask, new_state):
922        """Handler for the wnck_window state-changed event
923
924        If the app needs attention and we're not already flashing the icon
925        start it flashing. If the app icon is not visible, make it visible
926
927        If the app doesn't need attention and its icon is flashing, stop
928        it flashing
929
930        """
931
932        if ((new_state & Wnck.WindowState.DEMANDS_ATTENTION) != 0) or\
933           ((new_state & Wnck.WindowState.URGENT) != 0):
934
935            if not self.needs_attention:
936                self.needs_attention = True
937                self.attention_blink_on = False  # initial blink state = off
938                timer = AttentionTimer(self)
939
940                if not self.is_visible():
941                    self.show_icon()
942
943        else:
944            if self.needs_attention:
945                # we need to turn flashing off
946                self.needs_attention = False
947                self.queue_draw()
948
949                # the timer will handle the rest ....
950                # hiding the icon (if necessary) will be taken care of next
951                # time the user changes workspace
952
953    def get_num_windows(self, cur_ws=None):
954        """
955            Get the number of normal and dialog windows the app has open.
956            If cur_ws is specfied, then only windows on the specified workspace are counted
957
958            Params: cur_ws - an int representing the workspace number:
959
960            Returns: an int
961
962        """
963
964        num_win = 0
965
966        if self.bamf_app is not None:
967            for win in self.get_windows():
968                win_type = win.get_window_type()
969                if win_type in [Bamf.WindowType.NORMAL, Bamf.WindowType.DIALOG] and win.is_user_visible():
970                    if cur_ws is None:
971                        num_win += 1
972                    else:
973                        xid = win.get_xid()
974                        wnck_win = Wnck.Window.get(xid)
975                        if (wnck_win is not None) and wnck_win.is_on_workspace(cur_ws):
976                            num_win += 1
977        return num_win
978
979    def do_expose_event(self, drawing_area, event):
980        """The main drawing event for the docked app.
981
982        Does the following:
983            draw the app icon
984            if the mouse is over the app icon, highlight the icon
985            if the is running draw the app running indicators(according to the
986            applet orientation)
987            if the app is the foreground app, highlight the background with a
988            gradient fill
989            if the app is pulsing, draw the icon with a variable level of
990            transparency according to the pulse count
991            if the app is flashing, draw the icon either fully opaque or
992            completely transparent according to its flash state
993            if the app is being dragged to a new position on the dock, draw
994            a completely transparent background
995        Args:
996            drawing_area : the drawing area that related to the event. Will
997                           always be the same as self.drawing area
998            event        : in Gtk2 the event arguments, in Gtk3 a cairo context
999                           to draw on
1000
1001        """
1002
1003        # there are lots of drawing operations to be done, so do them to an
1004        # offscreen surface and when all is finished copy this to the docked
1005        # app
1006
1007        if self.applet_orient == MatePanelApplet.AppletOrient.DOWN or \
1008           self.applet_orient == MatePanelApplet.AppletOrient.UP:
1009            oss_w = self.drawing_area_size + ind_extra_s(self.indicator)
1010            oss_h = self.drawing_area_size
1011        else:
1012            oss_w = self.drawing_area_size
1013            oss_h = self.drawing_area_size + ind_extra_s(self.indicator)
1014
1015        offscreen_surface = cairo.Surface.create_similar(self.app_surface,
1016                                                         cairo.CONTENT_COLOR_ALPHA,
1017                                                         oss_w, oss_h)
1018
1019        ctx = cairo.Context(offscreen_surface)
1020
1021        if self.is_dragee is False:
1022            # convert the highlight values to their cairo equivalents
1023            red = self.highlight_color.r / 255
1024            green = self.highlight_color.g / 255
1025            blue = self.highlight_color.b / 255
1026
1027            dbgd = None
1028
1029            if self.applet_win is not None:
1030                scale_factor = self.applet_win.get_scale_factor()
1031            else:
1032                scale_factor = 1
1033
1034            if self.active_bg == IconBgType.UNITY_FLAT:
1035                dbgd = UnityFlatBackgroundDrawer(ctx, self.drawing_area_size,
1036                                                 self.applet_orient, red, green, blue,
1037                                                 self.is_running(), scale_factor)
1038            elif self.active_bg == IconBgType.UNITY:
1039                dbgd = UnityBackgroundDrawer(ctx, self.drawing_area_size,
1040                                             self.applet_orient, red, green, blue,
1041                                             self.is_running(), scale_factor)
1042            elif self.is_active:
1043                if self.active_bg == IconBgType.GRADIENT:
1044                    dbgd = DefaultBackgroundDrawer(ctx, self.drawing_area_size,
1045                                                   self.applet_orient, red, green, blue)
1046                else:
1047                    dbgd = AlphaFillBackgroundDrawer(ctx, self.drawing_area_size,
1048                                                     self.applet_orient, red, green, blue, 0.5)
1049
1050            if dbgd is not None:
1051                dbgd.draw()
1052
1053        # draw the app icon
1054        if self.active_bg in [IconBgType.UNITY_FLAT, IconBgType.UNITY]:
1055            pb_size = (self.drawing_area_size) * 0.75
1056            offset = self.drawing_area_size / 2 - pb_size / 2
1057            ctx.set_source_surface(self.app_surface, offset, offset)
1058        else:
1059            ctx.set_source_surface(self.app_surface, 3, 3)
1060
1061        if self.is_pulsing:
1062            # draw the icon semi-transparently according to how far through the
1063            # animation we are
1064
1065            half_way = int(CONST_PULSE_STEPS / 2)
1066            if self.pulse_step <= half_way:
1067                alpha = 1.0 - (self.pulse_step / half_way)
1068            else:
1069                alpha = 0.0 + (self.pulse_step - half_way) / half_way
1070
1071            ctx.paint_with_alpha(alpha)
1072
1073        elif self.needs_attention and self.attention_type == dock_prefs.AttentionType.BLINK:
1074            if self.attention_blink_on:
1075                ctx.paint()  # draw normally if in the flash on state
1076        elif self.is_dragee:
1077            ctx.paint_with_alpha(0.0)
1078        else:
1079            ctx.paint()
1080            ctx.save()
1081            if self.active_bg == IconBgType.UNITY_FLAT:
1082                if self.is_running():
1083                    dbgd.draw_shine()
1084            elif self.active_bg == IconBgType.UNITY:
1085                dbgd.draw_shine()
1086            ctx.restore()
1087
1088        if (self.has_mouse is True) and ((self.is_dragee is False) and (self.scroll_dir == ScrollType.SCROLL_NONE)):
1089            # lighten the icon
1090            ctx.set_operator(cairo.OPERATOR_ADD)
1091            ctx.paint_with_alpha(0.2)
1092            ctx.set_operator(cairo.OPERATOR_OVER)
1093        elif (self.has_mouse is True) and (self.scroll_dir != ScrollType.SCROLL_NONE):
1094            # this app indicates the dock can scroll, so we darken it
1095            ctx.set_operator(cairo.OPERATOR_DEST_OUT)
1096            ctx.paint_with_alpha(0.5)
1097            ctx.set_operator(cairo.OPERATOR_OVER)
1098
1099        # draw the app running indicators
1100        if (self.is_running()) and \
1101           (self.indicator != IndicatorType.NONE) and \
1102           (self.is_dragee is False):
1103
1104            # work out how many indicators to draw - either a single one or
1105            # one for each open window up to a maximum of 4, and take into
1106            # account the fact that we might only be showing indicators from
1107            # the current workspace
1108
1109            # get the number of indicators to show...
1110            if self.multi_ind is False and self.indicator != IndicatorType.SUBWAY:
1111                num_ind = 1
1112            else:
1113                num_ind = self.get_num_windows(self.ind_ws)
1114                if num_ind > 4:
1115                    num_ind = 4
1116
1117            ind = None
1118            if self.indicator == IndicatorType.LIGHT:
1119                ind = DefaultLightInd(ctx, self.drawing_area_size,
1120                                      self.applet_orient, num_ind)
1121            elif self.indicator == IndicatorType.DARK:
1122                ind = DefaultDarkInd(ctx, self.drawing_area_size,
1123                                     self.applet_orient, num_ind)
1124            elif self.indicator == IndicatorType.TBAR:
1125                ind = ThemeBarInd(ctx, self.drawing_area_size, self.applet_orient, self.applet)
1126            elif self.indicator == IndicatorType.TCIRC:
1127                ind = ThemeCircleInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind)
1128            elif self.indicator == IndicatorType.TSQUARE:
1129                ind = ThemeSquareInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind)
1130            elif self.indicator == IndicatorType.TTRI:
1131                ind = ThemeTriInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind)
1132            elif self.indicator == IndicatorType.TDIA:
1133                ind = ThemeDiaInd(ctx, self.drawing_area_size, self.applet_orient, self.applet, num_ind)
1134            elif self.indicator == IndicatorType.SUBWAY:
1135                ind = SubwayInd(ctx, self.drawing_area_size, self.applet_orient,
1136                                self.applet, num_ind, offscreen_surface, self.is_active)
1137
1138            if ind is not None:
1139                ind.draw()
1140
1141        # do we need a count?
1142        if self.show_count:
1143            self.draw_count(ctx)
1144
1145        if self.show_progress:
1146            self.draw_progress(ctx)
1147
1148        if self.needs_attention and self.attention_type == dock_prefs.AttentionType.SHOW_BADGE:
1149            self.draw_attention_badge(ctx)
1150
1151        if not build_gtk2:
1152            # scrolling only available in GTK3
1153            if self.has_mouse and (self.scroll_dir != ScrollType.SCROLL_NONE):
1154                if self.scroll_dir == ScrollType.SCROLL_UP:
1155                    self.draw_scroll_up(ctx)
1156                elif self.scroll_dir == ScrollType.SCROLL_DOWN:
1157                    self.draw_scroll_down(ctx)
1158
1159        # now draw to the screen
1160        if build_gtk2:
1161            screen_ctx = self.drawing_area.window.cairo_create()
1162            screen_ctx.rectangle(event.area.x, event.area.y,
1163                                 event.area.width, event.area.height)
1164            screen_ctx.clip()
1165
1166            alloc = self.drawing_area.get_allocation()
1167            if (self.applet_orient == MatePanelApplet.AppletOrient.UP) or \
1168               (self.applet_orient == MatePanelApplet.AppletOrient.DOWN):
1169                screen_ctx.set_source_surface(offscreen_surface, alloc.x, 0)
1170            else:
1171                screen_ctx.set_source_surface(offscreen_surface, 0, alloc.y)
1172
1173            screen_ctx.paint()
1174            screen_ctx = None
1175        else:
1176            event.set_source_surface(offscreen_surface, 0, 0)
1177            event.paint()
1178            alloc = self.drawing_area.get_allocation()
1179
1180        ctx = None
1181
1182    def draw_count(self, ctx):
1183        """ Draw the app's counter value
1184
1185        Args: ctx - the cairo context where the counter is to be drawn
1186        """
1187
1188        # drawing is done at a notional size  64x64 px, and then scaled
1189        # appropriately according to self.drawing_area_size
1190
1191        draw_size = 64.0
1192
1193        # height of the counter = 2 pix border top and bottom + 16 pix
1194        # internal height
1195        height = 20
1196
1197        # work out the appropriate font size to use - has to fit within the
1198        # borders and provide some space above and below the count_val
1199        reqd_font_height = height - 8
1200
1201        # find a font size where the count can be shown with the required height
1202        ctx.select_font_face("", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
1203        ctext = "%d" % self.count_val
1204        for fsize in range(24, 2, -1):
1205            ctx.set_font_size(fsize)
1206            extents = ctx.text_extents(ctext)
1207
1208            if extents[3] < reqd_font_height:
1209                font_size = fsize
1210                break
1211
1212        # work out an appropriate width for the counter
1213        inset = height / 2
1214        radius = inset - 1
1215        if int(extents[2] + extents[0]) > int(radius):
1216            width = extents[2] + extents[0] + radius
1217        else:
1218            width = height + inset
1219
1220        ctx.save()
1221
1222        # the background color of the count is the app's highlight colour
1223        # convert the highlight values to their cairo equivalents
1224        bred = self.highlight_color.r / 255
1225        bgreen = self.highlight_color.g / 255
1226        bblue = self.highlight_color.b / 255
1227
1228        # set an appropriate text and border color
1229        if bred + bgreen + bblue > 1.5:  # mid-level grey
1230            tred = tgreen = tblue = 0.0
1231        else:
1232            tred = tgreen = tblue = 1.0
1233
1234        # the count is placed in the upper right of the drawing area, and we need
1235        # to calculate it's position based on the notional DA_SIZE
1236        #
1237
1238        adj = 2
1239        left = draw_size - width + inset - adj
1240
1241        # do the drawing - attribution for the drawing code:
1242        # https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
1243        ctx.scale(self.drawing_area_size / draw_size,
1244                  self.drawing_area_size / draw_size)
1245
1246        ctx.move_to(left, height - 1 + adj)
1247        ctx.arc(left, inset + adj, radius, 0.5 * math.pi, 1.5 * math.pi)
1248        ctx.arc(draw_size - inset - adj, inset + adj, radius, 1.5 * math.pi, 0.5 * math.pi)
1249        ctx.line_to(left, height - 1 + adj)
1250        ctx.set_source_rgb(bred, bgreen, bblue)
1251        ctx.fill_preserve()
1252        ctx.set_source_rgb(tred, tgreen, tblue)
1253        ctx.set_line_width(2)
1254        ctx.stroke()
1255
1256        # draw the text
1257        ctx.move_to(left - inset + width / 2 - (extents[0] + extents[2] / 2),
1258                    (height / 2) + adj + extents[3] / 2)
1259        ctx.set_source_rgb(tred, tgreen, tblue)
1260        ctx.show_text(ctext)
1261
1262        ctx.restore()
1263
1264    def draw_progress(self, ctx):
1265        """ Draw a progress bar to show the app's progress value
1266
1267        Args: ctx - the cairo context where the counter is to be drawn
1268        """
1269
1270        def rounded_rectangle(cr, x, y, w, h, r=20):
1271            """ Convenience function to draw a rounded rectangle
1272                # Attribution:
1273                # https://stackoverflow.com/questions/2384374/rounded-rectangle-in-pygtk
1274                # This is just one of the samples from
1275                # http://www.cairographics.org/cookbook/roundedrectangles/
1276                #   A****BQ
1277                #  H      C
1278                #  *      *
1279                #  G      D
1280                #   F****E
1281            """
1282
1283            cr.move_to(x + r, y)                      # Move to A
1284            cr.line_to(x + w - r, y)                    # Straight line to B
1285            cr.curve_to(x + w, y, x + w, y, x + w, y + r)
1286            # Curve to C, Control points are both at Q
1287            cr.line_to(x + w, y + h - r)                  # Move to D
1288            cr.curve_to(x + w, y + h, x + w, y + h, x + w - r, y + h)  # Curve to E
1289            cr.line_to(x + r, y + h)                    # Line to F
1290            cr.curve_to(x, y + h, x, y + h, x, y + h - r)  # Curve to G
1291            cr.line_to(x, y + r)                      # Line to H
1292            cr.curve_to(x, y, x, y, x + r, y)             # Curve to A
1293
1294        # drawing is done on a to scale of 64x64 pixels and then scaled
1295        # down to the fit the app's drawing area
1296        draw_size = 64.0
1297
1298        # the foreground colour of the progress is the app's highlight colour
1299        # convert the highlight values to their cairo equivalents
1300        fred = self.highlight_color.r / 255
1301        fgreen = self.highlight_color.g / 255
1302        fblue = self.highlight_color.b / 255
1303
1304        # set an appropriate border color and also a background colour for
1305        # the progress bar, based on the highlight colour
1306        if fred + fgreen + fblue > 1.5:  # mid-level grey
1307            brd_red = brd_green = brd_blue = 0.0
1308            bk_red = bk_green = bk_blue = 1.0
1309        else:
1310            brd_red = brd_green = brd_blue = 1.0
1311            bk_red = bk_green = bk_blue = 0.0
1312
1313        height = 8              # total height of the progress bar
1314        line_width = 2          # border line width
1315        int_height = height - line_width * 2  # interior height
1316        left = 8.5
1317        width = draw_size - left * 2   # width of the progress bar
1318
1319        top = (draw_size / 8) * 5 + 0.5
1320
1321        ctx.save()
1322        ctx.scale(self.drawing_area_size / draw_size, self.drawing_area_size / draw_size)
1323
1324        # fill the interior with the background colour
1325        ctx.set_line_width(1)
1326        ctx.set_source_rgb(bk_red, bk_green, bk_blue)
1327        rounded_rectangle(ctx, left, top, width, height, 7)
1328        ctx.stroke_preserve()
1329        ctx.fill()
1330
1331        # fill part of the interior with a different colour, depending on
1332        # the progress value
1333        ctx.set_source_rgb(fred, fgreen, fblue)
1334        rounded_rectangle(ctx, left + line_width - 1, top,
1335                          (width - (line_width - 1) * 2) * self.progress_val,
1336                          height, 7)
1337        ctx.fill()
1338
1339        # draw exterior of the progress bar
1340        ctx.set_source_rgb(brd_red, brd_green, brd_blue)
1341        ctx.set_line_width(2)
1342        rounded_rectangle(ctx, left, top, width, height, 7)
1343        ctx.stroke()
1344
1345        ctx.restore()
1346
1347    def draw_attention_badge(self, ctx):
1348        """ Draw a badge on the app icon to indicate the app requires
1349            attention
1350
1351        Basically a copy and paste of draw_count...
1352
1353        Args: ctx - the cairo context where the counter is to be drawn
1354        """
1355
1356        # drawing is done at a notional size  64x64 px, and then scaled
1357        # appropriately according to self.drawing_area_size
1358
1359        draw_size = 64.0
1360
1361        # height of the exaclamation mark = 2 pix border top and bottom + 16 pix
1362        # internal height
1363        height = 20
1364
1365        # work out the appropriate font size to use - has to fit within the
1366        # borders and provide some space above and below the exclamation mark
1367        reqd_font_height = height - 8
1368
1369        # find a font size where the count can be shown with the required height
1370        ctx.select_font_face("", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
1371        ctext = "!"
1372        for fsize in range(24, 2, -1):
1373            ctx.set_font_size(fsize)
1374            extents = ctx.text_extents(ctext)
1375
1376            if extents[3] < reqd_font_height:
1377                font_size = fsize
1378                break
1379
1380        # work out an appropriate width for the badge
1381        inset = height / 2
1382        radius = inset - 1
1383        if int(extents[2] + extents[0]) > int(radius):
1384            width = extents[2] + extents[0] + radius
1385        else:
1386            width = height
1387            # width = height + inset
1388
1389        width = extents[2] + extents[0] + radius
1390        ctx.save()
1391
1392        # the background color of the badge is the app's highlight colour
1393        # convert the highlight values to their cairo equivalents
1394        bred = self.highlight_color.r / 255
1395        bgreen = self.highlight_color.g / 255
1396        bblue = self.highlight_color.b / 255
1397
1398        # set an appropriate text and border color
1399        if bred + bgreen + bblue > 1.5:  # mid-level grey
1400            tred = tgreen = tblue = 0.0
1401        else:
1402            tred = tgreen = tblue = 1.0
1403
1404        # the badge is placed in the upper left of the drawing area
1405        adj = 2
1406        left = inset
1407
1408        # do the drawing - attribution for the drawing code:
1409        # https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
1410        ctx.scale(self.drawing_area_size / draw_size,
1411                  self.drawing_area_size / draw_size)
1412
1413        ctx.move_to(left, height - 1 + adj)
1414        ctx.arc(left, inset + adj, radius, 0.5 * math.pi, 1.5 * math.pi)
1415        ctx.arc(left + width - inset - adj, inset + adj, radius, 1.5 * math.pi, 0.5 * math.pi)
1416        ctx.line_to(left, height - 1 + adj)
1417        ctx.set_source_rgb(bred, bgreen, bblue)
1418        ctx.fill_preserve()
1419        ctx.set_source_rgb(tred, tgreen, tblue)
1420        ctx.set_line_width(2)
1421        ctx.stroke()
1422
1423        # draw the text
1424        ctx.move_to(left + inset - (extents[0] + extents[2] / 2) - radius,
1425                    (height / 2) + adj + extents[3] / 2)
1426        ctx.set_source_rgb(tred, tgreen, tblue)
1427        ctx.show_text(ctext)
1428
1429        ctx.restore()
1430
1431    def draw_scroll_up(self, ctx):
1432        """ To indicate that the docked app can scroll up (or left on horizontal panels)
1433            draw an up (or left) arrow on the icon
1434
1435        Params :
1436            context : the docked app's cairo context for us to draw on
1437            size : the size of the context, in pixels
1438            orient : the orientation of the dock applet
1439        """
1440
1441        if self.drawing_area_size > 48:
1442            icon_size = Gtk.IconSize.DND
1443            icon_pix = 24
1444        else:
1445            icon_size = Gtk.IconSize.LARGE_TOOLBAR
1446            icon_pix = 16
1447
1448        if self.applet_orient in [MatePanelApplet.AppletOrient.UP,
1449                                  MatePanelApplet.AppletOrient.DOWN]:
1450
1451            arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_BACK, icon_size, None)
1452            arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2,
1453                                             GdkPixbuf.InterpType.BILINEAR)
1454
1455            Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, 0, self.drawing_area_size / 4)
1456
1457        else:
1458            arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_UP, icon_size, None)
1459            arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2,
1460                                             GdkPixbuf.InterpType.BILINEAR)
1461
1462            Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 4, 0)
1463
1464        ctx.paint()
1465
1466    def draw_scroll_down(self, ctx):
1467        """ To indicate that the docked app can scroll up (or left on horizontal panels)
1468            draw an up (or left) arrow on the icon
1469
1470        Params :
1471            context : the docked app's cairo context for us to draw on
1472            size : the size of the context, in pixels
1473            orient : the orientation of the dock applet
1474        """
1475
1476        if self.drawing_area_size > 48:
1477            icon_size = Gtk.IconSize.DND
1478            icon_pix = 24
1479        else:
1480            icon_size = Gtk.IconSize.LARGE_TOOLBAR
1481            icon_pix = 16
1482
1483        if self.applet_orient in [MatePanelApplet.AppletOrient.UP,
1484                                  MatePanelApplet.AppletOrient.DOWN]:
1485
1486            arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_FORWARD, icon_size, None)
1487            arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2,
1488                                             GdkPixbuf.InterpType.BILINEAR)
1489
1490            Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 2,
1491                                        self.drawing_area_size / 4)
1492
1493        else:
1494            arrow_pb = self.drawing_area.render_icon(Gtk.STOCK_GO_DOWN, icon_size, None)
1495            arrow_pb = arrow_pb.scale_simple(self.drawing_area_size / 2, self.drawing_area_size / 2,
1496                                             GdkPixbuf.InterpType.BILINEAR)
1497
1498            Gdk.cairo_set_source_pixbuf(ctx, arrow_pb, self.drawing_area_size / 4, self.drawing_area_size / 2)
1499
1500        ctx.paint()
1501
1502    def set_pixbuf(self, pixbuf):
1503        """Set the app pixbuf and calculate its average colour.
1504        """
1505
1506        self.app_pb = pixbuf
1507
1508        rht, ght, bht = self.highlight_color = get_backlight_color(pixbuf)
1509        self.highlight_color = ColorTup(r=rht, g=ght, b=bht)
1510
1511    def set_surface(self, surface):
1512        """Set the app surface
1513        """
1514
1515        self.app_surface = surface
1516
1517    def start_app(self):
1518        """Start the app or open a new window if it's already running
1519
1520            Use Gio.DesktopAppinfo as it supports startup notfication
1521        """
1522        # start the app
1523        try:
1524            run_it = self.desktop_ai.get_string("Exec")
1525        except:
1526            run_it = None
1527
1528        if run_it is not None:
1529
1530            # hack for Linux Mint:
1531            # Mint has several shortcuts for starting caja so that it can
1532            # be started in a specific directory e.g. home, /, etc
1533            # However, the main caja.desktop is responsible for starting the
1534            # user's desktop and this is the .desktop file the applet finds
1535            # first.
1536            # When the caja icon on the applet is clicked, caja is run as a
1537            # desktop window and no new file browser appears.
1538            # To get around this, we can simply check the command that is going
1539            # to be run and change it so that a caja window opens in the user's
1540            # home directory, which is the behaviour they'll probably be
1541            # expecting....
1542            if run_it == "/usr/bin/startcaja":
1543                run_it = "caja"
1544                self.run_cmd_line(run_it)
1545                return
1546
1547        if self.desktop_file is not None:
1548            gdai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file)
1549        else:
1550            gdai = None
1551
1552        disp = Gdk.Display.get_default()
1553        if build_gtk2:
1554            alc = Gdk.AppLaunchContext()
1555        else:
1556            alc = disp.get_app_launch_context()
1557
1558        alc.set_desktop(-1)  # use default screen & desktop
1559        alc.set_timestamp(Gtk.get_current_event_time())
1560        alc.connect("launch-failed", self.launch_failed)
1561
1562        # indicate we want startup notification
1563        if gdai is not None:
1564            self.startup_id = alc.get_startup_notify_id(gdai, [])
1565
1566            gdai.launch_uris_as_manager([], alc, GLib.SpawnFlags.SEARCH_PATH,
1567                                    None, None, None, None)
1568
1569        # make the app's icon pulse
1570        if not self.is_running():
1571            throbber = PulseTimer(self)
1572        else:
1573            # if the app is already running, we want the icon to pulse at most once only
1574            # For apps which don't open a new window, the pulse timer will end up cancelling
1575            # the unneeded startup notification
1576            throbber = PulseTimer(self, True)
1577
1578    def cancel_startup_notification(self):
1579        """
1580            Cancel any startup notification
1581        """
1582
1583        if build_gtk2:
1584            Gdk.notify_startup_complete_with_id(self.startup_id)
1585        else:
1586            display = Gdk.Display.get_default()
1587            display.notify_startup_complete(self.startup_id)
1588
1589        self.startup_id = None
1590
1591    def launch_failed(self, app_launch_context, startup_id):
1592        """Handler for app launch failure events
1593
1594        Cancel the startup notification
1595
1596            Args:
1597                app_launch_context : the Gdk.AppLaunchContext that failed
1598                startup_id : the startup notification id
1599        """
1600
1601        self.cancel_startup_notification()
1602
1603        display = Gdk.Display.get_default()
1604        display.notify_startup_complete(startup_id)
1605
1606    def run_cmd_line(self, cmd_line):
1607        """Run a command line.
1608
1609            Args:
1610                cmd_line - the command to run
1611        """
1612
1613        # TODO: this is old code and needs to be removed
1614
1615        # the command line may contain escape sequences, so unescape them....
1616        cmd_line = bytearray(cmd_line, "UTF-8")
1617        cmd_line = cmd_line.decode("unicode-escape")
1618
1619        # if an environment variable is specified, extract its name an value
1620        # Note: the .desktop file specification at
1621        # https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
1622        # does not mention this. Both Ubuntu
1623        # https://help.ubuntu.com/community/EnvironmentVariables#Launching_desktop_application_with_an_environment_variable
1624        # and Arch linux
1625        # https://wiki.archlinux.org/index.php/Desktop_entries#Modify_environment_variables
1626        # seem to indicate that only a single variable can be set and that
1627        # there are no spaces between the variable name, the '=' character and
1628        # variable's value .....
1629
1630        # so, if cmd_line begins with "env" it specifies an environment variable
1631        # to set, follwed by the app e.g. env LANG=he_IL.UTF-8 /usr/bin/pluma
1632        #
1633        if cmd_line.startswith("env"):
1634            cmd_parts = cmd_line.split(" ", 2)
1635            var_parts = cmd_parts[1].split("=")
1636            var_name = var_parts[0]
1637            var_value = var_parts[1]
1638
1639            # now we need to get the app path and args and carry on...
1640            cmd_line = cmd_parts[2]
1641        else:
1642            var_name = None
1643            var_value = None
1644
1645        # if any of the directories in cmd_line contain a " ", they need to be
1646        # escaped
1647        head, tail = os.path.split(cmd_line)
1648        if " " in head:
1649            head = head.replace(" ", "\\ ")
1650            cmd_line = head + "/" + tail
1651        app_info = Gio.AppInfo.create_from_commandline(cmd_line,
1652                                                       None,
1653                                                       Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION)
1654        alc = Gdk.AppLaunchContext()
1655        alc.set_desktop(-1)                   # use default screen & desktop
1656        alc.set_timestamp(Gtk.get_current_event_time())
1657
1658        # if the .desktop specfied an environment variable, set it
1659        if (var_name is not None) and (var_value is not None):
1660            alc.setenv(var_name, var_value)
1661
1662        file_list = GLib.List()
1663
1664        # app_info.launch(None, alc)
1665        self.startup_id = alc.get_startup_notify_id(app_info, [])
1666
1667        throbber = PulseTimer(self)
1668
1669    def run_rc_action(self, act_no):
1670        """ run the right click action specified by act_no
1671
1672        Args:
1673           act_no - integer, the action number to run
1674        """
1675
1676        if len(self.rc_actions) >= act_no:
1677            if build_gtk2:
1678                alc = Gdk.AppLaunchContext()
1679            else:
1680                disp = Gdk.Display.get_default()
1681                alc = disp.get_app_launch_context()
1682
1683            alc.set_desktop(-1)  # use default screen & desktop
1684            alc.set_timestamp(Gtk.get_current_event_time())
1685            alc.connect("launch-failed", self.launch_failed)
1686
1687            # indicate we want startup notification
1688            self.startup_id = alc.get_startup_notify_id(self.desktop_ai, [])
1689
1690            self.desktop_ai.launch_action(self.rc_actions[act_no - 1], alc)
1691            self.start_pulsing()
1692
1693    def get_rc_action(self, act_no):
1694        """ return a specified right click action's details
1695
1696        Args:
1697            act_no - integer, the specified action number
1698
1699        Returns:
1700                bool - True if the action exists, False otherwise
1701                string - the name of the action (i.e. the text to appear in the
1702                right click menu)
1703        """
1704
1705        if len(self.rc_actions) >= act_no:
1706            return True, self.desktop_ai.get_action_name(self.rc_actions[act_no - 1])
1707        else:
1708            return False, ""
1709
1710    def start_pulsing(self):
1711        """ start the dock icon pulsing
1712        """
1713
1714        throbber = PulseTimer(self)
1715
1716    def pulse_once(self):
1717        """ Make the dock icon pulse once"""
1718
1719        throbber = PulseTimer(self, True)
1720
1721    def set_dragee(self, is_dragee):
1722        """ Set the flag which indicates whether or not this app is being
1723            dragged to a new position on the dock
1724
1725        Set the value of the self.is_dragee flag and redraw the app icon
1726        """
1727
1728        self.is_dragee = is_dragee
1729        self.queue_draw()
1730
1731    def set_progress_visible(self, is_visible):
1732        """
1733            Update the progress visibility and cause the app's icon to be
1734            redrawn
1735
1736        Args:
1737            is_visible : whether the progress is to be displayed
1738        """
1739
1740        if is_visible != self.show_progress:
1741            self.show_progress = bool(is_visible)
1742            self.queue_draw()
1743
1744    def set_progress_value(self, val):
1745        """
1746            Update the progress value and cause the app's icon to be
1747            redrawn
1748
1749        Args:
1750            val        : the counter value
1751        """
1752
1753        # if the new progressvalue is the same as the old, then there's no need
1754        # to do anything...
1755        if val != self.progress_val:
1756            self.progress_val = val
1757            self.queue_draw()
1758
1759    def set_counter_visible(self, is_visible):
1760        """
1761            Update the counter visibility and cause the app's icon to be
1762            redrawn
1763
1764        Args:
1765            is_visible : whether the counter is to be displayed
1766        """
1767
1768        # if the new value is the same as the old, then there's no need
1769        # to do anything...
1770        if is_visible != self.show_count:
1771            self.show_count = bool(is_visible)
1772            self.queue_draw()
1773
1774    def set_counter_value(self, val):
1775        """
1776            Update the counter value and cause the app's icon to be
1777            redrawn
1778
1779        Args:
1780            val        : the counter value
1781        """
1782
1783        # if the new counter value is the same as the old, then there's no need
1784        # to do anything...
1785        if val != self.count_val:
1786            self.count_val = val
1787            self.queue_draw()
1788
1789    def set_scroll_dir(self, scroll_dir):
1790        """
1791            Sets the app's scroll direction
1792
1793        Param: scroll_dir - a docked_app_helpers.ScrollType
1794        """
1795
1796        self.scroll_dir = scroll_dir
1797
1798
1799def main():
1800    """Main function.
1801
1802    Debugging code can go here
1803    """
1804    pass
1805
1806
1807if __name__ == "__main__":
1808    main()
1809