1#!/usr/local/bin/python3.8
2
3import gi
4gi.require_version('Cvc', '1.0')
5gi.require_version('Gtk', '3.0')
6from gi.repository import Gtk, Cvc, GdkPixbuf, Gio
7from SettingsWidgets import SidePage, GSettingsSoundFileChooser
8from xapp.GSettingsWidgets import *
9import dbus
10
11CINNAMON_SOUNDS = "org.cinnamon.sounds"
12CINNAMON_DESKTOP_SOUNDS = "org.cinnamon.desktop.sound"
13MAXIMUM_VOLUME_KEY = "maximum-volume"
14
15DECAY_STEP = .15
16
17EFFECT_LIST = [
18    {"label": _("Starting Cinnamon"),           "schema": CINNAMON_SOUNDS,         "file": "login-file",        "enabled": "login-enabled"},
19    {"label": _("Leaving Cinnamon"),            "schema": CINNAMON_SOUNDS,         "file": "logout-file",       "enabled": "logout-enabled"},
20    {"label": _("Switching workspace"),         "schema": CINNAMON_SOUNDS,         "file": "switch-file",       "enabled": "switch-enabled"},
21    {"label": _("Opening new windows"),         "schema": CINNAMON_SOUNDS,         "file": "map-file",          "enabled": "map-enabled"},
22    {"label": _("Closing windows"),             "schema": CINNAMON_SOUNDS,         "file": "close-file",        "enabled": "close-enabled"},
23    {"label": _("Minimizing windows"),          "schema": CINNAMON_SOUNDS,         "file": "minimize-file",     "enabled": "minimize-enabled"},
24    {"label": _("Maximizing windows"),          "schema": CINNAMON_SOUNDS,         "file": "maximize-file",     "enabled": "maximize-enabled"},
25    {"label": _("Unmaximizing windows"),        "schema": CINNAMON_SOUNDS,         "file": "unmaximize-file",   "enabled": "unmaximize-enabled"},
26    {"label": _("Tiling and snapping windows"), "schema": CINNAMON_SOUNDS,         "file": "tile-file",         "enabled": "tile-enabled"},
27    {"label": _("Inserting a device"),          "schema": CINNAMON_SOUNDS,         "file": "plug-file",         "enabled": "plug-enabled"},
28    {"label": _("Removing a device"),           "schema": CINNAMON_SOUNDS,         "file": "unplug-file",       "enabled": "unplug-enabled"},
29    {"label": _("Showing notifications"),       "schema": CINNAMON_SOUNDS,         "file": "notification-file", "enabled": "notification-enabled"},
30    {"label": _("Changing the sound volume"),   "schema": CINNAMON_DESKTOP_SOUNDS, "file": "volume-sound-file", "enabled": "volume-sound-enabled"}
31]
32
33SOUND_TEST_MAP = [
34    #  name,             position,        icon name,                  row,  col,   pa id
35    [_("Front Left"),    "front-left",    "audio-speaker-left",         0,   0,      1],
36    [_("Front Right"),   "front-right",   "audio-speaker-right",        0,   2,      2],
37    [_("Front Center"),  "front-center",  "audio-speaker-center",       0,   1,      3],
38    [_("Rear Left"),     "rear-left",     "audio-speaker-left-back",    2,   0,      5],
39    [_("Rear Right"),    "rear-right",    "audio-speaker-right-back",   2,   2,      6],
40    [_("Rear Center"),   "rear-center",   "audio-speaker-center-back",  2,   1,      4],
41    [_("Subwoofer"),     "lfe",           "audio-subwoofer",            1,   1,      7],
42    [_("Side Left"),     "side-left",     "audio-speaker-left-side",    1,   0,      10],
43    [_("Side Right"),    "side-right",    "audio-speaker-right-side",   1,   2,      11]
44]
45
46def list_header_func(row, before, user_data):
47    if before and not row.get_header():
48        row.set_header(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
49
50class SoundBox(Gtk.Box):
51    def __init__(self, title):
52        Gtk.Box.__init__(self)
53        self.set_orientation(Gtk.Orientation.VERTICAL)
54        self.set_spacing(5)
55
56        label = Gtk.Label()
57        label.set_markup("<b>%s</b>" % title)
58        label.set_xalign(0.0)
59        self.add(label)
60
61        frame = Gtk.Frame()
62        frame.set_shadow_type(Gtk.ShadowType.IN)
63        frame_style = frame.get_style_context()
64        frame_style.add_class("view")
65        self.pack_start(frame, True, True, 0)
66
67        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
68        frame.add(main_box)
69
70        scw = Gtk.ScrolledWindow()
71        scw.expand = True
72        scw.set_min_content_height (450)
73        scw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
74        scw.set_shadow_type(Gtk.ShadowType.NONE)
75        main_box.pack_start(scw, True, True, 0)
76        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
77        scw.add(self.box)
78
79        self.list_box = Gtk.ListBox()
80        self.list_box.set_selection_mode(Gtk.SelectionMode.NONE)
81        self.list_box.set_header_func(list_header_func, None)
82        self.box.add(self.list_box)
83
84    def add_row(self, row):
85        self.list_box.add(row)
86
87class Slider(SettingsWidget):
88    def __init__(self, title, minLabel, maxLabel, minValue, maxValue, sizeGroup, step=None, page=None, value=0, gicon=None, iconName=None):
89        super(Slider, self).__init__()
90        self.set_orientation(Gtk.Orientation.VERTICAL)
91        self.set_spacing(5)
92        self.set_margin_bottom(5)
93
94        if sizeGroup == None:
95            sizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
96
97        if step == None:
98            step = (maxValue - minValue) / 100
99        if page == None:
100            page = (maxValue - minValue) / 10
101        self.adjustment = Gtk.Adjustment.new(value, minValue, maxValue, step, page, 0)
102
103        topBox = Gtk.Box()
104        self.leftBox = Gtk.Box()
105        self.rightBox = Gtk.Box()
106        topGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
107        topGroup.add_widget(self.leftBox)
108        topGroup.add_widget(self.rightBox)
109
110        # add label and icon (if specified)
111        labelBox = Gtk.Box(spacing=5)
112        if gicon != None:
113            appIcon = Gtk.Image.new_from_gicon(gicon, 2)
114            labelBox.pack_start(appIcon, False, False, 0)
115        elif iconName != None:
116            appIcon = Gtk.Image.new_from_icon_name(iconName, 2)
117            labelBox.pack_start(appIcon, False, False, 0)
118        self.label = Gtk.Label(title)
119        labelBox.pack_start(self.label, False, False, 0)
120        labelBox.set_halign(Gtk.Align.CENTER)
121
122        topBox.pack_start(self.leftBox, False, False, 0)
123        topBox.pack_start(labelBox, True, True, 0)
124        topBox.pack_start(self.rightBox, False, False, 0)
125
126        # add scale
127        sliderBox = Gtk.Box()
128        self.slider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, self.adjustment)
129        self.slider.props.draw_value = False
130
131        min_label= Gtk.Label()
132        max_label = Gtk.Label()
133        min_label.set_alignment(1.0, 0.75)
134        max_label.set_alignment(0.0, 0.75)
135        min_label.set_margin_right(6)
136        max_label.set_margin_left(6)
137        min_label.set_markup("<i><small>%s</small></i>" % minLabel)
138        max_label.set_markup("<i><small>%s</small></i>" % maxLabel)
139        sizeGroup.add_widget(min_label)
140        sizeGroup.add_widget(max_label)
141
142        sliderBox.pack_start(min_label, False, False, 0)
143        sliderBox.pack_start(self.slider, True, True, 0)
144        sliderBox.pack_start(max_label, False, False, 0)
145
146        self.pack_start(topBox, False, False, 0)
147        self.pack_start(sliderBox, False, False, 0)
148        self.show_all()
149
150    def setMark(self, val):
151        self.slider.add_mark(val, Gtk.PositionType.TOP, "")
152
153class VolumeBar(Slider):
154    def __init__(self, normVolume, maxPercent, title=_("Volume: "), gicon=None, sizeGroup=None):
155        self.normVolume = normVolume
156        self.volume = 0
157        self.isMuted = False
158        self.baseTitle = title
159
160        self.stream = None
161
162        self.mutedHandlerId = 0
163        self.volumeHandlerId = 0
164
165        super(VolumeBar, self).__init__(title, _("Softer"), _("Louder"), 0, maxPercent, sizeGroup, 1, 5, 0, gicon)
166        self.set_spacing(0)
167        self.set_border_width(2)
168        self.set_margin_left(23)
169        self.set_margin_right(23)
170        self.slider.set_sensitive(False)
171
172        self.muteImage = Gtk.Image.new_from_icon_name("audio-volume-muted-symbolic", 1)
173        self.muteSwitch = Gtk.ToggleButton()
174        self.muteSwitch.set_image(self.muteImage)
175        self.muteSwitch.set_relief(Gtk.ReliefStyle.NONE)
176        self.muteSwitch.set_active(False)
177        self.muteSwitch.set_sensitive(False)
178
179        self.leftBox.pack_start(self.muteSwitch, False, False, 0)
180
181        if maxPercent > 100:
182            self.setMark(100)
183
184        self.muteSwitchHandlerId = self.muteSwitch.connect("clicked", self.toggleMute)
185        self.adjustmentHandlerId = self.adjustment.connect("value-changed", self.onVolumeChanged)
186
187    def connectStream(self):
188        self.mutedHandlerId = self.stream.connect("notify::is-muted", self.setVolume)
189        self.volumeHandlerId = self.stream.connect("notify::volume", self.setVolume)
190        self.setVolume(None, None)
191
192    def disconnectStream(self):
193        if self.mutedHandlerId > 0:
194            self.stream.disconnect(self.mutedHandlerId)
195            self.mutedHandlerId = 0
196
197        if self.volumeHandlerId > 0:
198            self.stream.disconnect(self.volumeHandlerId)
199            self.volumeHandlerId = 0
200
201    def setStream(self, stream):
202        if self.stream and stream != self.stream:
203            self.disconnectStream()
204
205        self.stream = stream
206
207        self.connectStream()
208
209        self.slider.set_sensitive(True)
210        self.muteSwitch.set_sensitive(True)
211
212    def setVolume(self, a, b):
213        if self.stream.get_is_muted():
214            newVolume = 0
215            self.isMuted = True
216        else:
217            newVolume = int(round(self.stream.props.volume / self.normVolume * 100))
218            self.isMuted = False
219
220        self.volume = newVolume
221
222        self.adjustment.handler_block(self.adjustmentHandlerId)
223        self.adjustment.set_value(newVolume)
224        self.adjustment.handler_unblock(self.adjustmentHandlerId)
225
226        self.updateStatus()
227
228    def onVolumeChanged(self, adjustment):
229        newVolume = int(round(self.adjustment.get_value()))
230
231        muted = newVolume == 0
232
233        self.volume = newVolume
234
235        self.stream.handler_block(self.volumeHandlerId)
236        self.stream.set_volume(newVolume * self.normVolume / 100)
237        self.stream.push_volume()
238        self.stream.handler_unblock(self.volumeHandlerId)
239
240        if self.stream.get_is_muted() != muted:
241            self.setMuted(muted)
242
243        self.updateStatus()
244
245    def setMuted(self, muted):
246        self.isMuted = muted
247        self.stream.change_is_muted(muted)
248
249    def toggleMute(self, a=None):
250        self.setMuted(not self.isMuted)
251
252    def updateStatus(self):
253        self.muteSwitch.handler_block(self.muteSwitchHandlerId)
254        self.muteSwitch.set_active(self.isMuted)
255        self.muteSwitch.handler_unblock(self.muteSwitchHandlerId)
256
257        if self.isMuted:
258            self.muteImage.set_from_icon_name("audio-volume-muted-symbolic", 1)
259            self.label.set_label(self.baseTitle + _("Muted"))
260            self.muteSwitch.set_tooltip_text(_("Click to unmute"))
261        else:
262            self.muteImage.set_from_icon_name("audio-volume-high-symbolic", 1)
263            self.label.set_label(self.baseTitle + str(self.volume) + "%")
264            self.muteSwitch.set_tooltip_text(_("Click to mute"))
265
266class BalanceBar(Slider):
267    def __init__(self, type, minVal = -1, norm = 1, sizeGroup=None):
268        self.type = type
269        self.norm = norm
270        self.value = 0
271
272        if type == "balance":
273            title = _("Balance")
274            minLabel = _("Left")
275            maxLabel = _("Right")
276        elif type == "fade":
277            title = _("Fade")
278            minLabel = _("Rear")
279            maxLabel = _("Front")
280        elif type == "lfe":
281            title = _("Subwoofer")
282            minLabel = _("Soft")
283            maxLabel = _("Loud")
284
285        super(BalanceBar, self).__init__(title, minLabel, maxLabel, minVal, 1, sizeGroup, (1-minVal)/20.)
286
287        self.setMark(0)
288        self.slider.props.has_origin = False
289
290        self.adjustment.connect("value-changed", self.onLevelChanged)
291
292    def setChannelMap(self, channelMap):
293        self.channelMap = channelMap
294        self.channelMap.connect("volume-changed", self.getLevel)
295        self.set_sensitive(getattr(self.channelMap, "can_"+self.type)())
296        self.getLevel()
297
298    def getLevel(self, a=None, b=None):
299        value = round(getattr(self.channelMap, "get_"+self.type)(), 3)
300        if self.type == "lfe":
301            value = value / self.norm
302        if value == self.value:
303            return
304        self.value = value
305        self.adjustment.set_value(self.value)
306
307    def onLevelChanged(self, adjustment):
308        value = round(self.adjustment.get_value(), 3)
309        if self.value == value:
310            return
311        self.value = value
312        if self.type == "lfe":
313            value = value * self.norm
314        getattr(self.channelMap, "set_"+self.type)(value)
315
316class VolumeLevelBar(SettingsWidget):
317    def __init__(self, sizeGroup):
318        super(VolumeLevelBar, self).__init__()
319        self.set_orientation(Gtk.Orientation.VERTICAL)
320        self.set_spacing(5)
321
322        self.lastPeak = 0
323        self.monitorId = None
324        self.stream = None
325
326        self.pack_start(Gtk.Label(_("Input level")), False, False, 0)
327
328        levelBox = Gtk.Box()
329        self.levelBar = Gtk.LevelBar()
330
331        leftPadding = Gtk.Box()
332        sizeGroup.add_widget(leftPadding)
333        rightPadding = Gtk.Box()
334        sizeGroup.add_widget(rightPadding)
335
336        levelBox.pack_start(leftPadding, False, False, 0)
337        levelBox.pack_start(self.levelBar, True, True, 0)
338        levelBox.pack_start(rightPadding, False, False, 0)
339
340        self.pack_start(levelBox, False, False, 5)
341
342        self.levelBar.set_min_value(0)
343
344    def setStream(self, stream):
345        if self.stream != None:
346            self.stream.remove_monitor()
347            self.stream.disconnect(self.monitorId)
348        self.stream = stream
349        self.stream.create_monitor()
350        self.monitorId = self.stream.connect("monitor-update", self.update)
351
352    def update(self, stream, value):
353        if self.lastPeak >= DECAY_STEP and value < self.lastPeak - DECAY_STEP:
354            value = self.lastPeak - DECAY_STEP
355        self.lastPeak = value
356
357        self.levelBar.set_value(value)
358
359class ProfileSelector(SettingsWidget):
360    def __init__(self, controller):
361        super(ProfileSelector, self).__init__()
362        self.controller = controller
363        self.model = Gtk.ListStore(str, str)
364
365        self.combo = Gtk.ComboBox()
366        self.combo.set_model(self.model)
367        render = Gtk.CellRendererText()
368        self.combo.pack_start(render, True)
369        self.combo.add_attribute(render, "text", 1)
370        self.combo.set_id_column(0)
371
372        self.pack_start(Gtk.Label(_("Output profile")), False, False, 0)
373        button = Gtk.Button.new_with_label(_("Test sound"))
374        self.pack_end(button, False, False, 0)
375        self.pack_end(self.combo, False, False, 0)
376
377        button.connect("clicked", self.testSpeakers)
378        self.combo.connect("changed", self.onProfileSelect)
379
380    def setDevice(self, device):
381        self.device = device
382        # set the available output profiles in the combo box
383        profiles = device.get_profiles()
384        self.model.clear()
385        for profile in profiles:
386            self.model.append([profile.profile, profile.human_profile])
387
388        self.profile = device.get_active_profile()
389        self.combo.set_active_id(self.profile)
390
391    def onProfileSelect(self, a):
392        newProfile = self.combo.get_active_id()
393        if newProfile != self.profile and newProfile != None:
394            self.profile = newProfile
395            self.controller.change_profile_on_selected_device(self.device, newProfile)
396
397    def testSpeakers(self, a):
398        SoundTest(a.get_toplevel(), self.controller.get_default_sink())
399
400class Effect(GSettingsSoundFileChooser):
401    def __init__(self, info, sizeGroup):
402        super(Effect, self).__init__(info["label"], info["schema"], info["file"])
403
404        self.enabled_key = info["enabled"]
405
406        self.enabled_switch = Gtk.Switch()
407        self.pack_end(self.enabled_switch, False, False, 0)
408        self.reorder_child(self.enabled_switch, 1)
409
410        sizeGroup.add_widget(self.content_widget)
411
412        self.settings.bind(self.enabled_key, self.enabled_switch, "active", Gio.SettingsBindFlags.DEFAULT)
413
414class SoundTest(Gtk.Dialog):
415    def __init__(self, parent, stream):
416        Gtk.Dialog.__init__(self, _("Test Sound"), parent)
417
418        self.stream = stream
419        self.positions = []
420
421        grid = Gtk.Grid()
422        grid.set_column_spacing(75)
423        grid.set_row_spacing(75)
424        grid.set_column_homogeneous(True)
425        grid.set_row_homogeneous(True)
426        sizeGroup = Gtk.SizeGroup(Gtk.SizeGroupMode.BOTH)
427
428        index = 0
429        for position in SOUND_TEST_MAP:
430            container = Gtk.Box()
431            button = Gtk.Button()
432            sizeGroup.add_widget(button)
433            button.set_relief(Gtk.ReliefStyle.NONE)
434            box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
435            button.add(box)
436
437            icon = Gtk.Image.new_from_icon_name(position[2], Gtk.IconSize.DIALOG)
438            box.pack_start(icon, False, False, 0)
439            box.pack_start(Gtk.Label(position[0]), False, False, 0)
440
441            info = {"index":index, "icon":icon, "button":button}
442
443            button.connect("clicked", self.test, info)
444            container.add(button)
445            grid.attach(container, position[4], position[3], 1, 1)
446
447            index = index + 1
448            self.positions.append(info)
449
450        content_area = self.get_content_area()
451        content_area.set_border_width(12)
452        content_area.add(grid)
453
454        button = Gtk.Button.new_from_stock("gtk-close")
455        button.connect("clicked", self._destroy)
456        content_area.add(button)
457
458        self.show_all()
459        self.setPositionHideState()
460
461    def _destroy(self, widget):
462        self.destroy()
463
464    def test(self, b, info):
465        position = SOUND_TEST_MAP[info["index"]]
466
467        if position[1] == "lfe":
468            sound = "audio-test-signal"
469        else:
470            sound = "audio-channel-"+position[1]
471
472        session_bus = dbus.SessionBus()
473        sound_dbus = session_bus.get_object("org.cinnamon.SettingsDaemon.Sound", "/org/cinnamon/SettingsDaemon/Sound")
474        play = sound_dbus.get_dbus_method('PlaySoundWithChannel', 'org.cinnamon.SettingsDaemon.Sound')
475        play(0, sound, position[1])
476
477    def setPositionHideState(self):
478        map = self.stream.get_channel_map()
479        for position in self.positions:
480            index = position["index"]
481            if map.has_position(SOUND_TEST_MAP[index][5]):
482                position["button"].show()
483            else:
484                position["button"].hide()
485
486class Module:
487    name = "sound"
488    category = "hardware"
489    comment = _("Manage sound settings")
490
491    def __init__(self, content_box):
492        keywords = _("sound, media, music, speakers, audio, microphone, headphone")
493        self.sidePage = SidePage(_("Sound"), "cs-sound", keywords, content_box, module=self)
494        self.sound_settings = Gio.Settings(CINNAMON_DESKTOP_SOUNDS)
495
496    def on_module_selected(self):
497        if not self.loaded:
498            print("Loading Sound module")
499
500            self.outputDeviceList = Gtk.ListStore(str, # name
501                                                  str, # device
502                                                  bool, # active
503                                                  int, # id
504                                                  GdkPixbuf.Pixbuf) # icon
505
506            self.inputDeviceList = Gtk.ListStore(str, # name
507                                                 str, # device
508                                                 bool, # active
509                                                 int, # id
510                                                 GdkPixbuf.Pixbuf) # icon
511
512            self.appList = {}
513
514            self.inializeController()
515            self.buildLayout()
516
517        self.checkAppState()
518        self.checkInputState()
519
520    def buildLayout(self):
521        self.sidePage.stack = SettingsStack()
522        self.sidePage.add_widget(self.sidePage.stack)
523
524        ## Output page
525        page = SettingsPage()
526        self.sidePage.stack.add_titled(page, "output", _("Output"))
527
528        self.outputSelector = self.buildDeviceSelect("output", self.outputDeviceList)
529        outputSection = page.add_section(_("Device"))
530        outputSection.add_row(self.outputSelector)
531
532        devSettings = page.add_section(_("Device settings"))
533
534        # output profiles
535        self.profile = ProfileSelector(self.controller)
536        devSettings.add_row(self.profile)
537
538        sizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
539
540        # ouput volume
541        max_volume = self.sound_settings.get_int(MAXIMUM_VOLUME_KEY)
542        self.outVolume = VolumeBar(self.controller.get_vol_max_norm(), max_volume, sizeGroup=sizeGroup)
543        devSettings.add_row(self.outVolume)
544
545        # balance
546        self.balance = BalanceBar("balance", sizeGroup=sizeGroup)
547        devSettings.add_row(self.balance)
548        self.fade = BalanceBar("fade", sizeGroup=sizeGroup)
549        devSettings.add_row(self.fade)
550        self.woofer = BalanceBar("lfe", 0, self.controller.get_vol_max_norm(), sizeGroup=sizeGroup)
551        devSettings.add_row(self.woofer)
552
553        ## Input page
554        page = SettingsPage()
555        self.sidePage.stack.add_titled(page, "input", _("Input"))
556
557        self.inputStack = Gtk.Stack()
558        page.pack_start(self.inputStack, True, True, 0)
559
560        inputBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15)
561        self.inputSelector = self.buildDeviceSelect("input", self.inputDeviceList)
562        deviceSection = SettingsSection("Device")
563        inputBox.pack_start(deviceSection, False, False, 0)
564        deviceSection.add_row(self.inputSelector)
565
566        devSettings = SettingsSection(_("Device settings"))
567        inputBox.pack_start(devSettings, False, False, 0)
568
569        sizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
570
571        # input volume
572        self.inVolume = VolumeBar(self.controller.get_vol_max_norm(), max_volume, sizeGroup=sizeGroup)
573        devSettings.add_row(self.inVolume)
574
575        # input level
576        self.inLevel = VolumeLevelBar(sizeGroup)
577        devSettings.add_row(self.inLevel)
578        self.inputStack.add_named(inputBox, "inputBox")
579
580        noInputsMessage = Gtk.Box()
581        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
582        image = Gtk.Image.new_from_icon_name("action-unavailable-symbolic", Gtk.IconSize.DIALOG)
583        image.set_pixel_size(96)
584        box.pack_start(image, False, False, 0)
585        box.set_valign(Gtk.Align.CENTER)
586        label = Gtk.Label(_("No inputs sources are currently available."))
587        box.pack_start(label, False, False, 0)
588        noInputsMessage.pack_start(box, True, True, 0)
589        self.inputStack.add_named(noInputsMessage, "noInputsMessage")
590        self.inputStack.show_all()
591
592        ## Sounds page
593        page = SettingsPage()
594        self.sidePage.stack.add_titled(page, "sounds", _("Sounds"))
595
596        soundsVolumeSection = page.add_section(_("Sounds Volume"))
597        self.soundsVolume = VolumeBar(self.controller.get_vol_max_norm(), 100)
598        soundsVolumeSection.add_row(self.soundsVolume)
599
600        soundsSection = SoundBox(_("Sounds"))
601        page.pack_start(soundsSection, True, True, 0)
602        sizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
603        for effect in EFFECT_LIST:
604            soundsSection.add_row(Effect(effect, sizeGroup))
605
606        ## Applications page
607        page = SettingsPage()
608        self.sidePage.stack.add_titled(page, "applications", _("Applications"))
609
610        self.appStack = Gtk.Stack()
611        page.pack_start(self.appStack, True, True, 0)
612
613        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
614        self.appSettings = SoundBox(_("Applications"))
615        box.pack_start(self.appSettings, True, True, 0)
616        self.appStack.add_named(box, "appSettings")
617
618        noAppsMessage = Gtk.Box()
619        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
620        image = Gtk.Image.new_from_icon_name("action-unavailable-symbolic", Gtk.IconSize.DIALOG)
621        image.set_pixel_size(96)
622        box.pack_start(image, False, False, 0)
623        box.set_valign(Gtk.Align.CENTER)
624        label = Gtk.Label(_("No application is currently playing or recording audio."))
625        box.pack_start(label, False, False, 0)
626        noAppsMessage.pack_start(box, True, True, 0)
627        self.appStack.add_named(noAppsMessage, "noAppsMessage")
628
629        ## Settings page
630        page = SettingsPage()
631        self.sidePage.stack.add_titled(page, "settings", _("Settings"))
632
633        amplificationSection = page.add_section(_("Amplification"))
634        self.maxVolume = Slider(_("Maximum volume: %d") % max_volume + "%", _("Reduced"), _("Amplified"), 1, 150, None, step=1, page=10, value=max_volume, gicon=None, iconName=None)
635        self.maxVolume.adjustment.connect("value-changed", self.onMaxVolumeChanged)
636        self.maxVolume.setMark(100)
637        amplificationSection.add_row(self.maxVolume)
638
639    def onMaxVolumeChanged(self, adjustment):
640        newValue = int(round(adjustment.get_value()))
641        self.sound_settings.set_int(MAXIMUM_VOLUME_KEY, newValue)
642        self.maxVolume.label.set_label(_("Maximum volume: %d") % newValue + "%")
643        self.outVolume.adjustment.set_upper(newValue)
644        self.outVolume.slider.clear_marks()
645        if (newValue > 100):
646            self.outVolume.setMark(100)
647
648    def inializeController(self):
649        self.controller = Cvc.MixerControl(name = "cinnamon")
650        self.controller.connect("state-changed", self.setChannelMap)
651        self.controller.connect("output-added", self.deviceAdded, "output")
652        self.controller.connect("input-added", self.deviceAdded, "input")
653        self.controller.connect("output-removed", self.deviceRemoved, "output")
654        self.controller.connect("input-removed", self.deviceRemoved, "input")
655        self.controller.connect("active-output-update", self.activeOutputUpdate)
656        self.controller.connect("active-input-update", self.activeInputUpdate)
657        self.controller.connect("default-sink-changed", self.defaultSinkChanged)
658        self.controller.connect("default-source-changed", self.defaultSourceChanged)
659        self.controller.connect("stream-added", self.streamAdded)
660        self.controller.connect("stream-removed", self.streamRemoved)
661        self.controller.open()
662
663    def buildDeviceSelect(self, type, model):
664        select = Gtk.IconView.new_with_model(model)
665        select.set_margin(0)
666        select.set_pixbuf_column(4)
667        select.set_text_column(0)
668        select.set_column_spacing(0)
669
670        select.connect("selection-changed", self.setActiveDevice, type)
671
672        return select
673
674    def setActiveDevice(self, view, type):
675        selected = view.get_selected_items()
676        if len(selected) == 0:
677            return
678
679        model = view.get_model()
680        newDeviceId = model.get_value(model.get_iter(selected[0]), 3)
681        newDevice = getattr(self.controller, "lookup_"+type+"_id")(newDeviceId)
682        if newDevice != None and newDeviceId != getattr(self, type+"Id"):
683            getattr(self.controller, "change_"+type)(newDevice)
684            self.profile.setDevice(newDevice)
685
686    def deviceAdded(self, c, id, type):
687        device = getattr(self.controller, "lookup_"+type+"_id")(id)
688
689        iconTheme = Gtk.IconTheme.get_default()
690        gicon = device.get_gicon()
691        iconName = device.get_icon_name()
692        icon = None
693        if gicon is not None:
694            lookup = iconTheme.lookup_by_gicon(gicon, 32, 0)
695            if lookup is not None:
696                icon = lookup.load_icon()
697
698        if icon is None:
699            if (iconName is not None and "bluetooth" in iconName):
700                icon = iconTheme.load_icon("bluetooth", 32, 0)
701            else:
702                icon = iconTheme.load_icon("audio-card", 32, 0)
703
704        getattr(self, type+"DeviceList").append([device.get_description() + "\n" +  device.get_origin(), "", False, id, icon])
705
706        if type == "input":
707            self.checkInputState()
708
709    def deviceRemoved(self, c, id, type):
710        store = getattr(self, type+"DeviceList")
711        for row in store:
712            if row[3] == id:
713                store.remove(row.iter)
714                if type == "input":
715                    self.checkInputState()
716                return
717
718    def checkInputState(self):
719        if len(self.inputDeviceList) == 0:
720            self.inputStack.set_visible_child_name("noInputsMessage")
721        else:
722            self.inputStack.set_visible_child_name("inputBox")
723
724    def activeOutputUpdate(self, c, id):
725        self.outputId = id
726        device = self.controller.lookup_output_id(id)
727
728        self.profile.setDevice(device)
729
730        # select current device in device selector
731        i = 0
732        for row in self.outputDeviceList:
733            if row[3] == id:
734                self.outputSelector.select_path(Gtk.TreePath.new_from_string(str(i)))
735            i = i + 1
736
737        self.setChannelMap()
738
739    def activeInputUpdate(self, c, id):
740        self.inputId = id
741
742        # select current device in device selector
743        i = 0
744        for row in self.inputDeviceList:
745            if row[3] == id:
746                self.inputSelector.select_path(Gtk.TreePath.new_from_string(str(i)))
747            i = i + 1
748
749    def defaultSinkChanged(self, c, id):
750        defaultSink = self.controller.get_default_sink()
751        if defaultSink == None:
752            return
753        self.outVolume.setStream(defaultSink)
754        self.setChannelMap()
755
756    def defaultSourceChanged(self, c, id):
757        defaultSource = self.controller.get_default_source()
758        if defaultSource == None:
759            return
760        self.inVolume.setStream(defaultSource)
761        self.inLevel.setStream(defaultSource)
762
763    def setChannelMap(self, a=None, b=None):
764        if self.controller.get_state() == Cvc.MixerControlState.READY:
765            channelMap = self.controller.get_default_sink().get_channel_map()
766            self.balance.setChannelMap(channelMap)
767            self.fade.setChannelMap(channelMap)
768            self.woofer.setChannelMap(channelMap)
769
770    def streamAdded(self, c, id):
771        stream = self.controller.lookup_stream_id(id)
772
773        if stream in self.controller.get_sink_inputs():
774            name = stream.props.name
775
776            # FIXME: We use to filter out by PA_PROP_APPLICATION_ID.  But
777            # most streams report this as null now... why??
778            if name in ("speech-dispatcher", "libcanberra"):
779                # speech-dispatcher: orca/speechd/spd-say
780                # libcanberra: cinnamon effects, test sounds
781                return
782
783            if id in self.appList.keys():
784                # Don't add an input more than once
785                return
786
787            if name == None:
788                name = _("Unknown")
789
790            label = "%s: " % name
791
792            self.appList[id] = VolumeBar(self.controller.get_vol_max_norm(),
793                                         100,
794                                         label,
795                                         stream.get_gicon())
796            self.appList[id].setStream(stream)
797            self.appSettings.add_row(self.appList[id])
798            self.appSettings.list_box.invalidate_headers()
799            self.appSettings.show_all()
800        elif stream == self.controller.get_event_sink_input():
801            self.soundsVolume.setStream(stream)
802
803        self.checkAppState()
804
805    def streamRemoved(self, c, id):
806        if id in self.appList:
807            self.appList[id].get_parent().destroy()
808            self.appSettings.list_box.invalidate_headers()
809            del self.appList[id]
810            self.checkAppState()
811
812    def checkAppState(self):
813        if len(self.appList) == 0:
814            self.appStack.set_visible_child_name("noAppsMessage")
815        else:
816            self.appStack.set_visible_child_name("appSettings")
817