Support changed API in alsaaudio 0.4
[rox-volume.git] / volume.py
blob81a656bd3721fec05be9aa0cfd25d4a613f73fc5
1 """
2 volume.py (a volume control applet for the ROX Panel)
4 Copyright 2004 Kenneth Hayber <ken@hayber.us>
5 All rights reserved.
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
19 """
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
26 try:
27 import alsaaudio
28 except:
29 rox.croak(_("You need to install the pyalsaaudio module"))
31 APP_NAME = 'Volume'
32 APP_DIR = rox.app_dir
33 APP_SIZE = [28, 150]
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'):
49 try:
50 mixer_device = alsaaudio.cards().index(MIXER_DEVICE.value)
51 except ValueError:
52 mixer_device = 0
53 else:
54 mixer_device = MIXER_DEVICE.value
56 try:
57 ALSA_CHANNELS = []
58 for channel in alsaaudio.mixers(mixer_device):
59 id = 0
60 while (channel,id) in ALSA_CHANNELS:
61 id += 1
62 try:
63 mixer = alsaaudio.Mixer(channel, id, mixer_device)
64 except alsaaudio.ALSAAudioError:
65 continue
66 if len(mixer.volumecap()):
67 ALSA_CHANNELS.append(channel)
68 except:
69 pass
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)
79 menu = gtk.Menu()
80 button.set_menu(menu)
82 for name in ALSA_CHANNELS:
83 item = gtk.MenuItem(name)
84 menu.append(item)
85 item.show_all()
87 def update_channel():
88 i = -1
89 for kid in menu.get_children():
90 i += 1
91 item = kid.child
92 if not item:
93 item = button.child
94 label = item.get_text()
95 if label == option.value:
96 button.set_history(i)
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))
101 return [hbox]
102 OptionsBox.widget_registry['channel_list'] = build_channel_list
104 rox.app_options.notify()
107 class Volume(applet.Applet):
108 icons = []
109 size = 24
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')
117 if self.vertical:
118 self.set_size_request(8, -1)
119 self.box = gtk.VBox()
120 bar_orient = gtk.PROGRESS_LEFT_TO_RIGHT
121 else:
122 self.set_size_request(-1, 8)
123 self.box = gtk.HBox()
124 bar_orient = gtk.PROGRESS_BOTTOM_TO_TOP
126 self.add(self.box)
128 self.load_icons()
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', ''),
148 Menu.Separator(),
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)
155 self.thing = None
156 try:
157 self.mixer = alsaaudio.Mixer(VOLUME_CONTROL.value, 0, mixer_device)
158 except:
159 rox.info(_('Failed to open Mixer device "%s". Please select a different device.\n') % mixer_device)
160 return
162 self.get_volume()
163 self.update_ui()
164 self.show_all()
165 self.show()
167 if not SHOW_ICON.int_value:
168 self.image.hide()
169 if not SHOW_BAR.int_value:
170 self.bar.hide()
172 def load_icons(self):
173 self.icons = []
174 if THEME.value == 'gtk-theme':
175 try:
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))
181 return
182 except:
183 pass
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:
193 vol += 0.02
194 elif event.direction == 1:
195 vol -= 0.02
196 self.set_volume((vol*100, vol*100))
198 def event_callback(self, widget, rectangle):
199 """Called when the panel sends a size."""
200 if self.vertical:
201 size = rectangle[2]
202 else:
203 size = rectangle[3]
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)
212 self.size = size
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:
218 self.mute()
219 else:
220 if not self.hide_volume():
221 self.show_volume(event)
222 elif event.button == 3:
223 self.hide_volume()
224 self.menu.popup(self, event, self.position_menu)
226 def hide_volume(self, event=None):
227 """Destroy the popup volume control"""
228 if self.thing:
229 self.thing.destroy()
230 self.thing = None
231 return True
232 return False
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)
238 if pos: pos = pos[2]
239 if pos:
240 side, margin = pos.split(',')
241 margin = int(margin)
242 else:
243 side, margin = None, 2
244 return side
246 def set_position(self):
247 """Set the position of the popup"""
248 side = self.get_panel_orientation()
249 vertical = False
251 # widget (x, y, w, h, bits)
252 geometry = self.socket.get_geometry()
254 if side == 'Bottom':
255 vertical = True
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])
259 elif side == 'Top':
260 vertical = True
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])
264 elif side == 'Left':
265 vertical = False
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':
270 vertical = False
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])
274 else:
275 vertical = True
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])
279 return vertical
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()
294 self.thing.show()
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 """
302 try:
303 self.mixer.setvolume(vol[0], 0)
304 self.mixer.setvolume(vol[1], 1)
305 except:
306 pass
307 self.level = vol
308 self.update_ui()
310 def get_volume(self):
311 """Get the volume settings from the mixer"""
312 vol = self.mixer.getvolume()
313 self.level = vol
314 return (vol[0], vol[1])
316 def mute(self):
317 try:
318 mute = self.mixer.getmute()[0]
319 if mute:
320 self.mixer.setmute(0)
321 else:
322 self.mixer.setmute(2)
323 self.update_ui()
324 except:
325 rox.info(_('Device does not support Muting.'))
327 def update_ui(self):
328 vol = self.level
329 try: mute = self.mixer.getmute()[0]
330 except: mute = False
332 if (vol[0] <= 0) or mute:
333 self.pixbuf = self.icons[0]
334 elif vol[0] >= 66:
335 self.pixbuf = self.icons[3]
336 elif vol[0] >= 33:
337 self.pixbuf = self.icons[2]
338 else:
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]))
343 if self.thing:
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)
351 self.get_volume()
352 self.update_ui()
354 if SHOW_BAR.has_changed:
355 if SHOW_BAR.int_value:
356 self.bar.show()
357 else:
358 self.bar.hide()
360 if SHOW_ICON.has_changed:
361 if SHOW_ICON.int_value:
362 self.image.show()
363 else:
364 self.image.hide()
366 if THEME.has_changed:
367 self.load_icons()
368 self.update_ui()
370 def show_options(self, button=None):
371 """Options edit dialog"""
372 rox.edit_options()
374 def get_info(self):
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,))
382 def quit(self):
383 """Quit"""
384 self.destroy()