#!/usr/bin/python -OO
# -*- coding: utf-8 -*-
# vim: ts=4
###
#
# gmixer is the legal property of mehdi abaakouk <theli48@gmail.com>
# Copyright (c) 2008 Mehdi Abaakouk
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
#
###

import sys
import os
import signal
import locale
import gettext
import traceback
import ConfigParser
from optparse import OptionParser

import pygtk
pygtk.require("2.0")
import gobject
import gtk
import gtk.glade

from gtktrayicon import TrayIcon
import gst
import gst.interfaces
if gst.pygst_version < (0, 10, 8):
    raise ImportError,"Need Gstreamer >= 0.10.8"

CONFIG_DIR=os.path.expanduser(os.path.join("~",".config","gmixer"))
CONFIG_FILE=os.path.join(CONFIG_DIR,"config")

try: os.makedirs(CONFIG_DIR)
except OSError: pass
    
APPNAME="GMixer"
VERSION="1.3"

# From source
for base in [".",".."]:
    if os.path.isdir(os.path.join(base,"data")) and os.path.isfile(os.path.join(base,"setup.py")):
        DATADIR=os.path.join(base,"data")
        I18NDIR=os.path.join(base,"po")
        break
else: # From dist install
    for base in ["/usr/share","/usr/local/share"]:
        if os.path.isdir(os.path.join(base,"gmixer")):
            DATADIR=os.path.join(base,"gmixer")
            I18NDIR=os.path.join(base,"locales")
            break
    else:
        raise Exception("Failed find gmixer shared data")


STOCKS = [ "play","record","noplay","norecord","mixer","tone", \
        "headphones","phone","3dsound","video","chain","chain-broken" ]


__all__ = [ 'GMixer','VERSION','APPNAME' ]

factory = gtk.IconFactory()
for filename in STOCKS:
    pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(DATADIR,filename+".png"))
    factory.add(filename, gtk.IconSet(pixbuf))
factory.add_default()

STOCKS_THEME = [ "stock_mic","stock_line_in" , "stock_volume" ]
icon_theme = gtk.icon_theme_get_default()
for icon_name in STOCKS_THEME:
    try: pixbuf = icon_theme.load_icon(icon_name, 16, 16)
    except gobject.GError, exc:
        print "W:Failed to load icon",exc
    else: factory.add(icon_name, gtk.IconSet(pixbuf))

GMIXER_STOCK_SIZE = gtk.icon_size_register("gmixer",16,16)
GMIXER_STOCK_CHAIN_SIZE = gtk.icon_size_register("gmixer-chain",24,9)

DEFAULT_TRACK=[ "cd", "line", "mic", "pcm", "headphone", "speaker", \
    "volume", "master", "digital output", "recording", "front" ]

PIX=[
    ("master","tone"),
    ("pcm","tone"),
    ("volume","tone"),
    ("mix","mixer"),
    ("mic","stock_mic"),
    ("line","stock_line_in"),
    ("cd",gtk.STOCK_CDROM),
    ("speaker","stock_volume"),
    ("headphone","headphones"),
    ("phone","phone"),
    ("video","video"),
    ("3d","3dsound"),
]

TIMEOUT_PROGRESS_POPUP=1000
TRAY_SCROLL_STEP=5
VOLKEYS_UPDATE_STEP=10

locale.setlocale(locale.LC_ALL, '')
gettext.bindtextdomain(APPNAME, I18NDIR)
gettext.textdomain(APPNAME)
gettext.install(APPNAME, I18NDIR, unicode=1)

class GMixerAbout(gtk.AboutDialog):
    def __init__(self):
        gtk.AboutDialog.__init__(self)
    
        self.set_modal(True)
        self.set_comments(APPNAME+""" Gtk/Gstreamer Audio Mixer
        Copyright 2008 Mehdi Abaakouk <theli48@gmail.com>""")
        self.set_website("http://www.listen-project.org/")
        self.set_name(APPNAME)
        self.set_authors(["mehdi abaakouk <theli48@gmail.com>"])
        #self.set_translator_credits("")
        self.set_version(VERSION)
        self.set_logo(gtk.gdk.pixbuf_new_from_file(os.path.join(DATADIR,"mixer.png")))
        self.show_all()
        self.run()
        self.destroy()


class GSwitch(gtk.HBox):
    def __init__(self,mixer,track,statusbar = None):
        gtk.HBox.__init__(self)
        self.set_border_width(3)

        self._statusbar = statusbar
        self._track = track
        self._mixer = mixer
        self._label = gtk.Label(_(self._track.label))

        self._option =  gtk.CheckButton(_(self._track.label))
        if self._statusbar:
            self._option.connect("enter-notify-event",self._on_notify_event,1)
            self._option.connect("leave-notify-event",self._on_notify_event,0)

        self._option.set_active(self._get_active())
        self._option.chsid = self._option.connect("toggled",self._toggled)
        self.pack_start(self._option)


        self.show_all()

    def _on_notify_event(self,widget,event,show):
        if show :
            val = self._get_active() and _("Enable") or _("Disable")
            msg = _("Switch %s : %s"%(self._track.label,val))
            self._statusbar.push(self._statusbar.ctxid,msg)
        else:
            self._statusbar.pop(self._statusbar.ctxid)

    def _set_active(self,value):
        if self._track.flags & gst.interfaces.MIXER_TRACK_INPUT:
            ret = self._mixer.set_record(self._track,value)
        else:
            ret = self._mixer.set_mute(self._track,not value)
        return ret

    def _get_active(self):
        if self._track.flags & gst.interfaces.MIXER_TRACK_INPUT:
            active = bool(self._track.flags & gst.interfaces.MIXER_TRACK_RECORD)
        else:
            active = not bool(self._track.flags & gst.interfaces.MIXER_TRACK_MUTE)
        return active

    def _toggled(self,option):
        self._set_active(self._option.get_active())
        if self._statusbar:
            self._on_notify_event(None,None,0)
            self._on_notify_event(None,None,1)

    def refresh_volume(self):
        value = self._get_active()
        if self._option.get_active() != value:
            self._option.handler_block(self._option.chsid)
            self._option.set_active(value)
            self._option.handler_unblock(self._option.chsid)

class GVolume(gtk.VBox):
    _lockid = None
    def __init__(self,mixer,track,statusbar = None):
        gtk.VBox.__init__(self)
        self.set_border_width(3)

        self._vollock = False

        self._statusbar = statusbar
        self._track = track
        self._mixer = mixer
        self._label = gtk.Label(_(self._track.label))

        vols = self._mixer.get_volume(self._track)

        self._sliders = gtk.HBox()
        
        tvol = 0
        for channel in range(0,track.num_channels):
            adj = gtk.Adjustment(value=vols[channel] , lower=self._track.min_volume, \
                    upper=self._track.max_volume, \
                    step_incr=(self._track.max_volume - self._track.min_volume)/100.0 , \
                    page_incr=(self._track.max_volume - self._track.min_volume)/100.0 , \
                    page_size=0.0)
            adj.chsid = adj.connect("value_changed",self._value_changed,channel)
            slider = gtk.VScale(adj)
            slider.set_draw_value(False)
            slider.set_inverted(True)
            self._sliders.pack_start(slider,False,False,padding=2)
            tvol += vols[channel]
            chan = self._getchanname(channel)
            if self._statusbar:
                slider.connect("enter-notify-event",self._on_notify_event,channel,chan,1)
                slider.connect("leave-notify-event",self._on_notify_event,channel,chan,0)



        def add_btn_event(btn,msg):
            if self._statusbar:
                btn.connect("enter-notify-event",self._on_btn_notify_event,msg,1)
                btn.connect("leave-notify-event",self._on_btn_notify_event,msg,0)

        self._btns = gtk.HBox(homogeneous=True)
        for pix,stock in PIX:
            if self._track.label.lower().find(pix) != -1:
                self._image =  gtk.image_new_from_stock(stock,gtk.ICON_SIZE_MENU)
                break;
        else:
            self._image =  gtk.image_new_from_stock("tone",gtk.ICON_SIZE_MENU)
        if tvol == 0: stock = "noplay"
        else: stock = "play"
        if self._track.flags & gst.interfaces.MIXER_TRACK_MUTE : stock = "noplay"
        else: stock = "play"
        self._mute = gtk.Button()
        self.__set_btn_stock(self._mute,stock)
        self._mute.set_relief(gtk.RELIEF_NONE)
        self._mute.connect("clicked",self._clicked_mute)
        add_btn_event(self._mute,_("Mute/Unmute %s"%self._track.label))
        self._btns.pack_start(self._mute)

        if self._track.flags & gst.interfaces.MIXER_TRACK_INPUT:
            if self._track.flags & gst.interfaces.MIXER_TRACK_RECORD: stock = "record"
            else: stock = "norecord"
            self._record = gtk.Button()
            self.__set_btn_stock(self._record,stock)
            self._record.set_relief(gtk.RELIEF_NONE)
            self._record.connect("clicked",self._clicked_record)
            add_btn_event(self._record,_("Toggle audio recording from %s"%self._track.label))
            self._btns.pack_start(self._record)
        else:
            self._record = None
        self._linker = gtk.Button()
        self._linker.set_relief(gtk.RELIEF_NONE)
        if self._track.num_channels == 2:
            if vols[0] == vols[1]: stock="chain"
            else: stock = "chain-broken"
            self.__set_btn_stock(self._linker,stock)
            self._linker.connect("clicked",self._clicked_link)
            add_btn_event(self._linker,_("Link channels for %s together"%self._track.label))
            self._chain = True
        else:
            self._linker.set_sensitive(False)
            self._chain = False

        linkerbox = gtk.HBox(homogeneous=True)
        linkerbox.pack_start(self._linker,False,False)

        bottom_box = gtk.HBox()
        box = gtk.VBox()
        box.pack_start(linkerbox,False,False)
        box.pack_start(self._btns,False,False)
        bottom_box.pack_start(box,True,False)

        sliderbox = gtk.HBox(homogeneous=True)
        sliderbox.pack_start(self._sliders,False,False)

        self.pack_start(self._image,False,False)
        self.pack_start(self._label,False,False)
        self.pack_start(sliderbox,True,True)
        self.pack_start(bottom_box,False,False)
        self.show_all()
        self.set_no_show_all(True)
        self.hide()

    def _on_btn_notify_event(self,widget,event,msg,show):
        if show:
            self._statusbar.push(self._statusbar.ctxid,msg)
        else:
            self._statusbar.pop(self._statusbar.ctxid)

    def _on_notify_event(self,widget,event,n,chan,show):
        if show :
            val = list(self._mixer.get_volume(self._track))[n]
            msg = _("Volume of %s channel on %s : %d%%"%(chan,self._track.label,val))
            self._statusbar.push(self._statusbar.ctxid,msg)
        else:
            self._statusbar.pop(self._statusbar.ctxid)

    def refresh_volume(self):
        if self._vollock: return True
    
        mute_or_record = False
        if self._track.flags & gst.interfaces.MIXER_TRACK_MUTE : 
            mute_or_record = True
            stock = "noplay"
        else: stock = "play"

        self.__set_btn_stock(self._mute,stock)

        if self._track.flags & gst.interfaces.MIXER_TRACK_INPUT:
            if self._track.flags & gst.interfaces.MIXER_TRACK_RECORD: 
                stock = "record"
            else: 
                stock = "norecord"
                mute_or_record = True

            self.__set_btn_stock(self._record,stock)

        if mute_or_record: return True

        vols = list(self._mixer.get_volume(self._track))
        childs = self._sliders.get_children()
        for channel in range(0,self._track.num_channels):
            adj = childs[channel].get_adjustment()
            adj.handler_block(adj.chsid)
            adj.set_value(vols[channel])
            adj.handler_unblock(adj.chsid)
        if self._track.num_channels == 2:
            if vols[0] != vols[1]:
                stock = "chain-broken"
                self.__set_btn_stock(self._linker,stock)
                self._chain = False

        return True

    def _getchanname(self,channel):
        if self._track.num_channels == 1:
            chan = _("mono")
        elif self._track.num_channels == 2:
            chan = (channel == 0) and _("left") or _("right")
        else:
            chans ={
                0:_("front left"),
                1:_("front right"),
                2:_("rear left"),
                3:_("rear right"),
                4:_("front center"),
                5:_("LFE"),
                6:_("side left"),
                7:_("side right"),
            }
            chan = chans.get(channel,_("unknown"))
        return chan

    def _value_changed(self,adj,channel):
        if self._vollock: return 
        if self._lockid:
            gobject.source_remove(self._lockid)
            self._lockid = None

        self._vollock = True
        vols = list(self._mixer.get_volume(self._track))
        if self._track.num_channels == 2:
            if self._chain:
                adj2 = self._sliders.get_children()[int(not bool(channel))].get_adjustment()
                adj2.handler_block(adj2.chsid)

                vols[0] = adj.get_value()
                vols[1] = adj.get_value()
                self._mixer.set_volume(self._track,tuple(vols))
                adj2.set_value(vols[0])

                adj2.handler_unblock(adj2.chsid)
            else:
                vols[channel] = adj.get_value()
                self._mixer.set_volume(self._track,tuple(vols))
        else:
            vols[channel] = adj.get_value()
            self._mixer.set_volume(self._track,tuple(vols))

        chan = self._getchanname(channel)
        if self._statusbar:
            self._on_notify_event(None,None,channel,chan,0)
            self._on_notify_event(None,None,channel,chan,1)

        self._lockid = gobject.timeout_add(10,self._unlockvol)

    def _unlockvol(self):
        self._lockid = None
        self._vollock = False
        return False

    def _clicked_link(self,btn):
        self._vollock = True
        self._chain = not self._chain
        if not self._chain:
            stock = "chain-broken"
        else:
            stock = "chain"
            vols = list(self._mixer.get_volume(self._track))
            vol = (vols[0] + vols[1])/2
            self._mixer.set_volume(self._track,(vol,vol))
        self.__set_btn_stock(self._linker,stock)
        self._vollock = False

    def _clicked_record(self,btn):
        self._vollock = True
        if self._track.flags & gst.interfaces.MIXER_TRACK_RECORD:
            stock = "norecord"
            self._mixer.set_record(self._track,False)
        else:
            stock = "record"
            self._mixer.set_record(self._track,True)
            vols = tuple([ vol.get_adjustment().get_value() for vol in self._sliders.get_children()])
            self._mixer.set_volume(self._track,vols)
        self.__set_btn_stock(self._record,stock)
        self._vollock = False
    
    def _clicked_mute(self,btn):
        self._vollock = True
        if self._track.flags & gst.interfaces.MIXER_TRACK_MUTE:
            stock = "play"
            self._mixer.set_mute(self._track,False)
            vols = tuple([ vol.get_adjustment().get_value() for vol in self._sliders.get_children()])
            self._mixer.set_volume(self._track,vols)
        else:
            stock = "noplay"
            self._mixer.set_mute(self._track,True)
        self.__set_btn_stock(self._mute,stock)
        self._vollock = False
    
    def __set_btn_stock(self,btn,stock):
        size = GMIXER_STOCK_SIZE
        if stock.find("chain") != -1 : size = GMIXER_STOCK_CHAIN_SIZE
        image = gtk.image_new_from_stock(stock,size)
        if btn.child: btn.remove(btn.child)
        btn.add(image)
        btn.show_all()

class GMasterVolumePopup(gtk.Window):
    def __init__(self,mixer,track):
        gtk.Window.__init__(self,type=gtk.WINDOW_POPUP)
        self.set_decorated(False)
        
        self._track = track
        self._mixer = mixer

        self._gvol = GVolume(mixer,track)
        self._gvol.show()
        frame = gtk.Frame()
        #self._gvol._image.hide()
        self._gvol._label.hide()
        self._gvol._linker.hide()
        frame.add(self._gvol)
        frame.set_shadow_type(gtk.SHADOW_OUT)
        frame.show()
        self.add(frame)
        self.set_default_size(50,150)

        self.grab_focus()
        self.grab_add()
        self.show()

    def position_on(self,align_to):

        width, height = self.get_size()
        height += 2

        screen = self.get_screen()
        monitor_num = screen.get_monitor_at_window(align_to.window)
        if monitor_num < 0: monitor_num = 0
        monitor = screen.get_monitor_geometry (monitor_num)

        x, y = align_to.window.get_origin()

        x += align_to.allocation.x
        y += align_to.allocation.y

        if (y + align_to.allocation.height + height) <= monitor.y + monitor.height:
                y += align_to.allocation.height
        elif (y - height) >= monitor.y:
                y -= height
        elif monitor.y + monitor.height - (y + align_to.allocation.height) > y:
                y += align_to.allocation.height
        else:                                                                                                    
                y -= height

        if x < monitor.x:
                x = monitor.x
        elif x > max(monitor.x, monitor.x + monitor.width - width):
                x = max(monitor.x, monitor.x + monitor.width - width)
        else:
                x = (x - width/2 + align_to.allocation.width/2)
        self.move(x,y)

class GMasterProgressPopup(gtk.Window):
    def __init__(self,mixer,track):
        gtk.Window.__init__(self,type=gtk.WINDOW_POPUP)
        self.set_decorated(False)
        
        self._track = track
        self._mixer = mixer

        self._gvol = gtk.ProgressBar()

        self._icon = gtk.Image()
        self._icon.set_property("has-tooltip",True)

        box = gtk.HBox(spacing=6)
        box.set_border_width(6)
        box.pack_start(self._icon, True, True)
        box.pack_start(self._gvol, True, True)

        frame = gtk.Frame()
        frame.add(box)
        frame.set_shadow_type(gtk.SHADOW_OUT)

        self.add(frame)
        self.set_position(gtk.WIN_POS_CENTER)
        self.refresh_volume()
        self.show_all()
        self.grab_focus()
        self.grab_add()
        self.hide()

    def show(self):
        self.grab_focus()
        self.grab_add()
        gtk.Window.show(self)
    
    def hide(self):
        self.grab_remove()
        gtk.Window.hide(self)

    def refresh_volume(self):
        max = float( self._track.max_volume - self._track.min_volume )
        vol = float(list(self._mixer.get_volume(self._track))[0])
        vol = float(vol - self._track.min_volume)
        frac =  float(vol /  max)

        self._gvol.set_fraction( frac )
        self._gvol.set_text( "%d%%"%(frac*100) )

        if self._track.flags & gst.interfaces.MIXER_TRACK_MUTE:
            frac = 0

        if frac > 0.66 : img = "stock_volume-max"
        elif frac > 0.33: img = "stock_volume-med"
        elif frac > 0.0: img = "stock_volume-min"
        else: img = "stock_volume-mute"

        self._icon.set_from_icon_name(img, gtk.ICON_SIZE_SMALL_TOOLBAR)
        self._icon.set_tooltip_text(_("Volume : %d%%"%(frac*100)))


        return True


class GMixer(gobject.GObject):
    _profiles = {}
    _curmixer = None
    _mixers = []
    _gvolume_dic = {}
    _popup = None
    _timeout_tray_id = None
    def __init__(self, systray_mode = False, custom_mixer_cmd = None, pixmap_mode = False):
        gobject.GObject.__init__(self)

        self._custom_mixer_cmd_pid = None
        self._custom_mixer_cmd = custom_mixer_cmd
        if self._custom_mixer_cmd:
            self._systray_mode = True
        else:
            self._systray_mode = systray_mode
        self._nopixmap_mode = pixmap_mode

        self._init_mixers()
        self._load_defaults()
        self._load_config()
        
        if self._systray_mode: 
            self._init_tray_icon()

        if not self._custom_mixer_cmd:
            self.wTree = gtk.glade.XML(os.path.join(DATADIR,"gmixer.glade"))
            self._notebook = self.wTree.get_widget("notebook")

            self._playbackscroll = self.wTree.get_widget("playbackscroll")
            self._recordingscroll = self.wTree.get_widget("recordingscroll")
            self._switchscroll = self.wTree.get_widget("switchscroll")

            self._playbackscroll.show_all()
            self._playbackscroll.set_no_show_all(True)
            self._recordingscroll.show_all()
            self._recordingscroll.set_no_show_all(True)
            self._switchscroll.show_all()
            self._switchscroll.set_no_show_all(True)

            self._playbackbox = self.wTree.get_widget("playbackbox")
            self._recordingbox = self.wTree.get_widget("recordingbox")
            self._switchbox = self.wTree.get_widget("switchbox")
            self._devices = self.wTree.get_widget("devices").get_submenu()
            self._statusbar = self.wTree.get_widget("statusbar")
            self._statusbar.ctxid = self._statusbar.get_context_id("GMixer")

            item = None
            self._mixers.reverse()
            for mixer in self._mixers:
                devname = mixer.get_property("device-name")
                if not devname: devname = mixer.get_property("device")
                devname += " ("+mixer.get_factory().get_longname()+")"
                item = gtk.RadioMenuItem(group=item,label=devname)
                item.set_active(self._current_mixer == mixer)
                item.connect("activate",self._change_device,mixer)
                self._devices.prepend(item)
                if mixer != self._current_mixer: mixer.set_state(gst.STATE_NULL)

            dic = { 
                    "on_about_activate": lambda *args: GMixerAbout(),
                    "on_mainwindow_delete_event" : self._hide,
                    "on_btnquit_activate" : self._quit,
                    "on_preferences_activate" : self._open_preferences ,
                    }
            self.wTree.signal_autoconnect(dic)

            self._build_gvolumes()

            self._mainwin = self.wTree.get_widget("mainwindow")
            self._mainwin.set_default_size( \
                self._config.getint("settings","width") , \
                self._config.getint("settings","height") )

            gtk.window_set_default_icon_name("stock_volume-max")
            self._set_title()

        gobject.timeout_add(500,self._refresh_volume)

        if self._systray_mode:
            try: 
                import volkeys
            except: 
                pass
            else:
                self.volkeys = volkeys.VolKeys()
                self.volkeys.connect("vol_raise",self.__on_raise_volume_cb)
                self.volkeys.connect("vol_lower",self.__on_lower_volume_cb)
                self.volkeys.connect("vol_mute",self.__on_mute_volume_cb)
                self.__progress_popup = None
                self.__progress_popup_id = None
        elif not self._custom_mixer_cmd:
            self._show()

    def __on_raise_volume_cb(self, *args):
        track = self._get_master_track()
        values = list(self._current_mixer.get_volume(track))
        values = [ min(val + VOLKEYS_UPDATE_STEP, track.max_volume) for val in values ]
        self._current_mixer.set_volume(self._get_master_track(),tuple(values))
        self.__update_progress_popup()

    def __on_lower_volume_cb(self, *args):
        track = self._get_master_track()
        values = list(self._current_mixer.get_volume(track))
        values = [ max(val - VOLKEYS_UPDATE_STEP, track.min_volume) for val in values ]
        self._current_mixer.set_volume(self._get_master_track(),tuple(values))
        self.__update_progress_popup()

    def __on_mute_volume_cb(self, *args):
        track = self._get_master_track()
        if track.flags & gst.interfaces.MIXER_TRACK_MUTE:
            vols = tuple(self._current_mixer.get_volume(track))
            self._current_mixer.set_mute(track,False)
            self._current_mixer.set_volume(track,vols)
        else:
            self._current_mixer.set_mute(track,True)
        self.__update_progress_popup()

    def __update_progress_popup(self):
        if self.__progress_popup_id:
            gobject.source_remove(self.__progress_popup_id)
            self.__progress_popup_id = None
        if not self.__progress_popup:
            self.__progress_popup = GMasterProgressPopup(self._current_mixer,self._get_master_track())
        self.__progress_popup.show()
        self.__progress_popup.refresh_volume()
        self.__progress_popup_id = gobject.timeout_add(TIMEOUT_PROGRESS_POPUP, self.__close_progress_popup)

    def __close_progress_popup(self):
        if self.__progress_popup_id:
            gobject.source_remove(self.__progress_popup_id)
            self.__progress_popup_id = None
        self.__progress_popup.hide()
    
    def _on_configure_window(self,window,event):
        if self._mainwin.get_property("visible"):
            self._config.set("settings","x","%d"%event.x)
            self._config.set("settings","y","%d"%event.y)

    def _refresh_volume(self):
        if self._systray_mode: 
            self._update_tray()
        if not self._current_mixer:
            if self._mainwin.get_property("visible"):
                for obj in self._gvolume_dic.values():
                    obj.refresh_volume()
        if self._popup:
            self._popup._gvol.refresh_volume()
        return True

    def _init_mixers(self):
        features = [ feature for feature in gst.registry_get_default().get_feature_list(gst.ElementFactory) if feature.get_klass().rfind("Generic/Audio")!=-1 ]
        for feature in features:
            try: element = feature.create()
            except : continue
            if not [ i for i in element.get_factory().get_element_type().interfaces if i.name == "GstMixer" ]:
                continue
            if hasattr(element,"probe_property_name"):
                element.probe_property_name("device")
                devs = element.probe_get_values_name("device")
                for dev in devs:
                    element = feature.create()
                    element.set_property("device",dev)
                    element.set_state(gst.STATE_READY)
                    self._mixers.append(element)
            else:
                element.set_state(gst.STATE_READY)
                self._mixers.append(element)

    def _load_defaults(self):
        # Prefer alsamixer by default
        for mixer in self._mixers:
            if mixer.get_factory().get_name() == "alsamixer":
                self._current_mixer = mixer
                break
        else:
            self._current_mixer = self._mixers[0]

    def _load_config(self):
        self._config = ConfigParser.ConfigParser()
        self._config.add_section("settings")
        self._config.set("settings","x","-1")
        self._config.set("settings","y","-1")
        self._config.set("settings","mixer","")
        self._config.set("settings","width","500")
        self._config.set("settings","height","300")

        self._config.read(CONFIG_FILE)
        for mixer in self._mixers:
            if self._get_profile_name(mixer) == self._config.get("settings","mixer"):
                self._current_mixer = mixer
                break
        for pname in self._config.sections():
            if pname == "settings": continue
            self._profiles[pname] = {}
            for volname in self._config.options(pname):
                self._profiles[pname][volname] = self._config.getboolean(pname,volname)

    def _save_config(self):
        width, height = self._mainwin.get_size()
        x , y = self._mainwin.get_position()

        self._config.set("settings","mixer",self._get_profile_name(self._current_mixer))
        self._config.set("settings","x","%d"%x)
        self._config.set("settings","y","%d"%y)
        self._config.set("settings","width","%d"%width)
        self._config.set("settings","height","%d"%height)

        for pname,profile in self._profiles.iteritems():
            try: self._config.add_section(pname)
            except ConfigParser.DuplicateSectionError: pass
            for volname,value in profile.iteritems():
                self._config.set(pname,volname,str(int(value)))
        f = file(CONFIG_FILE,"w")
        self._config.write(f)
        f.close()

    def _get_profile_name(self,mixer):
        return mixer.get_factory().get_name()+":"+mixer.get_property("device")

    def _init_profile(self,profile):
        self._profiles[profile] = {}
        found = []
        for t in DEFAULT_TRACK:
            for track in self._current_mixer.list_tracks():
                volname = track.label.lower()
                if volname in found: continue
                if volname.find(t) != -1 :
                    found.append(volname)
                    self._profiles[profile][volname] = True
                    break;

    def _init_tray_icon(self):
        self._tray_image = gtk.Image()
        self._tray_image.set_property("has-tooltip",True)

        self._eventbox = gtk.EventBox()
        self._eventbox.connect("button_press_event", self._on_tray_press)
        self._eventbox.connect("scroll-event",self._on_tray_scroll)
        self._eventbox.add(self._tray_image)

        self._tray = TrayIcon("GMixer")
        self._tray.add(self._eventbox)
        self._update_tray()
        self._tray.show_all()

    def _get_master_track(self):
        for track in self._current_mixer.list_tracks():
            if track.flags & gst.interfaces.MIXER_TRACK_MASTER:
                return track
        # A bit ugly
        return self._current_mixer.list_tracks()[0]

    def _update_tray(self):
        track = self._get_master_track()
        max = float( track.max_volume - track.min_volume )
        vol = float(list(self._current_mixer.get_volume(track))[0])
        vol = float(vol - track.min_volume)
        frac =  float(vol /  max)
        if track.flags & gst.interfaces.MIXER_TRACK_MUTE:
            frac = 0

        if frac > 0.66 : img = "stock_volume-max"
        elif frac > 0.33: img = "stock_volume-med"
        elif frac > 0.0: img = "stock_volume-min"
        else: img = "stock_volume-mute"
        self._tray_image.set_from_icon_name(img, gtk.ICON_SIZE_SMALL_TOOLBAR)
        self._tray_image.set_tooltip_text(_("Volume : %d%%"%(frac*100)))

    def program_is_alive(self, pid, cmd):
        try:
            if not os.path.exists('/proc'):
                print "missing /proc"
                return True # no /proc, assume App is running

            try:
                f = open('/proc/%d/cmdline'% pid)
            except IOError, e:
                if e.errno == errno.ENOENT:
                    return False # file/pid does not exist
                raise

            n = f.read().lower()
            f.close()
            if n.find(cmd):
                return False
            return True # Running App found at pid
        except:
            import traceback
            traceback.print_exc()
        return True

    def _on_tray_press(self,eb,event):
        if event.button == 3 or (event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS):
            if self._custom_mixer_cmd:
                opened = False
                cmd = self._custom_mixer_cmd.split()
                pid = self._custom_mixer_cmd_pid
                if pid:
                    opened = self.program_is_alive(pid, cmd[0])

                if opened:
                    os.kill(pid, signal.SIGTERM)
                else:
                    self._custom_mixer_cmd_pid = os.spawnvp(os.P_NOWAIT, cmd[0], cmd)
            else:
                if self._mainwin.get_property("visible"):
                    self._hide()
                else:
                    self._show()
        elif event.button == 1:
            settings = gtk.settings_get_default()
            if self._timeout_tray_id == None:
                self._timeout_tray_id = gobject.timeout_add(settings.get_property("gtk-double-click-time"),self._tray_simple_click)
            else:
                gobject.source_remove(self._timeout_tray_id)
                self._timeout_tray_id = None

    def _tray_simple_click(self):
        if self._timeout_tray_id:
            self._timeout_tray_id = None
            if self._popup:
                self._tray_hide()
            else:
                self._popup = GMasterVolumePopup(self._current_mixer,self._get_master_track())
                self._popup.position_on(self._eventbox)

                pointer = gtk.gdk.pointer_grab(
                    self._popup.window, True,
                    gtk.gdk.BUTTON_PRESS_MASK |
                    gtk.gdk.BUTTON_RELEASE_MASK |
                    gtk.gdk.BUTTON_MOTION_MASK |
                    gtk.gdk.POINTER_MOTION_MASK |
                    gtk.gdk.SCROLL_MASK, None, None, gtk.get_current_event_time())
                keyboard = gtk.gdk.keyboard_grab(
                    self._popup.window, True, gtk.get_current_event_time())

                self._popup.connect('button-press-event',self._tray_hide)
        return False

    def _tray_hide(self,*args):
        self._popup.grab_remove()
        gtk.gdk.pointer_ungrab(gtk.get_current_event_time())
        gtk.gdk.keyboard_ungrab(gtk.get_current_event_time())

        self._popup.destroy()
        self._popup = None

    def _on_tray_scroll(self,eb,event):
        track = self._get_master_track()
        values = list(self._current_mixer.get_volume(track))
        if event.direction in [ gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT ]:
            values = [ min(val + TRAY_SCROLL_STEP,track.max_volume) for val in values ]
        else:
            values = [ max(val - TRAY_SCROLL_STEP,track.min_volume) for val in values ]
        self._current_mixer.set_volume(self._get_master_track(),tuple(values))

    def _set_title(self):
        mixerlabel = APPNAME+" - "+self._current_mixer.get_property("device-name")+" ("+self._current_mixer.get_factory().get_longname()+")"
        self._mainwin.set_title(mixerlabel)

    def _change_device(self,item,mixer):
        self._remove_gvolumes()
        self._current_mixer.set_state(gst.STATE_NULL)
        self._current_mixer = mixer
        self._current_mixer.set_state(gst.STATE_READY)
        self._build_gvolumes()
        self._set_title()

    def _remove_gvolumes(self):
        for box in self._playbackbox, self._recordingbox, self._switchbox:
            childs = box.get_children()
            for child in childs:
                box.remove(child)
        self._gvolume_dic = {}

    def _build_gvolumes_old(self):
        for n, track in enumerate(self._current_mixer.list_tracks()):
            if not track.flags & gst.interfaces.MIXER_TRACK_OUTPUT and \
                    not track.flags & gst.interfaces.MIXER_TRACK_INPUT:
                print "W:Track",track.label," have unknown flags", track.flags.value_names, " ", track.flags, track
                continue
            if track.num_channels == 0:
                obj = GSwitch(self._current_mixer,track,self._statusbar)
                sep = gtk.VSeparator()
                sep.show_all()
                sep.set_no_show_all(True)
                self._gvolume_dic[track.label.lower()] = obj
                self._switchbox.pack_start(obj,True,True,padding=3)
                self._switchbox.pack_start(sep,False,False,padding=3)
            else:
                obj = GVolume(self._current_mixer,track,self._statusbar)
                sep = gtk.VSeparator()
                sep.show_all()
                sep.set_no_show_all(True)
                if track.flags & gst.interfaces.MIXER_TRACK_OUTPUT:
                    self._playbackbox.pack_start(obj,True,False,padding=3)
                    self._playbackbox.pack_start(sep,False,False,padding=3)
                elif track.flags & gst.interfaces.MIXER_TRACK_INPUT:
                    self._recordingbox.pack_start(obj,True,False,padding=3)
                    self._recordingbox.pack_start(sep,False,False,padding=3)
                self._gvolume_dic[track.label.lower()] = obj
        self._set_gvolumes_visibility()
        
    def _build_gvolumes(self):
        profile = self._get_profile_name(self._current_mixer)
        if not self._profiles.has_key(profile):
            self._init_profile(profile)

        for n, track in enumerate(self._current_mixer.list_tracks()):
            volname = track.label.lower()
            if not track.flags: continue
            if not track.flags & gst.interfaces.MIXER_TRACK_OUTPUT and \
                    not track.flags & gst.interfaces.MIXER_TRACK_INPUT:
                print "W:Track",track.label," have unknown flags", track.flags.value_names, " ", track.flags, track
                continue
            if not self._profiles[profile].get(volname,False) : 
                continue
            if track.num_channels == 0:
                obj = GSwitch(self._current_mixer,track,self._statusbar)
                obj.show()
                sep = gtk.VSeparator()
                sep.show_all()
                sep.set_no_show_all(True)
                self._gvolume_dic[track.label.lower()] = obj
                self._switchbox.pack_start(obj,True,True,padding=3)
                self._switchbox.pack_start(sep,False,False,padding=3)
            else:
                obj = GVolume(self._current_mixer,track,self._statusbar)
                obj.show()
                sep = gtk.VSeparator()
                sep.show_all()
                sep.set_no_show_all(True)
                if track.flags & gst.interfaces.MIXER_TRACK_OUTPUT:
                    self._playbackbox.pack_start(obj,True,False,padding=3)
                    self._playbackbox.pack_start(sep,False,False,padding=3)
                elif track.flags & gst.interfaces.MIXER_TRACK_INPUT:
                    self._recordingbox.pack_start(obj,True,False,padding=3)
                    self._recordingbox.pack_start(sep,False,False,padding=3)
                self._gvolume_dic[track.label.lower()] = obj
        sep.hide()

        self._playbackscroll.set_property("visible", \
                bool([ w for w in self._playbackbox.get_children() if w.get_property("visible")]) )
        self._recordingscroll.set_property("visible", \
                bool([ w for w in self._recordingbox.get_children() if w.get_property("visible")]) )
        self._switchscroll.set_property("visible", \
                bool([ w for w in self._switchbox.get_children() if w.get_property("visible")]) )


    def _set_gvolumes_visibility(self):
        for obj in self._gvolume_dic.values():
            volname = obj._track.label.lower()
            profile = self._get_profile_name(self._current_mixer)
            if not self._profiles.has_key(profile):
                self._init_profile(profile)

            p = obj.get_parent()
            childs = p.get_children()
            position = p.child_get_property(obj,"position")
            if self._profiles[profile].get(volname,False) : 
                obj.show()
                childs[position + 1].show()
            else: 
                obj.hide()
                childs[position + 1].hide()
        
        for box in [ self._playbackbox, self._recordingbox ]:
            last_visible_sep = None
            for widget in box.get_children():
                if widget.get_property("visible"): last_visible_sep = widget
            if last_visible_sep: last_visible_sep.hide()

        self._playbackscroll.set_property("visible", \
                bool([ w for w in self._playbackbox.get_children() if w.get_property("visible")]) )
        self._recordingscroll.set_property("visible", \
                bool([ w for w in self._recordingbox.get_children() if w.get_property("visible")]) )
        self._switchscroll.set_property("visible", \
                bool([ w for w in self._switchbox.get_children() if w.get_property("visible")]) )

    def _hide(self,*args):
        x , y = self._mainwin.get_position()
        self._config.set("settings","x","%d"%x)
        self._config.set("settings","y","%d"%y)
        self._mainwin.hide_all()
        return True

    def _show(self,*args):
        self._mainwin.move( \
                self._config.getint("settings","x") , \
                self._config.getint("settings","y") )
        self._mainwin.show_all()
        return True

    def _quit(self,*args):
        self._save_config()
        self._remove_gvolumes()
        for mixer in self._mixers:
            mixer.set_state(gst.STATE_NULL)
        gtk.main_quit()
    
    def _open_preferences(self,*args):

        profile = self._get_profile_name(self._current_mixer)
        if not self._profiles.has_key(profile):
            self._init_profile(profile)

        COL_ACTIVE = 0
        COL_LABEL = 1
        COL_TRACK = 2

        d = gtk.Dialog(title=APPNAME+" "+_("Preferences"),
                parent = self._mainwin,  
                flags = gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR ,
                buttons = (gtk.STOCK_CLOSE,gtk.RESPONSE_CLOSE) )
        d.set_modal(False)

        text = gtk.Label()
        text.set_text_with_mnemonic(_("_Select track to be visible:"))
        text.set_alignment(0.0,0.5)
        text.set_padding(6,6)
        vbox = gtk.VBox()
        vbox.pack_start(text,False,False)

        store = gtk.ListStore(bool,str, object)
        tree = gtk.TreeView(store)
        tree.set_headers_visible(False)
        text.set_mnemonic_widget(tree)
        view = gtk.ScrolledWindow()
        view.set_shadow_type(gtk.SHADOW_IN)
        view.set_policy(gtk.POLICY_NEVER,gtk.POLICY_AUTOMATIC)
        view.set_size_request(-1,250)
        view.add(tree)
        sel = tree.get_selection()
        sel.set_mode(gtk.SELECTION_SINGLE)

        def cb_activated(widget,path,*args):
            store[path][COL_ACTIVE] = not store[path][COL_ACTIVE]
            track = store[path][COL_TRACK]
            volname = track.label.lower()
            self._profiles[profile][volname] = store[path][COL_ACTIVE]
            #self._set_gvolumes_visibility()
            self._remove_gvolumes()
            self._build_gvolumes()

        render = gtk.CellRendererToggle()
        render.connect("toggled",cb_activated)
        tree.connect("row-activated",cb_activated)
        
        col = gtk.TreeViewColumn("Active",render,active=COL_ACTIVE)
        tree.append_column(col)
        
        render = gtk.CellRendererText()
        col = gtk.TreeViewColumn("Track name",render,text=COL_LABEL)
        tree.append_column(col)
        
        tree.set_search_column(COL_LABEL)
        for track in self._current_mixer.list_tracks():
            volname = track.label.lower()
            active = self._profiles[profile].get(volname,False)
            store.append((active,track.label,track))

        vbox.pack_start(view,True,True)
        d.vbox.pack_start(vbox,True,True)
        d.show_all()
        d.run()
        d.destroy()

if __name__ == "__main__":
    usage = "usage: %prog [-d|--daemon] [-c|--custom-mixer-cmd]"
    optparser = OptionParser(usage=usage,version="%prog "+VERSION)
    optparser.add_option("-d","--daemon", action="store_true", dest="daemon", default=False)
    optparser.add_option("-n","--nopixmap", action="store_true", dest="pixmap", default=False)
    optparser.add_option("-c","--custom-mixer-cmd", action="store", dest="custom_mixer_cmd", default=None)
    (options, args) = optparser.parse_args(sys.argv[1:])

    if options.daemon:
        # Daemonise code found at:
        # http://code.activestate.com/recipes/278731/
        try:
            pid = os.fork()
        except OSError, e:
            raise Exception, "%s [%d]" % (e.strerror, e.errno)
        if pid != 0:
            os._exit(0)
        else:
            os.setsid()
            try:
                pid = os.fork()
            except OSError, e:
                raise Exception, "%s [%d]" % (e.strerror, e.errno)

            if pid != 0:
                os._exit(0)

    GMixer(options.daemon, options.custom_mixer_cmd, options.pixmap)
    gtk.main()
