2 volume.py (a volume control applet for the ROX Panel)
4 Copyright 2004 Kenneth Hayber <ken@hayber.us>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License.
11 This program is distributed in the hope that it will be useful
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 import rox
, sys
, os
, gtk
22 from rox
import app_options
, applet
, Menu
, InfoWin
, OptionsBox
23 from rox
.options
import Option
24 from volumecontrol
import VolumeControl
29 rox
.croak(_("You need to install the pyalsaaudio module"))
36 #Options.xml processing
37 rox
.setup_app_options(APP_NAME
, site
='hayber.us')
38 Menu
.set_save_name(APP_NAME
, site
='hayber.us')
40 MIXER_DEVICE
= Option('mixer_device', 'default')
41 #VOLUME_CONTROL = Option('volume_control', 'Master')
42 VOLUME_CONTROL
= Option('mixer_channels', 'PCM')
43 SHOW_ICON
= Option('show_icon', True)
44 SHOW_BAR
= Option('show_bar', False)
45 THEME
= Option('theme', 'gtk-theme')
47 # support two different alsaaudio APIs
48 if hasattr(alsaaudio
, 'cards'):
50 mixer_device
= alsaaudio
.cards().index(MIXER_DEVICE
.value
)
54 mixer_device
= MIXER_DEVICE
.value
58 for channel
in alsaaudio
.mixers(mixer_device
):
60 while (channel
,id) in ALSA_CHANNELS
:
63 mixer
= alsaaudio
.Mixer(channel
, id, mixer_device
)
64 except alsaaudio
.ALSAAudioError
:
66 if len(mixer
.volumecap()):
67 ALSA_CHANNELS
.append(channel
)
71 def build_channel_list(box
, node
, label
, option
):
72 hbox
= gtk
.HBox(False, 4)
74 hbox
.pack_start(box
.make_sized_label(label
), False, True, 0)
76 button
= gtk
.OptionMenu()
77 hbox
.pack_start(button
, True, True, 0)
82 for name
in ALSA_CHANNELS
:
83 item
= gtk
.MenuItem(name
)
89 for kid
in menu
.get_children():
94 label
= item
.get_text()
95 if label
== option
.value
:
98 def read_channel(): return button
.child
.get_text()
99 box
.handlers
[option
] = (read_channel
, update_channel
)
100 button
.connect('changed', lambda w
: box
.check_widget(option
))
102 OptionsBox
.widget_registry
['channel_list'] = build_channel_list
104 rox
.app_options
.notify()
107 class Volume(applet
.Applet
):
112 """An applet to control a sound card Master or PCM volume"""
113 def __init__(self
, filename
):
114 applet
.Applet
.__init
__(self
, filename
)
116 self
.vertical
= self
.get_panel_orientation() in ('Right', 'Left')
118 self
.set_size_request(8, -1)
119 self
.box
= gtk
.VBox()
120 bar_orient
= gtk
.PROGRESS_LEFT_TO_RIGHT
122 self
.set_size_request(-1, 8)
123 self
.box
= gtk
.HBox()
124 bar_orient
= gtk
.PROGRESS_BOTTOM_TO_TOP
129 self
.image
= gtk
.Image()
130 self
.box
.pack_start(self
.image
)
132 self
.bar
= gtk
.ProgressBar()
133 self
.bar
.set_orientation(bar_orient
)
134 self
.bar
.set_size_request(12,12)
135 self
.box
.pack_end(self
.bar
)
137 self
.tips
= gtk
.Tooltips()
139 rox
.app_options
.add_notify(self
.get_options
)
140 self
.connect('size-allocate', self
.event_callback
)
141 self
.connect('scroll_event', self
.button_scroll
)
143 self
.add_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
144 self
.connect('button-press-event', self
.button_press
)
145 self
.menu
= Menu
.Menu('main', [
146 Menu
.Action(_('Mixer'), 'run_mixer', ''),
147 Menu
.Action(_('Mute'), 'mute', ''),
149 Menu
.Action(_('Options'), 'show_options', '', gtk
.STOCK_PREFERENCES
),
150 Menu
.Action(_('Info'), 'get_info', '', gtk
.STOCK_DIALOG_INFO
),
151 Menu
.Action(_('Close'), 'quit', '', gtk
.STOCK_CLOSE
),
153 self
.menu
.attach(self
, self
)
157 self
.mixer
= alsaaudio
.Mixer(VOLUME_CONTROL
.value
, 0, mixer_device
)
159 rox
.info(_('Failed to open Mixer device "%s". Please select a different device.\n') % mixer_device
)
167 if not SHOW_ICON
.int_value
:
169 if not SHOW_BAR
.int_value
:
172 def load_icons(self
):
174 if THEME
.value
== 'gtk-theme':
176 theme
= gtk
.icon_theme_get_default()
177 self
.icons
.append(theme
.load_icon('audio-volume-muted', 24, 0))
178 self
.icons
.append(theme
.load_icon('audio-volume-low', 24, 0))
179 self
.icons
.append(theme
.load_icon('audio-volume-medium', 24, 0))
180 self
.icons
.append(theme
.load_icon('audio-volume-high', 24, 0))
184 theme_dir
= os
.path
.join(APP_DIR
, 'themes', THEME
.value
)
185 self
.icons
.append(gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(theme_dir
, 'audio-volume-muted.svg')))
186 self
.icons
.append(gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(theme_dir
, 'audio-volume-low.svg')))
187 self
.icons
.append(gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(theme_dir
, 'audio-volume-medium.svg')))
188 self
.icons
.append(gtk
.gdk
.pixbuf_new_from_file(os
.path
.join(theme_dir
, 'audio-volume-high.svg')))
190 def button_scroll(self
, window
, event
):
191 vol
= self
.bar
.get_fraction()
192 if event
.direction
== 0:
194 elif event
.direction
== 1:
196 self
.set_volume((vol
*100, vol
*100))
198 def event_callback(self
, widget
, rectangle
):
199 """Called when the panel sends a size."""
204 if size
!= self
.size
:
205 self
.resize_image(size
)
207 def resize_image(self
, size
):
208 """Called to resize the image."""
209 #I like the look better with the -2, there is no technical reason for it.
210 scaled_pixbuf
= self
.pixbuf
.scale_simple(size
-2, size
-2, gtk
.gdk
.INTERP_BILINEAR
)
211 self
.image
.set_from_pixbuf(scaled_pixbuf
)
214 def button_press(self
, window
, event
):
215 """Show/Hide the volume control on button 1 and the menu on button 3"""
216 if event
.button
== 1:
217 if event
.type == gtk
.gdk
._2BUTTON
_PRESS
:
220 if not self
.hide_volume():
221 self
.show_volume(event
)
222 elif event
.button
== 3:
224 self
.menu
.popup(self
, event
, self
.position_menu
)
226 def hide_volume(self
, event
=None):
227 """Destroy the popup volume control"""
234 def get_panel_orientation(self
):
235 """Return the panel orientation ('Top', 'Bottom', 'Left', 'Right')
236 and the margin for displaying a popup menu"""
237 pos
= self
.socket
.property_get('_ROX_PANEL_MENU_POS', 'STRING', False)
240 side
, margin
= pos
.split(',')
243 side
, margin
= None, 2
246 def set_position(self
):
247 """Set the position of the popup"""
248 side
= self
.get_panel_orientation()
251 # widget (x, y, w, h, bits)
252 geometry
= self
.socket
.get_geometry()
256 self
.thing
.set_size_request(APP_SIZE
[0], APP_SIZE
[1])
257 self
.thing
.move(self
.socket
.get_origin()[0],
258 self
.socket
.get_origin()[1]-APP_SIZE
[1])
261 self
.thing
.set_size_request(APP_SIZE
[0], APP_SIZE
[1])
262 self
.thing
.move(self
.socket
.get_origin()[0],
263 self
.socket
.get_origin()[1]+geometry
[3])
266 self
.thing
.set_size_request(APP_SIZE
[1], APP_SIZE
[0])
267 self
.thing
.move(self
.socket
.get_origin()[0]+geometry
[2],
268 self
.socket
.get_origin()[1])
269 elif side
== 'Right':
271 self
.thing
.set_size_request(APP_SIZE
[1], APP_SIZE
[0])
272 self
.thing
.move(self
.socket
.get_origin()[0]-APP_SIZE
[1],
273 self
.socket
.get_origin()[1])
276 self
.thing
.set_size_request(APP_SIZE
[0], APP_SIZE
[1])
277 self
.thing
.move(self
.socket
.get_origin()[0],
278 self
.socket
.get_origin()[1]-APP_SIZE
[1])
281 def show_volume(self
, event
):
282 """Display the popup volume control"""
284 self
.thing
= gtk
.Window(type=gtk
.WINDOW_POPUP
)
285 self
.thing
.set_type_hint(gtk
.gdk
.WINDOW_TYPE_HINT_MENU
)
286 self
.thing
.set_decorated(False)
288 self
.volume
= VolumeControl(0, 0, 0, True, None, self
.set_position())
289 self
.volume
.set_level(self
.get_volume())
290 self
.volume
.connect("volume_changed", self
.adjust_volume
)
292 self
.thing
.add(self
.volume
)
293 self
.thing
.show_all()
296 def adjust_volume(self
, vol
, channel
, vol_left
, vol_right
):
297 """Set the playback volume"""
298 self
.set_volume((vol_left
, vol_right
))
300 def set_volume(self
, vol
):
301 """Send the volume setting(s) to the mixer """
303 self
.mixer
.setvolume(vol
[0], 0)
304 self
.mixer
.setvolume(vol
[1], 1)
310 def get_volume(self
):
311 """Get the volume settings from the mixer"""
312 vol
= self
.mixer
.getvolume()
314 return (vol
[0], vol
[1])
318 mute
= self
.mixer
.getmute()[0]
320 self
.mixer
.setmute(0)
322 self
.mixer
.setmute(2)
325 rox
.info(_('Device does not support Muting.'))
329 try: mute
= self
.mixer
.getmute()[0]
332 if (vol
[0] <= 0) or mute
:
333 self
.pixbuf
= self
.icons
[0]
335 self
.pixbuf
= self
.icons
[3]
337 self
.pixbuf
= self
.icons
[2]
339 self
.pixbuf
= self
.icons
[1]
341 self
.resize_image(self
.size
)
342 self
.tips
.set_tip(self
, _('Volume control') + ': %d%%' % min(vol
[0], vol
[1]))
344 self
.volume
.set_level((vol
[0], vol
[1]))
345 self
.bar
.set_fraction(max(vol
[0], vol
[1])/100.0)
347 def get_options(self
):
348 """Used as the notify callback when options change"""
349 if VOLUME_CONTROL
.has_changed
:
350 self
.mixer
= alsaaudio
.Mixer(VOLUME_CONTROL
.value
, 0, mixer_device
)
354 if SHOW_BAR
.has_changed
:
355 if SHOW_BAR
.int_value
:
360 if SHOW_ICON
.has_changed
:
361 if SHOW_ICON
.int_value
:
366 if THEME
.has_changed
:
370 def show_options(self
, button
=None):
371 """Options edit dialog"""
375 """Display an InfoWin box"""
376 InfoWin
.infowin(APP_NAME
)
378 def run_mixer(self
, button
=None):
379 from rox
import filer
380 filer
.spawn_rox((APP_DIR
,))