gtk3 Volume Control for tint2

misko_2083

My attempt to make a volume control context menu that launches from tint2.
Uses amixer
It wasn't showing properly untill I've installed libgtk-3-0
Here is the launcher =>
/usr/local/bin/tint2launchers/07-sound.desktop
[Desktop Entry]
Type=Application
Encoding=UTF-8
Name=Sound
Exec=python3 /usr/local/bin/menu.py
Icon=audio-headphones
NoDisplay=false


py script
/usr/local/bin/menu.py
#!/usr/bin/env python3
#  Milos Pavlovic 2015 <mpsrbija@gmail.com>
#
#
#  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.


import os
import sys
import string
import subprocess
import shlex
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository.GdkPixbuf import Pixbuf

def execute(command):
    """function to exec switch"""
    p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return p.stdout

class Menu:

    def destroy(self, widget, data=None):
        Gtk.main_quit()

    def action(self, widget, event, execc, data=None):
        command = execc
        subprocess.Popen(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        Gtk.main_quit()

    def __init__(self):
        self.menu = Gtk.Menu()
        """ This is how we close the script
            connecting to the hide signal
        """
        self.menu.connect("hide", self.destroy)
        it=Gtk.IconTheme.get_default()
        height = 100 # Height workaround

        button= Gtk.Switch()
        button.connect("notify::active", self.on_switch_activated)
        cmd = 'amixer get Master | grep -q  off ; echo $?'
        result1 = os.popen(cmd)
        result1 = result1.readline()
        if int(result1) == 0:
           button.set_active(False)
        else:
           button.set_active(True)
        box = Gtk.Box(homogeneous =True)
        img = Gtk.Image(xalign=0)
        pixbuf=it.load_icon("emblem-sound",24,0)
        img.set_from_pixbuf(pixbuf)
        box.add(img)
        box.add(button)
        menuitem = Gtk.MenuItem()
        menuitem.add(box)
        menuitem.connect("button-press-event", self.mute_clicked)
        menuitem.show()
        self.menu.append(menuitem)

# adjustment (initial value, min value, max value,
        # step increment - press cursor keys to see!,
        # page increment - click around the handle to see!,
        # page size - not used here)
        ad1 = Gtk.Adjustment(0, 0, 100, 5, 10, 0)
        cmd = "/usr/bin/amixer sget Master | grep '\[' "
        result = os.popen(cmd)
        result = result.readline()
        find_start = result.find('[') + 1
        find_end = result.find('%]', find_start)
        audio_level = int(result[find_start:find_end])
        ad1. set_value(audio_level)
        # an horizontal scale
        self.h_scale = Gtk.Scale(
            orientation=Gtk.Orientation.HORIZONTAL, adjustment=ad1)
        # of integers (no digits)
        self.h_scale.set_digits(0)
        # that can expand horizontally if there is space in the grid
        self.h_scale.set_hexpand(True)
        # that is aligned at the top of the space allowed in the grid
        self.h_scale.set_valign(Gtk.Align.START)

        # we connect the signal "value-changed" emitted by the scale with the callback
        # function scale_moved
        self.h_scale.connect("value-changed", self.scale_moved)
        box = Gtk.Box()
        box.add(self.h_scale)
        menuitem = Gtk.MenuItem()
        menuitem.add(box)
        menuitem.show()
        self.menu.append(menuitem)

        self.menu.set_size_request(180, height) # Workaround for height
        self.menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
        self.menu.show_all()

    # Let's set the voulume here
    def scale_moved(self, event):
        val = "{0}".format(int(self.h_scale.get_value()))
        proc = subprocess.Popen('/usr/bin/amixer sset Master ' + str(val) + '%  2>&1 >/dev/null', shell=True, stdout=subprocess.PIPE)
        proc.wait()

    def on_switch_activated(self, switch, gparam):
        if switch.get_active():
            execute('amixer set Master unmute 2>&1 >/dev/null')
            state = "on"
        else:
            execute('amixer set Master mute 2>&1 >/dev/null')
            state = "off"

    # Here we toogle the mute on and off when menuitem is clicked   
    def mute_clicked(self, event, data=None):
        val = "toggle"
        proc = subprocess.Popen('/usr/bin/amixer set Master ' + str(val) + '  2>&1 >/dev/null', shell=True, stdout=subprocess.PIPE)
        proc.wait()

    def main(self):
        Gtk.main()

if __name__ == "__main__":

     app = Menu()
     app.main()

The script closes when clicked outside of the menu.
There is a mute switch but will work as a button also.
reference from here >>> http://vsido.org/index.php?topic=1089.msg12301#msg12301

VastOne

This is really cool misko_2083

I am going to play with this and get it to work on the top right tint2 and make it a default in VSIDO if that is all right with you

Well done mate, good solid work!
VSIDO      VSIDO Change Blog    

    I dev VSIDO

zephyr

@misko_2083: Dang, works very well and easy to configure. Great addition, thanks - z
CROWZ / STAR / Devuan / refracta / VSIDO

VastOne

@ misko_2083

You should let everyone know in your first post that the menu.py file must be made executable... 

Most know this or would figure it out, but having it there will save a step for you and for them... :D

Thanks
VSIDO      VSIDO Change Blog    

    I dev VSIDO

VastOne

Here it is in the Panel Tint2 on VSIDO



 
VSIDO      VSIDO Change Blog    

    I dev VSIDO

Snap

Cool. Thank you. I'm not using tint2 currently, but this one goes directly to the goodies stash.

misko_2083

Quote from: VastOne on December 17, 2015, 02:11:38 AM
This is really cool misko_2083

I am going to play with this and get it to work on the top right tint2 and make it a default in VSIDO if that is all right with you

Well done mate, good solid work!
Thank you.
Sure, I'm Ok with that VSIDO get's a Volume control.
Quote from: zephyr on December 17, 2015, 04:14:50 AM
@misko_2083: Dang, works very well and easy to configure. Great addition, thanks - z
Thanks Z
Quote from: VastOne on December 17, 2015, 04:50:28 AM
@ misko_2083

You should let everyone know in your first post that the menu.py file must be made executable... 

Most know this or would figure it out, but having it there will save a step for you and for them... :D

Thanks
If they are launching with python3 there is no need.  ;)
Quote from: VastOne on December 17, 2015, 04:53:53 AM
Here it is in the Panel Tint2 on VSIDO



 

That's cool. Minimalistic approach but sometimes it takes some time to launch. It's faster if it stays in the memory. I'll post in the next comment, it probably won't fit in this one.  :D
Quote from: Snap on December 17, 2015, 06:50:05 AM
Cool. Thank you. I'm not using tint2 currently, but this one goes directly to the goodies stash.
Welcome.

misko_2083

#7
@VastOne I've been playing with this and made a lot of changes. Script makes an icon in the tray area, while it's in the memory the icon will be there.
Launches faster.
There is a right and left click menu now. Left click menu is the same except the label is added.
Right-click menu can launch PulseAudio Volume Control, Alsamixer and About dialog. Also there is a Quit option.
#!/usr/bin/env python3
#  Milos Pavlovic 2015 <mpsrbija@gmail.com>
#
#
#  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.


import os
import sys
import string
import subprocess
import shlex
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository.GdkPixbuf import Pixbuf


# Here we define some icons
vsido_icon = "//usr/local/bin/images/vsidoorb_blk_sm_140.png"
notification_icon="/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

def execute(command):
    """function to exec switch"""
    p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    return p.stdout

class Menu:

    def destroy(self, widget, data=None):
        Gtk.main_quit()

    def action(self, widget, event, execc, data=None):
        command = execc
        subprocess.Popen(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def __init__(self):
        self.tray = Gtk.StatusIcon()
        self.tray.set_from_file(notification_icon)
        self.tray.connect('popup-menu', self.on_right_click)
        self.tray.connect('activate', self.on_left_click)
        self.tray.set_tooltip_text('VSIDO Volume Control')


    def on_left_click(self, widget):
        self.menu = Gtk.Menu()
        self.menu.set_border_width(6)
        height=115
        it=Gtk.IconTheme.get_default()

        button= Gtk.Switch()
        button.connect("notify::active", self.on_switch_activated)
        cmd = 'amixer get Master | grep -q  off ; echo $?'
        result1 = os.popen(cmd)
        result1 = result1.readline()
        if int(result1) == 0:
           button.set_active(False)
        else:
           button.set_active(True)
        box1 = Gtk.Grid(column_homogeneous=True,row_spacing=10)
        img = Gtk.Image(xalign=0)
        pixbuf=it.load_icon("emblem-sound",24,0)
        img.set_from_pixbuf(pixbuf)
        box1.add(img)
        box1.attach_next_to(button, img, Gtk.PositionType.RIGHT, 1, 1)
        menuitem_switch = Gtk.MenuItem()
        menuitem_switch.add(box1)
        menuitem_switch.connect("button-press-event", self.mute_clicked)
        menuitem_switch.show()
        self.menu.append(menuitem_switch)


        vsido=Gtk.Label("VSIDO Volume Control")
        menuitem_vsido = Gtk.MenuItem()
        menuitem_vsido.add(vsido)
        menuitem_vsido.set_sensitive(False)
        self.menu.append(menuitem_vsido)


# adjustment (initial value, min value, max value,
        # step increment - press cursor keys to see!,
        # page increment - click around the handle to see!,
        # page size - not used here)
        ad1 = Gtk.Adjustment(0, 0, 100, 5, 10, 0)
        cmd = "/usr/bin/amixer sget Master | grep '\[' "
        result = os.popen(cmd)
        result = result.readline()
        find_start = result.find('[') + 1
        find_end = result.find('%]', find_start)
        audio_level = int(result[find_start:find_end])
        ad1. set_value(audio_level)
        # an horizontal scale
        self.h_scale = Gtk.Scale(
            orientation=Gtk.Orientation.HORIZONTAL, adjustment=ad1)
        # of integers (no digits)
        self.h_scale.set_digits(0)
        # that can expand horizontally if there is space in the grid
        self.h_scale.set_hexpand(True)
        # that is aligned at the top of the space allowed in the grid
        self.h_scale.set_valign(Gtk.Align.START)

        # we connect the signal "value-changed" emitted by the scale with the callback
        # function scale_moved
        self.h_scale.connect("value-changed", self.scale_moved)
        box2 = Gtk.Grid()
        box2.add(self.h_scale)
        menuitem_scale = Gtk.MenuItem()
        menuitem_scale.add(box2)
        menuitem_scale.show()
        self.menu.append(menuitem_scale)

        self.menu.set_size_request(180, height) # Workaround for height
        self.menu.popup(None, None, None, self.tray, 1, Gtk.get_current_event_time())
        self.menu.show_all()

    def on_right_click(self, icon, button, time):
        it=Gtk.IconTheme.get_default()

        # Pulse audio-menu item
        box = Gtk.Box()
        box.set_spacing(10)
        self.menu = Gtk.Menu()
        img = Gtk.Image()
        try:
            pixbuf=it.load_icon('arts', 16,0)
        except:
            pixbuf=it.load_icon('gtk-stop', 16,0)
        img.set_from_pixbuf(pixbuf)
        label = Gtk.Label("PulseAudio Volume")
        box.add(img)
        box.add(label)
        box.show()
        pulse = Gtk.MenuItem()
        pulse.add(box)
        execc="launch pavucontrol"
        pulse.connect("button-press-event", self.action, execc)

        # Alsa mixer-menu item
        box = Gtk.Box()
        box.set_spacing(10)
        self.menu = Gtk.Menu()
        img = Gtk.Image()
        try:
            pixbuf=it.load_icon('audio-volume-high', 16,0)
        except:
            pixbuf=it.load_icon('gtk-stop', 16,0)
        img.set_from_pixbuf(pixbuf)
        label = Gtk.Label("AlsaMixer")
        box.add(img)
        box.add(label)
        box.show()
        alsa = Gtk.MenuItem()
        alsa.add(box)
        execc="xfce4-terminal -e alsamixer --title=AlsaMixer"
        alsa.connect("button-press-event", self.action, execc)

        # About-menu item
        box = Gtk.Box()
        box.set_spacing(10)
        self.menu = Gtk.Menu()
        img = Gtk.Image()
        try:
            pixbuf=it.load_icon('gtk-info', 16,0)
        except:
            pixbuf=it.load_icon('gtk-stop', 16,0)
        img.set_from_pixbuf(pixbuf)
        label = Gtk.Label("About")
        box.add(img)
        box.add(label)
        about = Gtk.MenuItem()
        about.add(box)

        # Quit-menu item
        box = Gtk.Box()
        box.set_spacing(10)
        self.menu = Gtk.Menu()
        img = Gtk.Image()
        try:
            pixbuf=it.load_icon('gtk-stock-cancel', 16,0)
        except:
            pixbuf=it.load_icon('gtk-stop', 16,0)
        label = Gtk.Label("About")
        label.set_label("Quit")
        box.add(img)
        box.add(label)
        quit = Gtk.MenuItem()
        quit.add(box)

        about.connect("button-press-event", self.show_about_dialog)
        quit.connect("activate", Gtk.main_quit)

        self.menu.append(pulse)
        self.menu.append(alsa)
        separator = Gtk.SeparatorMenuItem()
        self.menu.append(separator)
        self.menu.append(about)
        separator = Gtk.SeparatorMenuItem()
        self.menu.append(separator)
        self.menu.append(quit)

        self.menu.show_all()

        self.menu.popup(None, None, None, self.tray, button, time)

    def show_about_dialog(self, widget, time):
        about_dialog = Gtk.AboutDialog()
        about_dialog.set_destroy_with_parent (True)
        about_dialog.set_program_name("VSIDO Volume Control")
        about_dialog.set_website('http://vsido.org')
        about_dialog.set_website_label('vsido.org')
        about_dialog.set_icon(Pixbuf.new_from_file(vsido_icon))
        about_dialog.set_logo(Pixbuf.new_from_file(vsido_icon))
        about_dialog.set_copyright('Copyright 2015')
        about_dialog.set_comments((u'A tool for setting Volume'))
        about_dialog.set_license('''This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 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. ''')
        about_dialog.set_authors([u'Milos Pavlovic <mpsrbija@gmail.com>'])
        about_dialog.run()
        about_dialog.destroy()


    # Let's set the voulume here
    def scale_moved(self, event,  data=None):
        val = "{0}".format(int(self.h_scale.get_value()))
        proc = subprocess.Popen('/usr/bin/amixer sset Master ' + str(val) + '%  2>&1 >/dev/null', shell=True, stdout=subprocess.PIPE)
        proc.wait()

    def on_switch_activated(self, switch, gparam):
        if switch.get_active():
            execute('amixer set Master unmute 2>&1 >/dev/null')
            state = "on"
        else:
            execute('amixer set Master mute 2>&1 >/dev/null')
            state = "off"

    # Here we toogle the mute on and off when menuitem is clicked   
    def mute_clicked(self, event, data=None):
        val = "toggle"
        proc = subprocess.Popen('/usr/bin/amixer set Master ' + str(val) + '  2>&1 >/dev/null', shell=True, stdout=subprocess.PIPE)
        proc.wait()

    def main(self):
        Gtk.main()

if __name__ == "__main__":

     app = Menu()
     app.main()


VastOne

Incredible... These are the exact enhancements I was going to recommend to you

That is fantastic, thank you for this. This will be a standard on the next ISO's I build

Great work!
VSIDO      VSIDO Change Blog    

    I dev VSIDO

PackRat

about this line:

vsido_icon = "//usr/local/bin/images/vsidoorb_blk_sm_140.png"

is "//usr" a typo, or does the "//" mean something in python3?
I am tired of talk that comes to nothing.
-- Chief Joseph

...the sun, the darkness, the winds are all listening to what we have to say.
-- Geronimo

VastOne

#10
Good catch PackRat... the line just below that also has one where:

notification_icon="/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

has no spacing after the icon= area here

notification_icon="/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

should be

notification_icon = "/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

... but I ran the code with the errors and without and the code works just the same
VSIDO      VSIDO Change Blog    

    I dev VSIDO

misko_2083

Quote from: PackRat on December 18, 2015, 02:10:04 AM
about this line:

vsido_icon = "//usr/local/bin/images/vsidoorb_blk_sm_140.png"

is "//usr" a typo, or does the "//" mean something in python3?
Ah, it was a typo.  :D
Nice catch @PackRat
It's working because UNC paths can be used. Those are used when accessing directories and files on a network, but works on local files too.
It should be like this:
vsido_icon = "/usr/local/bin/images/vsidoorb_blk_sm_140.png"
Quote from: VastOne on December 18, 2015, 03:24:00 AM
Good catch PackRat... the line just below that also has one where:

notification_icon="/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

has no spacing after the icon= area here

notification_icon="/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

should be

notification_icon = "/usr/share/icons/LinuxLex-8/status/24/notification-audio-volume-high.png"

... but I ran the code with the errors and without and the code works just the same
@VastOne Python doesn't care about spaces just the indent. Though it looks better if it's properly formated. :)

VastOne

Ran into an issue with this on the new ISO builds

vastone@vsido:~$ python3 /usr/local/bin/menu.py

Traceback (most recent call last):
  File "/usr/local/bin/menu.py", line 27, in <module>
    require_version("Gtk", "3.0")
  File "/usr/lib/python3/dist-packages/gi/__init__.py", line 102, in require_version
    raise ValueError('Namespace %s not available' % namespace)
ValueError: Namespace Gtk not available


Python3 is installed

libgtk-3-0 is installed

Not sure what the cause is, I searched for a solution but came up empty ... It works flawlessly on this machine which leads me to believe there is a dependency this machine has met but the build environment does not have

I have a new install of this ready for any testing ... Let me know what is needed
VSIDO      VSIDO Change Blog    

    I dev VSIDO

misko_2083

Quote from: VastOne on December 19, 2015, 05:21:32 AM
Ran into an issue with this on the new ISO builds

vastone@vsido:~$ python3 /usr/local/bin/menu.py

Traceback (most recent call last):
  File "/usr/local/bin/menu.py", line 27, in <module>
    require_version("Gtk", "3.0")
  File "/usr/lib/python3/dist-packages/gi/__init__.py", line 102, in require_version
    raise ValueError('Namespace %s not available' % namespace)
ValueError: Namespace Gtk not available


Python3 is installed

libgtk-3-0 is installed

Not sure what the cause is, I searched for a solution but came up empty ... It works flawlessly on this machine which leads me to believe there is a dependency this machine has met but the build environment does not have

I have a new install of this ready for any testing ... Let me know what is needed
This one :)
apt-cache policy gir1.2-gtk-3.0
gir1.2-gtk-3.0:
  Installed: 3.18.5-1
  Candidate: 3.18.6-1
  Version table:
     3.18.6-1 500
        500 http://ftp.us.debian.org/debian sid/main i386 Packages
*** 3.18.5-1 100
        100 /var/lib/dpkg/status


VastOne

That was it... that brought in several more dependencies but we can live with their size

Thanks misko...  8)
VSIDO      VSIDO Change Blog    

    I dev VSIDO