#!/usr/bin/env python3

"""Provide functionality relating to an application dock

Manage a list of pinned and non-pinned dock apps.

Handle all mouse ui events for docked apps
Respond to window opening and closing events from libwnck
Respond to changes to the Gtk icon theme and update all docked apps
Load and save dock settings (e.g. pinned apps and indicator type)
Respond to Unity API DBus messages so that apps can display counts and
progress meters on their icons

respond to selections made in the applet right click menu, specifically
    : allow apps to be pinned to the dock
    : allow apps to unpinned from the dock
    : allow app icons be to moved to a different position on the dock
    : disply an About dialog
    : display a Preferences dialog
"""

# 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 Gdk
from gi.repository import MatePanelApplet
from gi.repository import GObject
from gi.repository import Wnck

from gi.repository import GdkPixbuf
from gi.repository import Gio
from gi.repository import GLib
import os
import os.path
import sys
import subprocess
from time import sleep
import dbus
from dbus.mainloop.glib import DBusGMainLoop

import docked_app
import dock_prefs
import dock_about
import dock_custom_launcher
import dock_win_list
import dock_action_list
import dock_xml
import dock_color_changer
import docked_app_helpers

from log_it import log_it as log_it


class DragMotionTimer(object):
    """Timer to allow us to track mouse motion during a drag and drop
       operation.

    Required because:
        we don't get drag-motion events when dragging app icons
        within the applet and we need to track the mouse x,y so that
        we can rearrange dock icons on the fly

    Instantiates a timer which periodically gets the root x,y position of
    the mouse and translates these to applet x,y coordinate

    Attributes:
        dragee : the docked app which is being dragged
        drag-ended : the drag and drop operation has finished
        timer_id = the id of the timer that is instantiated
        mouse = a Gdk.device we can query for the mouse position

    """

    def __init__(self, dragee, the_dock):
        """Init for the DragMotionTimer class.

        Sets everything up by creating the timer and setting a reference to
        the DockedApp being dragged

        Arguments:
            dragee : the DockedApp that is being dragged
            the_dock : the dock...
        """

        self.dragee = dragee
        self.drag_ended = False
        self.the_dock = the_dock
        self.old_drag_pos = -1

        # call the timer function 50 times per second
        self.timer_id = GObject.timeout_add(20, self.do_timer)

        # get the mouse device
        display = Gdk.Display.get_default()
        manager = display.get_device_manager()
        self.mouse = manager.get_client_pointer()

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

        If the drag operation has ended, delete the timer

        Move dock icons about etc....

        """

        # has the drag and drop ended?
        if self.drag_ended is True:
            GObject.source_remove(self.timer_id)
            return False

        none, x, y = self.mouse.get_position()

        dx, dy = self.the_dock.get_dock_root_coords()
        x = x - dx
        y = y - dy

        orient = self.the_dock.applet.get_orient()

        app_with_mouse = self.the_dock.get_app_at_mouse(x, y)
        if app_with_mouse is not None:
            app_name = app_with_mouse.app_name
        else:
            # we're not on the dock, so just record the current mouse x
            # or y and exit
            if (orient == MatePanelApplet.AppletOrient.UP) or \
               (orient == MatePanelApplet.AppletOrient.DOWN):
                self.old_drag_pos = x
            else:
                self.old_drag_pos = y

            return True

        if app_with_mouse == self.dragee:
            return True

        # get the coordinates within the applet of the app that has the
        # mouse

        ax, ay = self.the_dock.get_app_root_coords(app_with_mouse)
        ax = ax - dx
        ay = ay - dy

        orient = self.the_dock.applet.get_orient()
        if (orient == MatePanelApplet.AppletOrient.UP) or \
           (orient == MatePanelApplet.AppletOrient.DOWN):
            if self.old_drag_pos == -1:
                self.old_drag_pos = x
                return True

            if x > self.old_drag_pos:
                # we're moving left to right on a new app, so we need to
                # trigger an icon move at 40% of the icon width
                trig_val = ax + (self.dragee.drawing_area_size * 40) / 100
                if (self.old_drag_pos < trig_val) and (x >= trig_val):
                    new_pos = self.the_dock.app_list.index(app_with_mouse)
                    self.the_dock.move_app(self.dragee, new_pos)
            else:
                # moving right to left
                trig_val = ax + self.dragee.drawing_area_size - \
                           (self.dragee.drawing_area_size * 40) / 100
                if (self.old_drag_pos > trig_val) and (x <= trig_val):
                    new_pos = self.the_dock.get_app_position_in_dock(app_with_mouse)
                    self.the_dock.move_app(self.dragee, new_pos)

            self.old_drag_pos = x

        else:

            if self.old_drag_pos == -1:
                self.old_drag_pos = y
                return True

            if y > self.old_drag_pos:
                # we're moving top to bottom on a new app, so we need to
                # trigger an icon move at 40% of the icon height
                trig_val = ay + (self.dragee.drawing_area_size * 40) / 100
                if (self.old_drag_pos < trig_val) and (y >= trig_val):
                    new_pos = self.the_dock.app_list.index(app_with_mouse)
                    self.the_dock.move_app(self.dragee, new_pos)
            else:
                # moving bottom to top
                trig_val = ay + self.dragee.drawing_area_size - \
                           (self.dragee.drawing_area_size * 40) / 100
                if (self.old_drag_pos > trig_val) and (y <= trig_val):
                    new_pos = self.the_dock.get_app_position_in_dock(app_with_mouse)
                    self.the_dock.move_app(self.dragee, new_pos)

            self.old_drag_pos = y

        return True


class DragActivateTimer(object):
    """ Timer used when something other than a .desktop file is dragged onto the
        dock

    Instantiates a timer which will wait for a short interval and then activate
    a specified app's window. This will allow apps to drag data onto the dock,
    activate an app's window and then drop the data there. The short delay between
    the user dragging data onto the dock and the app activating provides a better
    user experience ....

    Attributes:
        the_app : the app whose window is to be acticvated
        timer_id = the id of the timer that is instantiated

    """

    def __init__(self, the_dock, the_app):
        """Init for the DragActionTimer class.

        Sets everything up by creating the timer and setting a reference to
        the DockedApp to be activated

        Arguments:
            the_dock : the dock...
            the_app  : the app to be activated

        """

        self.the_app = the_app
        self.the_dock = the_dock
        # wait .3 of a second ...
        # tion 50 timakemes per second
        self.timer_id = GObject.timeout_add(333, self.do_timer)

    def do_timer(self):
        """ Activate the app
        """

        if (self.the_app is not None) and (self.the_app.is_running()):
                if not self.the_app.is_active:
                    self.the_dock.minimize_or_restore_windows(self.the_app, None)

        self.timer_id = 0
        return False # we only run once...


class Dock(object):
    """The main application dock class

        Attributes:
            applet : the MATE panel applet
            wnck_screen : the currently active wnck_screen. Assumed to be the
                          default wnck_screen on applet start up
            window :
            app_list : the list of DockedApp objects. Will contain
                       running/non-running pinned apps and running unpinned
                       apps
            box    : A Gtk2 HBox or VBox (depending on the applet orientation)
                     or Gtk3 Grid containing the drawing areas of each of the
                     apps in app_list
            icontheme : used to load application icons and detect changes in
                        the icon theme
            about_win : the about window
            prefs_win : the preferences window
            ccl_win   : the create custom launcher window
            app_with_mouse : the DockedApp that the mouse is currently over
            active_app : the DockedApp that is currently the foreground app
            right_clicked_app: the app that was most recently right clicked
            settings_path : the GIO.Settings path for the applet
            settings : GIO.Settings - the settings for the applet
            indicator : the indicator type (e.g. light, dark, bar or None)
            fallback_bar_col : a list of the r,g,b elements of the colour to be
                               used when drawing bar indicators and the
                               current theme highlight colour can't be determined
                               (will be mainly of use in gtk2)
            active_bg : the type of background to use for the currently active app's
                        icon background (e.g. gradient fill or solid fill)
            multi_ind : whether or not multiple indicators are to be used
            show_all_apps : whether or not unpinned apps from all workspaces
                                     are displayed in the dock
            win_from_cur_ws_only : whether indicators and window list items are
                                   to be shown for the current workspace only
            change_panel_color : whether or not the color of MATE panels are to
                                 be changed to the dominant colour of the
                                 desktop wallpaper
            change_dock_color_only : whether or not all MATE panels are to have
                                     their colour changed or just the panel
                                     containing the dock
            panel_id : the toplevel id of the panel the applet is on
            dock_action_group : Gtk Action group containing all of the actions
                                for the applet right click menu
            app_win_list : a replacement for the default tooltip - a window
                            which lists the currently highlighted app's open
                            windows and allows the user to select one
            app_act_list :  a popup window which shows actions relating to the
                            currently highlighted app e.g. Pin/Unpin/Open new
                            window
            act_list_delay : the amount of time (in milliseconds) after which
                             the app's window list will pop up when the mouse
                             hovers over the app
            act_list_timer : timer object used to count act_list_delay
            panel_cc : used to change the colour of the MATE panel(s) to
                       the dominant color of the desktop wallpaper (if enabled
                        in the applet settings)
            panel_orient: i.e. 'top', 'bottom', 'left', 'right' -
                          obtained from dconf
            panel_x : the panel's x position on screen
            panel_y : the panel's y position on screen
            panel_size : the size of the panel
            applet_pos : the position of the applet(in pixels) in the panel
            dm_timer : timer to monitor mouse movement during app icon drag and drop
            da_timer : timer to provide a short delay before activating an app when
                       data other than a .desktop file is dragged on the applet

    """

    def __init__(self, applet):
        """Init the Dock.

        Load settings
        Setup the applet right click menu and actions
        Set default values
        """

        super().__init__()

        self.applet = applet    # the panel applet, in case we need it later

        self.app_list = []
        self.box = None
        self.icontheme = Gtk.IconTheme.get_default()
        self.icontheme.connect("changed", self.icon_theme_changed)

        self.window = None

        self.wnck_screen = Wnck.Screen.get_default()

        self.app_with_mouse = None      # the dock app that mouse is currently over
        self.active_app = None          # the currently active app
        self.right_clicked_app = None   # the app that most recently had a right click

        self.settings_path = self.applet.get_preferences_path()
        self.settings = Gio.Settings.new_with_path("org.mate.panel.applet.dock",
                                                   self.settings_path)

        # instantiate these - will be set up later
        self.object_settings = None
        self.panel_settings = None
        self.panel_event_handler_id = None
        self.panel_id = ""

        # specify the xml file to be used as an alternative storage location
        # for the applet settings
        self.xml_conf = os.path.expanduser("~/.config/mate_dock_applet.conf")

        self.prefs_win = None
        self.about_win = None
        self.ccl_win = None
        self.indicator = 0
        self.fallback_bar_col = None
        self.multi_ind = False
        self.click_restore_last_active = True
        self.show_all_apps = True
        self.win_from_cur_ws_only = False
        self.use_win_list = True
        self.panel_act_list = False
        self.change_panel_color = False
        self.change_dock_color_only = False
        self.active_bg = 0
        self.read_settings()
        self.set_fallback_bar_colour()

        # read the list of apps which are difficult to match with their
        # .desktop files
        self.app_match = self.read_app_match()
        self.dock_action_group = None

        self.app_win_list = dock_win_list.DockWinList(self.wnck_screen)
        self.app_win_list.icontheme = self.icontheme
        self.app_act_list = dock_action_list.DockActionList(self.wnck_screen)
        self.app_act_list.icontheme = self.icontheme
        self.act_list_delay = 1000
        self.act_list_timer = None

        self.panel_x = 0
        self.panel_y = 0
        self.panel_size = 0
        self.panel_orient = "top"
        self.applet_pos = 0

        self.dm_timer = None
        self.da_timer = None

        # create an event handler so that we can react to changes e.g
        # the panel the applet is on, or it's position on the panel
        applet_spath = self.settings_path[0: len(self.settings_path)-6]  # remove "prefs/" suffix
        self.object_settings = Gio.Settings.new_with_path("org.mate.panel.object", applet_spath)
        self.object_settings.connect("changed",
                                     self.applet_panel_settings_changed)

        self.setup_menu()

        self.panel_cc = dock_color_changer.PanelColorChanger()

        # we need to monitor the Unity dbus interface
        DBusGMainLoop(set_as_default=True)
        self.session_bus = dbus.SessionBus()

        # claim the Unity bus (Unity won't be using it...) so that clients know
        # to start using it
        self.session_bus.request_name("com.canonical.Unity",
                                      dbus.bus.NAME_FLAG_ALLOW_REPLACEMENT)

        # add a handler to listen in Unity dbus messages
        self.session_bus.add_signal_receiver(self.unity_cb_handler,
                                             dbus_interface="com.canonical.Unity.LauncherEntry",
                                             signal_name="Update")

        # instantiate a timer to perform further setup once the applet has been
        # fully created
        GObject.timeout_add(1000, self.do_delayed_setup)

    def __del__(self):
        """ Clean up ...
        """

        # release the unity bus
        self.session_bus.release_name("com.canonical.Unity")

    def do_delayed_setup(self):
        """ Perform setup operations that we couldn't do until the dock was
            fully instantiated

            Get the id of the panel the applet is on
            Do the initial panel colour change (if necessary)
            Set the minimise locations of open windows

        """

        self.get_panel_id()

        # now that we have our panel id, we can set use it to set to panel
        # colour changer if necessary
        if self.change_dock_color_only:
            self.panel_cc.set_single_panel(self.panel_id)
        else:
            self.panel_cc.set_single_panel("")

        # enable panel colour changing?
        if self.change_panel_color:
            self.panel_cc.enable_color_change()
            # we're starting up so need to do an initial panel colour change
            self.panel_cc.do_change_panel_color()

        # get info about the panel the applet is on
        self.get_applet_panel_info()

        self.set_all_apps_minimise_targets()
        return False        # cancel the timer

    def get_panel_id(self):
        """ Get the toplevel id of the panel the applet is on
        """

        # get the info from dconf settings
        self.panel_id = self.object_settings.get_string("toplevel-id")

    def get_applet_panel_custom_rgb(self):
        """ If the panel containing the applet has a custom background
            colour, return the colour

            Requires get_applet_panel_info to have been called at least
            once beforehand

        Returns:
            A tuple of 3 x int : the r,g,b components or None if a custom
            colour is not being used
        """

        settings_path = "/org/mate/panel/toplevels/%s/background/" % self.panel_id

        # get this panel's background settings
        psettings = Gio.Settings.new_with_path("org.mate.panel.toplevel.background",
                                               settings_path)

        if psettings.get_string("type") == "none":
            # colours from the current theme are being used
            return None
        else:
            # get the panel's custom colour rgb components
            colstr = psettings.get_string("color")

            if (colstr.startswith("rgba")) or (colstr.startswith("rgb")):
                colstrip = colstr.strip("rgba()")
                cols = colstrip.split(",")
                pr = int(cols[0])
                pg = int(cols[1])
                pb = int(cols[2])
            else:
                pr = int(colstr[1:3], 16)
                pg = int(colstr[3:5], 16)
                pb = int(colstr[5:7], 16)

        return [pr, pg, pb]

    def get_applet_panel_info(self):
        """ Read info from dconf regarding the panel the applet is on, and also
            the applet's position on the panel

            Note: the toplevel id of the panel must already have been read
        """

        def get_top_panel_height(mypanel):
            """ Look for a top panel and if one is found, return its height

            Args:
                mypanel - the toplevel id of the panel the applet is on

            Returns:
                The height of a top panel, or 0 is there is no top panel
            """

            settings_path = "/org/mate/panel/toplevels/"
            # get this panel's settings
            plist = Gio.Settings.new("org.mate.panel")

            toplevels = plist.get_value("toplevel-id-list").unpack()
            for toplevel in toplevels:
                if mypanel != toplevel:
                    tl_path = "/org/mate/panel/toplevels/%s/" % toplevel

                    # get this panel's settings
                    tpsettings = Gio.Settings.new_with_path("org.mate.panel.toplevel",
                                                            tl_path)

                    # get the info
                    panel_orient = tpsettings.get_string("orientation")
                    if panel_orient == "top":
                        panel_size = tpsettings.get_int("size")
                        return panel_size
            return 0

        self.applet_pos = self.object_settings.get_int("position")

        # get the settings path for the current panel
        settings_path = "/org/mate/panel/toplevels/%s/" % self.panel_id

        # get this panel's settings

        if self.panel_event_handler_id is not None:
            self.panel_settings.disconnect(self.panel_event_handler_id)

        try:
            self.panel_settings = Gio.Settings.new_with_path("org.mate.panel.toplevel",
                                                             settings_path)
        except TypeError:
            # this can happen when the applet quits and it is therefore
            # safe to ignore
            return

        # get the info
        self.panel_orient = self.panel_settings.get_string("orientation")
        self.panel_x = self.panel_settings.get_int("x")
        self.panel_y = self.panel_settings.get_int("y")
        self.panel_size = self.panel_settings.get_int("size")

        # if this is left or right oriented panel we need to account for the
        # height of any top panel so that minimise locations are correctly
        # calculated...
        if (self.panel_orient == "left") or (self.panel_orient == "right"):
            self.applet_pos += get_top_panel_height(self.panel_id)

        # finally, connect an event handler so that if this panel's settings
        # are changed i.e. orientation, size, we can respond to this
        self.panel_event_handler_id = self.panel_settings.connect("changed",
                                                                  self.panel_settings_changed)

    def applet_panel_settings_changed(self, settings, key):
        """ Callback for when the the applet settings with regards to it's
            panel are changed

            If the panel the applet is on is changed, update our panel id and
            recalculate all docked apps minimize positions accordingly

            If the appllet's position on its panel is changed, the minimize
            positions of all docked apps also need to be minimized
        """

        if key == "toplevel-id":
            self.get_panel_id()
            if self.change_dock_color_only:
                self.panel_cc.set_single_panel(self.panel_id)
                self.get_applet_panel_info()
                self.set_all_apps_minimise_targets()

            pass

        if key == "position":
            # we can get here during applet creation when we still might not
            # know out panel id so...
            if self.panel_id != "":
                self.get_applet_panel_info()
                self.set_all_apps_minimise_targets()

            pass

    def panel_settings_changed(self, settings, key):
        """ Callback for the settings of the panel the applet is on changed

        If the size or orientation of the panel changes, we need to recalculate
        app minimise locations
        """

        if (key == "orientation") or (key == "size"):
            self.get_applet_panel_info()
            self.set_all_apps_minimise_targets()
            for app in self.app_list:
                app.queue_draw()

    def read_settings(self):
        """ Read the current dock settings from dconf

            If this particular dock has not been run before and a settings xml
            file exists, import the settings and apply them to the new dock

        """

        # is this dock being run for the first time?
        if self.settings.get_boolean("first-run") is True:
            # this dock is being run for the first time, so if we have any
            # saved settings from other docks, import them. Note: prior to
            # V0.70 the applet presented a dialog asking the user whether or
            # not to import the previous settings. This dialog has been removed
            # and the previous settings are now silently imported to prevent
            # problems when swtiching to the Mutiny layout in Ubuntu
            # Mate 16.04

            xml_settings = dock_xml.read_xml(self.xml_conf)

            if xml_settings[0] is True:
                # the settings were read correctly, so set everything up

                pinned_apps = []
                for pinned_app in xml_settings[1]:
                    pinned_apps.append(pinned_app)

                self.indicator = xml_settings[2]
                self.show_all_apps = xml_settings[3]
                self.multi_ind = xml_settings[4]
                self.use_win_list = xml_settings[5]
                self.win_from_cur_ws_only = xml_settings[6]
                self.change_panel_color = xml_settings[7]
                self.change_dock_color_only = xml_settings[8]
                self.panel_act_list = xml_settings[9]
                self.active_bg = xml_settings[10]

                self.fallback_bar_col = []
                for col in xml_settings[11]:
                    self.fallback_bar_col.append(col)

                # now, immediately write the settings to dconf and back to the
                # config file so the dock can access them

                self.settings.set_value("pinned-apps", GLib.Variant('as',
                                        pinned_apps))
                self.settings.set_int("indicator-type", self.indicator)
                self.settings.set_boolean("multi-ind", self.multi_ind)
                self.settings.set_boolean("apps-from-all-workspaces",
                                          self.show_all_apps)
                self.settings.set_boolean("first-run", False)
                self.settings.set_boolean("use-win-list",
                                          self.use_win_list)
                self.settings.set_boolean("win-from-cur-workspace-only",
                                          self.win_from_cur_ws_only)
                self.settings.set_boolean("change-panel-color",
                                          self.change_panel_color)
                self.settings.set_boolean("change-panel-color-dock-only",
                                          self.change_dock_color_only)
                self.settings.set_boolean("panel-act-list",
                                          self.panel_act_list)
                self.settings.set_int("bg-type", self.active_bg)
                self.settings.set_value("fallback-bar-col", GLib.Variant('as',
                                        self.fallback_bar_col))

                dock_xml.write_xml(self.xml_conf, pinned_apps, self.indicator,
                                   self.show_all_apps, self.multi_ind,
                                   self.use_win_list,
                                   self.win_from_cur_ws_only,
                                   self.change_panel_color, self.change_dock_color_only,
                                   self.panel_act_list,
                                   self.active_bg,
                                   self.fallback_bar_col)
                return

        # we get here if there was no previous configuration, or the
        # configuration couldn't be read.
        # Where the configuration cou;n't be read this could be due to an
        # error or because new versions of the applet have added configuration
        # options not yet in the user's xml file. To recover, simply assume a
        # default set of options
        self.indicator = self.settings.get_int("indicator-type")
        self.multi_ind = self.settings.get_boolean("multi-ind")
        self.show_all_apps = self.settings.get_boolean("apps-from-all-workspaces")
        self.use_win_list = self.settings.get_boolean("use-win-list")
        self.click_restore_last_active = self.settings.get_boolean("click-restore-last-active")
        self.change_panel_color = self.settings.get_boolean("change-panel-color")
        self.change_dock_color_only = self.settings.get_boolean("change-panel-color-dock-only")
        self.panel_act_list = self.settings.get_boolean("panel-act-list")
        self.active_bg = self.settings.get_int("bg-type")
        self.fallback_bar_col = self.settings.get_value("fallback-bar-col").unpack()

    def write_settings(self):
        """Write the current dock settings.

        Write a list of all of the currently pinned apps .desktop files
        Write the indicator type, whether to use multiple indicators,
        and whether to show unpinned apps from all workspaces

        Set the first-run indicator to False
        """

        pinned_apps = []
        for dock_app in self.app_list:

            if dock_app.desktop_file is not None and dock_app.is_pinned:
                pinned_apps.append(os.path.basename(dock_app.desktop_file))

        if self.settings:
            self.settings.set_value("pinned-apps", GLib.Variant('as', pinned_apps))
            self.settings.set_int("indicator-type", self.indicator)
            self.settings.set_boolean("multi-ind", self.multi_ind)
            self.settings.set_boolean("apps-from-all-workspaces",
                                      self.show_all_apps)
            self.settings.set_boolean("win-from-cur-workspace-only",
                                      self.win_from_cur_ws_only)
            self.settings.set_boolean("use-win-list",
                                      self.use_win_list)
            self.settings.set_boolean("change-panel-color",
                                      self.change_panel_color)
            self.settings.set_boolean("change-panel-color-dock-only",
                                      self.change_dock_color_only)
            self.settings.set_boolean("panel-act-list", self.panel_act_list)
            self.settings.set_int("bg-type", self.active_bg)
            self.settings.set_boolean("first-run", False)
            self.settings.set_value("fallback-bar-col", GLib.Variant('as', self.fallback_bar_col))

        dock_xml.write_xml(self.xml_conf, pinned_apps, self.indicator,
                           self.show_all_apps, self.multi_ind,
                           self.use_win_list,
                           self.win_from_cur_ws_only,
                           self.change_panel_color,
                           self.change_dock_color_only,
                           self.panel_act_list,
                           self.active_bg,
                           self.fallback_bar_col)

    def read_app_match(self):
        """ Read an xml file which contains a list of apps which are difficult
            to match with their respective .desktop file.

        Returns:
            A list of tuples which containing the following:
                the app name (as reported by wnck)
                the app's wm_class (as reported by wnck)
                the .desktop file to be used by apps whose name or wm_class
                match the above
        """

        d, f = os.path.split(os.path.abspath(__file__))
        results = dock_xml.read_app_xml("%s/app_match.xml" % d)
        if results[0]:
            # the file was read successfully
            return results[1]
        else:
            return []

    def set_fallback_bar_colour(self):
        """ Set the colour to be used for drawing bar indicators when we the highlight colour
            from the theme can't be determined
        """

        r = int(self.fallback_bar_col[0]) / 255
        g = int(self.fallback_bar_col[1]) / 255
        b = int(self.fallback_bar_col[2]) / 255

        docked_app_helpers.ThemeBarInd.fallback_barcol = [r, g, b]

    def set_actions_for_app(self, app):
        """Show or hide actions in the context menu so that only relevant ones
           are shown for the specified app

           If the app is pinned, do not show the pin action.

           If the app in not pinned, don't show the unpin action.

           Depending on applet orientation actions to move the app left/right
           or up/down along the dock need to shown or hidden.

           If the app is the first or last on the dock, then the options to
           move it up/left or down/right will also need to be hidden

           Include the app name in the menu text e.g. Pin Caja to the dock

           If the app has more than one window on screen, show actions allowing
           the user to select one

           If the app is running and has one or more windows on screen, show
           an option allowing them to be closed

           If the app does not have a desktop file, show an option allowing a
           custom launcher to be created

           Show any right click options specified in the app's desktop file

        Args:
            app : The DockedApp
        """

        df_shortcut_1_action = self.dock_action_group.get_action("df_shortcut_1_action")
        df_shortcut_2_action = self.dock_action_group.get_action("df_shortcut_2_action")
        df_shortcut_3_action = self.dock_action_group.get_action("df_shortcut_3_action")
        df_shortcut_4_action = self.dock_action_group.get_action("df_shortcut_4_action")

        act_exists, act_name = app.get_rc_action(1)
        df_shortcut_1_action.set_visible(act_exists)
        if act_exists is True:
            df_shortcut_1_action.set_label(act_name)
            df_shortcut_1_action.set_icon_name(app.icon_name)

        act_exists, act_name = app.get_rc_action(2)
        df_shortcut_2_action.set_visible(act_exists)
        if act_exists is True:
            df_shortcut_2_action.set_label(act_name)
            df_shortcut_2_action.set_icon_name(app.icon_name)

        act_exists, act_name = app.get_rc_action(3)
        df_shortcut_3_action.set_visible(act_exists)
        if act_exists is True:
            df_shortcut_3_action.set_label(act_name)
            df_shortcut_3_action.set_icon_name(app.icon_name)

        act_exists, act_name = app.get_rc_action(4)
        df_shortcut_4_action.set_visible(act_exists)
        if act_exists is True:
            df_shortcut_4_action.set_label(act_name)
            df_shortcut_4_action.set_icon_name(app.icon_name)

        pin_action = self.dock_action_group.get_action("pin_action")
        unpin_action = self.dock_action_group.get_action("unpin_action")
        move_up_action = self.dock_action_group.get_action("move_up_action")
        move_down_action = self.dock_action_group.get_action("move_down_action")
        move_left_action = self.dock_action_group.get_action("move_left_action")
        move_right_action = self.dock_action_group.get_action("move_right_action")

        close_win_action = self.dock_action_group.get_action("close_win_action")
        close_win_action.set_visible(False)

        index = self.get_app_position_in_dock(app)
        pin_action.set_visible(not app.is_pinned)
        unpin_action.set_visible(app.is_pinned)

        orientation = self.applet.get_orient()

        if orientation == MatePanelApplet.AppletOrient.LEFT or \
           orientation == MatePanelApplet.AppletOrient.RIGHT:
            move_up_action.set_visible(index > 0)
            move_down_action.set_visible(index < (len(self.box.get_children()))-1)
            move_left_action.set_visible(False)
            move_right_action.set_visible(False)
            move_up_action.set_label("Move %s up the dock" % app.app_name)
            move_down_action.set_label("Move %s down the dock" % app.app_name)
        else:
            move_up_action.set_visible(False)
            move_down_action.set_visible(False)
            move_left_action.set_visible(index > 0)
            move_right_action.set_visible(index < (len(self.box.get_children()))-1)
            move_left_action.set_label("Move %s to the left on the dock" % app.app_name)
            move_right_action.set_label("Move %s to the right on the dock" % app.app_name)

        if pin_action.is_visible():
            pin_action.set_label("Pin %s" % app.app_name)
        else:
            unpin_action.set_label("Unpin %s" % app.app_name)

        # set the actions for selecting specific windows

        num_win = app.get_num_windows()
        if num_win == 1:
            close_win_action.set_label("Close %s" % app.app_name)
        else:
            close_win_action.set_label("Close all windows")

        if num_win > 0:
            close_win_action.set_visible(True)

        ccl_action = self.dock_action_group.get_action("ccl_action")
        ccl_action.set_visible(not app.has_desktop_file())

    def setup_menu(self):
        """Set up the actions and right click menu for the applet
        """

        # actions named df_shortcut_<x>_action are used for implementing
        # shortcuts/actions specified in an app's .desktop file

        self.dock_action_group = Gtk.ActionGroup("DockActions")
        self.dock_action_group.add_actions([
                            ("df_shortcut_1_action", None,
                             "df_shortcut_1_action", None, "df_shortcut_1_action",
                             self.df_shortcut_1),
                            ("df_shortcut_2_action", None,
                             "df_shortcut_2_action", None, "df_shortcut_2_action",
                             self.df_shortcut_2),
                            ("df_shortcut_3_action", None,
                             "df_shortcut_3_action", None, "df_shortcut_3_action",
                             self.df_shortcut_3),
                            ("df_shortcut_4_action", None,
                             "df_shortcut_4_action", None, "df_shortcut_4_action",
                             self.df_shortcut_4),
                            ("pin_action", Gtk.STOCK_ADD,
                             "_Pin app to the dock", None, "Pin app to the dock",
                             self.pin_app),
                            ("unpin_action", Gtk.STOCK_REMOVE,
                              "_Unpin app from the dock", None, "Unpin app from the dock",
                              self.unpin_app),
                             ("move_up_action", Gtk.STOCK_GO_UP,
                              "Move app _up the dock", None, "Move app up the dock",
                              self.move_app_up),
                             ("move_down_action", Gtk.STOCK_GO_DOWN,
                              "Move app _down the dock", None, "Move app down the dock",
                              self.move_app_down),
                             ("move_left_action", Gtk.STOCK_GO_BACK,
                              "Move app _left in the dock", None, "Move app left in the dock",
                              self.move_app_up),
                             ("move_right_action", Gtk.STOCK_GO_FORWARD,
                              "Move app _right in the dock", None, "Move app right in the dock",
                              self.move_app_down),
                             ("prefs_action", Gtk.STOCK_PREFERENCES,
                              "Dock P_references", None, "Dock Preferences",
                              self.show_prefs_win),
                             ("ccl_action", Gtk.STOCK_EXECUTE,
                              "Create custo_m launcher for this app", None, "Create custom launcher for this app",
                              self.show_ccl_win),
                             ("about_action", Gtk.STOCK_ABOUT,
                              "About...", None, "About...", self.show_about_win),
                             ("close_win_action", Gtk.STOCK_CLOSE,
                              "_Close", None, "Close", self.close_win)
                             ])

        menu_xml = '<menuitem name="df_shortcut_1_action" action="df_shortcut_1_action"/><separator/>'
        menu_xml += '<menuitem name="df_shortcut_2_action" action="df_shortcut_2_action"/><separator/>'
        menu_xml += '<menuitem name="df_shortcut_3_action" action="df_shortcut_3_action"/><separator/>'
        menu_xml += '<menuitem name="df_shortcut_4_action" action="df_shortcut_4_action"/><separator/>'

        menu_xml += '<menuitem name="close_win" action="close_win_action"/>'

        # we only need menu items for moving app icons for Gtk2
        # (Gtk3 does it with drag and drop)
        if build_gtk2:
            menu_xml += '<separator/><menuitem name="move_up" action="move_up_action"/>'
            menu_xml += '<menuitem name="move_down" action="move_down_action"/>'
            menu_xml += '<menuitem name="move_left" action="move_left_action"/>'
            menu_xml += '<menuitem name="move_right" action="move_right_action"/>'

        menu_xml += '<separator/><menuitem name="Pin" action="pin_action"/>'
        menu_xml += '<menuitem name="Unpin" action="unpin_action"/><separator/>'

        menu_xml += '<menuitem name="Preferences" action="prefs_action"/>'
        menu_xml += '<menuitem name="Create custom launcher" action="ccl_action"/>'
        menu_xml += '<menuitem name="About" action="about_action"/><separator/>'

        self.applet.setup_menu(menu_xml, self.dock_action_group)

    def df_shortcut_1(self, data=None):
        """Perform the app's 1st .desktop file specified shortcut/action
        """

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        print("app name = %s" % the_app.app_name)
        if the_app is not None:
            the_app.run_rc_action(1)

    def df_shortcut_2(self, data=None):
        """Perform the app's 1st .desktop file specified shortcut/action
        """

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        if the_app is not None:
            the_app.run_rc_action(2)

    def df_shortcut_3(self, data=None):
        """Perform the app's 1st .desktop file specified shortcut/action
        """

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        if the_app is not None:
            the_app.run_rc_action(3)

    def df_shortcut_4(self, data=None):
        """Perform the app's 1st .desktop file specified shortcut/action

        The app is specfied from the winwdow list
        """

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        if the_app is not None:
            the_app.run_rc_action(4)

    def unpin_app(self, data=None):
        """Unpin an app from the dock

        This action is performed from the action list or the pane;
        right click menu

        Unpin the app and update the dock settings.

        If the app is not running, remove it from the dock also
        """

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        if the_app is not None:
            the_app.is_pinned = False
            self.write_settings()
            if not the_app.is_running():
                self.remove_app_from_dock(the_app)
                self.right_clicked_app = None

    def pin_app(self, data=None):
        """Pin an app to the dock.

        Pin the app and update the dock settings"""

        # get the app in question
        if self.panel_act_list:
            the_app = self.right_clicked_app
        else:
            if self.app_act_list.get_visible():
                the_app = self.app_act_list.the_app
            else:
                the_app = self.right_clicked_app

        if the_app is not None:
            the_app.is_pinned = True
            self.write_settings()

    def get_app_position_in_dock(self, app):
        """ Get the position of a specified app in the dock.

        Args : app - A DockedApp

        Returns : the index of the app, or -1 if it wasn't found
        """

        index = 0  # used for gtk2 only
        for app_da in self.box.get_children():
            if app_da == app.drawing_area:
                if build_gtk2:
                    return index
                else:
                    if self.box.orientation == Gtk.Orientation.HORIZONTAL:
                        boxi = self.box.child_get_property(app_da,
                                                           "left-attach")
                    else:
                        boxi = self.box.child_get_property(app_da,
                                                           "top_attach")

                    return boxi

            index += 1

        return -1

    def move_app_up(self, data=None):
        """ Move the right clicked app up one position on the dock (or left if the
            panel is on the top or bottom of the screen).

        Moves the app and then recaculates the minimize location for it's
        windows.

        Writes the dock settings once all is done.
        """

        if self.right_clicked_app is not None:

            index = self.get_app_position_in_dock(self.right_clicked_app)
            if index > 0:

                app = self.app_list[index-1]
                # we need to move the app both in self.applist and self.box
                if build_gtk2:
                    self.box.reorder_child(self.right_clicked_app.drawing_area, index-1)
                else:
                    if self.box.orientation == Gtk.Orientation.HORIZONTAL:
                        prop = "left-attach"
                    else:
                        prop = "top-attach"

                    self.box.child_set_property(self.right_clicked_app.drawing_area, prop, index-1)
                    self.box.child_set_property(app.drawing_area, prop, index)

                self.app_list[index-1] = self.app_list[index]
                self.app_list[index] = app

                # recalculate the minimize targets for each app
                self.set_minimise_target(self.app_list[index-1])
                self.set_minimise_target(self.app_list[index])

                self.write_settings()

    def move_app(self, the_app, new_pos):
        """ Move a docked app to a new position in the dock, adjusting the
            the positions of other apps as necessary

        This is used during drag and drop operatations and is therefore gtk3
        only

        Args:
            the_app : the docked_app we're moving
            new_pos : int, the new position in the docked
        """

        old_pos = self.app_list.index(the_app)

        if self.box.orientation == Gtk.Orientation.HORIZONTAL:
            prop = "left-attach"
        else:
            prop = "top-attach"

        if new_pos > old_pos:
            step = 1
        else:
            step = -1

        i = old_pos + step

        # first, adjust the contents of the box
        while i != new_pos + step:
            app_to_move = self.app_list[i]
            self.box.child_set_property(app_to_move.drawing_area,
                                        prop, i - step)
            i += step

        # move the desired app
        self.box.child_set_property(the_app.drawing_area, prop, new_pos)

        # now move things around in the app list to match
        self.app_list.remove(the_app)
        self.app_list.insert(new_pos, the_app)

        # we need to redraw the icons and recalculate the minimise positions
        # of all apps
        self.set_all_apps_minimise_targets()
        for app in self.app_list:
            app.queue_draw()

        # save the new settings
        self.write_settings()

    def get_app_root_coords(self, app):
        """ Calculate and return the root x and y co-ordinates of the top left
            pixel of a docked app

        Args:
            app: the docked app

        Returns:
            two integers, the x and y coordinates
        """

        dock_x, dock_y = self.get_dock_root_coords()

        if (self.panel_orient == "top") or (self.panel_orient == "bottom"):
            min_x = dock_x + \
                    (self.get_app_position_in_dock(app) * app.drawing_area_size)
            if build_gtk2:
                min_x += self.get_app_position_in_dock(app)

            min_y = dock_y
        else:
            min_x = dock_x
            min_y = dock_y + \
                    (self.get_app_position_in_dock(app) * app.drawing_area_size)
            if build_gtk2:
                min_y += self.get_app_position_in_dock(app)

        return min_x, min_y

    def get_dock_root_coords(self):
        """ Get the root coords of the top left pixel of the dock

        Returns:
            two integers, the x and y coordinates
        """

        # get root coord from the applet window rather from panel settings...
        win = self.applet.props.window
        # check validity of win - can be None during applet creation...
        if win is None:
            return 0, 0

        if build_gtk2:
            # win.get_origin doesn't work on gtk2, so...
            dock_x, dock_y = win.get_root_coords(0, 0)
        else:
            thing, dock_x, dock_y = win.get_origin()

        return dock_x, dock_y

    def set_minimise_target(self, app):
        """ Calculate and set the minimise locations for an app's windows

        Args:
            app: the docked_app
        """

        min_x, min_y = self.get_app_root_coords(app)

        # its more visually appealing if we minimize to the centre of the app's
        #  icon, so reduce the size of the minimize areas and adjust the
        # coordinates ...
        adj = app.drawing_area_size/4
        min_x += adj
        min_y += adj
        app_w = app_h = app.drawing_area_size - (adj*2)

        app.set_all_windows_icon_geometry(min_x, min_y, app_w, app_h)

    def set_all_apps_minimise_targets(self):
        """ Calculate and set the window minimise locations for all app's
        """

        for app in self.app_list:
            self.set_minimise_target(app)

    def move_app_down(self, data=None):
        """ Move the right clicked app down one position on the dock (or right
            if the panel is on the top or bottom of the screen).

        Moves the app and then recaculates the minimize location for it's
        windows.

        Writes the dock settings once all is done.
        """

        if self.right_clicked_app is not None:

            index = self.get_app_position_in_dock(self.right_clicked_app)
            if index < len(self.box.get_children())-1:

                app = self.app_list[index+1]

                # we need to move the app both in self.applist and self.box
                if build_gtk2:
                    self.box.reorder_child(self.right_clicked_app.drawing_area,
                                           index+1)
                else:
                    if self.box.orientation == Gtk.Orientation.HORIZONTAL:
                        prop = "left-attach"
                    else:
                        prop = "top-attach"

                    self.box.child_set_property(self.right_clicked_app.drawing_area,
                                                prop, index+1)
                    self.box.child_set_property(app.drawing_area, prop, index)

                self.app_list[index+1] = self.app_list[index]
                self.app_list[index] = app

                # recalculate the minimize targets for each app
                self.set_minimise_target(self.app_list[index+1])
                self.set_minimise_target(self.app_list[index])

                self.write_settings()

    def show_prefs_win(self, data=None):
        """ Show the preferences window.

        If, necessary create the window and register a callback for the 'ok'
        button press

        If the window has already been shown, just show it again.
        """

        if self.prefs_win is None:
            self.prefs_win = dock_prefs.DockPrefsWindow(self.prefs_win_ok_cb,
                                                        self.app_list[0])
            self.prefs_win.set_indicator(self.indicator)
            self.prefs_win.set_multi_ind(self.multi_ind)
            self.prefs_win.set_show_unpinned_apps_on_all_ws(self.show_all_apps)
            self.prefs_win.set_use_win_list(self.use_win_list)
            self.prefs_win.set_change_panel_color(self.change_panel_color)
            self.prefs_win.set_change_dock_color_only(self.change_dock_color_only)
            self.prefs_win.set_pan_act(self.panel_act_list)
            self.prefs_win.set_bg(self.active_bg)
            self.prefs_win.set_fallback_bar_col(self.fallback_bar_col)
        else:
            self.prefs_win.show_all()

    def show_about_win(self, data=None):
        """ Show the About window.

        If, necessary create the window and show it.

        If the window has already been shown, just show it again.
        """
        if self.about_win is None:
            self.about_win = dock_about.AboutWindow()

        self.about_win.show_all()

    def prefs_win_ok_cb(self, widget, event):
        """ Callback for the 'ok' button on the preferences window.

        If the preferences have been changed then:
            write the new settings
            redraw each running app in app_list with the new indicator type

        Args:
            widget - the button the caused the event
            event - the event args
        """
        if (self.indicator != self.prefs_win.get_indicator_type()) or \
           (self.multi_ind != self.prefs_win.get_multi_ind()) or \
           (self.show_all_apps != self.prefs_win.get_show_unpinned_apps_on_all_ws()) or \
           (self.win_from_cur_ws_only != self.prefs_win.get_win_cur_ws_only()) or \
           (self.use_win_list != self.prefs_win.get_use_win_list()) or \
           (self.change_panel_color != self.prefs_win.get_change_panel_color()) or \
           (self.change_dock_color_only != self.prefs_win.get_change_dock_color_only()) or \
           (self.panel_act_list != self.prefs_win.get_pan_act()) or \
           (self.active_bg != self.prefs_win.get_bg()) or \
           (self.fallback_bar_col != self.prefs_win.get_fallback_bar_col()):

            self.indicator = self.prefs_win.get_indicator_type()
            self.multi_ind = self.prefs_win.get_multi_ind()
            self.show_all_apps = self.prefs_win.get_show_unpinned_apps_on_all_ws()
            self.win_from_cur_ws_only = self.prefs_win.get_win_cur_ws_only()
            self.use_win_list = self.prefs_win.get_use_win_list()

            new_panel_color_setting = self.change_panel_color != self.prefs_win.get_change_panel_color()
            self.change_panel_color = self.prefs_win.get_change_panel_color()

            self.change_dock_color_only = self.prefs_win.get_change_dock_color_only()
            if self.change_dock_color_only:
                self.panel_cc.set_single_panel(self.panel_id)
            else:
                self.panel_cc.set_single_panel("")

            if self.panel_act_list != self.prefs_win.get_pan_act():
                self.panel_act_list = self.prefs_win.get_pan_act()

            self.active_bg = self.prefs_win.get_bg()

            self.fallback_bar_col = self.prefs_win.get_fallback_bar_col()
            self.set_fallback_bar_colour()
            self.write_settings()

            # redraw everything here
            for app in self.app_list:
                app.set_indicator(self.indicator)
                app.set_multi_ind(self.multi_ind)
                app.set_active_bg(self.active_bg)
                if app.is_running():
                    app.queue_draw()
            self.show_or_hide_app_icons()
            self.show_or_hide_indicators()

            if new_panel_color_setting:
                # panel colour changing setting has been changed so we need to
                # enable or disable colour changing
                if self.change_panel_color:
                    self.panel_cc.enable_color_change()
                    self.panel_cc.do_change_panel_color()
                else:
                    self.panel_cc.disable_color_change()

        self.prefs_win.hide()

    def show_ccl_win(self, data=None):
        """ Show the create custom launcher window.

        If, necessary create the window and register a callback for the 'ok'
        button press

        If the window has already been shown, clear all of the fields
        before showing it
        """

        if self.ccl_win is None:
            self.ccl_win = dock_custom_launcher.DockCLWindow(self.ccl_win_ok_cb)
        else:
            self.ccl_win.set_default_values()

        self.ccl_win.name = self.right_clicked_app.app_name.strip()
        self.ccl_win.wm_class = self.right_clicked_app.wm_class_name
        self.ccl_win.show_all()

    def ccl_win_ok_cb(self, widget, event):
        """ Callback for the 'ok' button on the create custom launcher window.

        Check to ensure that all required fields (icon, launcher name and
        command) have been entered and display an error dialog if not.

        If all required fields have been entered, use the info from the window
        to create a .desktop file in ~/.local/share/applications

        The .desktop file will be named mda_<launcher name>.desktop - the
        initial 'mda_' will allow the applet to search for and priorities self
        created .desktop files over system created ones...

        Args:
            widget - the button the caused the event
            event - the event args
        """

        valid_launcher = False
        if self.ccl_win.name == "":
            error_text = "The name of the launcher has not been set"
        elif self.ccl_win.command == "":
            error_text = "The command of the launcher has not been set"
        elif self.ccl_win.icon_filename == "":
            error_text = "The icon of the launcher has not been set"
        else:
            valid_launcher = True

        if valid_launcher is False:
            md = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL,
                                   Gtk.MessageType.ERROR,
                                   Gtk.ButtonsType.OK,
                                   None)
            md.set_markup('<span size="x-large"><b>Cannot create launcher</b></span>')
            md.format_secondary_text(error_text)
            md.run()
            md.destroy()
            return

        else:
            self.ccl_win.hide()

            # the gnome developer docs at
            # https://developer.gnome.org/integration-guide/stable/desktop-files.html.en
            # state that .desktop filenames should not contain spaces, so....
            dfname = self.ccl_win.name.replace(" ", "-")

            local_apps = os.path.expanduser("~/.local/share/appplications")
            if not os.path.exists(local_apps):
                # ~/.local/share/applications doesn't exist, so create it
                os.mkdir(local_apps)
            dfname = os.path.expanduser("%s/mda-%s.desktop" % (local_apps, dfname))

            dfile = open(dfname, "w")
            dfile.write("[Desktop Entry]\n")
            dfile.write("Name=%s\n" % self.ccl_win.name)
            dfile.write("Type=Application\n")
            dfile.write("Comment=%s\n" % self.ccl_win.comment)
            dfile.write("Exec=%s\n" % self.ccl_win.command)
            dfile.write("Icon=%s\n" % self.ccl_win.icon_filename)
            dfile.write("StartupWMClass=%s\n" % self.ccl_win.wm_class)

            # Code below can be uncommented if adding terminal apps to the dock
            # everbecomes a needed thing
            # term_app = "%s" %self.ccl_win.is_terminal_app
            # dfile.write("Terminal=%s\n" %term_app.lower())

            # we don't want this launcher displayed in the MATe menu
            dfile.write("NoDisplay=true\n")

            dfile.close()

            # create a docked app from the .desktop we just created and add it
            # to the dock
            dock_app = docked_app.DockedApp()
            dock_app.desktop_file = dfname
            dock_app.read_info_from_desktop_file()
            dock_app.is_pinned = True

            if build_gtk2:
                dock_app.applet_win = self.applet.window
            else:
                dock_app.applet_win = self.applet.window.get_window()

            dock_app.applet = self.applet

            dock_app.applet_orient = self.applet.get_orient()
            dock_app.set_indicator(self.indicator)
            dock_app.set_multi_ind(self.multi_ind)
            dock_app.set_active_bg(self.active_bg)

            size = self.applet.get_size()
            self.set_app_icon(dock_app, size)
            self.app_list.append(dock_app)
            self.add_app(dock_app)
            self.show_or_hide_app_icons()
            self.show_or_hide_indicators()
            self.write_settings()

    def add_app_to_dock(self, desktop_file):
        """ Adds the app specified by a desktop file to the dock and pins it

        If the app is already present in the dock, no action is taken

        :param desktop_file: the .desktop_file of the app

        """

        for app in self.app_list:
            if app.desktop_file == desktop_file:
                return

        dock_app = docked_app.DockedApp()
        dock_app.desktop_file = desktop_file
        dock_app.read_info_from_desktop_file()
        dock_app.is_pinned = True

        if build_gtk2:
            dock_app.applet_win = self.applet.window
        else:
            dock_app.applet_win = self.applet.get_window()

        dock_app.applet = self.applet

        dock_app.applet_orient = self.applet.get_orient()
        dock_app.set_indicator(self.indicator)
        dock_app.set_multi_ind(self.multi_ind)
        dock_app.set_active_bg(self.active_bg)

        size = self.applet.get_size()
        self.set_app_icon(dock_app, size)
        self.app_list.append(dock_app)
        self.add_app(dock_app)
        self.show_or_hide_app_icons()
        self.show_or_hide_indicators()
        self.write_settings()

    def show_win(self, win_no):
        """
        Bring the specified window number of the right clicked app to the front

        Args:
            win_no - the window number, starting at 1

        """

        win_list = self.right_clicked_app.get_wnck_windows()
        wnck_win = win_list[win_no-1]
        wnck_win.activate(0)

    def close_win(self, data=None):
        """Close all windows for the right clicked app"""

        win_list = self.right_clicked_app.get_wnck_windows()
        for wnck_win in win_list:
            # have to send 0 as the event time because we have no event time to send
            wnck_win.close(0)

    def icon_theme_changed(self, icontheme):
        """ Callback for when the Gtk icon theme changes

        Load the new icon set
        Iterate through each app in self.app_list and get it to reload it's
        icon
    """

        self.icontheme.rescan_if_needed()

        size = self.applet.get_size()
        for app in self.app_list:
            self.set_app_icon(app, size)

    def copy_wnck_info_to_app(self, app_from_dock, app_from_wnck, wnck_window):
        """Copy info from wnck into a docked applet

        If the wnck app pid is already present in the docked app, just
        copy the windows xids that are not in the docked app

        If the wnck app pid is not present, copy all of the the app_info
        """

        if app_from_dock.wnck_class is None:
            app_from_dock.wnck_class = app_from_wnck.wnck_class
        app_from_dock.set_app_name(app_from_wnck.app_name)
        if app_from_dock.wm_class_name == "":
            app_from_dock.wm_class_name = app_from_wnck.wm_class_name

        if app_from_dock.wm_class_group_name == "":
            app_from_dock.wm_class_group_name = app_from_wnck.wm_class_group_name

        for wai in app_from_wnck.app_info:
            # look for a process in the docked app with an identical pid

            existing_pid = False
            for dai in app_from_dock.app_info:

                # different wnck_apps for a single app can share the same pid
                # so just check the wnck_apps for equivalency
                if dai.app == wai.app:
                    existing_pid = True

                if existing_pid is True:
                    # we need to merge info from the new details into the
                    # dock_app

                    for xid in wai.windows:
                        try:
                            unused_var = dai.windows.index(xid)
                            # if we get here, the window was found, so there's
                            # no need to do anything else

                        except ValueError:
                            # if we get here, the window wasn't found so it
                            # needs to be added
                            dai.windows.append(xid)

                    break

            if existing_pid is False:
                # add the info for the new process
                app_from_dock.app_info.append(wai)

    def find_desktop_file(self, df_name):
        """ Find the full filename of a specified .desktop file

        Search the following directories (and their subdirectories) for
        the specified filename
            /usr/share/applications
            /usr/local/share/applications
            ~/.local/share/applications

        Args :
            df_name : the name of the .desktop file e.g. pluma.desktop. The
                      .desktop extension must be included

        Returns:
            The full filename (path + filename) of the desktop file if it
            exists or "" otherwise
        """

        srch_dirs = ["/usr/share/applications/",
                     "/usr/local/share/applications/",
                     os.path.expanduser("~/.local/share/applications/")]

        for srch_dir in srch_dirs:
            for the_dir, dir_list, file_list in os.walk(srch_dir):
                try:
                    unused_var = file_list.index(df_name)

                    # if we get here the file is found
                    the_name = os.path.join(the_dir, df_name)
                    return the_name

                except ValueError:
                    pass

        return ""

    def setup_app_list(self):
        """Setup the list of docked apps.

        First, read the list of pinned apps from the settings and add them to
        the app list

        Then iterate through the running apps, and then either:
            if this is a non-pinned app add it to app list
            if this is a pinned app, integrate the running app info with the
            pinned app details already set up

        Also, set up event handlers allowing us keep track of window open and
        close events and active window change events
        """

        self.app_list = []

        pinned_apps = self.settings.get_value("pinned-apps").unpack()

        # the settings contain a list of .desktop files, so we need to find and
        # read each ile
        for pinned_app in pinned_apps:
            dock_app = docked_app.DockedApp()

            full_name = self.find_desktop_file(pinned_app)
            if full_name != "":
                dock_app.desktop_file = full_name
                if dock_app.read_info_from_desktop_file():
                    self.app_list.append(dock_app)

            dock_app.is_pinned = True

        self.wnck_screen.force_update()  # recommended per Wnck documentation

        for wnck_window in self.wnck_screen.get_windows():

            win_type = wnck_window.get_window_type()

            if ((win_type == Wnck.WindowType.NORMAL) or
                (win_type == Wnck.WindowType.DIALOG)) and \
                    wnck_window.is_skip_tasklist() is False:

                wnck_class = wnck_window.get_class_group()

                wnck_app = wnck_window.get_application()

                # setup a new docked app object with info from wnck
                dock_app = docked_app.DockedApp()

                if build_gtk2:
                    dock_app.applet_win = self.applet.window
                else:
                    dock_app.applet_win = self.applet.get_window()

                dock_app.applet = self.applet

                dock_app.setup_from_wnck(wnck_app, wnck_class, self.app_match)

                # if this is a pinned app, merge the running app details with
                # the item already set up in app_list
                for app in self.app_list:
                    if app.desktop_file == dock_app.desktop_file:
                        # copy the running app's info to the pinned dock item
                        self.copy_wnck_info_to_app(app, dock_app, wnck_window)

                        break
                else:
                    # it's not a pinned app so add it to the app list
                    self.app_list.append(dock_app)

        self.wnck_screen.connect("active-window-changed",
                                 self.active_win_changed)
        self.wnck_screen.connect("active-workspace-changed",
                                 self.active_workspace_changed)
        self.wnck_screen.connect("window-opened", self.window_opened)
        self.wnck_screen.connect("window_closed", self.window_closed)

    def active_workspace_changed(self, wnck_screen, previously_active_space):
        """ Event handler for the active workspace change even

        Show or hide pinned and unpinned dock apps as appropriate

        Arguments :
            wnck_screen : the screen that emitted the event. Will be the same
                          as self.wnck_screen
            previously_active_space : the workspace that was previously active

        """

        self.show_or_hide_app_icons()
        self.show_or_hide_indicators()

    def active_win_changed(self, wnck_screen, prev_active_window):
        """Event handler for the window change event

        Remove the highlighted background from any prevously active app and
        redraw its icon

        Set the new app as active and redraw it with a highlighted background

        Args:
            wnck_screen :  the screen on which the event occurred - will be the
                           same as self.wnck_screen
            prev_active_window : the wnck_window that was previously active

        """

        for app in self.app_list:
            if app.is_active is True:
                app.is_active = False
                app.queue_draw()

        new_win = self.wnck_screen.get_active_window()

        if new_win is not None:
            # the desktop itself can be the new active window. we need to
            # ignore it if so....
            # (fix for https://bugs.launchpad.net/ubuntu/+source/mate-dock-applet/+bug/1550392
            if new_win.get_name().lower() != "x-caja-desktop":
                new_cg = new_win.get_class_group()
                new_wm_class_name = new_cg.get_res_class()
                new_app = new_win.get_application()
                for app in self.app_list:
                    if app.wm_class_name == new_wm_class_name:
                        for aai in app.app_info:
                            if new_app == aai.app:
                                app.is_active = True
                                app.last_active_win = new_win
                                app.queue_draw()
                                break

    def window_opened(self, wnck_screen, wnck_window):
        """Event handler for the window_opened event

        If an aready running app has opened a new window, the window
        is added to to the running apps info

        If a newly lauched app is opening a new window, setup the app on the
        dock

        Args:
            wnck_screen : the screen on which the event occurred
            wnck_window : the newly opened window

        """

        # we're only interested when normal and dialog windows
        if (wnck_window.get_window_type() != Wnck.WindowType.NORMAL) and \
           (wnck_window.get_window_type() != Wnck.WindowType.DIALOG):
            return
        wnck_app = wnck_window.get_application()

        if not wnck_window.is_skip_tasklist():

            # setup a new DockedApp
            wnck_class = wnck_window.get_class_group()
            dock_app = docked_app.DockedApp()
            if build_gtk2:
                dock_app.applet_win = self.applet.window
            else:
                dock_app.applet_win = self.applet.get_window()
            dock_app.applet = self.applet

            dock_app.applet_orient = self.applet.get_orient()
            dock_app.set_indicator(self.indicator)
            dock_app.set_multi_ind(self.multi_ind)
            dock_app.set_active_bg(self.active_bg)
            got_desktop = dock_app.setup_from_wnck(wnck_app, wnck_class, self.app_match)

            # look for the app relating to the window in the dock
            in_dock = False
            for app in self.app_list:
                if got_desktop is True:
                    if app.desktop_file == dock_app.desktop_file:
                        in_dock = True
                # if we don't have the .desktop file then things get a little
                #  more complicated
                elif app.has_wnck_app(wnck_app) or \
                    ((app.wnck_class == dock_app.wnck_class or
                      app.wm_class_name.upper() == dock_app.wm_class_name.upper() and
                       # test below is fix for https://bugs.launchpad.net/ubuntu-mate/+bug/1555324
                      app.wm_class_name.strip() != "")):
                        in_dock = True

                if in_dock is True:
                    # found it, so copy the running app's info to the pinned
                    # dock item
                    self.copy_wnck_info_to_app(app, dock_app, wnck_window)

                    if app.startup_id is not None:
                        # if we have a startup id set this means the dock started the app. Since the app has
                        # now opened a new window we can now assume the app has started and end the notification
                        # process
                        app.cancel_startup_notification()

                    # set the minimize target for the new window
                    self.set_minimise_target(app)

                    # if the mouse pointer is over the app's dock icon,
                    # regenerate the list of right click menu items to take
                    # account of the new window
                    if app.has_mouse:
                        self.set_actions_for_app(app)

                    # start the dock icon pulsing to let the user know
                    # something has happened
                    if not app.is_pulsing:
                        app.start_pulsing()
                    break

            if in_dock is False:
                # append the app to the dock

                size = self.applet.get_size()
                self.set_app_icon(dock_app, size)
                self.app_list.append(dock_app)
                self.add_app(dock_app)
                self.show_or_hide_app_icons()
                self.show_or_hide_indicators()

    def window_closed(self, wnck_screen, wnck_window):
        """Event handler for the window_closed event

        Find the app relating to the closed window.

        If it has no more open windows mark it as not running and
        redraw its icon without the running indicator.

        If the are open windows remaining, remove the closed window from
        the apps list of windows

        Args:
            wnck_screen : the screen on which the event occurred
            wnck_window : the closed window

        """

        # we're only interested in the closure of normal and dialog windows
        if (wnck_window.get_window_type() != Wnck.WindowType.NORMAL) and \
           (wnck_window.get_window_type() != Wnck.WindowType.DIALOG):
            return

        wnck_app = wnck_window.get_application()
        if wnck_app is None:

            # this always seems to be the case - so we need to find the app
            # another way - look through each of the running apps for the one
            # which has the closed window in it's window list
            # this is done by using xids - using wnck_windows in the app_info
            # tuple didn't seem to work very well

            xid = wnck_window.get_xid()
            for app in self.app_list:
                for aai in app.app_info:
                    try:
                        # a valueerror exception occurs if the window is not
                        # found
                        unused_var = aai.windows.index(xid)

                        # we've found the app so remove the window from it's
                        # list
                        aai.windows.remove(xid)

                        do_redraw = False

                        # if the app has no more open windows, mark it as
                        # closed
                        if len(aai.windows) == 0:
                            # there no more open windows for this process. Does
                            # the app have any other running processes ?
                            if len(app.app_info) == 1:
                                # No, it doesn't ////

                                app.app_info = []
                                if not app.is_pinned:
                                    # remove the app from the dock
                                    self.remove_app_from_dock(app)
                                else:
                                    app.is_active = False
                                    do_redraw = True
                            else:
                                # remove this processes info from docked app
                                app.app_info.remove(aai)
                                do_redraw = True
                        else:
                            do_redraw = True

                        # was the closed window was the last active window for
                        # the app ?
                        if app.last_active_win == wnck_window:
                            app.last_active_win = None

                        if do_redraw:
                            app.queue_draw()

                        if app.has_mouse:
                            self.set_actions_for_app(app)
                        break

                    except ValueError:
                        pass

    def show_or_hide_app_icons(self):
        """ If we're only showing unpinned apps from the current workspace then
            then show/hide as appropriate.

            Finally, recalculate all app minimization targets
        """

        if self.show_all_apps:
            for app in self.app_list:
                if not app.is_visible():
                    app.show_icon()
        else:
            cur_ws = self.wnck_screen.get_active_workspace()
            for app in self.app_list:
                if not app.is_pinned:
                    if app.has_windows_on_workspace(cur_ws):
                        app.show_icon()
                    else:
                        app.hide_icon()

        # recalculate all apps icon geometry
        self.set_all_apps_minimise_targets()

    def show_or_hide_indicators(self):
        """ Show or hide app indicators as appropriate

        If we're only showing indicators for apps which have windows on the
        current workspace then set the apps hide_indicators setting as
        appropriate. Otherwise, set the hide_indicators setting to False.
        """

        cur_ws = self.wnck_screen.get_active_workspace()
        for app in self.app_list:
            if self.win_from_cur_ws_only:
                app.ind_ws = cur_ws
            else:
                app.ind_ws = None

    def remove_app_from_dock(self, app):
        """Remove an app from the dock.

        Remove the app from the app_list

        Remove the app's drawing area from self.box

        Args:
            app : the app to be removed

        """

        self.app_list.remove(app)
        num_apps = len(self.box.get_children())

        if build_gtk2:
            self.box.remove(app.drawing_area)
        else:
            # the row/column which contains the app needs to be
            # removed, so get the app's position in the grid
            pos = 0
            while pos < num_apps:
                if self.box.orientation == Gtk.Orientation.VERTICAL:
                    left = 0
                    top = pos
                else:
                    left = pos
                    top = 0

                if self.box.get_child_at(left, top) == app.drawing_area:
                    # we've found the app
                    if self.box.orientation == Gtk.Orientation.VERTICAL:
                        self.box.remove_row(pos)
                    else:
                        self.box.remove_column(pos)

                    app = None
                    return

                pos += 1

        app = None

    def set_app_icon(self, dock_app, size):
        """ Sets up an app's icon, scaling it to a specified size

        Select an appropriate icon size based on the specified size

        Load the app's icon, using a fallback STOCK_EXEC as a fallback

        Scale the icon to the specified size

        Args:
            dock_app : the DockedApp
            size : the required icon size in pixels
        """

        if size >= 56:
            icon_size = 64
            stock_size = Gtk.IconSize.DIALOG
        elif size >= 40:
            icon_size = 48
            stock_size = Gtk.IconSize.DIALOG
        elif size >= 28:
            icon_size = 32
            stock_size = Gtk.IconSize.DND
        elif size >= 20:
            icon_size = 24
            stock_size = Gtk.IconSize.LARGE_TOOLBAR
        else:
            icon_size = 16
            stock_size = Gtk.IconSize.BUTTON

        if self.icontheme.has_icon(dock_app.icon_name):

            icon_info = self.icontheme.choose_icon([dock_app.icon_name, None],
                                                   icon_size, 0)
            dock_app.icon_filename = icon_info.get_filename()

            try:
                pixbuf = icon_info.load_icon()
            except GLib.GError:
                # default to a stock icon if we couldn't load the app icon
                pixbuf = self.applet.render_icon(Gtk.STOCK_EXECUTE, stock_size,
                                                 None)
                dock_app.icon_filename = "STOCK_EXECUTE"

        else:
            # the default theme has no icon for the app, so there are a few
            # things we can do...
            #
            # 1 .. quick and dirty - check to see if the icon points to an actual file
            #                        or ...
            #                        look in /usr/share/icons/hicolor/<icon_size>x<iconsize>/
            #                        apps/ and
            #                        ~/.local/share/icons/icons/hicolor</icon_size>x<icon_size>/apps/
            #                        for the icon name or ...
            #                        look in /usr/share/pixmaps for an icon of any type with
            #                        the same name as the app
            #                        then ...
            #                        look in ~/.local/share/icons for an icon with the same name
            #                        and extension as the icon
            #
            # 2 .. sloooow         - iterate through each installed icon theme and try to
            #                        find the app - not implement for now

            # the png method. look for lower and uppercased variations of the filename
            # and note that all files in /usr/share/pixmaps are .png

            icon_file = ""
            if os.path.isfile(dock_app.icon_name):
                pixbuf = GdkPixbuf.Pixbuf.new_from_file(dock_app.icon_name)
            else:
                icon_name = dock_app.icon_name

                # look in the 'hicolor' icon directories for the icon file
                icon_path = "/usr/share/icons/hicolor/%dx%d/apps/" % (icon_size, icon_size)
                if os.path.isfile("%s%s" % (icon_path, icon_name)):
                    icon_file = "%s%s" % (icon_path, icon_name)
                else:
                    icon_path = os.path.expanduser("~/.local/share/icons/hicolor/%dx%d/apps/"
                                                   % (icon_size, icon_size))
                    if os.path.isfile("%s%s" % (icon_path, icon_name)):
                        icon_file = "%s%s" % (icon_path, icon_name)

                # if we still haven't found the icon, look in
                # /usr/share/pixmaps for a .png file
                if icon_file == "":
                    icon_name = os.path.splitext(dock_app.icon_name)[0]  # remove any extension
                    if os.path.isfile("/usr/share/pixmaps/%s.png" % icon_name):
                        icon_file = "/usr/share/pixmaps/%s.png" % icon_name
                    elif os.path.isfile("/usr/share/pixmaps/%s.png" % icon_name.upper()):
                        icon_file = "/usr/share/pixmaps/%s.png" % icon_name.upper()
                    elif os.path.isfile("/usr/share/pixmaps/%s.png" % icon_name.lower()):
                        icon_file = "/usr/share/pixmaps/%s.png" % icon_name.lower()

                # final attempt - look in ~/.local/share/icons for the icon
                if icon_file == "":
                    if os.path.isfile(os.path.expanduser("~/.local/share/icons/%s" % dock_app.icon_name)):
                        icon_file = os.path.expanduser("~/.local/share/icons/%s" % dock_app.icon_name)

                # if we've found an icon, load it
                if icon_file != "":
                        pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_file)
                else:
                    # if not, use a stock icon to represent the app
                    pixbuf = self.applet.render_icon(Gtk.STOCK_EXECUTE,
                                                     stock_size, None)
                    dock_app.icon_filename = "STOCK_EXECUTE"

        # scale the icon to the size that is available
        # Note - we're allowing for a 3 pixel border all around the icon,
        # hence using size-6
        pixbuf = pixbuf.scale_simple(size - 6, size - 6,
                                     GdkPixbuf.InterpType.BILINEAR)

        dock_app.set_drawing_area_size(size)
        dock_app.set_pixbuf(pixbuf)

    def add_app(self, dock_app):
        """
        Add an app's drawing area to the VBox/Hbox/Grid container

        Args:
            dock_app : the DockedApp
        """

        if build_gtk2:
            self.box.add(dock_app.drawing_area)
        else:
            pos = len(self.box.get_children())

            # use .attach instead of .add with Gtk.Grid - .add doesn't seem
            # to work properly with vertical orientation...
            if self.box.orientation == Gtk.Orientation.VERTICAL:
                self.box.attach(dock_app.drawing_area, 0, pos, 1, 1)
            else:
                self.box.attach(dock_app.drawing_area, pos, 0, 1, 1)

    def get_app_by_pos(self, position):
        """
            Get the app at a specified position in the dock

        :param self:
        :param position: int - the position of the app in self.app_list

        :return: a docked_app, or none if position exceeds the number of
                 apps in the dock

        """

        if position < len(self.app_list):
            return self.app_list[position]
        else:
            return None

    def create_box(self, orientation):
        """Create a vertical or horizontal (depending on the applet orientation)
           box to contain the docked apps areas.

        Args:
            orientation : the applet orientation
        """

        if orientation == MatePanelApplet.AppletOrient.LEFT or \
           orientation == MatePanelApplet.AppletOrient.RIGHT:
            if build_gtk2:
                self.box = Gtk.VBox()
            else:
                self.box = Gtk.Grid()
                self.box.orientation = Gtk.Orientation.VERTICAL
                self.box.row_spacing = 1

        else:
            if build_gtk2:
                self.box = Gtk.HBox()
            else:
                self.box = Gtk.Grid()
                self.box.orientation = Gtk.Orientation.HORIZONTAL

                self.box.column_spacing = 1

        if build_gtk2:
            self.box.set_spacing(1)

    def setup_dock(self):
        """Setup the dock."

        Add all pinned apps to the dock

        Add all non-pinned running apps to the dock

        Setup all apps according to the applet size and orientation

        """

        # make sure the applet is correctly oriented
        orientation = self.applet.get_orient()
        self.create_box(orientation)

        # setup up pinned and non-pinned running apps
        self.setup_app_list()
        applet_size = self.applet.get_size()

        # add the apps to the dock
        for dock_app in self.app_list:
            dock_app.applet_orient = orientation

            dock_app.applet = self.applet

            if build_gtk2:
                dock_app.applet_win = self.applet.window
            else:
                dock_app.applet_win = self.applet.get_window()

            self.set_app_icon(dock_app, applet_size)
            dock_app.set_indicator(self.indicator)
            dock_app.set_multi_ind(self.multi_ind)
            dock_app.set_active_bg(self.active_bg)
            self.add_app(dock_app)

        self.show_or_hide_app_icons()
        self.show_or_hide_indicators()

    def set_new_orientation(self, new_orient):
        """Change the dock applet to a new applet orientation

        For Gtk2:
            Remove all app's drawing areas from the V/HBox

            Remove the V/HBox and create a new one according to the new
            orientation

            Add all of the app's drawing areas to the new V/HBox

        For Gtk3:
            Change the orientation of the Gtk.Grid

            Swap the 'left-attach' and 'top-attach' properties of the grid's
            childeren

        Args:
            new_orient : the new applet orientation
        """

        # we need to re-read/refresh the panel information ...
        self.get_panel_id()
        self.get_applet_panel_info()

        if build_gtk2:
            for dock_app in self.app_list:
                self.box.remove(dock_app.drawing_area)

            self.applet.remove(self.box)
            self.box = None

            self.create_box(new_orient)

            for dock_app in self.app_list:
                self.box.add(dock_app.drawing_area)
                dock_app.applet_orient = new_orient

            self.applet.add(self.box)
        else:
            old_orient = self.box.orientation

            if (new_orient == MatePanelApplet.AppletOrient.RIGHT) or \
               (new_orient == MatePanelApplet.AppletOrient.LEFT):
                self.box.orientation = Gtk.Orientation.VERTICAL
            else:
                self.box.orientation = Gtk.Orientation.HORIZONTAL

            for child in self.box.get_children():
                ol = self.box.child_get_property(child, "left-attach")

                # if we've switched orientations then realign the grid contents
                if old_orient != self.box.orientation:
                    ot = self.box.child_get_property(child, "top-attach")
                    self.box.child_set_property(child, "left-attach", ot)
                    self.box.child_set_property(child, "top-attach", ol)

            for dock_app in self.app_list:
                dock_app.applet_orient = new_orient

    def get_app_at_mouse(self, mouse_x, mouse_y):
        """
        Find the app underneath the mouse cursor.

        Args:
            mouse_x : the x coord of the mouse
            mouse_y : the y coord of the mouse

        Returns:
            The app under the mouse, or None if one could not be found
        """

        for app in self.app_list:
            if app.is_visible():
                alloc = app.drawing_area.get_allocation()
                if (mouse_x >= alloc.x) and (mouse_x <= alloc.x + alloc.width):
                    if (mouse_y >= alloc.y) and \
                       (mouse_y <= alloc.y + alloc.height):
                        return app

        return None

    def reset_act_list_timer(self):
        """ Reset win_list timer

        If the timer is already instantiated, delete it.

        Start a new timer with the appropriate delay

        """

        if self.act_list_timer is not None:
            GObject.source_remove(self.act_list_timer)

        if not self.panel_act_list:
            self.act_list_timer = GObject.timeout_add(self.act_list_delay,
                                                      self.show_act_list)

    def stop_act_list_timer(self):
        """ Stop the win list timer
        """

        if self.act_list_timer is not None:
            GObject.source_remove(self.act_list_timer)
            self.act_list_timer = None

    def show_act_list(self):
        """ Show the the list of open windows and actions for the currently
            highlighted app

        If the window list is currently being shown then don't do anything,
        otherwise...

        Get the currently highlighted app. If the highlighted app is being
        launched or a window list is already being displayed for it, or a user
        interaction has already dismissed the window list, then do nothing

        Otherwise, fill the window list, set the window position and set the
        screen areas where the mouse must remain or the window list will hide
        """

        if self.app_win_list.get_visible():
            return

        highlighted_app = self.app_with_mouse

        self.right_clicked_app = highlighted_app
        # above is needed so that actions ivoked from the window list work correctly

        if highlighted_app is None:
            return

        # is the app being launched?
        if highlighted_app.is_pulsing:
            self.act_list_timer = None
            return False

        # always recreate the window list e.g. to account for windows being
        # opened/closed, the app being pinned/unpinned etc.

        if self.app_act_list is not None:
            self.app_act_list.destroy()

        self.set_actions_for_app(self.app_with_mouse)

        # first of all, add any shortcut actions specified in the app's
        # .desktop file

        df_shortcut_1_action = self.dock_action_group.get_action("df_shortcut_1_action")
        df_shortcut_2_action = self.dock_action_group.get_action("df_shortcut_2_action")
        df_shortcut_3_action = self.dock_action_group.get_action("df_shortcut_3_action")
        df_shortcut_4_action = self.dock_action_group.get_action("df_shortcut_4_action")
        pin_action = self.dock_action_group.get_action("pin_action")
        unpin_action = self.dock_action_group.get_action("unpin_action")

        self.app_act_list = dock_action_list.DockActionList(self.wnck_screen)
        self.app_act_list.icontheme = self.icontheme

        # get the panel custom background colour (if any) and then set the
        # window list colours
        self.app_act_list.set_colours(self.get_applet_panel_custom_rgb())

        self.app_act_list.the_app = highlighted_app

        if df_shortcut_1_action.is_visible():
            self.app_act_list.add_to_list(df_shortcut_1_action.get_label(),
                                          df_shortcut_1_action,
                                          True)
            if df_shortcut_2_action.is_visible():
                self.app_act_list.add_to_list(df_shortcut_2_action.get_label(),
                                              df_shortcut_2_action,
                                              True)
            if df_shortcut_3_action.is_visible():
                self.app_act_list.add_to_list(df_shortcut_3_action.get_label(),
                                              df_shortcut_3_action,
                                              True)
            if df_shortcut_4_action.is_visible():
                self.app_act_list.add_to_list(df_shortcut_4_action.get_label(),
                                              df_shortcut_4_action,
                                              True)
            self.app_act_list.add_separator()

        if pin_action.is_visible():
            self.app_act_list.add_to_list(pin_action.get_label(),
                                          pin_action, False)
        else:
            self.app_act_list.add_to_list(unpin_action.get_label(),
                                          unpin_action, False)

        self.app_act_list.clear_mouse_areas()

        applet_x, applet_y = self.get_dock_root_coords()
        applet_w = applet_h = highlighted_app.drawing_area_size

        self.app_act_list.set_applet_details(applet_x, applet_y,
                                             applet_w, applet_h)
        self.app_act_list.set_panel_orient(self.applet.get_orient())
        app_x, app_y = self.get_app_root_coords(highlighted_app)
        self.app_act_list.set_app_root_coords(app_x, app_y)

        self.app_act_list.show_all()

        self.act_list_timer = None
        return False

    def do_window_selection(self, app):
        """ Allow the user to change an app's currently active window

            If the dock's window list is being used, show or hide it as
            appropriate

            If the thumbnail preview option has been chosen, invoke Compiz via
            dbus

        """

        if self.use_win_list:
            self.show_or_hide_win_list()
        else:

            # get root window id
            if build_gtk2:
                # get_xid is non introspectable on gtk2, so use xwininfo
                # instead...
                rw_inf = subprocess.check_output(["xwininfo", "-root"])
                rw_inf = rw_inf.split()
                rwin = int(rw_inf[3], 16)
            else:
                rwin = Gdk.Screen.get_default().get_root_window().get_xid()

            try:
                compiz_service = self.session_bus.get_object('org.freedesktop.compiz',
                                '/org/freedesktop/compiz/scale/screen0/initiate_key')
                activate = compiz_service.get_dbus_method('activate',
                                                          'org.freedesktop.compiz')
                activate("root", rwin, "match",
                         "class=%s" % app.wm_class_name)
            except dbus.exceptions.DBusException:
                # e.g. Compiz is not installed, or dbus or scale plugin not
                # enabled...
                #
                # fallback to built in window list
                self.show_or_hide_win_list()

    def show_or_hide_win_list(self):
        """
            If the window list is visible, hide it, otherwise show it
        """

        visible = (self.app_win_list is not None) and self.app_win_list.get_visible()
        if visible:
            self.hide_win_list()
        else:
            self.show_win_list()

    def show_win_list(self):
        """ Show the the list of open windows and for the currently highlighted app

        Get the currently highlighted app. If the highlighted app is being
        launched or a window list is already being displayed for it, or a user
        interaction has already dismissed the window list, then do nothing

        Otherwise, fill the window list, set the window position and set the
        screen areas where the mouse must remain or the window list will hide
        """

        highlighted_app = self.app_with_mouse

        self.right_clicked_app = highlighted_app
        # abive is needed so that actions invoked from the window list work correctly

        if highlighted_app is None:
            return

        # is the app being launched?
        if highlighted_app.is_pulsing:
            self.act_list_timer = None
            return False

        # always recreate the window list e.g. to account for windows being
        # opened/closed

        if self.app_win_list is not None:
            self.app_win_list.destroy()

        self.set_actions_for_app(self.app_with_mouse)

        self.app_win_list = dock_win_list.DockWinList(self.wnck_screen)
        self.app_win_list.icontheme = self.icontheme

        # get the panel custom background colour (if any) and then set the
        # window list colours
        self.app_win_list.set_colours(self.get_applet_panel_custom_rgb())

        self.app_win_list.the_app = highlighted_app

        # add any open windows
        if highlighted_app.is_running():
            self.app_win_list.setup_list(self.win_from_cur_ws_only)

        self.app_win_list.clear_mouse_areas()

        applet_x, applet_y = self.get_dock_root_coords()
        applet_w = applet_h = highlighted_app.drawing_area_size

        self.app_win_list.set_applet_details(applet_x, applet_y,
                                             applet_w, applet_h)
        self.app_win_list.set_panel_orient(self.applet.get_orient())
        app_x, app_y = self.get_app_root_coords(highlighted_app)
        self.app_win_list.set_app_root_coords(app_x, app_y)

        self.app_win_list.show_all()

        self.act_list_timer = None
        return False

    def hide_win_list(self):
        """ Hide the window list """

        if self.app_win_list is not None:
            self.app_win_list.hide()

    def hide_act_list(self):
        """ Hide the action list """

        if self.app_act_list is not None:
            self.app_act_list.hide()

    def minimize_or_restore_windows(self, app, event):
        """ Minimize or restore an app's windows

        the action to perform (minimizing, moving workspace, activating)
        is decided as follows:
           if (the app's windows are all minimized) or
           (the app has one or more unminimized window but is not the active app)
           then
               restore the app's last active window or all windows (based on
               the user's settings). If the active window is on a  different
               workspace then activate that workspace
           else:
               the app is currently the active app so all of the app
               windows will be minimized

        but first, hide any app window list that is being shown and stop the
        window list timer

        Note: As of MATE 1.12 wnck_window.activate does not seem to work
              if we specify our own event time. However, if an event time of
              0 (i.e. now) is specfied all works as expected. However, when the
              applet is run from the command line, lots of these messages
              'Wnck-WARNING **: Received a timestamp of 0; window activation
               may not function properly' appear, so another solution may need
              to be found in the future

        Args:
            app: the docked app whose windows are to be minimized or restored
            event : the mouse click event

        """

        self.stop_act_list_timer()
        self.hide_win_list()

        win_list = app.get_wnck_windows()

        restore_win = (not app.has_unminimized_windows()) or \
                      (app.has_unminimized_windows() and
                      (app.is_active is False))
        if restore_win:
            last_active_win = app.last_active_win

            # the last active window may be set to None (e.g. if the app's
            # active window has been closed and no other window has been made
            # active afterwards). Therefore, if there is no active last window
            # activate the app's first normal window
            # (fix for https://bugs.launchpad.net/ubuntu/+source/mate-dock-applet/+bug/1550392)
            if last_active_win is None:
                for window in win_list:
                    win_type = window.get_window_type()
                    if ((win_type == Wnck.WindowType.NORMAL) or
                            (win_type == Wnck.WindowType.DIALOG)) and \
                            (not window.is_skip_tasklist()):
                        last_active_win = window
                        break

            # if we're restoring all windows, do this now before we finally
            # activate the last active window
            if self.click_restore_last_active is False:
                for window in win_list:
                    win_type = window.get_window_type()
                    if ((win_type == Wnck.WindowType.NORMAL) or
                            (win_type == Wnck.WindowType.DIALOG)) and \
                            (not window.is_skip_tasklist()) and \
                            (window != last_active_win):
                        window.activate(0)
                        sleep(0.01)     # allow the window manager time to  activate the window

                app.last_active_win = last_active_win

            wnck_screen = last_active_win.get_screen()
            wnck_aws = self.wnck_screen.get_active_workspace()
            wnck_ws = last_active_win.get_workspace()

            # the window's active workspace can be None if it is visible on
            # all workspaces or if it is not on any workspace (I'm looking at
            # you caja-desktop!!!!!)
            # (fix for https://bugs.launchpad.net/ubuntu/+source/mate-dock-applet/+bug/1550392 and
            # https://bugs.launchpad.net/ubuntu-mate/+bug/1555336 (regarding software updater))
            if wnck_aws is not None and wnck_ws is not None and \
               (wnck_aws != wnck_ws):
                wnck_ws.activate(0)
                sleep(0.01)

            # rarely, the last active win does not end up as the active window
            # if we activate here, so instead a workaround which seems to do
            # the trick is use a timer as below
            if event is None:
                # use the current time if we have no event
                event_time = Gtk.get_current_event_time()
            else:
                event_time = event.time

            GObject.timeout_add(20, win_activation_timer,
                                [last_active_win, event_time])

        else:
            # minimize all windows and do the last active window last of all

            last_active_win = app.last_active_win

            for window in win_list:
                win_type = window.get_window_type()
                if ((win_type == Wnck.WindowType.NORMAL) or
                    (win_type == Wnck.WindowType.DIALOG)) and \
                    (not window.is_skip_tasklist()) and \
                    (window != last_active_win):
                    window.minimize()
                    sleep(0.01)

            app.last_active_win = last_active_win
            if last_active_win is not None:
                last_active_win.minimize()
                sleep(0.01)

    def activate_window(self, win):
        """ Activate a wnck window, switching workspace as necessary

            Args:
                win : then wnck.window
        """
        if win is not None:
            # if the window to be activated is not on the current workspace,
            # switchto that workspace
            wnck_aws = self.wnck_screen.get_active_workspace()
            wnck_ws = win.get_workspace()

            # the windows's current workspace can be None if it is pinned to all
            # workspaces or it is not on any at all...
            # (fix for https://bugs.launchpad.net/ubuntu/+source/mate-dock-applet/+bug/1550392 and
            # https://bugs.launchpad.net/ubuntu-mate/+bug/1555336 (regarding software updater))
            if (wnck_aws is not None) and (wnck_ws is not None) and \
               (wnck_aws != wnck_ws):
                    wnck_ws.activate(0)
                    sleep(0.01)
            win.activate(0)

    def activate_first_window(self, app):
        """ Active the specified apps's first window, changing workspace as
            necessary
        """

        if app is None:
            return

        win = app.get_first_window()
        if win is None:
            return
        self.activate_window(win)

    def do_window_scroll(self, scroll_dir, event_time, the_app=None):
        """ Scroll to the next/previous window of the currently active app

        This function is called in response to the mouse scroll event on the
        panel applet and also when an applet keyboard shortcut (e.g. <Super>1)
        is used

        Depending on the scroll direction, make the next or previous window of
        the current app active. Scrolling will wrap around in both directions

        If the app only has one window or we don't know which window was last
        active (e.g. because the applet has only just started) then make the
        first window active

        If the new window is not on the current workspace, change to the
        relevant workspace

        Also, hide the app window list and stop any timer that might be running

        Args:
            scroll_dir : A GDK.ScrollDirection which indicates whether to go
                         forwards or backwards through the window list
            event_time : the time scroll event occurred
            the_app    : will indicate the app whose windows are to be scrolled
                         when a keyboard shortcut has been used.
        """

        # we're only interested in scroll up and down events....
        if (scroll_dir != Gdk.ScrollDirection.UP) and \
           (scroll_dir != Gdk.ScrollDirection.DOWN):
            return

        if the_app is None:
            # we've been called in response to a mouse scroll event, so we need to get
            # the app under the mouse
            if self.app_with_mouse is not None:
                app = self.app_with_mouse
            else:
                return
        else:
            app = the_app

        # if the app isn't running, there's nothing to do...
        if app.is_running() is False:
            return

        windows = app.get_wnck_windows()
        if (app.last_active_win is None) or (len(windows) == 1):
            new_index = 0
        else:
            # work out which window we want to activate
            try:
                index = windows.index(app.last_active_win)
            except ValueError:
                index = 0  # in case of error activate the first window

            if scroll_dir == Gdk.ScrollDirection.UP:
                if index == 0:
                    new_index = len(windows)-1
                else:
                    new_index = index - 1
            else:
                if index == len(windows)-1:
                    new_index = 0
                else:
                    new_index = index + 1

        # hide the window list and stop any timer
        self.hide_win_list()
        self.stop_act_list_timer()

        # if the new window is on a different workspace, we need to switch
        # workspace
        wnck_aws = self.wnck_screen.get_active_workspace()
        wnck_ws = windows[new_index].get_workspace()

        if wnck_aws is not None and (wnck_aws != wnck_ws):
            wnck_ws.activate(0)
            sleep(0.01)

        # activate the new window
        windows[new_index].activate(0)

    def get_dragee(self):
        """"
            Return the app which is currently marked as being dragged to a new
            position in the dock.

        Returns:
            a docked_app, or None is no apps are being dragged

        """

        for app in self.app_list:
            if app.is_dragee is True:
                return app

        return None

    def start_drag_motion_timer(self, dragee):
        """ Create a timer to allow us to monitor the mouse position during
            the drag/drop and rearrange dock icons on the fly

        Args: dragee - the docked_app which is being dragged

        """

        self.dm_timer = DragMotionTimer(dragee, self)

    def stop_drag_motion_timer(self):
        """  Stop the drag motion timer
        """

        self.dm_timer.drag_ended = True

    def start_da_timer(self, app):
        """
            Use a timer to before activating an app

        Args:
            app : the app to activate
        """

        # first of all, stop any other timer da_timer that may be running
        if (self.da_timer is not None) and (self.da_timer.timer_id != 0):
            GObject.source_remove(self.da_timer.timer_id)

        # create a new timer
        self.da_timer = DragActivateTimer(self, app)

    def get_app_under_mouse(self):
        """ Get the docked which is currently under the mouse cursor

        Returns : a docked_app, or None if the cursor is not over a docked_app
        """

        # get the mouse device so we can obtain the root coordinates of the
        # the cursor
        display = Gdk.Display.get_default()
        manager = display.get_device_manager()
        mouse = manager.get_client_pointer()
        none, x, y = mouse.get_position()
        dx, dy = self.get_dock_root_coords()

        # convert to applet coords
        x = x - dx
        y = y - dy

        return self.get_app_at_mouse(x, y)

    def unity_cb_handler(self, app_uri, args):
        """ Handler for Unity API dbus messages

        If the specified app is in the dock, forward the set the progress
        and/or count, and redraw the app's icon

        Args:
            app_uri : the basename of the .desktop file of the app
            args : the contents of the dbus message

        """

        # remove the leading part of the app uri
        df = app_uri.split("://")[1]

        # search for the an app which has the same desktop file name
        for app in self.app_list:
            app_df_path, app_df = os.path.split(app.desktop_file)
            if app_df == df:
                # we've found the app - update it...

                if "count-visible" in args:
                    app.set_counter_visible(args["count-visible"])
                if "count" in args:
                    app.set_counter_value(args["count"])

                if "progress-visible" in args:
                    app.set_progress_visible(args["progress-visible"])
                if "progress" in args:
                    app.set_progress_value(args["progress"])
                break


def win_activation_timer(args):
    """ Timer function to be called by GObject.timeout_add and which
        will activate a specified wnck window

    Args:
        args - a tuple containing these items
               args[0] - the wnck window to activate
               args[1] - the event time at which the timer was activated

    Returns:
        False - to cancel the timer
    """

    # as of MATE 1.12 it seems we need to use an event time of 0 (i.e. now)
    # to the window to activate properly
    args[0].activate(0)
    sleep(0.01)
    return False
