#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# gcdemu: Gtk+/AppIndicator CDEmu GUI
# Copyright (C) 2006-2014 Rok Mandeljc
#
# 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.

from __future__ import print_function

import argparse
import datetime
import os
import signal
import subprocess
import sys
import weakref

# gettext
import gettext

# Gtk, GObject, Notify, etc.
import gi

gi.require_version('GdkPixbuf', '2.0')
gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import GdkPixbuf
from gi.repository import Gtk
from gi.repository import Gio
from gi.repository import Notify

# AppIndicator
try:
    gi.require_version('AppIndicator3', '0.1')

    from gi.repository import AppIndicator3 as AppIndicator
    have_app_indicator = True
except:
    try:
        gi.require_version('AyatanaAppIndicator3', '0.1')

        from gi.repository import AyatanaAppIndicator3 as AppIndicator
        have_app_indicator = True
    except:
        have_app_indicator = False


# *** Globals ***
app_name = "gcdemu"
app_version = "3.2.6"
supported_daemon_interface_version = [ 7, 0 ]

# I18n
if sys.version_info[0] < 3:
    # Prior to python3, we need to explicitly enable unicode strings
    gettext.install(app_name, unicode=True)
else:
    gettext.install(app_name)


# Set process name
# "linux2" is for python2, "linux" is for python3
if sys.platform == "linux" or sys.platform == "linux2":
    # Process name must be a byte string...
    if isinstance(app_name, bytes):
        app_name_bytes = app_name
    else:
        app_name_bytes = app_name.encode('utf-8')

    try:
        import ctypes
        libc = ctypes.CDLL("libc.so.6")
        libc.prctl(15, app_name_bytes, 0, 0, 0) # 15 = PR_SET_NAME
    except Exception:
        pass


########################################################################
#               CDEmuDaemonProxy: Daemon proxy object                  #
########################################################################
class CDEmuDaemonProxy (GObject.GObject):
    _name = 'net.sf.cdemu.CDEmuDaemon'
    _object_path = '/Daemon'

    def __init__ (self):
        GObject.GObject.__init__(self)

        self.watcher_id = -1
        self.proxy_handler_ids = []
        self.proxy = None

    def cleanup (self):
        # Cancel watcher
        if self.watcher_id != -1:
            Gio.bus_unwatch_name(self.watcher_id)
            self.watcher_id = -1

        # Clean up the proxy
        if self.proxy:
            for handler in self.proxy_handler_ids:
                self.proxy.disconnect(handler)
            self.proxy_handler_ids = []
            self.proxy = None

    def connect_to_bus (self, use_system, autostart_daemon):
        # Cleanup
        self.cleanup()

        # Get the bus
        if use_system:
            bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
        else:
            bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)

        # Create proxy on our interface
        self.proxy = Gio.DBusProxy.new_sync(bus, 0, None, self._name, self._object_path, 'net.sf.cdemu.CDEmuDaemon', None)

        # Connect signal: g-signal
        handler_id = self.proxy.connect("g-signal", self.on_g_signal)
        self.proxy_handler_ids.append(handler_id)

        # Set up name watch; we do not try to autostart the daemon here
        self.watcher_id = Gio.bus_watch_name_on_connection(bus, self._name, 0, lambda c, n, no: self.emit("DaemonStarted"), lambda c, n: self.emit("DaemonStopped"))

        # Try to autostart daemon by pinging it
        if autostart_daemon:
            bus.call_sync(self._name, self._object_path, "org.freedesktop.DBus.Peer", "Ping", None, None, 0, -1, None)


    def on_g_signal (self, proxy, sender, signal, params):
        if signal == "DeviceStatusChanged":
            self.emit(signal, params[0])
        elif signal == "DeviceOptionChanged":
            self.emit(signal, params[0], params[1])
        elif signal == "DeviceMappingReady":
            self.emit(signal, params[0])
        elif signal == "DeviceAdded":
            self.emit(signal)
        elif signal == "DeviceRemoved":
            self.emit(signal)
        else:
            print("Unknown signal: ", sender, signal, params)


    def GetDaemonVersion (self):
        return self.proxy.GetDaemonVersion()

    def GetLibraryVersion (self):
        return self.proxy.GetLibraryVersion()

    def GetDaemonInterfaceVersion2 (self):
        return self.proxy.GetDaemonInterfaceVersion2()

    def EnumDaemonDebugMasks (self):
        return self.proxy.EnumDaemonDebugMasks()

    def EnumLibraryDebugMasks (self):
        return self.proxy.EnumLibraryDebugMasks()

    def EnumSupportedParsers (self):
        return self.proxy.EnumSupportedParsers()

    def EnumSupportedWriters (self):
        return self.proxy.EnumSupportedWriters()

    def EnumSupportedFilterStreams (self):
        return self.proxy.EnumSupportedFilterStreams()

    def EnumWriterParameters (self, writer_id):
        return self.proxy.EnumWriterParameters('(s)', writer_id)


    def GetNumberOfDevices (self):
        return self.proxy.GetNumberOfDevices()


    def DeviceGetMapping (self, device_number):
        return self.proxy.DeviceGetMapping('(i)', device_number)

    def DeviceGetStatus (self, device_number):
        return self.proxy.DeviceGetStatus('(i)', device_number)

    def DeviceLoad (self, device_number, filenames, parameters):
        return self.proxy.DeviceLoad('(iasa{sv})', device_number, filenames, parameters)

    def DeviceCreateBlank (self, device_number, filename, parameters):
        return self.proxy.DeviceCreateBlank('(isa{sv})', device_number, filename, parameters)

    def DeviceUnload (self, device_number):
        return self.proxy.DeviceUnload('(i)', device_number)

    def DeviceGetOption (self, device_number, option_name):
        return self.proxy.DeviceGetOption('(is)', device_number, option_name)

    def DeviceSetOption (self, device_number, option_name, option_value):
        return self.proxy.DeviceSetOption('(isv)', device_number, option_name, option_value)


    def AddDevice (self):
        return self.proxy.AddDevice()

    def RemoveDevice (self):
        return self.proxy.RemoveDevice()

GObject.type_register(CDEmuDaemonProxy)
GObject.signal_new("DaemonStarted", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("DaemonStopped", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())

GObject.signal_new("DeviceStatusChanged", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, ))

GObject.signal_new("DeviceOptionChanged", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, GObject.TYPE_STRING ))

GObject.signal_new("DeviceMappingReady", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, ))

GObject.signal_new("DeviceAdded", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("DeviceRemoved", CDEmuDaemonProxy,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())

########################################################################
#                   CDEmu: D-BUS interface object                      #
########################################################################
class CDEmu (GObject.GObject):
    def __init__ (self):
        GObject.GObject.__init__(self)

        self.daemon_proxy = CDEmuDaemonProxy()
        self.daemon_proxy.connect("DaemonStarted", lambda o: self.on_daemon_started())
        self.daemon_proxy.connect("DaemonStopped", lambda o: self.on_daemon_stopped())

        self.daemon_proxy.connect("DeviceAdded", lambda o: self.on_device_added())
        self.daemon_proxy.connect("DeviceRemoved", lambda o: self.on_device_removed())

        self.initial = True
        self.devices = []

    def cleanup (self):
        # Cleanup the devices
        self.devices = []


    def on_daemon_started (self):
        if not self.initial:
            self.emit("daemon-started")

        # Establish connection to daemon
        self.establish_connection()

        # Reset the initial run flag
        self.initial = False

    def on_daemon_stopped (self):
        if not self.initial:
            self.emit("daemon-stopped")

        # Cleanup
        self.emit("connection-lost")
        self.cleanup()

        # Reset the initial run flag
        self.initial = False

    def connect_to_bus (self, use_system, autostart_daemon):
        # Disconnect
        self.emit("connection-lost")

        # Cleanup
        self.cleanup()

        # Connect
        self.initial = True
        try:
            self.daemon_proxy.connect_to_bus(use_system, autostart_daemon)
        except GLib.Error as e:
            self.emit("error", _("Daemon autostart error"), _("Daemon autostart failed. Error:\n%s") % (e))


    def establish_connection (self):
        # Get the daemon interface version
        self.interface_version = self.daemon_proxy.GetDaemonInterfaceVersion2()

        # Validate the daemon interface version
        if self.interface_version[0] != supported_daemon_interface_version[0] or self.interface_version[1] < supported_daemon_interface_version[1]:
            self.emit("error", _("Incompatible daemon interface"), _("CDEmu daemon interface version %i.%i detected, but version %i.%i is required!") % (self.interface_version[0], self.interface_version[1], supported_daemon_interface_version[0], supported_daemon_interface_version[1]))
            # Cleanup
            self.cleanup()
            return

        # Get daemon version
        self.daemon_version = self.daemon_proxy.GetDaemonVersion()

        # Get library version
        self.library_version = self.daemon_proxy.GetLibraryVersion()

        # Get daemon debug masks
        self.daemon_debug_masks = self.daemon_proxy.EnumDaemonDebugMasks()

        # Get library debug masks
        self.library_debug_masks = self.daemon_proxy.EnumLibraryDebugMasks()

        # Get supported parsers
        self.supported_parsers = self.daemon_proxy.EnumSupportedParsers()

        # Get supported writers
        self.supported_writers = []
        writers = self.daemon_proxy.EnumSupportedWriters()
        for writer in writers:
            writer_id = writer[0]
            writer_name = writer[1]
            parameter_sheet = self.daemon_proxy.EnumWriterParameters(writer_id)
            self.supported_writers.append((writer[0], writer[1], parameter_sheet))

        # Get supported filter streams
        self.supported_filter_streams = self.daemon_proxy.EnumSupportedFilterStreams()

        # Get number of devices
        num_devices = self.daemon_proxy.GetNumberOfDevices()

        # Create devices
        for i in range(num_devices):
            self.devices.append(CDEmuDevice(i, self.daemon_proxy))

        # Emit signal
        self.emit("connection-established", num_devices)

    def on_device_added (self):
        # Create device object and append it to list
        num = len(self.devices)
        self.devices.append(CDEmuDevice(num, self.daemon_proxy))
        self.emit("device-added", num)

    def on_device_removed (self):
        # Remove from list
        dev = self.devices.pop()
        self.emit("device-removed", len(self.devices))

    def add_device (self):
        try:
            self.daemon_proxy.AddDevice()
        except GLib.Error as e:
            self.emit("error", _("Failed to add device"), _("Failed to add new device. Error:\n%s") % (e))

    def remove_device (self):
        try:
            self.daemon_proxy.RemoveDevice()
        except GLib.Error as e:
            self.emit("error", _("Failed to remove device"), _("Failed to remove device. Error:\n%s") % (e))

GObject.type_register(CDEmu)
GObject.signal_new("error", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_STRING, GObject.TYPE_STRING, ))
GObject.signal_new("daemon-started", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("daemon-stopped", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("connection-established", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, ))
GObject.signal_new("connection-lost", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("device-added", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, ))
GObject.signal_new("device-removed", CDEmu,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_INT, ))


########################################################################
#                 CDEmuDevice: device representation                   #
########################################################################
class CDEmuNeedPassword (Exception):
    def __init__ (self):
        pass

class CDEmuDevice (GObject.GObject):
    def __init__ (self, d, proxy):
        GObject.GObject.__init__(self)

        # Device number
        self.number = d

        # Daemon proxy
        self.proxy = proxy

        # Device status
        [ self.loaded, self.filenames ] = self.proxy.DeviceGetStatus(self.number)

        # Device mapping
        [ self.sr_path, self.sg_path ] = self.proxy.DeviceGetMapping(self.number)

        # Get device options
        self.dpm_emulation = self.proxy.DeviceGetOption(self.number, "dpm-emulation")
        self.tr_emulation = self.proxy.DeviceGetOption(self.number, "tr-emulation")
        self.bad_sector_emulation = self.proxy.DeviceGetOption(self.number, "bad-sector-emulation")
        self.dvd_report_css = self.proxy.DeviceGetOption(self.number, "dvd-report-css")
        self.device_id = self.proxy.DeviceGetOption(self.number, "device-id")
        self.daemon_debug_mask = self.proxy.DeviceGetOption(self.number, "daemon-debug-mask")
        self.library_debug_mask = self.proxy.DeviceGetOption(self.number, "library-debug-mask")

        # Note: we connect the following using weakref, otherwise we get circular references
        # and devices never get destroyed
        self.proxy.connect("DeviceStatusChanged", lambda p, d, obj=weakref.ref(self): obj().on_status_changed(d) if obj() else None)
        self.proxy.connect("DeviceOptionChanged", lambda p, d, o, obj=weakref.ref(self): obj().on_option_changed(d, o) if obj() else None)
        self.proxy.connect("DeviceMappingReady", lambda p, d, obj=weakref.ref(self): obj().on_mapping_ready(d) if obj() else None)


    def on_status_changed (self, d):
        if d != self.number:
            return

        # We may fail to retrieve status when a device was removed
        try:
            [ self.loaded, self.filenames ] = self.proxy.DeviceGetStatus(self.number)
            self.emit("status-changed")
        except:
            pass

    def on_option_changed (self, d, option):
        if d != self.number:
            return

        if option == "device-id":
            self.device_id = self.proxy.DeviceGetOption(self.number, "device-id")
            self.emit("device-id-changed")
        elif option == "dpm-emulation":
            self.dpm_emulation = self.proxy.DeviceGetOption(self.number, "dpm-emulation")
            self.emit("dpm-emulation-changed")
        elif option == "tr-emulation":
            self.tr_emulation = self.proxy.DeviceGetOption(self.number, "tr-emulation")
            self.emit("tr-emulation-changed")
        elif option == "bad-sector-emulation":
            self.bad_sector_emulation = self.proxy.DeviceGetOption(self.number, "bad-sector-emulation")
            self.emit("bad-sector-emulation-changed")
        elif option == "dvd-report-css":
            self.dvd_report_css = self.proxy.DeviceGetOption(self.number, "dvd-report-css")
            self.emit("dvd-report-css-changed")
        elif option == "daemon-debug-mask":
            self.daemon_debug_mask = self.proxy.DeviceGetOption(self.number, "daemon-debug-mask")
            self.emit("daemon-debug-mask-changed")
        elif option == "library-debug-mask":
            self.library_debug_mask = self.proxy.DeviceGetOption(self.number, "library-debug-mask")
            self.emit("library-debug-mask-changed")
        else:
            print("Unknown option: %s!" % option)


    def on_mapping_ready (self, d):
        if d != self.number:
            return

        # Check if mappings really changed
        changed = False
        [ sr_path, sg_path ] = self.proxy.DeviceGetMapping(self.number)

        if sr_path != self.sr_path:
            self.sr_path = sr_path
            changed = True

        if sg_path != self.sg_path:
            self.sg_path = sg_path
            changed = True

        if changed:
            self.emit("mapping-changed")


    def unload_device (self):
        try:
            self.proxy.DeviceUnload(self.number)
        except GLib.Error as e:
            self.emit("error", _("Failed to unload device #%02d:\n%s") % (self.number, e))


    def load_device (self, filenames, params = {}):
        try:
            self.proxy.DeviceLoad(self.number, filenames, params)
        except GLib.Error as e:
            if "net.sf.cdemu.CDEmuDaemon.errorMirage.EncryptedImage" in str(e):
                # Need password, raise proper exception...
                raise CDEmuNeedPassword()
            else:
                self.emit("error", _("Failed to load image %s to device #%02d:\n%s") % (";".join(filenames), self.number, e))

    def create_blank_disc (self, filename, params = {}):
        try:
            self.proxy.DeviceCreateBlank(self.number, filename, params)
        except GLib.Error as e:
            self.emit("error", _("Failed to create blank disc on device #%02d:\n%s") % (self.number, e))


    def set_device_id (self, value):
        if self.device_id == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "device-id", GLib.Variant('(ssss)', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set device ID for device #%02d to %s:\n%s") % (self.number, value, e))


    def set_dpm_emulation (self, value):
        if self.dpm_emulation == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "dpm-emulation", GLib.Variant('b', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set DPM emulation for device #%02d to %i:\n%s") % (self.number, value, e))

    def set_tr_emulation (self, value):
        if self.tr_emulation == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "tr-emulation", GLib.Variant('b', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set TR emulation for device #%02d to %i:\n%s") % (self.number, value, e))

    def set_bad_sector_emulation (self, value):
        if self.bad_sector_emulation == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "bad-sector-emulation", GLib.Variant('b', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set bad sector emulation for device #%02d to %i:\n%s") % (self.number, value, e))

    def set_dvd_report_css (self, value):
        if self.dvd_report_css == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "dvd-report-css", GLib.Variant('b', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set DVD report CSS/CPPM for device #%02d to %i:\n%s") % (self.number, value, e))


    def set_daemon_debug_mask (self, value):
        if self.daemon_debug_mask == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "daemon-debug-mask", GLib.Variant('i', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set daemon debug mask for device #%02d to 0x%X:\n%s") % (self.number, value, e))

    def set_library_debug_mask (self, value):
        if self.library_debug_mask == value:
            return

        try:
            self.proxy.DeviceSetOption(self.number, "library-debug-mask", GLib.Variant('i', value))
        except GLib.Error as e:
            self.emit("error", _("Failed to set library debug mask for device #%02d to 0x%X:\n%s") % (self.number, value, e))

GObject.type_register(CDEmuDevice)
GObject.signal_new("mapping-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("status-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("device-id-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("dpm-emulation-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("tr-emulation-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("bad-sector-emulation-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("dvd-report-css-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("daemon-debug-mask-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("library-debug-mask-changed", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("error", CDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_STRING, ))


########################################################################
#             gCDEmuDeviceStatusPage: device status page               #
########################################################################
class gCDEmuDeviceStatusPage (Gtk.Grid):
    def __init__ (self):
        Gtk.Grid.__init__(self)
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.set_row_spacing(5)
        self.set_column_spacing(5)

        # *** Frame: status ***
        frame = Gtk.Frame.new(_("Status"))
        frame.set_label_align(0.50, 0.50)
        frame.set_border_width(2)
        self.add(frame)

        grid = Gtk.Grid.new()
        grid.set_border_width(5)
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)

        frame.add(grid)

        # Loaded
        label = Gtk.Label.new(_("Loaded: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 0, 1, 1)

        label2 = Gtk.Label.new()
        label2.set_hexpand(True)
        grid.attach_next_to(label2, label, Gtk.PositionType.RIGHT, 1, 1)
        self.label_loaded = label2

        # Filename(s)
        label = Gtk.Label.new(_("File name(s): "))
        label.set_hexpand(True)
        grid.attach(label, 0, 1, 1, 1)

        label2 = Gtk.Label.new()
        label2.set_justify(Gtk.Justification.CENTER)
        label2.set_hexpand(True)
        grid.attach_next_to(label2, label, Gtk.PositionType.RIGHT, 1, 1)
        self.label_filename = label2

        # Separator
        separator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
        separator.set_hexpand(True)
        grid.attach(separator, 0, 2, 2, 1)

        # Load/create blank buttons
        button = Gtk.Button.new_with_label(_("Load"))
        button.set_hexpand(True)
        grid.attach(button, 0, 3, 1, 1)
        button.connect("clicked", lambda b: self.emit("load-unload-device"))
        self.button_load = button

        button = Gtk.Button.new_with_label(_("Create blank"))
        button.set_hexpand(True)
        grid.attach_next_to(button, self.button_load, Gtk.PositionType.RIGHT, 1, 1)
        button.connect("clicked", lambda b: self.emit("create-blank-disc"))
        self.button_create_blank = button

        # *** Frame: mapping ***
        frame = Gtk.Frame.new(_("Device mapping"))
        frame.set_label_align(0.50, 0.50)
        frame.set_border_width(2)
        self.add(frame)

        grid = Gtk.Grid.new()
        frame.add(grid)
        grid.set_border_width(5)
        grid.set_column_spacing(5)
        grid.set_row_spacing(2)

        # SCSI CD-ROM device
        label = Gtk.Label.new(_("SCSI CD-ROM device: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 0, 1, 1)

        label2 = Gtk.Label.new()
        label2.set_hexpand(True)
        grid.attach_next_to(label2, label, Gtk.PositionType.RIGHT, 1, 1)
        self.label_dev_sr = label2

        # SCSI generic device
        label = Gtk.Label.new(_("SCSI generic device: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 1, 1, 1)

        label2 = Gtk.Label.new()
        grid.attach_next_to(label2, label, Gtk.PositionType.RIGHT, 1, 1)
        self.label_dev_sg = label2

        # "Remove device" button
        # Center horizontally, put 5 pixels above the bottom (and make
        # sure it expands vertically)
        button = Gtk.Button.new_with_label(_("Remove device"))
        self.add(button)
        button.set_vexpand(True)
        button.set_valign(Gtk.Align.END)
        button.set_halign(Gtk.Align.CENTER)
        button.set_margin_bottom(5)
        button.connect("clicked", lambda b: self.emit("remove-device"))
        self.button_remove = button

        self.show_all()


    def set_device_mapping (self, sg_path, sr_path):
        self.label_dev_sr.set_text(sr_path)
        self.label_dev_sg.set_text(sg_path)

    def set_status (self, loaded, filenames):
        if loaded:
            text = "\n".join(map(os.path.basename, filenames))

            self.label_loaded.set_label(_("Yes"))
            self.label_filename.set_label(text)
            self.button_load.set_label(_("Unload"))

            self.button_create_blank.set_sensitive(False)
        else:
            self.label_loaded.set_label(_("No"))
            self.label_filename.set_label("")
            self.button_load.set_label(_("Load"))

            self.button_create_blank.set_sensitive(True)

    def set_removable (self, value):
        self.button_remove.set_sensitive(value)


GObject.type_register(gCDEmuDeviceStatusPage)
GObject.signal_new("load-unload-device", gCDEmuDeviceStatusPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("create-blank-disc", gCDEmuDeviceStatusPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())
GObject.signal_new("remove-device", gCDEmuDeviceStatusPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())

########################################################################
#            gCDEmuDeviceOptionsPage: device options page              #
########################################################################
class gCDEmuDeviceOptionsPage (Gtk.Grid):
    def __init__ (self):
        Gtk.Grid.__init__(self)
        self.set_orientation(Gtk.Orientation.VERTICAL)
        self.set_row_spacing(5)

        # *** Device ID ***
        # Device ID
        frame = Gtk.Frame.new(_("Device ID"))
        frame.set_label_align(0.50, 0.50)
        frame.set_border_width(2)
        self.add(frame)

        grid = Gtk.Grid.new()
        grid.set_border_width(5)
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)

        frame.add(grid)

        # Vendor ID
        label = Gtk.Label.new(_("Vendor ID: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 0, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_max_length(8)
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_vendor_id = entry

        # Product ID
        label = Gtk.Label.new(_("Product ID: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 1, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_max_length(16)
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_product_id = entry

        # Revision
        label = Gtk.Label.new(_("Revision: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 2, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_max_length(4)
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_revision = entry

        # Vendor-specific
        label = Gtk.Label.new(_("Vendor-specific: "))
        label.set_hexpand(True)
        grid.attach(label, 0, 3, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_max_length(20)
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_vendor_specific = entry

        # Separator
        separator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
        grid.attach(separator, 0, 4, 2, 1)

        # Button
        button = Gtk.Button.new_with_label(_("Set device ID"))
        button.set_hexpand(True)
        grid.attach(button, 0, 5, 2, 1)
        button.connect("clicked", self.on_set_device_id_clicked)

        # *** DPM emulation ***
        checkbutton = Gtk.CheckButton.new_with_label(_("DPM emulation"))
        checkbutton.connect("toggled", lambda t: self.emit("dpm-emulation-changed", t.get_active()))
        self.add(checkbutton)
        self.checkbutton_dpm = checkbutton

        # *** Transfer rate emulation ***
        checkbutton = Gtk.CheckButton.new_with_label(_("Transfer rate emulation"))
        checkbutton.connect("toggled", lambda t: self.emit("tr-emulation-changed", t.get_active()))
        self.add(checkbutton)
        self.checkbutton_tr = checkbutton

        # *** Bad sector emulation ***
        checkbutton = Gtk.CheckButton.new_with_label(_("Bad sector emulation"))
        checkbutton.connect("toggled", lambda t: self.emit("bad-sector-emulation-changed", t.get_active()))
        self.add(checkbutton)
        self.checkbutton_bad_sector = checkbutton

        # *** DVD report CSS ***
        checkbutton = Gtk.CheckButton.new_with_label(_("DVD report CSS/CPPM"))
        checkbutton.connect("toggled", lambda t: self.emit("dvd-report-css-changed", t.get_active()))
        self.add(checkbutton)
        self.checkbutton_dvd_report_css = checkbutton

        self.show_all()


    def set_device_id (self, device_id):
        self.entry_vendor_id.set_text(device_id[0])
        self.entry_product_id.set_text(device_id[1])
        self.entry_revision.set_text(device_id[2])
        self.entry_vendor_specific.set_text(device_id[3])

    def on_set_device_id_clicked (self, button):
        identity = ( self.entry_vendor_id.get_text(),
               self.entry_product_id.get_text(),
               self.entry_revision.get_text(),
               self.entry_vendor_specific.get_text() )
        self.emit("device-id-changed", identity)

    def set_dpm_emulation (self, value):
        self.checkbutton_dpm.set_active(value)

    def set_tr_emulation (self, value):
        self.checkbutton_tr.set_active(value)

    def set_bad_sector_emulation (self, value):
        self.checkbutton_bad_sector.set_active(value)

    def set_dvd_report_css (self, value):
        self.checkbutton_dvd_report_css.set_active(value)

GObject.type_register(gCDEmuDeviceOptionsPage)
GObject.signal_new("device-id-changed", gCDEmuDeviceOptionsPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_PYOBJECT,))
GObject.signal_new("dpm-emulation-changed", gCDEmuDeviceOptionsPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_BOOLEAN,))
GObject.signal_new("tr-emulation-changed", gCDEmuDeviceOptionsPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_BOOLEAN,))
GObject.signal_new("bad-sector-emulation-changed", gCDEmuDeviceOptionsPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_BOOLEAN,))
GObject.signal_new("dvd-report-css-changed", gCDEmuDeviceOptionsPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_BOOLEAN,))


########################################################################
#             gCDEmuDeviceDebugMaskPage: debug mask page               #
########################################################################
class gCDEmuDeviceDebugMaskPage (Gtk.Frame):
    def __init__ (self, name, masks_list):
        Gtk.Frame.__init__(self)
        self.set_label_align(0.50, 0.50)

        self.set_label(name)
        self.entries = []

        grid = Gtk.Grid.new()
        grid.set_orientation(Gtk.Orientation.VERTICAL)
        grid.set_border_width(5)
        grid.set_row_spacing(2)
        self.add(grid)

        # Create checkboxes
        for mask in masks_list:
            checkbutton = Gtk.CheckButton.new_with_label(mask[0])
            checkbutton.weight = mask[1]
            checkbutton.set_hexpand(True)
            grid.add(checkbutton)

            self.entries.append(checkbutton)

        # Separator
        separator = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
        separator.set_hexpand(True)
        separator.set_property("margin-bottom", 5)
        separator.set_property("margin-top", 5)
        grid.add(separator)

        # Button
        button = Gtk.Button.new_with_label(_("Set debug mask"))
        button.set_hexpand(True)
        grid.add(button)
        button.connect("clicked", self.on_set_debug_mask_clicked)

        self.show_all()


    def set_debug_mask (self, value):
        for entry in self.entries:
            entry.set_active(value & entry.weight)

    def on_set_debug_mask_clicked (self, button):
        # Get value
        value = 0
        for entry in self.entries:
            value |= entry.weight * entry.get_active()

        self.emit("debug-mask-changed", value)

GObject.type_register(gCDEmuDeviceDebugMaskPage)
GObject.signal_new("debug-mask-changed", gCDEmuDeviceDebugMaskPage,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_UINT,))


########################################################################
#           gCDEMuFileChooserDialog: file chooser dialog               #
########################################################################
class gCDEMuFileChooserDialog (Gtk.FileChooserDialog):
    def __init__ (self, parsers, filter_streams):
        Gtk.FileChooserDialog.__init__(self, title=_("Open file"), action=Gtk.FileChooserAction.OPEN)
        self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Open"), Gtk.ResponseType.ACCEPT)

        self.set_select_multiple(True)
        self.set_local_only(False)

        # Set filters based on supported parser information
        filter_entry = Gtk.FileFilter.new()
        filter_entry.set_name(_("All files"))
        filter_entry.add_pattern("*")
        self.add_filter(filter_entry)

        all_images = Gtk.FileFilter.new()
        all_images.set_name(_("All image files"))
        self.add_filter(all_images)

        for info in parsers:
            for description, mime_type in info[2]:
                filter_entry = Gtk.FileFilter.new()

                filter_entry.set_name(description)
                filter_entry.add_mime_type(mime_type)

                all_images.add_mime_type(mime_type)

                self.add_filter(filter_entry)

        for info in filter_streams:
            for description, mime_type in info[3]:
                filter_entry = Gtk.FileFilter.new()

                filter_entry.set_name(description)
                filter_entry.add_mime_type(mime_type)

                all_images.add_mime_type(mime_type)

                self.add_filter(filter_entry)

        # Extra options
        widget = self.create_extra_options_widget()
        self.set_extra_widget(widget)

    def run (self):
        # Clear parameters
        self.combobox_encoding.set_active(0)
        self.entry_password.set_text("")

        # Run the dialog
        return Gtk.FileChooserDialog.run(self)

    def get_parameters (self):
        parameters = {}

        # Encoding
        i = self.combobox_encoding.get_active_iter()
        encoding = self.combobox_encoding.get_model().get_value(i, 1)
        if encoding:
            parameters["encoding"] = GLib.Variant("s", encoding)

        # Password
        password = self.entry_password.get_text()
        if password:
            parameters["password"] = GLib.Variant("s", password)

        return parameters

    def create_extra_options_widget (self):
        # Expander with grid
        expander = Gtk.Expander.new(_("Extra Options"))

        grid = Gtk.Grid.new()
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)
        expander.add(grid)

        # *** Encoding ***
        # Label
        label = Gtk.Label.new(_("Encoding: "))
        grid.attach(label, 0, 0, 1, 1)

        # Combo box
        liststore = self.get_encodings_list()
        combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
        combobox.set_entry_text_column(0)
        grid.attach_next_to(combobox, label, Gtk.PositionType.RIGHT, 1, 1)
        self.combobox_encoding = combobox

        # Autocompletion
        completion = Gtk.EntryCompletion.new()
        completion.set_model(liststore)
        completion.set_text_column(0)
        combobox.get_child().set_completion(completion)


        # *** Password ***
        # Label
        label = Gtk.Label.new(_("Password: "))
        grid.attach(label, 0, 1, 1, 1)

        # Entry
        entry = Gtk.Entry.new()
        entry.set_visibility(False)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_password = entry


        expander.show_all()

        return expander

    def get_encodings_list (self):
        # Get list of encodings supported by iconv (since that's what
        # GLib and consequently CDEmu/libMirage uses)
        encodings = []
        try:
            cmd = [ 'iconv' , '--list']
            iconv = subprocess.Popen(cmd, env = {'LANG' : 'C'}, stdout=subprocess.PIPE, stdin=None, stderr=None)
            iconv_lines = iconv.communicate()[0]
            # Note: In Python 3 the communicate() method returns a bytearray instead of a string.
            if type(iconv_lines) != str:
                iconv_lines = iconv_lines.decode()
            encodings = [ line.strip('/').lower() for line in iconv_lines.splitlines() ]
        except OSError:
            # Probably due to iconv missing; not a big deal, we just won't
            # display encodings list
            pass

        # Create ListStore
        liststore = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_STRING ])
        liststore.append( ("Autodetect", "") )
        for encoding in encodings:
            liststore.append( (encoding, encoding) )

        return liststore



########################################################################
#               gCDEMuPasswordDialog: password dialog                  #
########################################################################
class gCDEMuPasswordDialog (Gtk.Dialog):
    def __init__ (self, *args, **kwargs):
        Gtk.Dialog.__init__(self, *args, **kwargs)
        self.set_title(_("Enter password"))
        self.add_buttons(_("OK"), Gtk.ResponseType.OK, _("Cancel"), Gtk.ResponseType.REJECT)
        self.set_default_response(Gtk.ResponseType.OK)

        # Grid
        grid = Gtk.Grid.new()
        grid.set_border_width(5)
        grid.set_row_spacing(5)
        grid.set_column_spacing(5)
        self.vbox.add(grid)

        # Label
        label = Gtk.Label.new(_("The image you are trying to load is encrypted."))
        grid.attach(label, 0, 0, 2, 1)

        # Label & text entry
        label = Gtk.Label.new(_("Password: "))
        grid.attach(label, 0, 1, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        entry.connect("activate", lambda w: self.response(Gtk.ResponseType.OK)) # Allow the user to press enter to do OK
        entry.set_visibility(False)
        self.entry = entry

        self.vbox.show_all()

    def get_password (self):
        return self.entry.get_text()

    def run (self):
        # Clear the password field
        self.entry.set_text("")

        # Chain to parent
        return super(gCDEMuPasswordDialog, self).run()


########################################################################
#   gCDEMuCreateBlankImageDialog: blank disc image creation dialog     #
########################################################################
class gCDEMuCreateBlankImageDialog (Gtk.Dialog):
    class ValidationError (Exception): pass

    def __init__ (self, writers):
        Gtk.Dialog.__init__(self)
        self.set_title(_("Create blank disc image"))
        self.add_buttons(_("OK"), Gtk.ResponseType.ACCEPT, _("Cancel"), Gtk.ResponseType.REJECT)

        self.writers = writers

        # Frame: image settings
        frame = Gtk.Frame.new(_("Image"))
        frame.set_label_align(0.50, 0.50)
        frame.set_border_width(2)
        self.vbox.add(frame)

        grid = Gtk.Grid.new()
        grid.set_border_width(5)
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)
        frame.add(grid)

        # Filename
        label = Gtk.Label.new(_("Filename: "))
        grid.attach(label, 0, 0, 1, 1)

        entry = Gtk.Entry.new()
        entry.set_hexpand(True)
        grid.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1)
        self.entry_filename = entry

        button = Gtk.Button.new_with_label(_("Choose"))
        grid.attach_next_to(button, entry, Gtk.PositionType.RIGHT, 1, 1)
        button.connect("clicked", lambda b: self.choose_file())

        # Medium type
        label = Gtk.Label.new(_("Medium type: "))
        grid.attach(label, 0, 1, 1, 1)

        liststore = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_STRING ])
        liststore.append( ("CD-R 74 min (650 MB)", "cdr74") )
        liststore.append( ("CD-R 80 min (700 MB)", "cdr80") )
        liststore.append( ("CD-R 90 min (800 MB)", "cdr90") )
        liststore.append( ("CD-R 99 min (900 MB)", "cdr99") )
        liststore.append( ("DVD+R SL", "dvd+r") )
        liststore.append( ("BD-R SL", "bdr") )

        combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
        combobox.set_entry_text_column(0)
        combobox.set_active(0)
        combobox.set_hexpand(True)
        grid.attach_next_to(combobox, label, Gtk.PositionType.RIGHT, 2, 1)
        self.combobox_medium = combobox

        completion = Gtk.EntryCompletion.new()
        completion.set_model(liststore)
        completion.set_text_column(0)
        combobox.get_child().set_completion(completion)

        # Writer
        label = Gtk.Label.new(_("Writer: "))
        grid.attach(label, 0, 2, 1, 1)

        liststore = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_INT ])
        for i, writer in enumerate(self.writers):
            liststore.append( (writer[0], i) )

        combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
        combobox.set_entry_text_column(0)
        combobox.set_hexpand(True)
        grid.attach_next_to(combobox, label, Gtk.PositionType.RIGHT, 2, 1)
        combobox.connect("changed", lambda c: self.build_writer_options())
        self.combobox_writer = combobox

        completion = Gtk.EntryCompletion.new()
        completion.set_model(liststore)
        completion.set_text_column(0)
        combobox.get_child().set_completion(completion)

        # Frame: writer options
        frame = Gtk.Frame.new(_("Writer options"))
        frame.set_label_align(0.50, 0.50)
        frame.set_border_width(2)
        self.vbox.add(frame)

        self.frame_writer = frame

        self.writer_options_ui = None
        self.writer_options_widgets = None

        self.vbox.show_all()
        self.frame_writer.hide()

    def choose_file (self):
        dialog = Gtk.FileChooserDialog(
            title=_("Choose file"),
            parent=self,
            action=Gtk.FileChooserAction.SAVE)
        dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, _("Choose"), Gtk.ResponseType.ACCEPT)
        dialog.set_local_only(False)
        dialog.set_do_overwrite_confirmation(True)

        if dialog.run() == Gtk.ResponseType.ACCEPT:
            self.entry_filename.set_text(dialog.get_filename())

        dialog.destroy()

    def build_writer_options (self):
        # Destroy old GUI and widgets map
        if self.writer_options_ui is not None:
            self.writer_options_ui.destroy()
            self.writer_options_ui = None

        if self.writer_options_widgets is not None:
            self.writer_options_widgets = None

        # Hide Writer frame
        self.frame_writer.hide()

        # Get selected writer
        i = self.combobox_writer.get_active_iter()
        if i is None:
            return
        writer_idx = self.combobox_writer.get_model().get_value(i, 1)
        writer = self.writers[writer_idx]

        parameter_sheet = writer[2]

        # Create GUI
        grid = Gtk.Grid.new()
        grid.set_border_width(5)
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)
        self.frame_writer.add(grid)

        self.writer_options_ui = grid # Store so we can destroy it
        self.writer_options_widgets = {} # ID -> widget map

        row = 0

        for parameter_entry in parameter_sheet:
            parameter_id = parameter_entry[0]
            parameter_name = parameter_entry[1]
            parameter_description = parameter_entry[2]
            parameter_default = parameter_entry[3]
            parameter_enum = parameter_entry[4]

            needs_label = True

            if len(parameter_enum):
                # Enum; create a combo box
                # Store
                liststore = Gtk.ListStore.new([ GObject.TYPE_STRING, GObject.TYPE_STRING ])
                for enum_value in parameter_enum:
                    liststore.append( (enum_value, enum_value) )

                # Combo box
                widget = Gtk.ComboBox.new_with_model_and_entry(liststore)
                widget.set_entry_text_column(0)

                completion = Gtk.EntryCompletion.new()
                completion.set_model(liststore)
                completion.set_text_column(0)
                widget.get_child().set_completion(completion)

                # Default value
                for entry in liststore:
                    if entry[1] == parameter_default:
                        widget.set_active_iter(entry.iter)
                        break
            elif isinstance(parameter_default, str):
                # String; create a text entry
                widget = Gtk.Entry.new()
                # Default value
                widget.set_text(parameter_default)
            elif isinstance(parameter_default, bool):
                # Boolean; create a check button
                widget = Gtk.CheckButton.new_with_label(parameter_name)
                widget.set_tooltip_text(parameter_description)
                needs_label = False
                # Default value
                widget.set_active(parameter_default)
            elif isinstance(parameter_default, int):
                # Integer; create a pin button
                widget = Gtk.SpinButton.new()
                # Default value
                widget.set_value(parameter_default)
            else:
                continue

            if needs_label:
                label = Gtk.Label.new("%s: " % parameter_name)
                label.set_tooltip_text(parameter_description)
                grid.attach(label, 0, row, 1, 1)

                widget.set_hexpand(True)
                grid.attach_next_to(widget, label, Gtk.PositionType.RIGHT, 1, 1)
            else:
                grid.attach(widget, 0, row, 2, 1)

            self.writer_options_widgets[parameter_id] = widget

            row += 1

        # Show Writer frame
        self.frame_writer.show_all()

    def get_filename (self):
        return self.entry_filename.get_text()

    def get_writer_parameters (self):
        parameters = {}

        # Writer ID
        i = self.combobox_writer.get_active_iter()
        writer_idx = self.combobox_writer.get_model().get_value(i, 1)
        parameters["writer-id"] = GLib.Variant("s", self.writers[writer_idx][0])

        # Medium type
        i = self.combobox_medium.get_active_iter()
        parameters["medium-type"] = GLib.Variant("s", self.combobox_medium.get_model().get_value(i, 1))

        # Writer parameters
        # NOTE: we do not need to wrap items() in list(), because we do
        # not modify the source dictionary during iteration
        for parameter_id, widget in self.writer_options_widgets.items():
            if isinstance(widget, Gtk.Entry):
                # Text entry
                value = GLib.Variant("s", widget.get_text())
            elif isinstance(widget, Gtk.SpinButton):
                # Spin button
                value = GLib.Variant("i", widget.get_value())
            elif isinstance(widget, Gtk.CheckButton):
                # Check button
                value = GLib.Variant("b", widget.get_active())
            elif isinstance(widget, Gtk.ComboBox):
                # Combo box
                i = widget.get_active_iter()
                value = GLib.Variant("s", widget.get_model().get_value(i, 1))
            else:
                continue

            parameters[parameter_id] = value

        return parameters

    def run (self):
        # Run in loop and validate when user clicks OK
        while True:
            # Chain to parent
            result = super(gCDEMuCreateBlankImageDialog, self).run()
            if result == Gtk.ResponseType.ACCEPT:
                # Validate
                try:
                    # Image filename must be set
                    if self.entry_filename.get_text() == "":
                        raise self.ValidationError(_("Image filename/basename not set!"))
                    # Writer must be chosen
                    if self.combobox_writer.get_active_iter() is None:
                        raise self.ValidationError(_("No image writer is chosen!"))
                    break
                except self.ValidationError as e:
                    # Show error dialog
                    message = Gtk.MessageDialog(
                        parent=self,
                        message_type=Gtk.MessageType.ERROR,
                        buttons=Gtk.ButtonsType.CLOSE,
                        text=e)
                    message.set_title(_("Invalid option"))
                    message.run()
                    message.destroy()
            else:
                break

        return result


########################################################################
#                            gCDEmuDevice                              #
########################################################################
class gCDEmuDevice (GObject.GObject):
    def connect_signal (self, obj, sig, callback):
        signal = obj.connect(sig, callback)
        self.signals.append((obj, signal))


    def __init__ (self, cdemu, d):
        GObject.GObject.__init__(self)

        self.signals = []

        self.number = d
        self.device = cdemu.devices[d]

        # Create menu item
        self.menu_item = Gtk.MenuItem()
        self.menu_item.show()
        # Note: signal for menu item is connected in the child class,
        # because Gtk and AppIndicator versions handle different signals

        # Create file chooser dialog
        self.file_chooser = gCDEMuFileChooserDialog(cdemu.supported_parsers, cdemu.supported_filter_streams)

        # Create blank image creation dialog
        self.blank_disc_creation_dialog = gCDEMuCreateBlankImageDialog(cdemu.supported_writers)

        # Create password dialog
        self.password_dialog = gCDEMuPasswordDialog()

        # *** GUI ***
        # Create dialog
        self.dialog = Gtk.Dialog()
        self.dialog.set_title(_("Device #%02d: properties") % d)
        self.dialog.add_buttons(_("Close"), Gtk.ResponseType.CLOSE)
        self.dialog.set_border_width(5)
        self.dialog.vbox.set_border_width(5)
        # Hide the dialog when closed
        self.connect_signal(self.dialog, "delete-event", lambda w, e: w.hide() or True)
        self.connect_signal(self.dialog, "response", lambda w, e: w.hide() or True)

        # Label
        label = Gtk.Label(label="<b><big>" + (_("Device #%02d") % (self.number)) + "</big></b>")
        label.show()
        label.set_use_markup(True)
        self.dialog.vbox.add(label)

        # Notebook
        notebook = Gtk.Notebook()
        notebook.show()
        notebook.set_tab_pos(Gtk.PositionType.LEFT)
        notebook.set_border_width(5)
        self.dialog.vbox.add(notebook)

        # Page: status
        self.page_status = gCDEmuDeviceStatusPage()
        self.page_status.set_device_mapping(self.device.sg_path, self.device.sr_path)
        notebook.append_page(self.page_status, Gtk.Label(label=_("Status")))

        self.connect_signal(self.page_status, "load-unload-device", lambda w: self.load_unload_device())
        self.connect_signal(self.page_status, "create-blank-disc", lambda w: self.create_blank_disc())
        self.connect_signal(self.page_status, "remove-device", lambda w: cdemu.remove_device())

        # Page: options
        self.page_options = gCDEmuDeviceOptionsPage()
        notebook.append_page(self.page_options, Gtk.Label(label=_("Options")))
        self.connect_signal(self.page_options, "device-id-changed", lambda w, id: self.device.set_device_id(id))
        self.connect_signal(self.page_options, "dpm-emulation-changed", lambda w, v: self.device.set_dpm_emulation(v))
        self.connect_signal(self.page_options, "tr-emulation-changed", lambda w, v: self.device.set_tr_emulation(v))
        self.connect_signal(self.page_options, "bad-sector-emulation-changed", lambda w, v: self.device.set_bad_sector_emulation(v))
        self.connect_signal(self.page_options, "dvd-report-css-changed", lambda w, v: self.device.set_dvd_report_css(v))

        # Page: daemon debug mask
        self.page_daemon = gCDEmuDeviceDebugMaskPage(_("Daemon debug mask"), cdemu.daemon_debug_masks)
        notebook.append_page(self.page_daemon, Gtk.Label(label=_("Daemon")))
        self.connect_signal(self.page_daemon, "debug-mask-changed", lambda w, v: self.device.set_daemon_debug_mask(v))

        # Page: library debug mask
        self.page_library = gCDEmuDeviceDebugMaskPage(_("Library debug mask"), cdemu.library_debug_masks)
        notebook.append_page(self.page_library, Gtk.Label(label=_("Library")))
        self.connect_signal(self.page_library, "debug-mask-changed", lambda w, v: self.device.set_library_debug_mask(v))

        # Connect device signals
        self.connect_signal(self.device, "mapping-changed", lambda w: self.page_status.set_device_mapping(self.device.sg_path, self.device.sr_path))
        self.connect_signal(self.device, "status-changed", lambda w: self.update_status(True))
        self.connect_signal(self.device, "device-id-changed", lambda w: self.update_device_id(True))
        self.connect_signal(self.device, "dpm-emulation-changed", lambda w: self.update_dpm_emulation(True))
        self.connect_signal(self.device, "tr-emulation-changed", lambda w: self.update_tr_emulation(True))
        self.connect_signal(self.device, "bad-sector-emulation-changed", lambda w: self.update_bad_sector_emulation(True))
        self.connect_signal(self.device, "dvd-report-css-changed", lambda w: self.update_dvd_report_css(True))
        self.connect_signal(self.device, "daemon-debug-mask-changed", lambda w: self.update_daemon_debug_mask(True))
        self.connect_signal(self.device, "library-debug-mask-changed", lambda w: self.update_library_debug_mask(True))
        self.connect_signal(self.device, "error", lambda w, t: self.show_error(t))

        # Some pages require refresh when they are shown...
        self.connect_signal(notebook, "switch-page", lambda n, p, pp: self.refresh_pages())
        self.connect_signal(self.dialog, "show", lambda w: self.refresh_pages())

        # Manually perform the initial update...
        self.update_status(False)
        self.update_device_id(False)
        self.update_dpm_emulation(False)
        self.update_tr_emulation(False)
        self.update_bad_sector_emulation(False)
        self.update_dvd_report_css(False)
        self.update_daemon_debug_mask(False)
        self.update_library_debug_mask(False)


    def cleanup (self):
        # Release reference to device
        self.device = None

        # Cleanup signal handlers
        for (obj, handler) in self.signals:
            obj.disconnect(handler)

        # Explicitly destroy dialogs, because they might still be running
        self.dialog.destroy()
        self.file_chooser.destroy()
        self.menu_item.destroy()
        self.password_dialog.destroy()

    def update_status (self, notify = False):
        # Menu item
        if self.device.loaded:
            images = os.path.basename(self.device.filenames[0]) # Make it short
            if len(self.device.filenames) > 1:
                images += ", ..." # Indicate there's more than one file
            self.menu_item.set_label("%s #%02d: %s" % (_("Device"), self.device.number, images))
        else:
            self.menu_item.set_label("%s #%02d: %s" % (_("Device"), self.device.number, _("Empty")))

        # Status page in the device dialog
        self.page_status.set_status(self.device.loaded, self.device.filenames)

        # Notification
        if notify:
            if self.device.loaded:
                self.emit("device-notification", _("Device status change"), _("Device #%02d has been loaded.") % (self.device.number))
            else:
                self.emit("device-notification", _("Device status change"), _("Device #%02d has been emptied.") % (self.device.number))

    def update_device_id (self, notify = False):
        self.page_options.set_device_id(self.device.device_id)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its device ID changed:\n  Vendor ID: '%s'\n  Product ID: '%s'\n  Revision: '%s'\n  Vendor-specific: '%s'") % (self.device.number, self.device.device_id[0], self.device.device_id[1], self.device.device_id[2], self.device.device_id[3]))

    def update_dpm_emulation (self, notify = False):
        self.page_options.set_dpm_emulation(self.device.dpm_emulation)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its DPM emulation option changed. New value: %s") % (self.device.number, self.device.dpm_emulation))

    def update_tr_emulation (self, notify = False):
        self.page_options.set_tr_emulation(self.device.tr_emulation)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its TR emulation option changed. New value: %s") % (self.device.number, self.device.tr_emulation))

    def update_bad_sector_emulation (self, notify = False):
        self.page_options.set_bad_sector_emulation(self.device.bad_sector_emulation)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its bad sector emulation option changed. New value: %s") % (self.device.number, self.device.bad_sector_emulation))

    def update_dvd_report_css (self, notify = False):
        self.page_options.set_dvd_report_css(self.device.dvd_report_css)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its DVD report CSS/CPPM option changed. New value: %s") % (self.device.number, self.device.dvd_report_css))

    def update_daemon_debug_mask (self, notify = False):
        self.page_daemon.set_debug_mask(self.device.daemon_debug_mask)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its daemon debug mask changed. New value: 0x%X") % (self.device.number, self.device.daemon_debug_mask))

    def update_library_debug_mask (self, notify = False):
        self.page_library.set_debug_mask(self.device.library_debug_mask)
        if notify:
            self.emit("device-notification", _("Device option change"), _("Device #%02d has had its library debug mask changed. New value: 0x%X") % (self.device.number, self.device.library_debug_mask))

    def load_unload_device (self):
        if self.device.loaded:
            self.device.unload_device()
        else:
            result = self.file_chooser.run()
            self.file_chooser.hide()
            if result == Gtk.ResponseType.ACCEPT:
                # Get filename and parameters
                filenames = self.file_chooser.get_filenames()
                parameters = self.file_chooser.get_parameters()

                # Try loading
                try:
                    self.device.load_device(filenames, parameters)
                except CDEmuNeedPassword as c:
                    # Get password
                    result = self.password_dialog.run()
                    self.password_dialog.hide()
                    if result == Gtk.ResponseType.OK:
                        password = self.password_dialog.get_password()
                        # Try loading with overriden password
                        parameters["password"] = GLib.Variant("s", password)
                        self.device.load_device(filenames, parameters)

    def create_blank_disc (self):
        result = self.blank_disc_creation_dialog.run()
        self.blank_disc_creation_dialog.hide()
        if result == Gtk.ResponseType.ACCEPT:
            filename = self.blank_disc_creation_dialog.get_filename()
            writer_parameters = self.blank_disc_creation_dialog.get_writer_parameters()
            self.device.create_blank_disc(filename, writer_parameters)

    def refresh_pages (self):
        # Some pages have fields that can be altered without applying the
        # change. We want to reset those changes when a page is changed
        # or when the dialog is shown...
        self.update_device_id(False)
        self.update_daemon_debug_mask(False)
        self.update_library_debug_mask(False)


    def show_error (self, text):
         # Show error dialog
        message = Gtk.MessageDialog(
            parent=None,
            message_type=Gtk.MessageType.ERROR,
            buttons=Gtk.ButtonsType.CLOSE,
            text=text)
        message.set_title(_("Device error"))
        message.run()
        message.destroy()

    def set_removable (self, value):
        # Forward to status page
        self.page_status.set_removable(value)

GObject.type_register(gCDEmuDevice)
GObject.signal_new("device-notification", gCDEmuDevice,
    GObject.SignalFlags.RUN_FIRST,
    None,
    (GObject.TYPE_STRING, GObject.TYPE_STRING))


class gCDEmuDevice_Gtk (gCDEmuDevice):
    def __init__ (self, cdemu, d):
        gCDEmuDevice.__init__(self, cdemu, d)

        self.connect_signal(self.menu_item, "button-press-event", self.on_menu_item_button_press_event)

    def on_menu_item_button_press_event (self, widget, event):
        if event.button == 1:
            # Left click: device dialog
            self.dialog.present() # Make sure the window is always on top
            # Don't "run" the dialog, because we want it to be non-modal
            #self.dialog.run()
            #self.dialog.hide()
        else:
            # Right click: quick load/unload
            self.load_unload_device()

    def update_status (self, notify = False):
        gCDEmuDevice.update_status(self, notify)

        if self.device.loaded:
            self.menu_item.set_tooltip_text(_("Left click for Property Dialog, right click to unload."))
        else:
            self.menu_item.set_tooltip_text(_("Left click for Property Dialog, right click to load."))


class gCDEmuDevice_Indicator (gCDEmuDevice):
    def __init__ (self, cdemu, d):
        gCDEmuDevice.__init__(self, cdemu, d)

        self.menu_item.set_tooltip_text(_("Click for Property Dialog"))

        self.connect_signal(self.menu_item, "activate", self.on_menu_item_activate)

    def on_menu_item_activate (self, widget):
        # Left click: device dialog
        self.dialog.present() # Make sure the window is always on top
        # Don't "run" the dialog, because we want it to be non-modal
        #self.dialog.run()
        #self.dialog.hide()

########################################################################
#                               gCDEmu                                 #
########################################################################
class gCDEmuTray (GObject.GObject):
    def load_logo (self):
        # Lookup "icon" in standard paths
        icon_info = Gtk.IconTheme().lookup_icon("gcdemu", 0, 0)
        if icon_info is None:
            print("Pixmap '%s' not found in standard pixmap paths!" % ("gcdemu"))
            self.logo = None
            return
        logo_filename = icon_info.get_filename()
        if logo_filename is None:
            print("Failed to get icon filename; Do you have the librsvg2 package?")
            self.logo = None
            return

        # Load
        self.logo = GdkPixbuf.Pixbuf.new_from_file_at_size(logo_filename, 156, 156)


    def cleanup (self):
        # Default state: disconnected
        self.connected = False
        self.update_icon()

        self.item_new_device.set_sensitive(False)

        # Remove devices
        num_devices = len(self.devices)
        for i in reversed(range(num_devices)):
            self.remove_device(i)

    def connect_to_daemon (self):
        # Cleanup
        self.cleanup()

        # Establish connection
        self.cdemu.connect_to_bus(self.use_system_bus, self.daemon_autostart)


    def __init__ (self):
        GObject.GObject.__init__(self)

        self.connected = False
        self.devices = []

        # *** Configuration ***
        self.settings = Gio.Settings.new("net.sf.cdemu.gcdemu")

        # Watch over our settings
        self.settings.connect("changed", lambda s, k: self.on_settings_changed(k))

        # Notifications
        if Notify.init(app_name):
            self.show_notifications = self.settings.get_boolean("show-notifications")
        else:
            # Disable because pynotify initialization failed
            self.show_notifications = False

        # Which bus to use
        self.use_system_bus = self.settings.get_boolean("use-system-bus")

        # Daemon autostart
        self.daemon_autostart = self.settings.get_boolean("daemon-autostart")

        # Icon policy
        self.icon_policy = self.settings.get_string("icon-policy")

        # Icon names
        self.icon_connected = self.settings.get_string("icon-connected")
        self.icon_disconnected = self.settings.get_string("icon-disconnected")

        # Load logo
        self.load_logo()

        # *** The About dialog ***
        self.about = Gtk.AboutDialog()
        self.about.set_name(app_name)
        self.about.set_version(app_version)
        self.about.set_copyright("Copyright (C) 2006-%d Rok Mandeljc"  % (datetime.date.today().year))
        self.about.set_comments(_("A GUI for controlling CDEmu devices."))
        self.about.set_website("http://cdemu.sf.net")
        self.about.set_website_label(_("The CDEmu project website"))
        self.about.set_authors([ "Rok Mandeljc <rok.mandeljc@gmail.com>" ])
        self.about.set_artists([ "Rômulo Fernandes <abra185@gmail.com>" ])
        self.about.set_translator_credits(_("translator-credits"))
        self.about.set_logo(self.logo)

        # *** Menu ***
        self.menu = Gtk.Menu()

        # Devices
        item = Gtk.MenuItem.new_with_label(_("Devices"))
        item.set_sensitive(False)
        self.menu.append(item)

        item = Gtk.MenuItem.new_with_label(_("New device..."))
        item.set_sensitive(False)
        item.connect("activate", self.on_new_device_activate)
        self.menu.append(item)
        self.item_new_device = item

        # Separator
        self.menu.append(Gtk.SeparatorMenuItem())

        # Use system bus
        #item = Gtk.CheckMenuItem.new_with_mnemonic(_("Use _system bus"))
        #item.set_active(self.use_system_bus)
        #item.connect("toggled", self.on_use_system_bus_toggled)
        #self.menu.append(item)
        #self.item_use_system_bus = item

        # Show notifications
        item = Gtk.CheckMenuItem.new_with_mnemonic(_("Show _notifications"))
        item.set_active(self.show_notifications)
        item.connect("toggled", self.on_show_notifications_toggled)
        self.menu.append(item)
        self.item_show_notifications = item

        # Separator
        self.menu.append(Gtk.SeparatorMenuItem())

        # About
        item = Gtk.MenuItem.new_with_label(_("About"))
        item.connect("activate", self.on_about_activated)
        self.menu.append(item)

        # Separator
        self.menu.append(Gtk.SeparatorMenuItem())

        # Quit
        item = Gtk.MenuItem.new_with_label(_("Quit"))
        item.connect("activate", self.on_quit_activated)
        self.menu.append(item)

        self.menu.show_all()

        # *** Create the CDEmu object ***
        self.cdemu = CDEmu()
        self.cdemu.connect("daemon-started", self.on_daemon_started)
        self.cdemu.connect("daemon-stopped", self.on_daemon_stopped)
        self.cdemu.connect("connection-established", self.on_connection_established)
        self.cdemu.connect("connection-lost", self.on_connection_lost)
        self.cdemu.connect("error", lambda c, t, m: self.show_error(t, m))
        self.cdemu.connect("device-added", lambda c, d: self.on_device_added(d))
        self.cdemu.connect("device-removed", lambda c, d: self.on_device_removed(d))


    def on_settings_changed (self, key):
        if key == "use-system-bus":
            self.use_system_bus = self.settings.get_boolean(key)
            print("New setting: use system bus: %d" % self.use_system_bus)
            self.item_use_system_bus.set_active(self.use_system_bus)
            self.cdemu.connect_to_bus(self.use_system_bus, self.daemon_autostart)
        elif key == "show-notifications":
            self.show_notifications = self.settings.get_boolean(key)
            self.item_show_notifications.set_active(self.show_notifications)
            print("New setting: show notifications: %d" % self.show_notifications)
        elif key == "icon-connected":
            self.icon_connected = self.settings.get_string(key)
            self.update_icon()
        elif key == "icon-disconnected":
            self.icon_disconnected = self.settings.get_string(key)
            self.update_icon()
        elif key == "daemon-autostart":
            self.daemon_autostart = self.settings.get_boolean(key)
            print("New setting: daemon autostart: %d" % self.daemon_autostart)
        elif key == "icon-policy":
            self.icon_policy = self.settings.get_string(key)
            self.update_icon()
        else:
            print("Unknown setting key: %s!" % key)

    def on_about_activated (self, menuitem):
        self.about.run()
        self.about.hide()

    def on_quit_activated (self, menuitem):
        self.emit("quit")

    def on_use_system_bus_toggled (self, checkmenuitem):
        self.settings.set_boolean("use-system-bus", checkmenuitem.get_active())
        return

    def on_show_notifications_toggled (self, checkmenuitem):
        self.settings.set_boolean("show-notifications", checkmenuitem.get_active())
        return


    def on_connection_established (self, cdemu, num_devices):
        self.connected = True
        self.update_icon()

        self.item_new_device.set_sensitive(True)

        # Create devices GUI
        for i in range(num_devices):
            # Add device
            self.add_device(i)

    def on_connection_lost (self, cdemu):
        self.cleanup()

    def on_daemon_started (self, cdemu):
        self.show_notification(_("Daemon started"), _("CDEmu daemon has been started."), "dialog-information")

    def on_daemon_stopped (self, cdemu):
        self.show_notification(_("Daemon stopped"), _("CDEmu daemon has been stopped."), "dialog-information")

    def on_device_notification (self, device, summary, body):
        self.show_notification(summary, body, "dialog-information")


    def show_notification (self, summary, body, icon = None):
        if self.show_notifications:
            n = Notify.Notification.new(summary, body, icon)
            n.show()

    def show_error (self, title, text):
         # Show error dialog
        message = Gtk.MessageDialog(
            parent=None,
            message_type=Gtk.MessageType.ERROR,
            buttons=Gtk.ButtonsType.CLOSE,
            text=text)
        message.set_title(title)
        message.run()
        message.destroy()


    def on_new_device_activate (self, menuitem):
        self.cdemu.add_device()


    def on_device_added (self, num):
        self.add_device(num)
        self.show_notification(_("Device added"), _("Device #%02d has been created.") % num, "dialog-information")

    def on_device_removed (self, num):
        self.remove_device(num)
        self.show_notification(_("Device removed"), _("Device #%02d has been removed.") % num, "dialog-information")

    def add_device (self, num):
        # Create device dialog
        device = self.create_device(self.cdemu, num)
        self.devices.append(device)

        # Add device's menu item to the devices menu
        self.menu.insert(device.menu_item, 1+num)
        # Connect signal
        device.connect("device-notification", self.on_device_notification)

        # Set the just-added device as "removable", and mark the one
        # before it as "non-removable"...
        self.devices[-1].set_removable(True)
        if len(self.devices) > 1:
            self.devices[-2].set_removable(False)

    def remove_device (self, num):
        # Remove from list
        device = self.devices.pop(num)
        # Cleanup the device
        device.cleanup()

        # Mark the last device in the list as "removable"
        if len(self.devices) > 0:
            self.devices[-1].set_removable(True)


GObject.type_register(gCDEmuTray)
GObject.signal_new("quit", gCDEmuTray,
    GObject.SignalFlags.RUN_FIRST,
    None,
    ())


class gCDEmuTray_Gtk (gCDEmuTray):
    def __init__ (self):
        gCDEmuTray.__init__(self)

        # *** The status icon ***
        self.tray_icon = Gtk.StatusIcon()

        self.tray_icon.set_visible(True)
        self.tray_icon.connect('activate', self.on_activate)
        self.tray_icon.connect('popup-menu', self.on_popup_menu)

        # Update icon
        self.update_icon()

    def create_device (self, cdemu, d):
        return gCDEmuDevice_Gtk(cdemu, d)


    def on_activate (self, status_icon):
        # Popup the devices menu (simulate the 1st button click)
        self.menu.popup(None, None, status_icon.position_menu, self.tray_icon, 1, Gtk.get_current_event_time())

    def on_popup_menu (self, status_icon, button, activate_time):
        # Popup the menu
        self.menu.popup(None, None, status_icon.position_menu, self.tray_icon, button, activate_time)

    def update_icon (self):
        # Show/hide icon
        if self.icon_policy == "always":
            visible = True
        elif self.icon_policy == "never":
            visible = False
        elif self.icon_policy in [ "when_connected", "when-connected" ]:
            if self.connected:
                visible = True
            else:
                visible = False
        else:
            # Fix invalid setting...
            print("Unknown icon policy '%s'! Using 'always' by default!" % (self.icon_policy))
            self.icon_policy = "always"
            visible = True
        self.tray_icon.set_visible(visible)

        # Set icon
        if self.connected:
            icon_name = self.icon_connected
            tooltip_text = _('gCDEmu - connected')
        else:
            icon_name = self.icon_disconnected
            tooltip_text = _('gCDEmu - disconnected')

        if icon_name is None:
            icon_name = "image-missing" # Fallback icon

        self.tray_icon.set_from_icon_name(icon_name)
        self.tray_icon.set_tooltip_text(tooltip_text)


class gCDEmuTray_Indicator (gCDEmuTray):
    def __init__ (self):
        gCDEmuTray.__init__(self)

        # AppIndicator
        self.indicator = AppIndicator.Indicator.new(app_name, "gcdemu", AppIndicator.IndicatorCategory.HARDWARE)
        self.indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE)
        self.indicator.set_menu(self.menu)

        # Update icon
        self.update_icon()

    def create_device (self, cdemu, d):
        return gCDEmuDevice_Indicator(cdemu, d)

    def update_icon (self):
        # Set icons
        self.indicator.set_icon_full(self.icon_connected, "Connected")
        self.indicator.set_attention_icon_full(self.icon_disconnected, "Disconnected")

        # Show/hide icon
        if self.icon_policy == "always":
            visible = True
        elif self.icon_policy == "never":
            visible = False
        elif self.icon_policy in [ "when_connected", "when-connected" ]:
            if self.connected:
                visible = True
            else:
                visible = False
        else:
            # Fix invalid setting...
            print("Unknown icon policy '%s'! Using 'always' by default!" % (self.icon_policy))
            self.icon_policy = "always"
            visible = True

        # Set the status (change icon)
        if visible:
            if self.connected:
                self.indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE)
            else:
                self.indicator.set_status(AppIndicator.IndicatorStatus.ATTENTION)
        else:
            self.indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE)



########################################################################
#                                Main                                  #
########################################################################
class gCDEmuApplication (Gtk.Application):
    def __init__ (self):
        Gtk.Application.__init__(self, application_id="net.sf.cdemu.GCDEmu", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        self.arguments = None
        self.tray = None

    def do_activate (self):
        if self.tray:
            # If tray already exists, do nothing...
            return

        # Create hidden window to keep GtkApplication happy
        window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
        window.set_title("gCDEmu")
        self.add_window(window)

        # Create tray
        if self.args.tray_mode == "gtk":
            # Gtk tray icon
            self.tray = gCDEmuTray_Gtk()
        elif self.args.tray_mode == "indicator":
            # AppIndicator
            if not have_app_indicator:
                message = Gtk.MessageDialog(
                    parent=None,
                    message_type=Gtk.MessageType.ERROR,
                    buttons=Gtk.ButtonsType.CLOSE,
                    text=_("Failed to load AppIndicator library!"))
                message.set_title(_("AppIndicator not available"))
                message.run()
                message.destroy()
                sys.exit(-1)

            self.tray = gCDEmuTray_Indicator()
        else:
            # Autodetect: if AppIndicator is available, prefer it over
            # Gtk tray
            if have_app_indicator:
                print("AppIndicator tray icon mode")
                self.tray = gCDEmuTray_Indicator()
            else:
                print("Gtk tray icon mode")
                self.tray = gCDEmuTray_Gtk()

        self.tray.connect("quit", lambda o: self.quit())

        # Connect to daemon
        self.tray.connect_to_daemon()

    def do_command_line (self, args):
        # Default command-line handler
        Gtk.Application.do_command_line(self, args)

        # We parse arguments only if they are local, as it would be very
        # inconvenient for command-line parser to blow up in our face for
        # a secondary-instance (which does nothing in our case, anyway)
        # and bring down the primary instance as well. Now, in an ideal
        # world, we would take care of this in do_local_command_line(),
        # however as of GIO 2.38, overriding that one causes a segfault...
        if not args.get_is_remote():
            # Command-line parser
            parser = argparse.ArgumentParser(prog="gcdemu", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
            parser.add_argument("--tray-mode", type=str, nargs="?", choices=["gtk", "indicator", "auto"], default="auto", help=_("gCDEmu tray mode"))

            self.args = parser.parse_args(args.get_arguments()[1:])

            # Activate the app
            self.do_activate()

        return False

if __name__ == "__main__":
    app = gCDEmuApplication()
    signal.signal(signal.SIGINT, signal.SIG_DFL) # Make Ctrl+C work
    status = app.run(sys.argv)
    sys.exit(status)
