#!/usr/bin/env python3
"""Provide functionality relating to an app in a dock.

    Allow info relating to a running app (e.g. icon, command line,
    .desktop file location, running processes, open windows etc) to be
    obtained from the information that libWnck provides

    Provide a surface on which the application's icon and the running indicator
    can be drawn

    Ensure that the app's icon and indicator are always drawn correctly
    according to the size and orienation of the panel

    Provide visual feedback to the user when an app is lauched by pulsating
    the app's icon

    Draw a highlight around the app's icon if it is the foreground app

    Maintain a list of all of the app's running processes and their windows

    Ensure that the application's windows visually minimise to the
    application's icon on the dock
"""

#
# Copyright (C) 1997-2003 Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# Author:
#     Robin Thompson

# do not change the value of this variable - it will be set during build
# according to the value of the --with-gtk3 option used with .configure
build_gtk2 = False

import gi

if build_gtk2:
    gi.require_version("Gtk", "2.0")
    gi.require_version("Wnck", "1.0")
else:
    gi.require_version("Gtk", "3.0")
    gi.require_version("Wnck", "3.0")

gi.require_version("MatePanelApplet", "4.0")

from gi.repository import Gtk
from gi.repository import MatePanelApplet
from gi.repository import Gdk
from gi.repository import Wnck

from gi.repository import Gio
from gi.repository import GObject
import cairo
import math
import xdg.DesktopEntry as DesktopEntry
import os
import os.path
import subprocess

from collections import namedtuple

import dock_prefs

from log_it import log_it as log_it

ColorTup = namedtuple('ColorTup', ['r', 'g', 'b'])
AppInfoTup = namedtuple('AppInfoTup', ['app', 'pid', 'windows'])
ActionTup = namedtuple('ActionTup', ['name', 'command'])


def im_get_comp_color(filename):
    """Find the complimentary colour of the average colour of an image.

    Uses ImageMagick to read and process the image

    Args:
        filename : the filename of the image

    Returns:
        a tuple of r,g,b values (0-255)

    """

    cmdstr = "convert " + filename + " -colors 16 -depth 8 -format ""%c"" " + \
             "histogram:info:|sort -rn|head -n 1| grep -oe '#[^\s]*'"
    cmd = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE)

    for line in cmd.stdout:
        pass

    ll1 = str(line)
    astr = ll1[2:9]

    cmdstr = "convert xc:'" + astr + "' -modulate 100,100,0 -depth 8 txt:"
    cmd = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE)

    for line in cmd.stdout:
        pass

    l1p = str(line)
    lll = l1p.split(" ")
    astr = lll
    astr = lll[3].lstrip("#")
    if len(astr) == 8:
        # remove alpha channel
        astr = astr[0:6]

    red = int(astr[0:2], 16)
    green = int(astr[2:4], 16)
    blue = int(astr[4:6], 16)
    return red, green, blue


def get_avg_color(pixbuf):
    """calculate the average colour of a pixbuf.

    Read all of the pixel values in a pixbuf (excluding those which are below a
    certain alpha value) and calculate the average colour of all the contained
    colours

    Args:
        pixbuf  : a pixbuf object containing the image

    Returns:
        a tuple of r,g,b values (0-255)
    """

    width = pixbuf.props.width
    rowstride = pixbuf.props.rowstride
    height = pixbuf.props.height
    has_alpha = pixbuf.get_has_alpha()
    pixels = pixbuf.get_pixels()
    nchannels = pixbuf.get_n_channels()
    # convert the pixels into an rgb array with alpha channel
    data = []
    for y_count in range(height-1):
        x_count = 0
        while x_count < (width * nchannels):
            pix_red = pixels[x_count+(rowstride*y_count)]
            pix_green = pixels[x_count+1+(rowstride*y_count)]
            pix_blue = pixels[x_count+2+(rowstride*y_count)]
            if has_alpha:
                pix_alpha = pixels[x_count+3+(rowstride*y_count)]
            else:
                pix_alpha = 255

            data.append([pix_red, pix_green, pix_blue, pix_alpha])

            x_count += nchannels

    red = 0
    green = 0
    blue = 0
    num_counted = 0

    for pixels in range(len(data)):
        if data[pixels][3] > 200:       # only count pixel if alpha above this
                                        # level
            red += data[pixels][0]
            green += data[pixels][1]
            blue += data[pixels][2]
            num_counted += 1

    if num_counted > 0:
        ravg = int(red/num_counted)
        gavg = int(green/num_counted)
        bavg = int(blue/num_counted)
    else:
        # in case of a bad icon assume a grey average colour
        # this should fix a division by zero error that occurred at this point
        # in the code, but which I've only seen once and never been able to
        # duplicate
        ravg = gavg = bavg = 128

    return ravg, gavg, bavg

CONST_PULSE_STEPS = 20
CONST_PULSE_DELAY = 40


class PulseTimer(object):
    """Class to help provide feedback when a user launches an app from the dock.

    Instantiates a timer which periodically redraws an application in the dock
    at various transparency levels until the timer has been run a certain
    number of times

    Attributes:
        app = the DockedApp object which we want to pulsate
        timer_id = the id of the timer that is instantiated

    """

    def __init__(self, app):
        """Init for the PulseTimer class.

        Sets everything up by creating the timer, setting a reference to the
        DockedApp and telling the app that it is pulsing

        Arguments:
            app : the DockedApp object
        """

        self.app = app
        self.app.pulse_step = 0
        self.app.is_pulsing = True
        self.timer_id = GObject.timeout_add(CONST_PULSE_DELAY, self.do_timer)

    def do_timer(self):
        """The timer function.

        Increments the number of times the time function has been called. If it
        hasn't reached the maximum number, increment the app's pulse counter.
        If the maximum number has been reached, stop the app pulsing and
        delete the timer.

        Redraw the app's icon
        """

        if self.app.pulse_step != CONST_PULSE_STEPS:
            self.app.pulse_step += 1
        else:
            self.app.is_pulsing = False
            GObject.source_remove(self.timer_id)

        self.app.queue_draw()

        return True

CONST_FLASH_DELAY = 330


class FlashTimer(object):
    """Class to help provide visual feedback when an app requries user attention.

    Instantiates a timer which periodically causes the app's dock icon to flash
    on and off until the app no longer needs attention

    Attributes:
        app = the DockedApp object which we want to flashh
        timer_id = the id of the timer that is instantiated

    """

    def __init__(self, app):
        """Init for the FlashTimer class.

        Sets everything up by creating the timer, setting a reference to the
        DockedApp and setting the inital flash state to off

        Arguments:
            app : the DockedApp object
        """

        self.app = app
        self.app.is_flashing = True
        self.app.flash_on = False
        self.timer_id = GObject.timeout_add(CONST_FLASH_DELAY, self.do_timer)

        # make the app redraw itself
        app.queue_draw()

    def do_timer(self):
        """The timer function.

        If the app no longer needs attention, stop it flashing and delete
        the timer. Otherwise, invert the flash.

        Finally,Redraw the app's icon
        """

        if self.app.is_flashing:
            self.app.flash_on = not self.app.flash_on
        else:
            GObject.source_remove(self.timer_id)

        self.app.queue_draw()

        return True


class DockedApp(object):
    """Provide a docked app class

    Attributes:
        app_info    : list of AppInfoTups to hold details of all running
                      processes the app has
        wnck_class  : the WnckClass object relating to the app
        app_name    : e.g. Google Chrome, used for tooltips and the applet
                      right click menu etc
        rc_actions  : list of ActionTups to hold details of right click actions
                      the app supports
        cmd_line    : the command line and arguments used to start the app
        icon_name   : name of the app icon
        icon_filename : the filename of the app icon
        desktop_file : the filename of the app's .desktop file
        wm_class_name:
        applet_win  : the Gdk.Window of the panel applet
        applet_orient : the applet orientation

        drawing_area: Gtk.Label- provides a surface on which the app icon can
                              be drawn
        drawing_area_size : the size in pixels (height AND width) that we have
                            to draw in
        is_pulsing  : boolean - True if the app is pulsing
        pulse_step  : a count of how far we are through the pulse animation
        app_pb      : a pixbuf of the app's icon
        highlight_colour : ColorTup of the colours used to highlight the app
                           when it is foreground
        is_active   : boolean - True = the app is the foreground app
        has_mouse   : boolean - True = the mouse is over the app's icon
        is_pinned   : boolean - Whether or not the app is pinned to the dock
        indicator   : the type of indictor (light or dark) to draw under
                      running apps
        ind_ws      : wnck_workspace or None - if set, indicators are to be
                      drawn for windows on the specified workspace
        last_active_win : the wnck_window of the app's last active window

        is_dragee  : boolean - indicates whether or not the app's icon is
                     being dragged to a new position on the dock
    """

    def __init__(self):
        """ Init for the DockApplet class.

            Create a surface to draw the app icon on
            Set detault values
        """

        super().__init__()

        self.app_info = []
        self.wnck_class = None
        self.app_name = ""
        self.rc_actions = []
        self.cmd_line = ""
        self.icon_name = ""
        self.icon_filename = ""
        self.desktop_file = ""
        self.wm_class_name = ""
        self.icon_geometry_set = False
        self.applet_win = None
        self.applet_orient = None
        self.ind_ws = None

        # all drawing is done to a Gtk.Label rather than e.g. a drawing area
        # or event box this allows panel transparency/custom backgrounds to be
        # honoured
        # However, the downside of this is that mouse events cannot be handled
        # by this class and instead have to be done by the applet itself

        self.drawing_area = Gtk.Label()
        self.drawing_area.set_app_paintable(True)
        self.drawing_area_size = 0

        self.is_pulsing = False
        self.pulse_step = 0

        self.is_flashing = False
        self.flash_on = False

        self.app_pb = None

        self.highlight_color = ColorTup(r=0.0, g=0.0, b=0.0)

        self.is_active = False
        self.has_mouse = False

        self.is_pinned = False

        # set defaults
        self.indicator = dock_prefs.IndicatorType.LIGHT     # light indicator
        self.multi_ind = False                              # single indicator
        self.last_active_win = None

        # set up event handler for the draw/expose event
        if build_gtk2:
            self.drawing_area.connect("expose-event", self.do_expose_event)
        else:
            self.drawing_area.connect("draw", self.do_expose_event)

        self.is_dragee = False

    def has_wnck_app(self, wnck_app):
        """ see if this app has a process with the specified wnck_app

        Returns True if the wnck_app is found, False otherwise
        """

        ret_val = False
        for aai in self.app_info:
            if aai.app == wnck_app:
                ret_val = True
                break

        return ret_val

    def setup_from_wnck(self, wnck_app, wnck_class, app_match_list):
        """Set up for an already running app, obtaining info from libWnck.

        Will attempt to get the app path, icon path, .desktop file, currently
        open windows and pids.

        Args:
            wnck_app    : a WnckApplication object relating to the app
            wnck_class  : a WnckClass obect relating to the app
            app_match_list : a list of tuples containing details of apps which
                             are hard to match with their .desktop files

        Returns: True if successful, False otherwise
        """

        new_app_info = AppInfoTup(app=wnck_app,
                                  pid=wnck_app.get_pid(), windows=[])
        self.wnck_class = wnck_class
        self.set_app_name(wnck_app.get_name())
#        if build_gtk2:
        self.wm_class_name = wnck_class.get_res_class()
#        else:
#            self.wm_class_namw = wnck_class.get_id()

        # get the currently open windows
        for win in wnck_app.get_windows():
            win.connect("state-changed", self.win_state_changed)
            win_class = win.get_class_group()
            win_class_name = win_class.get_res_class()

            if ((win.get_window_type() == Wnck.WindowType.NORMAL) or
                    (win.get_window_type() == Wnck.WindowType.DIALOG)) and \
                    (self.wm_class_name == win_class_name) and \
                    (win.is_skip_tasklist() is False):
                new_app_info.windows.append(win.get_xid())
        self.app_info.append(new_app_info)
        if new_app_info.pid != 0:
            self.get_cmdline_from_pid(new_app_info.pid)

        # first of all, see if we can get this app's .desktop file from the
        # list of hard to match apps
        # Note: this is not intended to be a permanent feature. Hopefully the
        # Gtk3 version of wnck will allow better app matching and if not, other
        # window manageing libraries can be investigated
        for app in app_match_list:
            if (wnck_app.get_name() == app[0]) or (win_class_name == app[1]):
                desktop_file = "/usr/share/applications/%s.desktop" % app[2]
                if (os.path.exists(desktop_file)):
                    self.desktop_file = desktop_file
                    return self.read_info_from_desktop_file()

        # secondly, search for the required .desktop file
        if not self.get_desktop_from_custom_launcher(os.path.expanduser("~/.local/share/applications/")):
            if not self.get_desktop_from_app_info(os.path.expanduser("~/.local/share/applications/")):
                if not self.brute_force_find_desktop_file(os.path.expanduser("~/.local/share/applications/")):
                    if not self.get_desktop_from_app_info("/usr/share/applications/"):
                      if not self.get_desktop_from_app_info("/usr/local/share/applications/"):
                            if not self.brute_force_find_desktop_file("/usr/share/applications/"):
                                if not self.brute_force_find_desktop_file("/usr/local/share/applications/"):
                                    log_it("Unable to match app")
                                    log_it("App name: %s" %wnck_app.get_name())
                                    log_it("Wm class: %s" %win_class_name)
                                    log_it("cmd line: %s" %self.cmd_line)
                                    return False

        # now that we have the .desktop file, we can get the icon etc
        return self.read_info_from_desktop_file()

    def set_app_name(self, app_name):
        """sets the app name.

        Stores the entire name, which may or may not also contain a
        document title or other app specific info. This will need to
        be parsed when necessary to obtain the actual app name

        Args: The app name

        """

        self.app_name = app_name

    def get_cmdline_from_pid(self, pid):
        """ Find the command line and arguments used to launch the app

        Use the ps command to return the command line and arguments
        for the specified pid

        Set self.path to the full command line

        Args:
            pid - a process id

        """

        cmdstr = "xargs -0 < /proc/%d/cmdline" % pid

        cmd = subprocess.Popen(cmdstr, shell=True, stdout=subprocess.PIPE)

        for line in cmd.stdout:
            pass

        if line is not None:
            self.cmd_line = line.decode("utf-8")

    def get_num_windows(self):
        """Return the number of windows this app and all of its processes have open

        Returns:
            the number of windows
        """

        num_win = 0
        for app_list in self.app_info:
            for wnck_win in app_list.app.get_windows():
                win_type = wnck_win.get_window_type()
                win_cg = wnck_win.get_class_group()
                win_wm_class_name = win_cg.get_res_class()
                if ((win_type == Wnck.WindowType.NORMAL) or
                    (win_type == Wnck.WindowType.DIALOG)) and \
                    (win_wm_class_name.lower() == self.wm_class_name.lower()) \
                    and (wnck_win.is_skip_tasklist() is False):
                    num_win += 1

        return num_win

    def get_wnck_windows(self):
        """Return a list of all of the wnck_windows the app has open"""

        win_list = []
        for app_list in self.app_info:
            for wnck_win in app_list.app.get_windows():
                win_type = wnck_win.get_window_type()
                win_cg = wnck_win.get_class_group()
                win_wm_class_name = win_cg.get_res_class()
                if ((win_type == Wnck.WindowType.NORMAL) or
                    (win_type == Wnck.WindowType.DIALOG)) and \
                    (win_wm_class_name.lower() == self.wm_class_name.lower()) and \
                    (wnck_win.is_skip_tasklist() is False):
                    win_list.append(wnck_win)

        return win_list

    def has_windows_on_workspace(self, wnck_workspace):
        """ test whether the app has at least one window open on a specified
            workspace

        Args:
            wnck_workspace - the workspace to check for

        Returns:
            boolean
        """

        win_list = self.get_wnck_windows()
        for win in win_list:
            win_ws = win.get_workspace()
            if win_ws == wnck_workspace:
                return True

        return False

    def has_unminimized_windows(self):
        """ test whether the app has at least one unminimized window

        Returns:
            boolean
        """

        win_list = self.get_wnck_windows()
        for win in win_list:
            if not win.is_minimized():
                return True

        return False

    def hide_icon(self):
        """ Hides the app's icon"""

        self.drawing_area.set_visible(False)

    def show_icon(self):
        """ Shows the app's icon"""

        self.drawing_area.set_visible(True)

    def is_visible(self):
        """ Method which returns whether or not the app's icon is visible

        Returns:
            boolean
        """
        return self.drawing_area.get_visible()

    def get_desktop_from_app_info(self, srch_dir):
        """Attempt to find the .desktop file for the app based on the app
           wm_class and app name. Will search for files in srch_dir and its
           sub directories.

        In addition to <self.wm_class_name>.desktop looks for the following
        variations of name for the desktop flie:
            lowercase<self.wm_class_name>.desktop
            uppercase<self.wm_class_name>.desktop
            <self.app_name>.desktop
            lowercase<self.app_name>.desktop
            <self.app_name> with space characters replaced by '-'.desktop
            lowercase<self.app_name> with space characters replaced by
                "-",desktop
            <self.wm_class_name>.desktop

        If the destop file is found, self.desktop_file is set accordingly,
        otherwise self.desktop_file is set to None

        Args:
            srch_dir : The directory to be searced for the desktop file

        Returns:
            True if the desktop file was found, False otherwise

        """

        for the_dir, dir_list, file_list in os.walk(srch_dir):

            dfname = the_dir + self.wm_class_name + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.wm_class_name.lower() + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.wm_class_name.upper() + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.app_name + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.app_name.lower() + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.app_name.replace(" ", "-") + ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

            dfname = the_dir + self.app_name.replace(" ", "-").lower() + \
                ".desktop"
            if os.path.isfile(dfname):
                self.desktop_file = dfname
                return True

        self.desktop_file = None
        return False

    def brute_force_find_desktop_file(self, srch_dir):
        """Attempt to find the .desktop file for an app by examining
           all of the .desktop files in a specified directory and its
           subdirectories.

        A match occurs when any of the following condition are met:

        the name field in the .desktop file is the same as self.app_name
        the uppercased first word of self.app_name is the same as the
        the uppercased name field of the .desktop file
        the Exec field is found within self.cmd_line
        the StartupWMClass is the same as self.wm_class_name

        If a match is found, self.desktop_file is set accordingly

        Note - the app must be running, and self.pid, self.cmd_line and
        self.app_name must have been set up

        Args:
            srch_dir  - the directory to search

        Returns:
            True if the .desktop file was found, False otherwise

        """

        # if the search dir doesn't exist, don't do anything
        if os.path.isdir(srch_dir) is False:
            return False

        # split the app name into it's various parts using ' - ' as a
        # delimeter
        name_parts = self.app_name.upper().split(" - ")
        for name in name_parts:
            name = name.strip()

        # split the command line into the command and various arguments
        # note: strings that contain spaces will obviously be split into
        # more than one part, but that's not a problem as we don't care
        # about string aguments
        cmd_parts = self.cmd_line.split()

        # search search_dir and any sub-directories it contains for .desktop
        # files
        for the_dir, dir_list, file_list in os.walk(srch_dir):
            for the_file in file_list:

                if the_file.endswith(".desktop"):

                    the_de = DesktopEntry.DesktopEntry(os.path.join(the_dir,
                                                                    the_file))

                    # if the app name in the desktop file matches any of the
                    # parts of the app name, we have a match
                    try:
                        unused_var = name_parts.index(the_de.getName().upper())
                        name_part_found = True
                    except ValueError:
                        name_part_found = False

                    # remove command line args from the Exec field of the
                    # .desktop
                    de_exec = the_de.getExec()
                    exec_found = False
                    if (de_exec is not None) and (de_exec != ""):
                        de_exec = de_exec.split()

                        # now that we have the app command line we can simply
                        # check that it ends with the same command as the
                        # .desktop file contains. So...
                        exec_found = self.cmd_line.strip().endswith(de_exec[0])

                        if not exec_found:
                            # we can now search within self.cmd_line for
                            # de_exec
                            # Note: we don't test for equality because some
                            # apps are launched via another (e.g. python apps)
                            # and therefore will be one of the command line
                            # args. An example of this is Ubuntu Software
                            # Center - in the .desktop file the Exec field is
                            # '/usr/bin/software-center' whilst its command
                            # line while running is
                            # 'usr/bin/python /usr/bin/software-center'
                            for exec_part in de_exec:
                                try:
                                    unused_var = cmd_parts.index(exec_part)
                                    exec_found = True
                                except ValueError:
                                    pass

                                if exec_found:
                                    break

                    # check that the wm_classes match
                    wm_class_found = False
                    de_wm_class = the_de.getStartupWMClass()
                    if ((de_wm_class is not None) and (de_wm_class != "")) and \
                        ((self.wm_class_name is not None) and
                         (self.wm_class_name != "")):
                        # if both the .desktop and the app have a wm_class then
                        # they MUST match for the app to be found, and
                        # nm_part_found and exec_found become irrelevant

                        if (de_wm_class.lower() == self.wm_class_name.lower()):
                            wm_class_found = True

                        exec_found = wm_class_found
                        name_part_found = wm_class_found

                    if (name_part_found is True) or \
                       (exec_found is True) or \
                       (wm_class_found is True):
                        self.desktop_file = os.path.join(the_dir, the_file)
                        return True

        self.desktop_file = None
        return False

    def get_desktop_from_custom_launcher(self, srch_dir):
        """ Search the custom launchers in a specified directory for
            one where the Exec field is found within self.cmd_line

        If a match is found found, self.desktop_file is set accordingly

        Note: All custom launchers  .desktop filenames start
              with "mda_"

        Args:
            srch_dir : the directory to search

        Returns:
            True if a match was found, False otherwise
        """

        # if the search dir doesn't exist, don't do anything
        if os.path.isdir(srch_dir) is False:
            return False

        for the_file in os.listdir(srch_dir):
            if (the_file.startswith("mda_")) and \
               (the_file.endswith(".desktop")):
                the_de = DesktopEntry.DesktopEntry(srch_dir+the_file)

                # remove command line args from the Exec field of the .desktop
                de_exec = the_de.getExec().split(None, 1)[0]

                if (self.cmd_line.find(de_exec) != -1) and \
                   (self.wm_class_name.upper() ==
                    the_de.getStartupWMClass().upper()):
                    self.desktop_file = srch_dir+the_file
                    return True

    def set_all_windows_icon_geometry(self, x, y, width, height):
        """Set the location on screen where all of the app's windows will be
           minimised to.

        Args:
            x : The X position in root window coordinates
            y : The Y position in root window coordinates
            width: the width of the minimise location
            height:  the height of the minimise location

        """

        # iterate through of the windows this process has open and set their
        # minimize location
        for window in self.get_wnck_windows():
            win_type = window.get_window_type()
            if ((win_type == Wnck.WindowType.NORMAL) or
                (win_type == Wnck.WindowType.DIALOG)) and \
                (window.is_skip_tasklist() == False):

                window.set_icon_geometry(x, y, width, height)

        return True

    def get_allocation(self):
        """ Returns the allocated position and size of the app's icon within the applet
        """

        alloc = self.drawing_area.get_allocation()
        return alloc.x, alloc.y, alloc.width, alloc.height

    def set_drawing_area_size(self, size):
        """Set the size request of the app's drawing area.

        The drawing area is always presumeed to be square

        Args :
            size : the size in pixels

        """
        self.drawing_area_size = size
        self.drawing_area.set_size_request(size, size)

    def queue_draw(self):
        """Queue the app's icon to be redrawn.
        """
        self.drawing_area.queue_draw()

    def set_indicator(self, indicator):
        """Set the running indicator type to the value specified

        Args:
            indicator - the indicator type
        """
        self.indicator = indicator

    def set_multi_ind(self, multi_ind):
        """ Set whether to use an indicator for each open window

        Args:
            multi_ind - boolean
        """
        self.multi_ind = multi_ind

    def is_running(self):
        """Is the app running or not?

        Returns:
            True if the app is running, False if not
        """

        ret_val = False
        for ainf in self.app_info:
            if ainf.pid != -1:
                ret_val = True
                break

        return ret_val

    def has_desktop_file(self):
        """ Does the app have a .desktop file?

        Returns: True if there is a desktop file, False otherwise
        """

        return self.desktop_file is not None

    def read_info_from_desktop_file(self):
        """Attempt to read from read the app's desktop file.

        Will try to read the icon name and app name from the desktop file
        Will also get the executeable path if we don't already have this
        Will read the details of any right click menu options the .desktop
        file defines

        Returns:
            True if successful, False otherwise
        """

        if self.desktop_file:
            dfile = DesktopEntry.DesktopEntry(self.desktop_file)
            self.app_name = dfile.getName()
            self.icon_name = dfile.getIcon()

            # if the desktop file does not specify an icon name, use the app
            # name instead
            if (self.icon_name is None) or (self.icon_name == ""):
                self.icon_name = self.app_name.lower()

                # hack for the MATE application browser app, where the
                # .desktop file on Ubuntu does not specify an icon
                if self.icon_name == "application browser":
                    self.icon_name = "computer"

            # the command specified in the .desktop should always be used to
            # launch the app
            self.cmd_line = dfile.getExec()

            # now read the right click actions the app supports. These can be
            # specified in two ways - by a key named
            # 'X-Ayatana-Desktop-Shortcuts' or by an Actions key

            xads = dfile.get('X-Ayatana-Desktop-Shortcuts')
            if (xads is not None) and (xads != ""):
                # the shortcut ids are in the the form of a semi-colon
                # separated string
                for rca in xads.split(";"):
                    rcname = dfile.get("Name",
                                       group="%s Shortcut Group" % rca)
                    rcexec = dfile.get("Exec",
                                       group="%s Shortcut Group" % rca)
                    if (rcname != "") and (rcexec != ""):
                        new_action = ActionTup(name=rcname, command=rcexec)
                        self.rc_actions.append(new_action)

            for action in dfile.getActions():
                rcname = dfile.get("Name", group="Desktop Action %s" % action)
                rcexec = dfile.get("Exec", group="Desktop Action %s" % action)
                if (rcname != "") and (rcexec != ""):
                    new_action = ActionTup(name=rcname, command=rcexec)
                    self.rc_actions.append(new_action)

            return True

        return False

    def app_has_custom_launcher(self):
        """ Determines whether the docked app has a custom launcher

        Examine the .desktop filename. If it starts with
        "~/.local/share/applications/mda_" the app has a custom launcher

        Returns : True if the app has a custom launcher, False otherwise
        """

        cl_start = os.path.expanduser("~/.local/share/applications/mda_")
        return os.path.expanduser(self.desktop_file).beginswith(cl_start)

    def win_state_changed(self, wnck_win, changed_mask, new_state):
        """Handler for the wnck_window state-changed event

        If the app needs attention and we're not already flashing the icon
        start it flashing. If the app icon is not visible, make it visible

        If the app doesn't need attention and its icon is flashing, stop
        it flashing

        """

        if ((new_state & Wnck.WindowState.DEMANDS_ATTENTION) != 0) or\
           ((new_state & Wnck.WindowState.URGENT) != 0):

            if not self.is_flashing:
                self.is_flashing = True
                self.flash_on = False  # initial flash state = off
                Flasher = FlashTimer(self)

                if not self.is_visible():
                    self.show_icon()

        else:
            if self.is_flashing:
                # we need to turn flashing off
                self.is_flashing = False
                self.queue_draw()

                # the timer will handle the rest ....
                # hiding the icon (if necessary) will be taken care of next
                # time the user changes workspace

    def do_expose_event(self, drawing_area, event):
        """The main drawing event for the docked app.

        Does the following:
            draw the app icon
            if the mouse is over the app icon, highlight the icon
            if the is running draw the app running indicators(according to the
            applet orientation)
            if the app is the foreground app, highlight the background with a
            gradient fill
            if the app is pulsing, draw the icon with a variable level of
            transparency according to the pulse count
            if the app is flashing, draw the icon either fully opaque or
            completely transparent according to its flash state
            if the app is being dragged to a new position on the dock, draw
            a completely transparent background
        Args:
            drawing_area : the drawing area that related to the event. Will
                           always be the same as self.drawing area
            event        : in Gtk2 the event arguments, in Gtk3 a cairo context
                           to draw on

        """

        # there are lots of drawing operations to be done, so do them to an
        # offscreen surface and when all is finished copy this to the docked
        # app
        offscreen_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                               self.drawing_area_size,
                                               self.drawing_area_size)
        ctx = cairo.Context(offscreen_surface)

        # convert the highlight values to their cairo equivalents
        red = self.highlight_color.r / 255
        green = self.highlight_color.g / 255
        blue = self.highlight_color.b / 255

        if self.is_active and (self.is_dragee is False):
            # draw a background gradient according to the applet orientation
            if self.applet_orient == MatePanelApplet.AppletOrient.RIGHT:
                pattern = cairo.LinearGradient(0, 0, self.drawing_area_size, 0)
                pattern.add_color_stop_rgba(0.0, red, green, blue, 1)
                pattern.add_color_stop_rgba(1.0, red, green, blue, 0)
            elif self.applet_orient == MatePanelApplet.AppletOrient.LEFT:
                pattern = cairo.LinearGradient(self.drawing_area_size, 0, 0, 0)
                pattern.add_color_stop_rgba(0.0, red, green, blue, 1)
                pattern.add_color_stop_rgba(1.0, red, green, blue, 0)
            elif self.applet_orient == MatePanelApplet.AppletOrient.DOWN:
                pattern = cairo.LinearGradient(0, 0, 0, self.drawing_area_size)
                pattern.add_color_stop_rgba(0.0, red, green, blue, 1)
                pattern.add_color_stop_rgba(1.0, red, green, blue, 0)
            else:
                pattern = cairo.LinearGradient(0, self.drawing_area_size, 0, 0)
                pattern.add_color_stop_rgba(0.0, red, green, blue, 1)
                pattern.add_color_stop_rgba(1.0, red, green, blue, 0)

            ctx.rectangle(0, 0, self.drawing_area_size, self.drawing_area_size)
            ctx.set_source(pattern)
            ctx.fill()

        # draw the app icon
        Gdk.cairo_set_source_pixbuf(ctx, self.app_pb, 3, 3)
        ctx.rectangle(0, 0, self.drawing_area_size, self.drawing_area_size)

        if self.is_pulsing:
            # draw the icon semi-transparently according to how far through the
            # animation we are

            half_way = CONST_PULSE_STEPS/2
            if self.pulse_step <= half_way:
                alpha = 1.0 - (self.pulse_step / half_way)
            else:
                alpha = 0.0 + (self.pulse_step - half_way) / half_way

            ctx.paint_with_alpha(alpha)

        elif self.is_flashing:
            if self.flash_on:
                ctx.paint()  # draw normally if in the flash on state
        elif self.is_dragee:
            ctx.paint_with_alpha(0.0)
        else:
            ctx.paint()

        if (self.has_mouse is True) and (self.is_dragee is False):
            # lighten the icon
            ctx.set_operator(cairo.OPERATOR_ADD)
            ctx.paint_with_alpha(0.2)
            ctx.set_operator(cairo.OPERATOR_OVER)

        # draw the app running indicators
        if (self.is_running()) and \
           (self.indicator != dock_prefs.IndicatorType.NONE) and \
           (self.is_dragee is False):

            # work out how many indicators to draw - either a single one or
            # one for each open window up to a maximum of 4, and take into
            # account the fact that we might only be showing indicators from
            # the current workspace

            if self.ind_ws is None:
                # show indicators for windows from all workspaces
                if self.multi_ind is False:
                    num_ind = 1
                else:
                    num_ind = self.get_num_windows()
                    if num_ind > 4:
                        num_ind = 4
            else:
                # get count the number of windows on the current workspace
                num_ind = 0
                for win in self.get_wnck_windows():
                    if win.is_on_workspace(self.ind_ws):
                        num_ind += 1

                if self.multi_ind is False:
                    if num_ind > 1:
                        num_ind = 1
                else:
                    if num_ind > 4:
                        num_ind = 4

            if self.applet_orient == MatePanelApplet.AppletOrient.RIGHT:
                ind_x = 2
                ind_y = (self.drawing_area_size-4)/(num_ind+1) + 2

            elif self.applet_orient == MatePanelApplet.AppletOrient.LEFT:
                ind_x = self.drawing_area_size - 1
                ind_y = (self.drawing_area_size - 4)/(num_ind+1) + 2

            elif self.applet_orient == MatePanelApplet.AppletOrient.DOWN:
                ind_x = (self.drawing_area_size - 4)/(num_ind+1) + 2
                ind_y = 2
            else:
                ind_x = (self.drawing_area_size - 4)/(num_ind+1) + 2
                ind_y = self.drawing_area_size - 1

            this_ind = 1
            while this_ind <= num_ind:
                rad_patt = cairo.RadialGradient(ind_x, ind_y, 2,
                                                ind_x, ind_y, 4)

                # do a light or dark indicator as necessary
                if self.indicator == dock_prefs.IndicatorType.LIGHT:
                    rad_patt.add_color_stop_rgba(0, 0.9, 0.9, 0.9, 1)
                    rad_patt.add_color_stop_rgba(1, 0.0, 0.0, 0.0, 0)
                else:
                    rad_patt.add_color_stop_rgba(0, 0.0, 0.0, 0.0, 1)
                    rad_patt.add_color_stop_rgba(1, 0.9, 0.9, 0.9, 0)

                ctx.set_source(rad_patt)
                ctx.arc(ind_x, ind_y, 6, 0, 2*math.pi)

                if num_ind > 1:
                    if (self.applet_orient == MatePanelApplet.AppletOrient.RIGHT) or \
                       (self.applet_orient == MatePanelApplet.AppletOrient.LEFT):
                        ind_y += (self.drawing_area_size - 6)/(num_ind+1)
                    else:
                        ind_x += (self.drawing_area_size - 6)/(num_ind+1)

                this_ind += 1
                ctx.fill()

        # now draw to the screen
        if build_gtk2:
            screen_ctx = self.drawing_area.window.cairo_create()
            screen_ctx.rectangle(event.area.x, event.area.y,
                                 event.area.width, event.area.height)
            screen_ctx.clip()

            alloc = self.drawing_area.get_allocation()
            if (self.applet_orient == MatePanelApplet.AppletOrient.UP) or \
               (self.applet_orient == MatePanelApplet.AppletOrient.DOWN):
                screen_ctx.set_source_surface(offscreen_surface, alloc.x, 0)
            else:
                screen_ctx.set_source_surface(offscreen_surface, 0, alloc.y)

            screen_ctx.paint()
            screen_ctx = None
        else:
            event.set_source_surface(offscreen_surface, 0, 0)
            event.paint()
            alloc = self.drawing_area.get_allocation()

        ctx = None

    def set_pixbuf(self, pixbuf):
        """Set the app pixbuf and calculate its average colour.
        """

        self.app_pb = pixbuf

        rht, ght, bht = self.highlight_color = get_avg_color(pixbuf)
        self.highlight_color = ColorTup(r=rht, g=ght, b=bht)

    def start_app(self):
        """Start the app or open a new window if it's already running
        """
        the_de = DesktopEntry.DesktopEntry(self.desktop_file)
        run_it = the_de.getExec()
        if run_it is not None:

            # hack for Linux Mint:
            # Mint has several shortcuts for starting caja so that it can
            # be started in a specific directory e.g. home, /, etc
            # However, the main caja.desktop is responsible for starting the
            # user's desktop and this is the .desktop file the applet finds
            # first.
            # When the caja icon on the applet is clicked, caja is run as a
            # desktop window and no new file browser appears.
            # To get around this, we can simply check the command that is going
            # to be run and change it so that a caja window opens in the user's
            # home directory, which is the behaviour they'll probably be
            # expecting....
            if run_it == "/usr/bin/startcaja":
                run_it = "caja"

            # remove any command line arguments beginning with ' %'
            # TODO - check if still really necessary
            if " %" in run_it:
                i = run_it.rfind(" %")
                run_it = run_it[0:i]

        # start the app
        self.run_cmd_line(run_it)

    def run_cmd_line(self, cmd_line):
        """Run a command line. To be used when starting an app for the first
           time or when running an action/shortcut specfied in the app's
           .desktop file

            Args:
                cmd_line - the command to run
        """

        # the command line may contain escape sequences, so unescape them....
        cmd_line = bytearray(cmd_line, "UTF-8")
        cmd_line = cmd_line.decode("unicode-escape")

        # if any of the directories in cmd_line contain a " ", they need to be
        # escaped
        head,tail = os.path.split(cmd_line)
        if " " in head:
            head = head.replace(" ", "\ ")
            cmd_line = head + "/" + tail
        app_info = Gio.AppInfo.create_from_commandline(cmd_line,
                                                       None,
                                                       Gio.AppInfoCreateFlags.NONE)
        alc = Gdk.AppLaunchContext()
        alc.set_desktop(-1)                   # use default screen & desktop
        app_info.launch()
        # set the app icon pulsing
        throbber = PulseTimer(self)

    def run_rc_action(self, act_no):
        """ run the right click action specified by act_no

        Args:
           act_no - integer, the action number to run
        """

        if len(self.rc_actions) >= act_no:
            self.run_cmd_line(self.rc_actions[act_no-1].command)

    def get_rc_action(self, act_no):
        """ return a specified right click action's details

        Args:
            act_no - integer, the specified action number

        Returns:
                bool - True if the action exists, False otherwise
                string - the name of the action (i.e. the text to appear in the
                right click menu)
                string - the command line to the be run
        """

        if len(self.rc_actions) >= act_no:
            return (True, self.rc_actions[act_no-1].name,
                    self.rc_actions[act_no-1].command)
        else:
            return (False, "", "")

    def start_pulsing(self):
        """ start the dock icon pulsing
        """

        throbber = PulseTimer(self)

    def set_dragee(self, is_dragee):
        """ Set the flag which indicates whether or not this app is being
            dragged to a new position on the dock

        Set the value of the self.is_dragee flag and redraw the app icon
        """

        self.is_dragee = is_dragee
        self.queue_draw()

def main():
    """Main function.

    Debugging code can go here
    """
    pass

if __name__ == "__main__":
    main()
