#!/usr/bin/python3
#
# Polychromatic 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 3 of the License, or
# (at your option) any later version.
#
# Polychromatic 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 Polychromatic. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2015-2016 Terry Cain <terry@terrys-home.co.uk>
#               2015-2024 Luke Horwell <code@horwell.me>
#
"""
Control devices from the desktop environment's indicator applet or system tray.

Supports AppIndicator, Ayatana Indicators or GTK Status Icon.
"""

import argparse
import atexit
import os
import signal
import sys

import gi
import setproctitle

gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk

import polychromatic.bulkapply as bulkapply
import polychromatic.effects as effects
import polychromatic.preferences as pref
import polychromatic.procpid as procpid
from polychromatic.backends._backend import Backend
from polychromatic.base import PolychromaticBase as PolychromaticBase

VERSION = "0.9.5"
SUPPORTED = ["GtkStatusIcon"]

try:
    gi.require_version('AyatanaAppIndicator3', '0.1')
    from gi.repository import AyatanaAppIndicator3
    SUPPORTED.append("AyatanaAppIndicator3")
except (ImportError, ValueError):
    pass

try:
    gi.require_version("AppIndicator3", "0.1")
    from gi.repository import AppIndicator3
    SUPPORTED.append("AppIndicator3")
except (ImportError, ValueError):
    pass


class Indicator(PolychromaticBase):
    # TODO: Refactor into classes

    def __init__(self):
        """
        List of technologies that can power the indicator/applet/tray icon.

        For naming, this will be internally referred to as "indicator"
        """
        self.indicator = None
        self.root_menu = None
        self.mode = self.preferences["tray"]["mode"]

        # Try to find a suitable renderer.
        if self.mode == pref.TRAY_AUTO:
            if "AyatanaAppIndicator3" in SUPPORTED:
                self.mode = pref.TRAY_AYATANA
            elif "AppIndicator3" in SUPPORTED:
                self.mode = pref.TRAY_APPINDICATOR
            else:
                self.mode = pref.TRAY_GTK_STATUS

        self.names = {
            pref.TRAY_APPINDICATOR: "AppIndicator3",
            pref.TRAY_GTK_STATUS: "GTKStatusIcon",
            pref.TRAY_AYATANA: "AyatanaAppIndicator3",
        }

        # User can override the default/preferred tray technology.
        if args.force_appindicator:
            self.mode = pref.TRAY_APPINDICATOR
        elif args.force_ayatana:
            self.mode = pref.TRAY_AYATANA
        elif args.force_gtk_status:
            self.mode = pref.TRAY_GTK_STATUS

        # Checks the specified mode is available.
        # Fallback order: Ayatana -> AppIndicator3 -> GTK Status
        if self.mode == pref.TRAY_APPINDICATOR and not "AppIndicator3" in SUPPORTED:
            self.dbg.stdout("AppIndicator3 not available, falling back to AyatanaAppIndicator3...", self.dbg.warning)
            self.mode = pref.TRAY_AYATANA

        if self.mode == pref.TRAY_AYATANA and not "AyatanaAppIndicator3" in SUPPORTED:
            self.dbg.stdout("AyatanaAppIndicator3 not available, falling back to AppIndicator3...", self.dbg.warning)
            self.mode = pref.TRAY_APPINDICATOR

        if self.mode == pref.TRAY_APPINDICATOR and not "AppIndicator3" in SUPPORTED:
            self.dbg.stdout("AppIndicator3 not available, falling back to GTK Status Icon...", self.dbg.warning)
            self.mode = pref.TRAY_GTK_STATUS

    def setup(self, icon_path):
        """
        Creates the object, depending on backend in use.

        Params:
            icon_path       (str)       Absolute path to the icon
        """
        self.dbg.stdout("Initialising " + self.names[self.mode] + "...", self.dbg.action, 1)

        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA]:
            self.indicator = appindicator.Indicator.new("polychromatic-tray-applet", icon_path, appindicator.IndicatorCategory.APPLICATION_STATUS) # pylint: disable=used-before-assignment
            self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)

        elif self.mode == pref.TRAY_GTK_STATUS:
            self.indicator = Gtk.StatusIcon()
            self.indicator.set_name("polychromatic-tray-applet")
            self.indicator.set_from_file(icon_path)
            self.indicator.set_visible(True)

        self.dbg.stdout("Initialised " + self.names[self.mode], self.dbg.success, 1)

    def create_menu(self):
        """
        Returns the root menu object.
        """
        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA, pref.TRAY_GTK_STATUS]:
            self.root_menu = Gtk.Menu()
            return self.root_menu

    def create_menu_item(self, menu, label, enabled, function=None, function_params=None, icon_path=None):
        """
        Returns a menu item object for appending into a menu.

        Params:
            menu                (obj)   create_menu() or create_submenu() object
            label               (str)   Text to display to the user.
            enabled             (bool)  Whether the selection should be highlighted or not.
            function            (obj)   Callback when button is clicked.
            function_params     (list)  Functions to pass the callback function.
            icon_path           (str)   Path to image file.
        """
        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA, pref.TRAY_GTK_STATUS]:
            if icon_path and os.path.exists(icon_path):
                item = Gtk.ImageMenuItem(label=label)
                item.set_sensitive(enabled)
                item.show()

                img = Gtk.Image()
                img.set_from_file(icon_path)
                item.set_image(img)
            else:
                item = Gtk.MenuItem(label=label)
                item.set_sensitive(enabled)
                item.show()

            if function and not function_params:
                item.connect("activate", function)
            elif function and function_params:
                item.connect("activate", function, function_params)

            menu.append(item)
            return item

    def add_menu_item(self, menu, menu_item):
        """
        Add a menu item to the specified menu.
        """
        menu.append(menu_item)

    def create_submenu(self, label, enabled, icon_path=None):
        """
        Create a submenu object, one to add to the parent menu, and the other
        containing the new menu object itself.

        Params:
            label               (str)   Text to display to the user.
            enabled             (bool)  Whether the selection should be highlighted or not.

        Returns list of objects:
            menu                Menu object (to contain new child menu items)
            item                Menu item object (for appending to parent menu)
        """
        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA, pref.TRAY_GTK_STATUS]:
            if icon_path and os.path.exists(icon_path):
                item = Gtk.ImageMenuItem(label=label)
                item.set_sensitive(enabled)
                item.show()

                img = Gtk.Image()
                img.set_from_file(icon_path)
                item.set_image(img)            # GTK3: Deprecated, no replacement!
            else:
                item = Gtk.MenuItem(label=label)
                item.set_sensitive(enabled)
                item.show()

            menu = Gtk.Menu()
            menu.show()
            item.set_submenu(menu)

            return[menu, item]

    def create_seperator(self, menu):
        """
        Returns a seperator object.

        Params:
            menu                (obj)   create_menu() or create_submenu() object.
        """
        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA, pref.TRAY_GTK_STATUS]:
            sep = Gtk.SeparatorMenuItem()
            sep.show()
            self.add_menu_item(menu, sep)

    def open_dialog(self, title, message):
        """
        Connect an event to an item to show a error dialog box.
        """
        dialog = Gtk.MessageDialog()
        dialog.add_button("_OK", Gtk.ResponseType.OK)
        dialog.message_type = Gtk.MessageType.ERROR
        dialog.set_title(title)
        dialog.set_markup(message)
        dialog.default_width = 350
        dialog.show()
        dialog.run()
        dialog.destroy()

    def finalize(self):
        """
        Connects the event for when the menu is opened/clicked.
        """
        if self.mode in [pref.TRAY_APPINDICATOR, pref.TRAY_AYATANA]:
            self.indicator.set_title("Polychromatic")
            self.indicator.set_menu(self.root_menu)

        elif pref.TRAY_GTK_STATUS:
            def _show_menu_cb(widget, button, time, data=None):
                self.root_menu.show_all()
                self.root_menu.popup(None, None, Gtk.StatusIcon.position_menu, self.indicator, button, time)

            def _click_menu_cb(widget):
                cb.launch_controller(None)
            self.menu = self.root_menu
            self.indicator.connect("popup-menu", _show_menu_cb)
            self.indicator.connect("activate", _click_menu_cb)


class PolychromaticTrayApplet(PolychromaticBase):
    """
    Provides the logic for providing quick access to device options from the
    system tray or notification area.
    """
    def start(self):
        """
        Begin execution of the tray applet.
        """
        # Show error if no backends are available
        if len(self.middleman.backends) == 0:
            self._setup_failed(self._("No backends available"), self.common.get_icon("general", "unknown"))
            return False

        # Build the applet
        self.build_indicator()
        self.dbg.stdout("Finished setting up applet.", self.dbg.success, 1)

    def _get_icon(self, img_dir, icon):
        """
        Returns the path for a Polychromatic icon.

        Params:
            img_dir         (str)   Folder inside img, e.g. "effects"
            icon            (str)   Filename excluding the extension.
        """
        return self.common.get_icon(img_dir, icon)

    def _get_tray_icon(self):
        """
        Returns path for tray icon.
        """
        return self.common.get_tray_icon(dbg, self.preferences["tray"]["icon"])

    def _create_menu_item_if_controller_present(self, menu, label, open_to, icon_path=None):
        """
        Creates a menu item, but conditionally checks if the Controller is
        installed. This is for buttons that would open the Controller to edit
        the feature via a GUI.
        """
        is_installed = procpid.ProcessManager().is_component_installed("controller")
        indicator.create_menu_item(menu, label, is_installed, cb.launch_controller, open_to, icon_path)

    def _setup_failed(self, message, icon_path):
        """
        A simple menu is displayed when something goes wrong.
        """
        self.dbg.stdout("Assembling error applet...", self.dbg.action, 1)

        error_icon = self.common.get_tray_icon(dbg, "img/tray/error.svg")
        indicator.setup(error_icon)

        menu = indicator.create_menu()
        indicator.create_menu_item(menu, message, False, None, None, icon_path)
        indicator.create_seperator(menu)
        indicator.create_menu_item(menu, self._("Refresh"), True, cb.retry_applet, None, self._get_icon("general", "refresh"))
        self._create_menu_item_if_controller_present(menu, self._("Troubleshoot"), "troubleshoot", self._get_icon("general", "preferences"))
        indicator.create_seperator(menu)
        self._create_menu_item_if_controller_present(menu, self._("Open Controller"), None, self._get_icon("general", "controller"))
        indicator.create_menu_item(menu, self._("Quit"), True, cb.quit)

        indicator.finalize()

        self.dbg.stdout(message, self.dbg.error)

    def build_indicator(self):
        """
        Populates the menu for the tray applet.
        """
        indicator.setup(self._get_tray_icon())

        self.dbg.stdout("Creating menus...", self.dbg.action, 1)
        menu = indicator.create_menu()

        # List devices and their submenus.
        devices = self.middleman.get_devices()
        if len(devices) == 0:
            self.dbg.stdout("No devices found.", self.dbg.error, 1)
            indicator.create_menu_item(menu, self._("No devices found"), False, icon_path=self._get_icon("general", "unknown"))

        # Order devices A-Z for consistency
        sorted_devices = sorted(devices, key=lambda device: device.name)

        for device in sorted_devices:
            indicator.add_menu_item(menu, self.build_device_submenu(device))

        for device in self.middleman.get_unsupported_devices():
            indicator.create_menu_item(menu, "{0} {1}".format(self._("Unrecognised:"), device.name), False, icon_path=device.form_factor["icon"])

        if len(devices) == 0:
            indicator.create_seperator(menu)
            indicator.create_menu_item(menu, self._("Refresh"), True, cb.retry_applet, None, self._get_icon("general", "refresh"))
            indicator.create_menu_item(menu, self._("Troubleshoot"), True, cb.launch_controller, "troubleshoot", self._get_icon("general", "preferences"))

        # When multiple devices are present, show 'apply to all' actions
        if len(devices) > 1:
            bulk_menu, bulk_menu_item = indicator.create_submenu(self._("Apply to All"), True, self._get_icon("devices", "all"))
            indicator.create_seperator(menu)
            indicator.add_menu_item(menu, bulk_menu_item)
            self.create_bulk_menu(bulk_menu)

        # General Options
        indicator.create_seperator(menu)
        self._create_menu_item_if_controller_present(menu, self._("Open Controller"), None, self._get_icon("general", "controller"))

        # When tray applet is only installed, show toggle for auto start
        if not procpid.ProcessManager().is_component_installed("controller"):
            def _toggle_auto_start(params=None):
                new_value = not self.preferences["tray"]["autostart"]
                self.preferences["tray"]["autostart"] = new_value
                pref.save_file(app.paths.preferences, self.preferences)
                _update_auto_start_toggle(new_value)

            auto_start_item = indicator.create_menu_item(menu, self._("Autostart"), True, _toggle_auto_start, None, self._get_icon("general", "tray-applet"))

            def _update_auto_start_toggle(is_enabled):
                # FIXME: HACK: Not framework agnostic!
                img = Gtk.Image()
                if is_enabled:
                    auto_start_item.set_label(self._("Disable automatic start at login"))
                    img.set_from_file(self._get_icon("general", "close"))
                else:
                    auto_start_item.set_label(self._("Start automatically at login"))
                    img.set_from_file(self._get_icon("general", "ok"))
                auto_start_item.set_image(img)

            _update_auto_start_toggle(self.preferences["tray"]["autostart"])

        indicator.create_menu_item(menu, self._("Quit"), True, cb.quit)
        indicator.finalize()

    def build_device_submenu(self, device):
        """
        Assembles the menu (and submenus) for the specified Backend.DeviceItem()
        """
        device_icon = device.form_factor["icon"]
        multiple_zones = len(device.zones) > 1
        can_set_colours = False

        submenu, submenu_item = indicator.create_submenu(device.name, True, device_icon)
        self.dbg.stdout("- " + device.name, self.dbg.action, 1)

        for zone in device.zones:
            # Create label when multiple zones are present
            if multiple_zones:
                indicator.create_menu_item(submenu, zone.label, False, icon_path=zone.icon)

            # Add options
            for option in zone.options:
                # Show colour items later if device supports RGB
                if option.colours_required > 0:
                    can_set_colours = True

                if isinstance(option, (Backend.EffectOption, Backend.MultipleChoiceOption)):
                    # Option does not accept parameters, just a button
                    if len(option.parameters) == 0:
                        indicator.create_menu_item(submenu, option.label, True, cb.apply_option, [option, None, 0, device], option.icon)
                    else:
                        # Create a submenu for effects
                        indicator.add_menu_item(submenu, self.create_parameters_menu(option, device))

                        for param in option.parameters:
                            if param.colours_required > 0:
                                can_set_colours = True

                elif isinstance(option, Backend.SliderOption):
                    indicator.add_menu_item(submenu, self.create_slider_menu(option, device))

                elif isinstance(option, Backend.ToggleOption):
                    indicator.add_menu_item(submenu, self.create_toggle_menu(option, device))

            # Seperator multiple zones if applicable
            if multiple_zones:
                indicator.create_seperator(submenu)

        # Add DPI
        if device.dpi:
            dpi_menu, dpi_menu_item = indicator.create_submenu(self._("DPI"), True, self._get_icon("general", "dpi"))
            stages = device.dpi.default_stages
            total = len(stages)

            # Use custom stages if the user saved them for this device
            # TODO: Should reuse this code
            dpi_file = os.path.join(self.paths.dpi, device.name + ".list")
            if os.path.exists(dpi_file):
                values = []
                with open(dpi_file, "r") as f:
                    for line in f.readlines():
                        try:
                            dpi_x, dpi_y = line.split(",")
                            values.append([int(dpi_x), int(dpi_y)])
                        except ValueError:
                            # TODO: Output to stderr
                            pass
                if values:
                    stages = values

            for index, value in enumerate(stages):
                dpi_x, dpi_y = value
                label = str(dpi_x) if dpi_x == dpi_y else f"{dpi_x},{dpi_y}"
                icon = None
                if index == 0:
                    icon = self._get_icon("general", "dpi-slow")
                elif index == total - 1:
                    icon = self._get_icon("general", "dpi-fast")
                indicator.create_menu_item(dpi_menu, label, True, cb.set_dpi, [device, value], icon)
            indicator.add_menu_item(submenu, dpi_menu_item)

        if device.matrix:
            effect_items = fileman_effects.get_item_list_by_key_filter("map_device", device.name)
            effect_items = sorted(effect_items, key=lambda item: item["name"])
            effects_menu, effects_menu_item = indicator.create_submenu(self._("Effects"), True, self._get_icon("general", "effects"))

            if len(effect_items) > 0:
                for index in effect_items:
                     indicator.create_menu_item(effects_menu, index["name"], True, cb.set_custom_effect, [device, index["path"]], index["icon"])
            else:
                indicator.create_menu_item(effects_menu, self._("No custom effects"), False)

            indicator.create_seperator(submenu)
            indicator.add_menu_item(submenu, effects_menu_item)
            indicator.create_seperator(effects_menu)
            indicator.create_menu_item(effects_menu, self._("Edit Effects..."), True, cb.launch_controller, "effects", self._get_icon("general", "edit"))

        # Colour (repeats last effect)
        if can_set_colours:
            colour_menu, colour_menu_item = indicator.create_submenu(self._("Primary Colour"), True, self._get_icon("general", "palette"))

            index = colours_mono if device.monochromatic else saved_colours

            for pos in range(0, len(index)):
                try:
                    name = index[pos]["name"]
                    hex_value = index[pos]["hex"]
                    indicator.create_menu_item(colour_menu, name, True, cb.set_colour_primary, [device, hex_value], self._get_colour_icon(hex_value))
                except Exception:
                    self.dbg.stdout("Ignoring invalid colour data at index position " + str(pos), self.dbg.error)

            if not device.monochromatic:
                indicator.create_seperator(colour_menu)
                indicator.create_menu_item(colour_menu, self._("Custom..."), True, cb.set_custom_colour, [device], self._get_icon("general", "palette"))
                self._create_menu_item_if_controller_present(colour_menu, self._("Edit Colours..."), "colours", self._get_icon("general", "edit"))

            if multiple_zones:
                indicator.create_menu_item(submenu, self._("All Zones"), False, icon_path=device_icon)
            indicator.add_menu_item(submenu, colour_menu_item)

        return submenu_item

    def create_parameters_menu(self, option, device):
        """
        Creates the submenu for an option containing parameters from a multiple
        choice or effect list.
        """
        submenu, submenu_item = indicator.create_submenu(option.label, True, option.icon)

        for param in option.parameters:
            indicator.create_menu_item(submenu, param.label, True, cb.apply_option, [option, param.data, param.colours_required, device], param.icon)

        return submenu_item

    def create_slider_menu(self, option, device):
        """
        Creates the submenu for an option that is a variable slider. This will
        create five calculated intervals (e.g. 0-100 -> 0,25,50,75,100)
        """
        submenu, submenu_item = indicator.create_submenu(option.label, True, option.icon)

        min = int(option.min)
        max = int(option.max)
        step = int(option.step)

        if min == 0 and max == 100:
            step = int((max - min) / 4)
        elif min == 1 and max == 100:
            step = 10

        for no in range(max, min - step, 0 - step):
            # Brightness uses custom icons
            icon = option.icon
            if option.uid == "brightness":
                icon = self._get_icon("params", str(no))

            suffix = option.suffix if no == 1 else option.suffix_plural
            indicator.create_menu_item(submenu, str(no) + suffix, True, cb.apply_option, [option, int(no), 0, device], icon)

        return submenu_item

    def create_toggle_menu(self, option, device):
        """
        Creates the submenu for an option that is either on/off.
        """
        submenu, submenu_item = indicator.create_submenu(option.label, True, option.icon)

        # Some IDs have submenu icons
        label_enable = option.label_enable if option.label_enable else self._("On")
        label_disable = option.label_disable if option.label_disable else self._("Off")
        indicator.create_menu_item(submenu, label_enable, True, cb.apply_option, [option, True, 0, device], option.icon_enable)
        indicator.create_menu_item(submenu, label_disable, True, cb.apply_option, [option, False, 0, device], option.icon_disable)

        return submenu_item

    def create_dialog_control(self, option):
        """
        Creates a submenu containing the message that would appear in a dialog.
        """
        # TODO: When class refactor is completed, use actual dialogs
        submenu, submenu_item = indicator.create_submenu(option.button_label, True, option.icon)
        for line in option["message"].split("\n"):
            indicator.create_menu_item(submenu, line, False)
        return submenu_item

    def create_bulk_menu(self, bulk_menu):
        """
        Creates a submenu containing options to "apply to all" connected devices.
        """
        bulk_options = bulkapply.BulkApplyOptions(self.middleman)

        def _create_bulk_submenu(options, label, icon):
            if not options:
                return
            submenu, item = indicator.create_submenu(label, True, icon)
            for option in options:
                indicator.create_menu_item(submenu, option.label, True, option.apply, None, option.icon)
            indicator.add_menu_item(bulk_menu, item)

        _create_bulk_submenu(bulk_options.brightness, self._("Brightness"), self._get_icon("options", "brightness"))
        _create_bulk_submenu(bulk_options.effects, self._("Effects"), self._get_icon("general", "effects"))
        _create_bulk_submenu(bulk_options.colours, self._("Primary Colour"), self._get_icon("general", "palette"))

    def _get_colour_icon(self, colour_hex):
        """
        Generates a colour PNG, and returns the path so it can be used as an icon.

        Params:
            colour_hex      Hex value, e.g. "#00FF00"
        """
        return self.common.generate_colour_bitmap(dbg, colour_hex)


class Callback():
    """
    Contains functions that run when a menu item is clicked.
    """
    @staticmethod
    def _catch_command_error(device=Backend.DeviceItem, err=""):
        """
        Shows a error message when a command sent to a device was unsuccessful.
        """
        backend_name = device.backend.name
        exception = common.get_exception_as_string(err)
        my_fault = common.is_exception_fault_by_app(err)

        dbg.stdout(exception, dbg.error)

        if my_fault:
            indicator.open_dialog(_("Request Failed"),
                                  _("There's a fault with Polychromatic's integration with [OpenRazer] for this device or option.").replace("[OpenRazer]", backend_name) + "\n\n" + \
                                  _("Try restarting the tray applet. To see error details, run 'polychromatic-tray-applet' in the terminal and repeat this action."))
        else:
            indicator.open_dialog(_("Communication Error"),
                                  _("While sending this command to [OpenRazer], an error occurred outside of Polychromatic's control.").replace("[OpenRazer]", backend_name) + "\n\n" + \
                                  _("Try restarting [OpenRazer] and Polychromatic and give it another go. If this error appears again, it's likely this needs fixing in [OpenRazer].").replace("[OpenRazer]", backend_name))

    @staticmethod
    def launch_controller(item, tab=None):
        """
        Option clicked to open the Controller application.

        Params:
            tab     (Optional) Open a specific tab in the Controller
        """
        if tab:
            dbg.stdout("=> Launching Controller to '{0}' tab/section...".format(tab), dbg.action, 1)
        else:
            dbg.stdout("=> Launching Controller...", dbg.action, 1)

        common.execute_polychromatic_component(dbg, "controller", tab)

    @staticmethod
    def quit(item):
        """
        Option clicked to quit the application.
        """
        Gtk.main_quit()

    @staticmethod
    def retry_applet(item):
        """
        Option clicked to restart the tray applet.
        """
        dbg.stdout("Restarting applet...", dbg.action, 1)
        os.execv(__file__, sys.argv)

    @staticmethod
    def apply_option(item, attr):
        """
        Option clicked to set an effect for a device.

        Params:
            attr        [<Backend.Option object>, <Backend.Option.Parameter or None>]
        """
        option, param_data, param_colours, device = attr

        # If this is an effect, stop any software effects currently running.
        if isinstance(option, Backend.EffectOption):
            app.middleman.stop_software_effect(device.serial)

        # If the user set a primary colour in this session, use that.
        global primary_colour
        if primary_colour:
            if option.colours_required > 0 or param_colours > 0:
                option.colours[0] = primary_colour

        dbg.stdout("Setting option '{0}' (data: {1})'".format(str(option), str(param_data)), dbg.action, 1)
        try:
            option.apply(param_data)
        except Exception as e:
            Callback._catch_command_error(device, e)

    @staticmethod
    def set_colour_primary(item, attr):
        """
        Option clicked to choose a different primary colour.

        Params:
            attr    [device, hex_string]
        """
        device, hex_value = attr

        # Remember this colour if the user chooses another colour-enabled effect later
        global primary_colour
        primary_colour = hex_value

        dbg.stdout("Applying colour '{0}' to all zones in '{1}'".format(hex_value, device.name), dbg.action, 1)
        try:
            app.middleman.set_colour_for_active_effect_device(device, hex_value)
        except Exception as e:
            Callback._catch_command_error(device, e)

    @staticmethod
    def set_dpi(item, attr):
        """
        Option clicked to set the DPI (both X/Y) of a device.

        Params:
            attr    [device, value]
        """
        device, value = attr
        dpi_x, dpi_y = value

        dbg.stdout("Setting DPI to {0}".format(value), dbg.action, 1)
        try:
            device.dpi.set(dpi_x, dpi_y)
        except Exception as e:
            Callback._catch_command_error(device, e)

    @staticmethod
    def set_custom_colour(item, attr):
        """
        Option clicked to open a colour picker to choose any colour for the
        specified device.

        Params:
            attr    [device]
        """
        device = attr[0]
        attr[1]

        # TODO: Refactor later. Move to Indicator()
        dbg.stdout("Opened GTK colour picker.", dbg.debug, 1)
        color_selection_dlg = Gtk.ColorSelectionDialog(_("Set Primary Colour"))
        color_selection_result = color_selection_dlg.run()

        if color_selection_result == Gtk.ResponseType.OK:
            # Parse colour from GTK
            result_gdk_colour = color_selection_dlg.get_color_selection().get_current_color()
            red = int(result_gdk_colour.red_float * 255)
            green = int(result_gdk_colour.green_float * 255)
            blue = int(result_gdk_colour.blue_float * 255)
            hex_value = common.rgb_to_hex([red, green, blue])

            # Apply colour to device
            dbg.stdout("Set custom colour for {0} to '{1}'".format(device.name, hex_value), dbg.debug, 1)
            try:
                app.middleman.set_colour_for_active_effect_device(device, hex_value)
            except Exception as e:
                Callback._catch_command_error(device, e)

        color_selection_dlg.destroy()

    @staticmethod
    def set_custom_effect(item, attr):
        """
        Option clicked to activate an effect for the specified device.

        Params:
            attr    [device, <path to file>]
        """
        device, path = attr

        device_name = device.name
        dbg.stdout("Playing effect '{0}' on '{1}'".format(path, device_name), dbg.debug, 1)
        procmgr = procpid.ProcessManager("helper")
        procmgr.start_component(["--run-fx", path, "--device-name", device_name])


def parse_parameters():
    """
    Parses the optional parameters for the tray applet.
    """
    global _
    parser = argparse.ArgumentParser(add_help=False)
    parser._optionals.title = _("Optional arguments")
    parser.add_argument("-h", "--help", help=_("Show this help message and exit"), action="help")
    parser.add_argument("-v", "--verbose", help=_("Be verbose to stdout"), action="store_true")
    parser.add_argument("--version", help=_("Print program version and exit"), action="store_true")
    parser.add_argument("--locale", help=_("Force a specific locale, e.g. de_DE"), action="store")
    parser.add_argument("--force-appindicator", help=_("Render using AppIndicator3"), action="store_true")
    parser.add_argument("--force-ayatana", help=_("Render using Ayatana Indicators"), action="store_true")
    parser.add_argument("--force-gtk-status", help=_("Render using GTK Status (legacy)"), action="store_true")

    args = parser.parse_args()

    if args.version:
        app_version, git_commit, py_version = app.common.get_versions(VERSION)
        print("Polychromatic", app_version)
        if git_commit:
            print("Commit:", git_commit)
        print("Python:", py_version)
        print("GTK:", "{0}.{1}.{2}".format(Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION))
        sys.exit(0)

    if args.verbose:
        dbg.verbose_level = 1
        dbg.stdout(_("Verbose enabled"), dbg.debug, 1)

    try:
        if os.environ["POLYCHROMATIC_DEV_CFG"] == "true":
            dbg.verbose_level = 1
            dbg.stdout("Verbose enabled (development mode)", dbg.action, 1)
    except KeyError:
        pass

    if args.locale:
        app.reinit_locales(args.locale)

    return args


if __name__ == "__main__":
    # Appear as its own process.
    setproctitle.setproctitle("polychromatic-tray-applet")

    # TODO: Refactor later
    app = PolychromaticTrayApplet()
    dbg = app.dbg
    middleman = app.middleman
    common = app.common
    _ = app._
    args = parse_parameters()

    # Handle signals sent to process
    def _restart_applet(num, frame):
        dbg.stdout("Received USR2 signal. Reloading...", dbg.action, 1)
        process.start_component()
        _stop_applet()

    def _stop_applet(num=None, frame=None):
        dbg.stdout("Received INT or USR1 signal. Exiting...", dbg.action, 1)
        process.release_component_pid()
        Gtk.main_quit()

    signal.signal(signal.SIGINT, _stop_applet)
    signal.signal(signal.SIGUSR1, _restart_applet)
    signal.signal(signal.SIGUSR2, _stop_applet)
    atexit.register(_stop_applet)

    # Only run one tray applet at a time - if one is running, replace it.
    process = procpid.ProcessManager("tray-applet")
    if process.is_another_instance_is_running():
        process.stop()
    process.set_component_pid()

    args = parse_parameters()

    # Which AppIndicator? Both use the same API.
    if "AyatanaAppIndicator3" in SUPPORTED:
        appindicator = AyatanaAppIndicator3
    elif "AppIndicator3" in SUPPORTED:
        appindicator = AppIndicator3

    # Initialise devices
    app.init_base(__file__, sys.argv)
    app.middleman.init()
    # TODO: Add menu option to show error details (for middleman.import_errors)

    # Load data into memory
    saved_colours = pref.get_colour_list(_)
    colours_mono = common.get_green_shades(_)
    fileman_effects = effects.EffectFileManagement()
    primary_colour = None

    # Initialise the indicator
    cb = Callback()
    indicator = Indicator()
    app = PolychromaticTrayApplet()
    app.start()

    Gtk.main()
