#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#

from gi import require_version
require_version('Gtk', '3.0')
from gi.repository import Gtk
require_version('AppIndicator3', '0.1')
from gi.repository import AppIndicator3 as appIndicator
require_version('Notify', '0.7')
from gi.repository import Notify
require_version('GdkPixbuf', '2.0')
from gi.repository.GdkPixbuf import Pixbuf
from gi.repository.GLib import timeout_add, source_remove, idle_add, unix_signal_add, PRIORITY_HIGH
from sys import exit as sysExit
from webbrowser import open_new as openNewBrowser
from signal import SIGTERM, SIGINT
from os.path import exists as pathExists, join as pathJoin, relpath as relativePath, expanduser
from os import getenv, getpid, geteuid
from daemon import YDDaemon
from tools import copyFile, deleteFile, makeDirs, shortPath, CVal, Config, activateActions, checkAutoStart
from tools import setProcName, argParse, check_output, call, pathExists, LOGGER, _
from datetime import datetime

APPNAME = 'yandex-disk-indicator'
APPVER = '1.12.0'
#
COPYRIGHT = 'Copyright ' + '\u00a9' + ' 2013-' + str(datetime.today().year) + ' Sly_tom_cat'
#
LICENSE = """
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 3 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, see http://www.gnu.org/licenses
"""


class Notification:
    """ On-screen notification """


    def __init__(self, title):
        """ Initialize notification engine """
        if not Notify.is_initted():
            Notify.init(APPNAME)
        self.title = title
        self.note = None


    def send(self, messg):
        # global APPLOGO
        LOGGER.debug('Message: %s | %s', self.title, messg)
        if self.note is not None:
            try:
                self.note.close()
            except:
                pass
            self.note = None
        try:                            # Create notification
            self.note = Notify.Notification.new(self.title, messg)
            self.note.set_image_from_pixbuf(APPLOGO)
            self.note.show()              # Display new notification
        except:
            LOGGER.error('Message engine failure')


# ################### Indicatior class ################### #
class Indicator(YDDaemon):
    """ Yandex.Disk appIndicator class """


    # ###### YDDaemon virtual classes/methods implementations
    def error(self, errStr, cfgPath):
        """ Error handler GUI implementation """
        # it must handle two types of error cases:
        # - yandex-disk is not installed (errStr=='' in that case) - just show error message and return
        # - yandex-disk is not configured (errStr!='' in that case) - suggest to configure it and run ya-setup if needed
        if errStr == '':
            text1 = _('Yandex.Disk Indicator: daemon start failed')
            buttons = Gtk.ButtonsType.OK
            text2 = (_('Yandex.Disk utility is not installed.\n' +
                       'Visit www.yandex.ru, download and install Yandex.Disk daemon.'))
        else:
            text1 = _('Yandex.Disk Indicator: daemon start failed')
            buttons = Gtk.ButtonsType.OK_CANCEL
            text2 = (_('Yandex.Disk daemon failed to start because it is not' +
                       ' configured properly\n\n' + errStr + '\n\n' +
                       '  To configure it up: press OK button.\n  Press Cancel to exit.'))
        dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO, buttons, text1)
        dialog.format_secondary_text(text2)
        dialog.set_icon(APPLOGO)
        response = dialog.run()

        if errStr != '' and response == Gtk.ResponseType.OK:  # Launch Set-up utility
            LOGGER.debug('starting configuration utility')
            retCode = call([pathJoin(APPINSTPATH, 'ya-setup'), cfgPath])
        else:
            retCode = 1
        dialog.destroy()
        return retCode              # 0 when error is not critical or fixed (daemon has been configured via ya-setup)


    def change(self, vals):
        """ Implementation of daemon class call-back function

        NOTE: it is called not from main thread, so it have to add action in main GUI loop queue

        It handles daemon status changes by updating icon, creating messages and also update
        status information in menu (status, sizes and list of last synchronized items).
        It is called when daemon detects any change of its status.
        """
        LOGGER.info('%sChange event: %s', self.ID, ','.join(['stat' if vals['statchg'] else '',
                                                             'size' if vals['szchg'] else '',
                                                             'last' if vals['lastchg'] else '']))


        def do_change(vals, path):
            """ Update information in menu """
            self.menu.update(vals, path)
            # Handle daemon status change by icon change
            if vals['status'] != vals['laststatus']:
                LOGGER.info('Status: %s ->  %s', vals['laststatus'], vals['status'])
                self.updateIcon(vals['status'])          # Update icon
                # Create notifications for status change events
                if APPCONF['notifications']:
                    if vals['laststatus'] == 'none':       # Daemon has been started
                        self.notify.send(_('Yandex.Disk daemon has been started'))
                    if vals['status'] == 'busy':           # Just entered into 'busy'
                        self.notify.send(_('Synchronization started'))
                    elif vals['status'] == 'idle':         # Just entered into 'idle'
                        if vals['laststatus'] == 'busy':     # ...from 'busy' status
                            self.notify.send(_('Synchronization has been completed'))
                    elif vals['status'] == 'paused':       # Just entered into 'paused'
                        if vals['laststatus'] not in ['none', 'unknown']:  # ...not from 'none'/'unknown' status
                            self.notify.send(_('Synchronization has been paused'))
                    elif vals['status'] == 'none':         # Just entered into 'none' from some another status
                        if vals['laststatus'] != 'unknown':  # ... not from 'unknown'
                            self.notify.send(_('Yandex.Disk daemon has been stopped'))
                    else:                                  # status is 'error' or 'no-net'
                        self.notify.send(_('Synchronization ERROR'))
            # Remember current status (required for Preferences dialog)
            self.currentStatus = vals['status']

        idle_add(do_change, vals, self.config['dir'])

    """ Own classes/methods """

    def __init__(self, path, ID):
        # Create indicator notification engine
        self.notify = Notification(_('Yandex.Disk ') + ID)
        # Setup icons theme
        self.setIconTheme(APPCONF['theme'])
        # Create staff for icon animation support (don't start it here)

        self._seqNum = 2  # Number current busy icon

        def iconAnimation():          # Changes busy icon by loop (triggered by self.timer)
            # As it called from timer (main GUI thread) there is no need to use idle_add here
            # Set next animation icon
            self.ind.set_icon_full(pathJoin(self.themePath, 'yd-busy' + str(self._seqNum) + '.png'), '')
            # Calculate next icon number
            self._seqNum = self._seqNum % 5 + 1   # 5 icon numbers in loop (1-2-3-4-5-1-2-3...)
            return True                           # True required to continue triggering by timer

        self.iconTimer = self.Timer(777, iconAnimation, start=False)
        # Create App Indicator
        self.ind = appIndicator.Indicator.new(
            "yandex-disk-%s" % ID[1: -1],
            self.icon['paused'],
            appIndicator.IndicatorCategory.APPLICATION_STATUS)
        self.ind.set_status(appIndicator.IndicatorStatus.ACTIVE)
        self.menu = self.Menu(self, ID)               # Create menu for daemon
        self.ind.set_menu(self.menu)                  # Attach menu to indicator
        # Initialize Yandex.Disk daemon connection object
        super().__init__(path, ID)
        self.currentStatus = None                 # Current daemon status


    def setIconTheme(self, theme):
        """ Determine paths to icons according to current theme """
        # global APPINSTPATH, APPCONFPATH
        theme = 'light' if theme else 'dark'
        # Determine theme from application configuration settings
        defaultPath = pathJoin(APPINSTPATH, 'icons', theme)
        userPath = pathJoin(APPCONFPATH, 'icons', theme)
        # Set appropriate paths to all status icons
        self.icon = {}
        for status in ['idle', 'error', 'paused', 'none', 'no_net', 'busy']:
            name = ('yd-ind-pause.png' if status in {'paused', 'none', 'no_net'} else
                    'yd-busy1.png' if status == 'busy' else
                    'yd-ind-' + status + '.png')
            userIcon = pathJoin(userPath, name)
            self.icon[status] = userIcon if pathExists(userIcon) else pathJoin(defaultPath, name)
            # userIcon corresponds to busy icon on exit from this loop
        # Set theme paths according to existence of first busy icon
        self.themePath = userPath if pathExists(userIcon) else defaultPath


    def updateIcon(self, status):       # Change indicator icon according to just changed daemon status
        """ Set icon according to the current daemon status """
        self.ind.set_icon_full(self.icon[status], '')
        # Handle animation
        if status == 'busy':        # Just entered into 'busy' status
            self._seqNum = 2          # Next busy icon number for animation
            self.iconTimer.start()    # Start animation timer
        else:
            self.iconTimer.stop()     # Stop animation timer when status is not busy


    class Menu(Gtk.Menu):               # Indicator menu

        def __init__(self, daemon, ID):
            self.daemon = daemon                      # Store reference to daemon object for future usage
            self.folder = ''
            super().__init__()                        # Initialize menu
            self.ID = ID
            if self.ID != '':                         # Add addition field in multidaemon mode
                self.yddir = Gtk.MenuItem(label='');  self.yddir.set_sensitive(False);   self.append(self.yddir)
            self.status = Gtk.MenuItem(label='');   self.status.connect("activate", self.showOutput)
            self.append(self.status)
            self.used = Gtk.MenuItem(label='');     self.used.set_sensitive(False)
            self.append(self.used)
            self.free = Gtk.MenuItem(label='');     self.free.set_sensitive(False)
            self.append(self.free)
            self.last = Gtk.MenuItem(label=_('Last synchronized items'))
            self.last.set_sensitive(False)
            self.lastItems = Gtk.Menu()               # Sub-menu: list of last synchronized files/folders
            self.last.set_submenu(self.lastItems)     # Add submenu (empty at the start)
            self.append(self.last)
            self.append(Gtk.SeparatorMenuItem.new())  # -----separator--------
            self.daemon_ss = Gtk.MenuItem(label='')         # Start/Stop daemon: Label is depends on current daemon status
            self.daemon_ss.connect("activate", self.startStopDaemon)
            self.append(self.daemon_ss)
            self.open_folder = Gtk.MenuItem(label=_('Open Yandex.Disk Folder'))
            self.open_folder.connect("activate", lambda w: self.openPath(w, self.folder))
            self.append(self.open_folder)
            open_web = Gtk.MenuItem(label=_('Open Yandex.Disk on the web'))
            open_web.connect("activate", self.openInBrowser, _('https://disk.yandex.com'))
            self.append(open_web)
            self.append(Gtk.SeparatorMenuItem.new())  # -----separator--------
            self.preferences = Gtk.MenuItem(label=_('Preferences'))
            self.preferences.connect("activate", Preferences)
            self.append(self.preferences)
            open_help = Gtk.MenuItem(label=_('Help'))
            m_help = Gtk.Menu()
            help1 = Gtk.MenuItem(label=_('Yandex.Disk daemon'))
            help1.connect("activate", self.openInBrowser, _('https://yandex.com/support/disk/'))
            m_help.append(help1)
            help2 = Gtk.MenuItem(label=_('Yandex.Disk Indicator'))
            help2.connect("activate", self.openInBrowser,
                          _('https://github.com/slytomcat/yandex-disk-indicator/wiki/Yandex-disk-indicator'))
            m_help.append(help2)
            open_help.set_submenu(m_help)
            self.append(open_help)
            self.about = Gtk.MenuItem(label=_('About'));    self.about.connect("activate", self.openAbout)
            self.append(self.about)
            self.append(Gtk.SeparatorMenuItem.new())  # -----separator--------
            close = Gtk.MenuItem(label=_('Quit'))
            close.connect("activate", self.close)
            self.append(close)
            self.show_all()
            # Define user readable statuses dictionary
            self.YD_STATUS = {'idle': _('Synchronized'), 'busy': _('Sync.: '), 'none': _('Not started'),
                              'paused': _('Paused'), 'no_net': _('Not connected'), 'error': _('Error')}


        def update(self, vals, yddir):  # Update information in menu
            self.folder = yddir
            # Update status data on first run or when status has changed
            if vals['statchg'] or vals['laststatus'] == 'unknown':
                self.status.set_label(_('Status: ') + self.YD_STATUS[vals['status']] +
                                      (vals['progress'] if vals['status'] == 'busy' else
                                       ' '.join((':', vals['error'], shortPath(vals['path']))) if vals['status'] == 'error' else ''))
                # Update pseudo-static items on first run or when daemon has stopped or started
                if 'none' in (vals['status'], vals['laststatus']) or vals['laststatus'] == 'unknown':
                    started = vals['status'] != 'none'
                    self.status.set_sensitive(started)
                    # zero-space UTF symbols are used to detect requered action without need to compare translated strings
                    self.daemon_ss.set_label(('\u2060' + _('Stop Yandex.Disk daemon')) if started else
                                             ('\u200B' + _('Start Yandex.Disk daemon')))
                    if self.ID != '':                             # Set daemon identity row in multidaemon mode
                        self.yddir.set_label(self.ID + _('  Folder: ') + (shortPath(yddir) if yddir else '< NOT CONFIGURED >'))
                    self.open_folder.set_sensitive(yddir != '')  # Activate Open YDfolder if daemon configured
            # Update sizes data on first run or when size data has changed
            if vals['szchg'] or vals['laststatus'] == 'unknown':
                self.used.set_label(_('Used: ') + vals['used'] + '/' + vals['total'])
                self.free.set_label(_('Free: ') + vals['free'] + _(', trash: ') + vals['trash'])
            # Update last synchronized sub-menu on first run or when last data has changed
            if vals['lastchg'] or vals['laststatus'] == 'unknown':
                # Update last synchronized sub-menu
                self.lastItems.destroy()                      # Disable showing synchronized sub menu while updating it - temp fix for #197
                self.lastItems = Gtk.Menu()                   # Create new/empty Sub-menu:
                for filePath in vals['lastitems']:            # Create new sub-menu items
                    # Create menu label as file path (shorten it down to 50 symbols when path length > 50
                    # symbols), with replaced underscore (to disable menu acceleration feature of GTK menu).
                    widget = Gtk.MenuItem.new_with_label(shortPath(filePath))
                    filePath = pathJoin(yddir, filePath)        # Make full path to file
                    if pathExists(filePath):
                        widget.set_sensitive(True)                # If it exists then it can be opened
                        widget.connect("activate", self.openPath, filePath)
                    else:
                        widget.set_sensitive(False)               # Don't allow to open non-existing path
                    self.lastItems.append(widget)
                self.last.set_submenu(self.lastItems)
                # Switch off last items menu sensitivity if no items in list
                self.last.set_sensitive(vals['lastitems'])
                LOGGER.debug("Sub-menu 'Last synchronized' has %s items", str(len(vals['lastitems'])))

            self.show_all()                                 # Renew menu


        def openAbout(self, widget):            # Show About window
            # global APPLOGO, APPINDICATORS
            for i in APPINDICATORS:
                i.menu.about.set_sensitive(False)           # Disable menu item
            aboutWindow = Gtk.AboutDialog()
            aboutWindow.set_logo(APPLOGO);   aboutWindow.set_icon(APPLOGO)
            aboutWindow.set_program_name(_('Yandex.Disk indicator'))
            aboutWindow.set_version(_('Version ') + APPVER)
            aboutWindow.set_copyright(COPYRIGHT)
            aboutWindow.set_license(LICENSE)
            aboutWindow.set_authors([_('Sly_tom_cat <slytomcat@mail.ru> '),
                                     _('\nSpecial thanks to:'),
                                     _(' - Snow Dimon https://habrahabr.ru/users/Snowdimon/ - author of ya-setup utility'),
                                     _(' - Christiaan Diedericks https://www.thefanclub.co.za/ - author of Grive tools'),
                                     _(' - ryukusu_luminarius <my-faios@ya.ru> - icons designer'),
                                     _(' - metallcorn <metallcorn@jabber.ru> - icons designer'),
                                     _(' - Chibiko <zenogears@jabber.ru> - deb package creation assistance'),
                                     _(' - RingOV <ringov@mail.ru> - localization assistance'),
                                     _(' - GreekLUG team https://launchpad.net/~greeklug - Greek translation'),
                                     _(' - Peyu Yovev <spacy00001@gmail.com> - Bulgarian translation'),
                                     _(' - Eldar Fahreev <fahreeve@yandex.ru> - FM actions for Pantheon-files'),
                                     _(' - Ace Of Snakes <aceofsnakesmain@gmail.com> - optimization of FM actions for Dolphin'),
                                     _(' - Ivan Burmin https://github.com/Zirrald - ya-setup multilingual support'),
                                     _('And to all other people who contributed to this project via'),
                                     _(' - Ubuntu.ru forum http://forum.ubuntu.ru/index.php?topic=241992'),
                                     _(' - github.com https://github.com/slytomcat/yandex-disk-indicator')])
            aboutWindow.set_resizable(False)
            aboutWindow.run()
            aboutWindow.destroy()
            for i in APPINDICATORS:
                i.menu.about.set_sensitive(True)            # Enable menu item


        def showOutput(self, widget):           # Request for daemon output
            widget.set_sensitive(False)                         # Disable menu item


            def displayOutput(outText, widget):
                # # # NOTE: it is called not from main thread, so it have to add action in main loop queue
                def do_display(outText, widget):
                    # global APPLOGO
                    statusWindow = Gtk.Dialog(_('Yandex.Disk daemon output message'))
                    statusWindow.set_icon(APPLOGO)
                    statusWindow.set_border_width(6)
                    statusWindow.add_button(_('Close'), Gtk.ResponseType.CLOSE)
                    textBox = Gtk.TextView()                            # Create text-box to display daemon output
                    # Set output buffer with daemon output in user language
                    textBox.get_buffer().set_text(outText)
                    textBox.set_editable(False)
                    # Put it inside the dialogue content area
                    statusWindow.get_content_area().pack_start(textBox, True, True, 6)
                    statusWindow.show_all();  statusWindow.run();   statusWindow.destroy()
                    widget.set_sensitive(True)                          # Enable menu item
                idle_add(do_display, outText, widget)

            self.daemon.output(lambda t: displayOutput(t, widget))


        def openInBrowser(self, _, url):   # Open URL
            openNewBrowser(url)


        def startStopDaemon(self, widget):      # Start/Stop daemon
            action = widget.get_label()[:1]
            # zero-space UTF symbols are used to detect requered action without need to compare translated strings
            if action == '\u200B':    # Start
                self.daemon.start()
            elif action == '\u2060':  # Stop
                self.daemon.stop()


        def openPath(self, _, path):       # Open path
            LOGGER.info("Opening '%s'", path)
            if pathExists(path):
                try:
                    call(['xdg-open', path])
                except:
                    LOGGER.error("Start of '%s' failed", path)


        def close(self, _):                # Quit from indicator
            appExit()


    class Timer:                        # Timer implementation (GUI related)
        """Timer class methods:

        __init__ - initialize the timer object with specified interval and handler. Start it if start value True.
        start    - Start timer if it is not started yet.
        stop     - Stop running timer or do nothing if it is not running.

        Interface variables:

        active   - True when timer is currently running, otherwise - False

        """

        def __init__(self, interval, handler, start=True):
            self.interval = interval          # Timer interval (ms)
            self.handler = handler            # Handler function
            self.active = False               # Current activity status
            if start:
                self.start()                    # Start timer if required


        def start(self):       # Start inactive timer or update if it is active
            if not self.active:
                self.timer = timeout_add(self.interval, self.handler)
                self.active = True
                # LOGGER.debug('timer started %s %s', self.timer, interval)


        def stop(self):                     # Stop active timer
            if self.active:
                # LOGGER.debug('timer to stop %s', self.timer)
                source_remove(self.timer)
                self.active = False


# ### Application functions and classes
class Preferences(Gtk.Dialog):
    """ Preferences window of application and daemons """


    class excludeDirsList(Gtk.Dialog):
        """ Excluded dirs dialogue """


        def __init__(self, widget, parent, dcofig):   # show current list
            self.dconfig = dcofig
            self.parent = parent
            Gtk.Dialog.__init__(self, title=_('Folders that are excluded from synchronization'),
                                parent=parent, flags=1)
            self.set_icon(APPLOGO)
            self.set_size_request(400, 300)
            self.add_button(_('Add catalogue'),
                            Gtk.ResponseType.APPLY).connect("clicked", self.addFolder, self)
            self.add_button(_('Remove selected'),
                            Gtk.ResponseType.REJECT).connect("clicked", self.deleteSelected)
            self.add_button(_('Close'),
                            Gtk.ResponseType.CLOSE).connect("clicked", self.exitFromDialog)
            self.exList = Gtk.ListStore(bool, str)
            view = Gtk.TreeView(model=self.exList)
            render = Gtk.CellRendererToggle()
            render.connect("toggled", self.lineToggled)
            view.append_column(Gtk.TreeViewColumn(" ", render, active=0))
            view.append_column(Gtk.TreeViewColumn(_('Path'), Gtk.CellRendererText(), text=1))
            scroll = Gtk.ScrolledWindow()
            scroll.add_with_viewport(view)
            self.get_content_area().pack_start(scroll, True, True, 6)
            # Populate list with paths from "exclude-dirs" property of daemon configuration
            self.dirset = [val for val in CVal(self.dconfig.get('exclude-dirs', None))]
            for val in self.dirset:
                self.exList.append([False, val])
            # LOGGER.debug(str(self.dirset))
            self.show_all()


        def exitFromDialog(self, widget):     # Save list from dialogue to "exclude-dirs" property
            if self.dconfig.changed:
                eList = CVal()                                      # Store path value from dialogue rows
                for i in self.dirset:
                    eList.add(i)
                self.dconfig['exclude-dirs'] = eList.get()          # Save collected value
            # LOGGER.debug(str(self.dirset))
            self.destroy()                                        # Close dialogue


        def lineToggled(self, _, path):  # Line click handler, it switch row selection
            self.exList[path][0] = not self.exList[path][0]


        def deleteSelected(self, _):     # Remove selected rows from list
            listIiter = self.exList.get_iter_first()
            while listIiter is not None and self.exList.iter_is_valid(listIiter):
                if self.exList.get(listIiter, 0)[0]:
                    self.dirset.remove(self.exList.get(listIiter, 1)[0])
                    self.exList.remove(listIiter)
                    self.dconfig.changed = True
                else:
                    listIiter = self.exList.iter_next(listIiter)
            # LOGGER.debug(str(self.dirset))


        def addFolder(self, widget, parent):  # Add new path to list via FileChooserDialog
            dialog = Gtk.FileChooserDialog(_('Select catalogue to add to list'), parent, Gtk.FileChooserAction.SELECT_FOLDER,
                                           (_('Close'), Gtk.ResponseType.CANCEL, _('Select'), Gtk.ResponseType.ACCEPT))
            dialog.set_default_response(Gtk.ResponseType.CANCEL)
            dialog.set_select_multiple(True)
            rootDir = self.dconfig['dir']
            dialog.set_current_folder(rootDir)
            if dialog.run() == Gtk.ResponseType.ACCEPT:
                for path in dialog.get_filenames():
                    if path.startswith(rootDir):
                        path = relativePath(path, start=rootDir)
                        if path not in self.dirset:
                            self.exList.append([False, path])
                            self.dirset.append(path)
                            self.dconfig.changed = True
            dialog.destroy()
            # LOGGER.debug(str(self.dirset))


    def __init__(self, widget):
        # global config, APPINDICATORS, APPLOGO
        # Preferences Window routine
        for i in APPINDICATORS:
            i.menu.preferences.set_sensitive(False)   # Disable menu items to avoid multi-dialogs creation
        # Create Preferences window
        super().__init__(_('Yandex.Disk-indicator and Yandex.Disks preferences'), flags=1)
        self.set_icon(APPLOGO)
        self.set_border_width(6)
        self.add_button(_('Close'), Gtk.ResponseType.CLOSE)
        pref_notebook = Gtk.Notebook()              # Create notebook for indicator and daemon options
        self.get_content_area().add(pref_notebook)  # Put it inside the dialogue content area
        # --- Indicator preferences tab ---
        preferencesBox = Gtk.VBox(spacing=5)
        cb = []
        for key, msg in [('autostart', _('Start Yandex.Disk indicator when you start your computer')),
                         ('notifications', _('Show on-screen notifications')),
                         ('theme', _('Prefer light icon theme')),
                         ('fmextensions', _('Activate file manager extensions'))]:
            cb.append(Gtk.CheckButton(msg))
            cb[-1].set_active(APPCONF[key])
            cb[-1].connect("toggled", self.onButtonToggled, cb[-1], key)
            preferencesBox.add(cb[-1])
        # --- End of Indicator preferences tab --- add it to notebook
        pref_notebook.append_page(preferencesBox, Gtk.Label(_('Indicator settings')))
        # Add daemos tabs
        for i in APPINDICATORS:
            # --- Daemon start options tab ---
            optionsBox = Gtk.VBox(spacing=5)
            key = 'startonstartofindicator'           # Start daemon on indicator start
            cbStOnStart = Gtk.CheckButton(_('Start Yandex.Disk daemon %swhen indicator is starting')
                                          % i.ID)
            cbStOnStart.set_tooltip_text(_("When daemon was not started before."))
            cbStOnStart.set_active(i.config[key])
            cbStOnStart.connect("toggled", self.onButtonToggled, cbStOnStart, key, i.config)
            optionsBox.add(cbStOnStart)
            key = 'stoponexitfromindicator'           # Stop daemon on exit
            cbStoOnExit = Gtk.CheckButton(_('Stop Yandex.Disk daemon %son closing of indicator') % i.ID)
            cbStoOnExit.set_active(i.config[key])
            cbStoOnExit.connect("toggled", self.onButtonToggled, cbStoOnExit, key, i.config)
            optionsBox.add(cbStoOnExit)
            frame = Gtk.Frame()
            frame.set_label(_("NOTE! You have to reload daemon %sto activate following settings") % i.ID)
            frame.set_border_width(6)
            optionsBox.add(frame)
            framedBox = Gtk.VBox(homogeneous=True, spacing=5)
            frame.add(framedBox)
            key = 'read-only'                         # Option Read-Only    # daemon config
            cbRO = Gtk.CheckButton(_('Read-Only: Do not upload locally changed files to Yandex.Disk'))
            cbRO.set_tooltip_text(_("Locally changed files will be renamed if a newer version of this " +
                                    "file appear in Yandex.Disk."))
            cbRO.set_active(i.config[key])
            key = 'overwrite'                         # Option Overwrite    # daemon config
            overwrite = Gtk.CheckButton(_('Overwrite locally changed files by files' +
                                          ' from Yandex.Disk (in read-only mode)'))
            overwrite.set_tooltip_text(_("Locally changed files will be overwritten if a newer " +
                                         "version of this file appear in Yandex.Disk."))
            overwrite.set_active(i.config[key])
            overwrite.set_sensitive(i.config['read-only'])
            cbRO.connect("toggled", self.onButtonToggled, cbRO, 'read-only', i.config, overwrite)
            framedBox.add(cbRO)
            overwrite.connect("toggled", self.onButtonToggled, overwrite, key, i.config)
            framedBox.add(overwrite)
            # Excude folders list
            exListButton = Gtk.Button(_('Excluded folders List'))
            exListButton.set_tooltip_text(_("Folders in the list will not be synchronized."))
            exListButton.connect("clicked", self.excludeDirsList, self, i.config)
            framedBox.add(exListButton)
            # --- End of Daemon start options tab --- add it to notebook
            pref_notebook.append_page(optionsBox, Gtk.Label(_('Daemon %soptions') % i.ID))
        self.set_resizable(False)
        self.show_all()
        self.run()
        if APPCONF.changed:
            APPCONF.save()                              # Save app config
        for i in APPINDICATORS:
            if i.config.changed:
                i.config.save()                         # Save daemon options in config file
            i.menu.preferences.set_sensitive(True)      # Enable menu items
        self.destroy()


    def onButtonToggled(self, _, button, key, dconfig=None, ow=None):
        """ Handle clicks on controls """
        toggleState = button.get_active()
        LOGGER.debug('Togged: %s  val: %s', key, str(toggleState))
        # Update configurations
        if key in ['read-only', 'overwrite', 'startonstartofindicator', 'stoponexitfromindicator']:
            dconfig[key] = toggleState                # Update daemon config
            dconfig.changed = True
        else:
            APPCONF.changed = True                     # Update application config
            APPCONF[key] = toggleState
        if key == 'theme':
            for i in APPINDICATORS:                    # Update all APPINDICATORS' icons
                i.setIconTheme(toggleState)           # Update icon theme
                i.updateIcon(i.currentStatus)         # Update current icon
        elif key == 'autostart':
            if toggleState:
                copyFile(APPAUTOSTARTSRC, APPAUTOSTARTDST)
            else:
                deleteFile(APPAUTOSTARTDST)
        elif key == 'fmextensions':
            if not button.get_inconsistent():         # It is a first call
                if not activateActions(toggleState, APPINSTPATH):
                    toggleState = not toggleState         # When activation/deactivation is not success: revert settings back
                    button.set_inconsistent(True)         # set inconsistent state to detect second call
                    button.set_active(toggleState)        # set check-button to reverted status
                    # set_active will raise again the 'toggled' event
            else:                                     # This is a second call
                button.set_inconsistent(False)          # Just remove inconsistent status
        elif key == 'read-only':
            ow.set_sensitive(toggleState)


def appExit():
    """ Exit from application (it closes all APPINDICATORS) """
    # global APPINDICATORS
    LOGGER.debug("Exit started")
    for i in APPINDICATORS:
        i.exit()
    Gtk.main_quit()


# ##################### MAIN #########################
if __name__ == '__main__':
    # Application constants
    # See APPNAME and APPVER in the beginnig of the code
    APPHOME = 'yd-tools'
    APPINSTPATH = pathJoin('/usr/share', APPHOME)
    APPLOGO = Pixbuf.new_from_file(pathJoin(APPINSTPATH, 'icons/yd-128.png'))
    APPCONFPATH = pathJoin(getenv("HOME"), '.config', APPHOME)
    # Define .desktop files locations for indicator auto-start facility
    APPAUTOSTARTSRC = '/usr/share/applications/Yandex.Disk-indicator.desktop'
    APPAUTOSTARTDST = expanduser('~/.config/autostart/Yandex.Disk-indicator.desktop')

    # Get command line arguments or their default values
    args = argParse(APPVER)

    # Change the process name
    setProcName(APPHOME)

    # Check for already running instance of the indicator application
    # Get PIDs of all runnig processes (of current user) with name 'yd-tools' and compare it with current process PID
    if str(getpid()) != check_output(["pgrep", '-u', str(geteuid()), APPHOME], universal_newlines=True).strip():
        sysExit(_('The indicator instance is already running.'))

    # Set user specified logging level
    LOGGER.setLevel(args.level)

    # Report app version and logging level
    LOGGER.info('%s v.%s', APPNAME, APPVER)
    LOGGER.debug('Logging level: %s', str(args.level))

    # Application configuration
    """
    User configuration is stored in ~/.config/<APPHOME>/<APPNAME>.conf file.
    This file can contain comments (line starts with '#') and config values in
    form: key=value[,value[,value ...]] where keys and values can be quoted ("...") or not.
    The following key words are reserved for configuration:
      autostart, notifications, theme, fmextensions and daemons.

    The dictionary 'config' stores the config settings for usage in code. Its values are saved to
    config file on exit from the Menu.Preferences dialogue or when there is no configuration file
    when application starts.

    Note that daemon settings ('dir', 'read-only', 'overwrite' and 'exclude_dir') are stored
    in ~/ .config/yandex-disk/config.cfg file. They are read in YDDaemon.__init__() method
    (in dictionary YDDaemon.config). Their values are saved to daemon config file also
    on exit from Menu.Preferences dialogue.

    Additionally 'startonstartofindicator' and 'stoponexitfromindicator' values are added into daemon
    configuration file to provide the functionality of obsolete 'startonstart' and 'stoponexit'
    values for each daemon individually.
    """
    APPCONF = Config(pathJoin(APPCONFPATH, APPNAME + '.conf'))
    # Read some settings to variables, set default values and update some values
    APPCONF['autostart'] = checkAutoStart(APPAUTOSTARTDST)
    # Setup on-screen notification settings from config value
    APPCONF.setdefault('notifications', True)
    APPCONF.setdefault('theme', False)
    APPCONF.setdefault('fmextensions', True)
    APPCONF.setdefault('daemons', '~/.config/yandex-disk/config.cfg')
    # Is it a first run?
    if not APPCONF.readSuccess:
        LOGGER.info('No config, probably it is a first run.')
        # Create application config folders in ~/.config
        try:
            makeDirs(APPCONFPATH)
            makeDirs(pathJoin(APPCONFPATH, 'icons/light'))
            makeDirs(pathJoin(APPCONFPATH, 'icons/dark'))
            # Copy icon themes readme to user config catalogue
            copyFile(pathJoin(APPINSTPATH, 'icons/readme'), pathJoin(APPCONFPATH, 'icons/readme'))
        except:
            sysExit(_('Can\'t create configuration files in %s') % APPCONFPATH)
        # Activate indicator automatic start on system start-up
        if not pathExists(APPAUTOSTARTDST):
            try:
                makeDirs(expanduser('~/.config/autostart'))
                copyFile(APPAUTOSTARTSRC, APPAUTOSTARTDST)
                APPCONF['autostart'] = True
            except:
                LOGGER.error('Can\'t activate indicator automatic start on system start-up')

        # Activate FM actions according to config (as it is first run)
        activateActions(APPCONF['fmextensions'], APPINSTPATH)
        # Default settings should be saved (later)
        APPCONF.changed = True

    # Add new daemon if it is not in current list
    daemons = [expanduser(d) for d in CVal(APPCONF['daemons'])]
    if args.cfg:
        args.cfg = expanduser(args.cfg)
        if args.cfg not in daemons:
            daemons.append(args.cfg)
            APPCONF.changed = True
    # Remove daemon if it is in the current list
    if args.rcfg:
        args.rcfg = expanduser(args.rcfg)
        if args.rcfg in daemons:
            daemons.remove(args.rcfg)
            APPCONF.changed = True
    # Check that at least one daemon is in the daemons list
    if not daemons:
        sysExit(_('No daemons specified.\nCheck correctness of -r and -c options.'))
    # Update config if daemons list has been changed
    if APPCONF.changed:
        APPCONF['daemons'] = CVal(daemons).get()
        # Update configuration file
        APPCONF.save()

    # Make indicator objects for each daemon in daemons list
    APPINDICATORS = []
    for dm in daemons:
        APPINDICATORS.append(Indicator(dm, _('#%d ') % len(APPINDICATORS) if len(daemons) > 1 else ''))

    # Register the SIGINT/SIGTERM handler for graceful exit when indicator is killed
    unix_signal_add(PRIORITY_HIGH, SIGINT, appExit)
    unix_signal_add(PRIORITY_HIGH, SIGTERM, appExit)
    # Start GTK Main loop
    Gtk.main()