#!/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 orientation of the panel

    Provide visual feedback to the user when an app is launched 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 = True

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
from gi.repository import GLib

import cairo
import math
import xdg.DesktopEntry as DesktopEntry
import os
import os.path
import subprocess
import re

from collections import namedtuple

import dock_prefs
from docked_app_helpers import DefaultBackgroundDrawer, AlphaFillBackgroundDrawer, DefaultLightInd,\
     DefaultDarkInd, ThemeBarInd


from log_it import log_it as log_it

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


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, once_only=False):
        """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
            once_only : do only one iteration of pulsing, then stop
        """

        self.app = app
        self.app.pulse_step = 0
        self.app.is_pulsing = True
        self.once_only = once_only
        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:
            # if we're starting the app for the first time (with startup notification)
            # and it still hasn't finished loading, carry on pulsing
            if (self.app.startup_id is not None) and not self.once_only:
                self.app.pulse_step = 0
            else:
                # we only want the icon to pulse one
                # if we have a startup_id the notification needs to be cancelled
                if self.app.startup_id is not None:
                    self.app.cancel_startup_notification()

                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  : A list of strings containing the names of the additional application
                      actions suppported by the app
        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
        desktop_ai   : a Gio.GDesktopAppInfo read from the .desktop file
        startup_id   : id used for startup notifications
        wm_class_name:
        wm_class_group_name:
        applet_win  : the Gdk.Window of the panel applet
        applet      : 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 (e.g. light or dark) to draw under
                      running apps
        active_bg   : the type of background (e.g. gradient or solid fill) to
                      be drawn when the app is the active app
        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

        show_progress : boolean - indicates whether or not to display a
                        progress indicator on the app's icon
        progress_val  : the progress value( 0 to 1.0)
        show_count   : boolean - indicates whether or not to display a
                        count value on the app's icon
        count_val   : the value of the count
    """

    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.desktop_ai = None
        self.wm_class_name = ""
        self.wm_class_group_name = ""
        self.icon_geometry_set = False
        self.applet_win = None
        self.applet_orient = None
        self.ind_ws = None
        self.startup_id = None
        self.applet = 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.active_bg = dock_prefs.ActiveBgType.GRADIENT

        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

        self.show_progress = False
        self.progress_val = 0.0
        self.show_count = False
        self.count_val = 0

    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 self.wm_class_group_name == "":
            #    self.wm_class_group_name = win.get_class_group_name()

            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 get_first_window(self):
        """ Return the first window the app has open

        Returns: a wnck.window, or None (if the app has no open windows)
        """

        if self.get_num_windows() == 0:
            return None
        else:
            win_list = self.get_wnck_windows()
            return win_list[0]

    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

        """

        # TODO: replace DesktopEntry with Gio.DesktopAppInfo...

        # 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:
                        # if the exec line specfies an environment variable to
                        # set, we need to get rid of this part
                        if de_exec.startswith("env"):
                            cmd_parts = de_exec.split(" ", 2)
                            de_exec = cmd_parts[2]

                        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
                        # try matching just the last part, removing -bin from the name
                        if not exec_found:
                            de_exec = de_exec[0]
                            de_exec = re.sub("-bin", "", de_exec)

                            if "/" in de_exec:
                                if de_exec.split("/")[-1] in self.cmd_line:
                                    exec_found = True

                    # 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
        """

        # TODO: replace DesktopEntry with Gio.DesktopAppInfo... and then
        # TODO: remove xdg.DesktopEntry from imports and list of dependecies

        # 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() is 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_active_bg (self, active_bg):
        """Set the active background type to the value specified

        Args:
            active_bg - the background type

        """

        self.active_bg = active_bg

    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:
            if os.path.isabs(self.desktop_file):
                self.desktop_ai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file)
            else:
                self.desktop_ai = Gio.DesktopAppInfo.new(self.desktop_file)

            self.app_name = self.desktop_ai.get_string("Name")
            self.icon_name = self.desktop_ai.get_string("Icon")

            # 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"

            # get the command specified in the .desktop file used to launch the app
            self.cmd_line = self.desktop_ai.get_string("Exec")

            # get the list of addtional application actions (to be activated by right
            # clicking the app's dock icon)
            self.rc_actions = self.desktop_ai.list_actions()

            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)

        if self.is_active and (self.is_dragee is False):

            # 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.active_bg == dock_prefs.ActiveBgType.GRADIENT:
                dbgd = DefaultBackgroundDrawer(ctx, self.drawing_area_size,
                                               self.applet_orient, red, green, blue)
            else:
                dbgd = AlphaFillBackgroundDrawer(ctx, self.drawing_area_size,
                                                 self.applet_orient, red, green, blue, 0.5)

            dbgd.draw()

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

        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

            ind = None
            if self.indicator == dock_prefs.IndicatorType.LIGHT:
                ind = DefaultLightInd(ctx, self.drawing_area_size,
                                      self.applet_orient, num_ind)
            elif self.indicator == dock_prefs.IndicatorType.DARK:
                ind = DefaultDarkInd(ctx, self.drawing_area_size,
                                      self.applet_orient, num_ind)
            elif self.indicator == dock_prefs.IndicatorType.TBAR:
                ind = ThemeBarInd(ctx, self.drawing_area_size, self.applet_orient, self.applet)

            if ind is not None:
                ind.draw()

        # do we need a count?
        if self.show_count:
            self.draw_count(ctx)

        if self.show_progress:
            self.draw_progress(ctx)

        # 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 draw_count(self, ctx):
        """ Draw the app's counter value

        Args: ctx - the cairo context where the counter is to be drawn
        """

        # drawing is done at a notional size  64x64 px, and then scaled
        # appropriately according to self.drawing_area_size

        draw_size = 64.0

        # height of the counter = 2 pix border top and bottom + 16 pix
        # internal height
        height = 20

        # work out the appropriate font size to use - has to fit within the
        # borders and provide some space above and below the count_val
        reqd_font_height = height - 8

        # find a font size where the count can be shown with the required height
        ctx.select_font_face("", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
        ctext = "%d" % self.count_val
        for fsize in range(24, 2, -1):
            ctx.set_font_size(fsize)
            extents = ctx.text_extents(ctext)

            if extents[3] < reqd_font_height:
                font_size = fsize
                break

        # work out an appropriate width for the counter
        inset = height/2
        radius = inset-1
        if int(extents[2] + extents[0]) > int(radius):
            width = extents[2] + extents[0] + radius
        else:
            width = height + inset

        ctx.save()

        # the background color of the count is the app's highlight colour
        # convert the highlight values to their cairo equivalents
        bred = self.highlight_color.r / 255
        bgreen = self.highlight_color.g / 255
        bblue = self.highlight_color.b / 255

        # set an appropriate text and border color
        if bred + bgreen + bblue > 1.5:  # mid-level grey
            tred = tgreen = tblue = 0.0
        else:
            tred = tgreen = tblue = 1.0

        # the count is placed in the upper right of the drawing area, and we need
        # to calculate it's position based on the notional DA_SIZE
        #

        adj = 2
        left = draw_size - width + inset - adj

        # do the drawing - attribution for the drawing code:
        # https://bazaar.launchpad.net/~unity-team/unity/trunk/view/head:/launcher/LauncherIcon.cpp
        ctx.scale(self.drawing_area_size/draw_size,
                  self.drawing_area_size/draw_size)

        ctx.move_to(left, height - 1 + adj)
        ctx.arc(left, inset + adj, radius, 0.5 * math.pi, 1.5*math.pi)
        ctx.arc(draw_size-inset-adj, inset + adj, radius, 1.5*math.pi, 0.5*math.pi)
        ctx.line_to(left, height - 1 + adj)
        ctx.set_source_rgb(bred, bgreen, bblue)
        ctx.fill_preserve()
        ctx.set_source_rgb(tred, tgreen, tblue)
        ctx.set_line_width(2)
        ctx.stroke()

        # draw the text
        ctx.move_to(left - inset + width/2 - (extents[0]+extents[2]/2),
                    (height/2) + adj + extents[3]/2)
        ctx.set_source_rgb(tred, tgreen, tblue)
        ctx.show_text(ctext)

        ctx.restore()

    def draw_progress(self, ctx):
        """ Draw a progress bar to show the app's progress value

        Args: ctx - the cairo context where the counter is to be drawn
        """

        def rounded_rectangle(cr, x, y, w, h, r=20):
            """ Convenience function to draw a rounded rectangle, really needs
                to be in a library module somewhere, as its used bythe window
                list

                # Attribution:
                # https://stackoverflow.com/questions/2384374/rounded-rectangle-in-pygtk
                # This is just one of the samples from
                # http://www.cairographics.org/cookbook/roundedrectangles/
                #   A****BQ
                #  H      C
                #  *      *
                #  G      D
                #   F****E
            """

            cr.move_to(x + r, y)                      # Move to A
            cr.line_to(x + w - r, y)                    # Straight line to B
            cr.curve_to(x + w, y, x + w, y, x + w, y + r)
            # Curve to C, Control points are both at Q
            cr.line_to(x + w, y + h - r)                  # Move to D
            cr.curve_to(x + w, y + h, x + w, y + h, x + w - r, y + h)  # Curve to E
            cr.line_to(x + r, y + h)                    # Line to F
            cr.curve_to(x, y + h, x, y + h, x, y + h - r)  # Curve to G
            cr.line_to(x, y+r)                      # Line to H
            cr.curve_to(x, y, x, y, x+r, y)             # Curve to A

        # drawing is done on a to scale of 64x64 pixels and then scaled
        # down to the fit the app's drawing area
        draw_size = 64.0

        # the foreground colour of the progress is the app's highlight colour
        # convert the highlight values to their cairo equivalents
        fred = self.highlight_color.r / 255
        fgreen = self.highlight_color.g / 255
        fblue = self.highlight_color.b / 255

        # set an appropriate border color and also a background colour for
        # the progress bar, based on the highlight colour
        if fred + fgreen + fblue > 1.5:  # mid-level grey
            brd_red = brd_green = brd_blue = 0.0
            bk_red = bk_green = bk_blue = 1.0
        else:
            brd_red = brd_green = brd_blue = 1.0
            bk_red = bk_green = bk_blue = 0.0

        height = 8              # total height of the progress bar
        line_width = 2          # border line width
        int_height = height - line_width * 2  # interior height
        left = 8
        width = draw_size - left * 2   # width of the progress bar

        top = (draw_size/8) * 5

        ctx.save()
        ctx.scale(self.drawing_area_size/draw_size, self.drawing_area_size/draw_size)

        # draw exterior of the progress bar
        ctx.set_source_rgb(brd_red, brd_green, brd_blue)
        ctx.set_line_width(2)
        rounded_rectangle(ctx, left, top, width, height, 7)
        ctx.stroke()

        # fill the interior with the background colour
        ctx.set_line_width(1)
        ctx.set_source_rgb(bk_red, bk_green, bk_blue)
        rounded_rectangle(ctx, left + line_width, top + line_width,
                          width - line_width*2, height-line_width*2,
                          2)

        ctx.fill()

        # fill part of the interior with a different colour, depending on
        # the progress value
        ctx.set_source_rgb(fred, fgreen, fblue)
        rounded_rectangle(ctx, left + line_width, top + line_width,
                          (width - line_width*2) * self.progress_val,
                          height-line_width*2, 2)
        ctx.fill()
        ctx.restore()

    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

            Use Gio.DesktopAppinfo as it supports startup notfication
        """
        # start the app
        run_it = self.desktop_ai.get_string("Exec")

        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"
                self.run_cmd_line(run_it)
                return

        gdai = Gio.DesktopAppInfo.new_from_filename(self.desktop_file)
        disp = Gdk.Display.get_default()
        if build_gtk2:
            alc = Gdk.AppLaunchContext()
        else:
            alc = disp.get_app_launch_context()
    
        alc.set_desktop(-1)  # use default screen & desktop
        alc.set_timestamp(Gtk.get_current_event_time())
        alc.connect("launch-failed", self.launch_failed)

        # indicate we want startup notification
        self.startup_id = alc.get_startup_notify_id(gdai, [])

        gdai.launch_uris_as_manager([], alc, GLib.SpawnFlags.SEARCH_PATH,
                                    None, None, None, None)

        # make the app's icon pulse
        if not self.is_running():
            throbber = PulseTimer(self)
        else:
            # if the app is already running, we want the icon to pulse at most once only
            # For apps which don't open a new window, the pulse timer will end up cancelling
            # the unneeded startup notification
            throbber = PulseTimer(self, True)

    def cancel_startup_notification(self):
        """
            Cancel any startup notification
        """
        if build_gtk2:
            Gdk.notify_startup_complete_with_id(self.startup_id)
        else:
            display = Gdk.Display.get_default()
            display.notify_startup_complete(self.startup_id)
           
        self.startup_id = None

    def launch_failed(self, app_launch_context, startup_id):
        """Handler for app launch failure events

        Cancel the satartup notification

            Args:
                app_launch_context : the Gdk.AppLaunchContext that failed
                startup_id : the startup notification id
        """

        self.cancel_startup_notification()

        display = Gdk.Display.get_default()
        display.notify_startup_complete(startup_id)

    def run_cmd_line(self, cmd_line):
        """Run a command line.

            Args:
                cmd_line - the command to run
        """

        # TODO: this is old code and needs to be removed

        # 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 an environment variable is specified, extract its name an value
        # Note: the .desktop file specification at
        # https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
        # does not mention this. Both Ubuntu
        # https://help.ubuntu.com/community/EnvironmentVariables#Launching_desktop_application_with_an_environment_variable
        # and Arch linux
        # https://wiki.archlinux.org/index.php/Desktop_entries#Modify_environment_variables
        # seem to indicate that only a single variable can be set and that
        # there are no spaces between the variable name, the '=' character and
        # variable's value .....

        # so, if cmd_line begins with "env" it specifies an environment variable
        # to set, follwed by the app e.g. env LANG=he_IL.UTF-8 /usr/bin/pluma
        #
        if cmd_line.startswith("env"):
            cmd_parts = cmd_line.split(" ", 2)
            var_parts = cmd_parts[1].split("=")
            var_name = var_parts[0]
            var_value = var_parts[1]

            # now we need to get the app path and args and carry on...
            cmd_line = cmd_parts[2]
        else:
            var_name = None
            var_value = None

        # 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.SUPPORTS_STARTUP_NOTIFICATION)
        alc = Gdk.AppLaunchContext()
        alc.set_desktop(-1)                   # use default screen & desktop
        alc.set_timestamp(Gtk.get_current_event_time())

        # if the .desktop specfied an environment variable, set it
        if (var_name is not None) and (var_value is not None):
            alc.setenv(var_name, var_value)

        file_list = GLib.List()

        # app_info.launch(None, alc)
        self.startup_id = alc.get_startup_notify_id(app_info, [])

        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:
            if build_gtk2:
                alc = Gdk.AppLaunchContext()
            else:
                disp = Gdk.Display.get_default()
                alc = disp.get_app_launch_context()

            alc.set_desktop(-1)  # use default screen & desktop
            alc.set_timestamp(Gtk.get_current_event_time())
            alc.connect("launch-failed", self.launch_failed)

            # indicate we want startup notification
            self.startup_id = alc.get_startup_notify_id(self.desktop_ai, [])

            self.desktop_ai.launch_action(self.rc_actions[act_no-1], alc)
            self.start_pulsing()

    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)
        """

        if len(self.rc_actions) >= act_no:
            return True, self.desktop_ai.get_action_name(self.rc_actions[act_no-1])
        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 set_progress_visible(self, is_visible):
        """
            Update the progress visibility and cause the app's icon to be
            redrawn

        Args:
            is_visible : whether the progress is to be displayed
        """

        if is_visible != self.show_progress:
            self.show_progress = bool(is_visible)
            self.queue_draw()

    def set_progress_value(self, val):
        """
            Update the progress value and cause the app's icon to be
            redrawn

        Args:
            val        : the counter value
        """

        # if the new progressvalue is the same as the old, then there's no need
        # to do anything...
        if val != self.progress_val:
            self.progress_val = val
            self.queue_draw()

    def set_counter_visible(self, is_visible):
        """
            Update the counter visibility and cause the app's icon to be
            redrawn

        Args:
            is_visible : whether the counter is to be displayed
        """

        # if the new value is the same as the old, then there's no need
        # to do anything...
        if is_visible != self.show_count:
            self.show_count = bool(is_visible)
            self.queue_draw()

    def set_counter_value(self, val):
        """
            Update the counter value and cause the app's icon to be
            redrawn

        Args:
            val        : the counter value
        """

        # if the new counter value is the same as the old, then there's no need
        # to do anything...
        if val != self.count_val:
            self.count_val = val
            self.queue_draw()


def main():
    """Main function.

    Debugging code can go here
    """
    pass

if __name__ == "__main__":
    main()
