├── .gitignore ├── Changelog.md ├── ReadMe.md ├── build ├── install ├── package ├── contents │ ├── code │ │ └── peak │ │ │ ├── .gitignore │ │ │ ├── ReadMe.md │ │ │ ├── lib_pulseaudio.py │ │ │ └── peak_monitor.py │ ├── config │ │ ├── config.qml │ │ └── main.xml │ ├── images │ │ ├── buildskins │ │ ├── volumeslider-default.svg │ │ └── volumeslider.svg │ └── ui │ │ ├── AppletConfig.qml │ │ ├── DialogApplet.qml │ │ ├── DynamicFilterModel.qml │ │ ├── IconLabelButton.qml │ │ ├── IconToolButton.qml │ │ ├── InputManager.qml │ │ ├── MediaController.qml │ │ ├── MixerItem.qml │ │ ├── MixerItemGroup.qml │ │ ├── Mpris2DataSource.qml │ │ ├── PulseObjectDialog.qml │ │ ├── VerticalVolumeSlider.qml │ │ ├── VolumePeaksManager.qml │ │ ├── code │ │ ├── Icon.js │ │ ├── PulseObjectCommands.js │ │ └── Utils.js │ │ ├── config │ │ ├── ConfigApplet.qml │ │ ├── ConfigComboBox.qml │ │ └── ConfigStreamRestore.qml │ │ ├── lib │ │ ├── AppletIcon.qml │ │ ├── AppletVersion.qml │ │ ├── ConfigPage.qml │ │ ├── ContextMenu.qml │ │ ├── ContextMenuItem.qml │ │ ├── ContextSubMenu.qml │ │ └── ExecUtil.qml │ │ └── main.qml ├── metadata.desktop └── translate │ ├── ReadMe.md │ ├── build │ ├── fr.po │ ├── merge │ ├── nl.po │ ├── plasmoidlocaletest │ └── template.pot ├── plugin ├── .gitignore ├── CMakeLists.txt ├── ReadMe.md ├── build ├── install ├── plugin.cpp ├── plugin.h ├── qmldir ├── volumepeaks.cpp └── volumepeaks.h ├── reinstall └── run /.gitignore: -------------------------------------------------------------------------------- 1 | *.plasmoid 2 | *.qmlc 3 | *.jsc 4 | *.mo 5 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## v26 - November 20 2020 2 | 3 | * Support Plasma 5.20's osd.show(percent, maxPercent). 4 | * Fix workaround for opening custom popup when in system tray. 5 | * Use radio button instead of a checkmark for default device (Issue #19) with a QQC2 ToolTip on hover. 6 | * Update i18n scripts. 7 | * Use some of plasma-pa's improvements on port selection. 8 | * Add support for left clicking device icon to set default device instead of showing the context menu by @linchangyi (Pull Request #20 and #21) 9 | * Applet name correction in documentation by @luema (Pull Request #18) 10 | 11 | ## v25 - June 24 2020 12 | 13 | * Only jump to 0%/100% if there's less than `step/2` remaining (Issue #17) 14 | * Use `kmix` icon instead of the `speaker` icon for "speaker" pulseaudio sinks. It looks more like a speaker. 15 | * Increment using config step percentage when scrolling the sliders themselves. 16 | * Show Plasma 5.14's VolumeObject.rawChannels in the PulseObject's properties popup. 17 | * Use aec_method="webrtc" argument with echocancel. 18 | 19 | ## v24 - January 28 2019 20 | 21 | * Add @RValeye's french translations (Issue #8). 22 | * Add @Vistaus dutch translations (Pull Request #9). 23 | * Round up the volume percentage increment to avoid 50 => 59% instead of 60% when you have 10 steps. 24 | * Scale properties dialog sizes by dpi + fix the headings when scrolling. 25 | * Code cleanup. 26 | 27 | ## v23 - June 5 2018 28 | 29 | * Add "Profile" submenu for devices to quickly switch HDMI => Speakers for certain laptops, or from Stereo to Surround. Creating a submenu required a workaround to avoid a SegFault (plasmashell crash). While the workaround does work, please report if plasmashell crashes when opening a device's context menu. 30 | * Don't send multiple "set volume" events when changing a left/right/etc channel volume. 31 | * Add ability to open the a stream's context menu via the keyboard "Menu" key. 32 | * Attempts to fix the Media Controller's various glitches. 33 | 34 | ## v22 - December 26 2017 35 | 36 | * Reimplement mouse wheel mute, wheel to control volume which was accidentally removed in the v21 refactor. 37 | 38 | ## v21 - December 6 2017 39 | 40 | * Add (proper) support for use in the system tray. Will open in it's own popup window. 41 | * Unchecking echo cancellation will also uncheck the "list to device" if listening to the echo cancelled stream. 42 | 43 | ## v20 - November 11 2017 44 | 45 | * Use the same shape for the slider handle as Windows 7. 46 | * Automatically close popup when selecting default device (can disable in config). 47 | * Implement toggle for echo cancelling and microphone loopback. 48 | * Show checkmark next to the default speaker/mic when 2+ devices. 49 | * Hide virtual streams by default (configurable). 50 | * Scale panel icon to the same maximum size as the other icons in Plasma 5.10. 51 | * Begin packaging translations in the `*.plasmoid` (requires KDE Frameworks v37 to work). Reused some translations from the default volume and mediacontroller widgets. 52 | 53 | ## v19 - March 29 2017 54 | 55 | * Can now use keyboard navigation to select a stream. 56 | ** Left/Right: Select speaker/mic/app/etc. 57 | ** Up/Down: Increase/decrease selected volume (by same amount as volume keys). 58 | ** M: Mute/unmute selected stream. 59 | ** 0-9: Set volume to 0%-90% 60 | ** Enter: If a mic/speaker is selected, make it the default device. 61 | * Selected stream will have a pulsating outline. Current default speaker is selected by default. Outline is hidden if you open the mixer with the mouse, but shortcuts will still work. 62 | * Global shortcut will now toggle the popup. 63 | * Fix the blue on gray theme using the color scheme rather than hardcoded colours. 64 | * Fix toggling the volume boost, snapping the value to just over 100% causing it to remain in "boosted" state (with a max of 150%). 65 | * Drag to 1% intervals when volume boosted (instead of 1.5%, 3%, 4.5%, etc). 66 | * Possibly fix a binding loop when checking if you can seek through a song. 67 | 68 | ## v18 - March 23 2017 69 | 70 | * Raised minimum requirements to Plasma 5.8 71 | * [upstream] Add volume feedback 72 | * Show current version in the config. 73 | 74 | ## v17 - March 21 2017 75 | 76 | * Fix for the media slider starting at the length of the previous song. Thanks davidedmundson. 77 | * Get rid of the 1px outline on the volume slider groove. 78 | * The new volume slider layout will now be coloured based on the desktop theme. 79 | * The previous volume slider theme/colouring (light blue on gray) can be selected in the settings. 80 | * Add time elapsed & time left next to the song's progressbar like the default media controller widget. Both are toggleable, along with the option to show the total duration of the song. 81 | 82 | ## v16 - March 15 2017 83 | 84 | * Make the icon+label into a button that opens the context menu. 85 | * You can now drag a microphone onto a recording app to change it's input. I only tested this with SimpleStreamRecorder and it added recorded both the desktop output and the microphone output at the same time rather than switching from one to the other. 86 | * Overlay 'emblem-unlocked' when app isn't using the default speaker/mic. I may change the icon if a better one is recommended. 87 | * Fuss with the volume slider triangle. It will now be thicker when volume is boosted. 88 | * Make the group title (Apps/Mics/Speakers) into a button. It will probably be used for filtering unwanted streams in the future, but for now it just lists the items in it's group. 89 | * Fix the label/icon when using the echo-cancel pulseaudio module. 90 | 91 | ## v15 - March 14 2017 92 | 93 | * Reskin the volume sliders to be triangular similar to kmix/win7. 94 | * Allow placing the media controller at the top of the popup. 95 | * Make the media controller slider taller. 96 | * Scale the widget based on the DPI. 97 | * Remove context menu link to the kcm like the default widget. It's still availble with "Audio Volume Settings..." > "Audio Volume". 98 | * Map speakers with names starting with "bluez_sink." to a bluetooth icon. 99 | * Add a properties dialog listing all the values for a speaker/app/microphone. 100 | * Use 'google-chrome' icon for "chrome (deleted)" streams. 101 | * Use the "microphone volume/mute" icons from the OSD for a microphones mute button. 102 | * Add toggle for showing the OSD. 103 | * [upstream] Mute volume when the slider is at 0%. 104 | * When using the mediakeys, jump to 100%/0% if less than 1 step away. 105 | * Compare the port key for "headphone" instead of the localized "Headphone" when deciding on the icon. 106 | * Fix the mute button icon's hover effects. 107 | * Fix all strings for localization with i18n. 108 | * Russian translations are available in RosaLinux's ABF: https://abf.rosalinux.ru/victorr2007/plasma5-applet-volumewin7mixer 109 | * Use doubles instead of ints for the mpris2/media controller's position/duration which are in microseconds since it was overflowing on songs/movies longer than 33 minutes. 110 | 111 | ## v14 - September 7 2016 112 | 113 | * Show current song in tooltip. 114 | * Use description instead of the internal name on mics/speakers. 115 | * Use the video-television icon for the hdmi speaker. 116 | 117 | ## v13 - August 26 2016 118 | 119 | * Add media controls based on the default widget + mediacontrollercompact. You can disable it in the settings. 120 | * Icons now follow the theme color. 121 | 122 | ## v12 - August 24 2016 123 | 124 | * Use heaphones icon when port is set to Headphones. 125 | * Add context menu to quickly: Change the default speaker/microphone, Volume Boost the steam, Change the port (eg: headphones). 126 | * Mute button now mutes instead of volume boosts. 127 | * Fix drag and drop device selection. 128 | * Show output device id in app tooltip. 129 | * Add group for "Recording Apps" (eg: VirtualBox). 130 | * Add standard pin to keep mixer open. 131 | * Optionally move all app streams to the newly selected default device. 132 | 133 | ## v11 - July 7 2016 134 | 135 | * This requires KDE 5.7+ 136 | * Bump version ahead of the AUR package which bumped versions before I started versioning. 137 | * Merged from upstream (plasma-pa): 138 | * Use the default speaker volume in the panel icon. 139 | * Media keys only control the default speaker. 140 | * Volume Boost to 150%. Can be toggled per app by clicking the speaker icon (formerly the mute button). I will be moving that button to a context menu as soon as I figure out how. Example: https://streamable.com/oqt4 141 | * Don't disable the slider when muted, alowing the user to change the volume without unmuting. 142 | * Handle Microphone shortcuts. 143 | 144 | ## v2 - May 13 2016 145 | 146 | * Supports KDE 5.5 and KDE 5.6 (Maybe 5.4?) 147 | * Custom vertical volume slider. 148 | * Configurable number of steps to reach 100% volume with media keys. 149 | * Add links to alsamixer and pavucontrol in context menu. 150 | * Merged from upstram (plasma-pa): 151 | * Drag and drop to move app output to a specific speaker. 152 | 153 | ## v1 - ? 154 | 155 | * Vertical volume sliders. 156 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Audio Volume (Win7 Mixer) 2 | 3 | https://store.kde.org/p/1100894/ 4 | 5 | A fork of the [default volume plasmoid](https://github.com/KDE/plasma-pa/tree/Plasma/5.5/applet) with a Windows 7 theme (vertical sliders). 6 | 7 | ## Screenshot 8 | 9 | ![](https://i.imgur.com/OeC9Zhc.png) 10 | 11 | 12 | ## A) Install via KDE Plasma 13 | 14 | 1. Right Click Panel > Panel Options > Add Widgets 15 | 2. Get New Widgets > Download New Widgets 16 | 3. Search: Win7 Volume Mixer 17 | 5. Install 18 | 6. After installing, System Tray Settings > Extra Items > Uncheck "Audio Volume". This will hide the default audio widget. 19 | 7. Drag "Audio Volume (Win7)" to your panel. 20 | 21 | ## B) Install via GitHub 22 | 23 | ``` 24 | git clone https://github.com/Zren/plasma-applet-volumewin7mixer.git 25 | cd plasma-applet-volumewin7mixer 26 | ./install 27 | ``` 28 | 29 | To update, run the `./update` script. It will run a `git pull` then reinstall the applet. Please note this script will restart plasmashell (so you don't have to relog)! 30 | 31 | ## C) Install via Package Manager 32 | 33 | Some awesome users seemed to have packaged this applet under `plasma5-applets-volumewin7mixer`. 34 | 35 | * Arch: https://aur.archlinux.org/packages/plasma5-applets-volumewin7mixer/ 36 | * Chakra: (Out of Date) https://chakralinux.org/ccr/packages.php?ID=7763 37 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 7 3 | 4 | rm package/contents/code/peak/__pycache__/*.pyc 5 | rmdir package/contents/code/peak/__pycache__ 6 | 7 | if [ -d "package/translate" ]; then 8 | echo "[build] translate dir found, running merge." 9 | (cd package/translate && sh ./merge && sh ./build) 10 | if [ "$(git diff --stat .)" != "" ]; then 11 | echo "[build] Changed detected. Cancelling build." 12 | git diff --stat . 13 | exit 14 | fi 15 | fi 16 | 17 | plasmoidName=$(kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name") 18 | plasmoidName="${plasmoidName##*.}" # Strip namespace (Eg: "org.kde.plasma.") 19 | plasmoidVersion=$(kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Version") 20 | filenameTag="-plasma5.12" 21 | rm ${plasmoidName}-v*.plasmoid 22 | cd package 23 | filename=${plasmoidName}-v${plasmoidVersion}${filenameTag}.plasmoid 24 | zip -r $filename * 25 | mv $filename ../$filename 26 | cd .. 27 | echo "md5: $(md5sum $filename | awk '{ print $1 }')" 28 | echo "sha256: $(sha256sum $filename | awk '{ print $1 }')" 29 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 2 3 | 4 | kpackagetool5 -t Plasma/Applet -i package 5 | -------------------------------------------------------------------------------- /package/contents/code/peak/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /package/contents/code/peak/ReadMe.md: -------------------------------------------------------------------------------- 1 | * `lib_pulseaudio.py` 2 | https://github.com/Valodim/python-pulseaudio 3 | By Vincent Breitmoser, under LGPL 4 | * `peak_monitor.py` 5 | http://freshfoo.com/posts/pulseaudio_monitoring/ 6 | http://freshfoo.com/posts/raspberry_pi_vu_meter/ 7 | https://bitbucket.org/mjs0/raspberry_pi-vu_meter/src 8 | By Menno Finlay-Smits 9 | 10 | `peak_monitor.py` has been modified so that the monitor is filtered out (pretend to be a PulseAudio Control stream). It will also be modified further to monitor the peaks of Sources, SourceOutputs, and SinkInputs. 11 | -------------------------------------------------------------------------------- /package/contents/code/peak/peak_monitor.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | # See http://freshfoo.com/blog/pulseaudio_monitoring for information on how 4 | # this module works. 5 | 6 | import sys 7 | from queue import Queue 8 | from ctypes import POINTER, c_ubyte, c_void_p, c_ulong, cast, sizeof 9 | 10 | # From https://github.com/Valodim/python-pulseaudio 11 | from lib_pulseaudio import * 12 | 13 | class PeakMonitor(object): 14 | 15 | def __init__(self, stream_type, device_index, stream_index=-1, rate=30): 16 | self.stream_type = stream_type 17 | self.device_index = device_index 18 | self.stream_index = stream_index 19 | self.rate = rate 20 | 21 | # Wrap callback methods in appropriate ctypefunc instances so 22 | # that the Pulseaudio C API can call them 23 | self._context_notify_cb = pa_context_notify_cb_t(self.context_notify_cb) 24 | self._stream_read_cb = pa_stream_request_cb_t(self.stream_read_cb) 25 | self._stream_suspended_cb = pa_stream_notify_cb_t(self.stream_suspended_cb) 26 | 27 | # stream_read_cb() puts peak samples into this Queue instance 28 | self._samples = Queue(maxsize=rate) 29 | 30 | # Create the mainloop thread and set our context_notify_cb 31 | # method to be called when there's updates relating to the 32 | # connection to Pulseaudio 33 | _mainloop = pa_threaded_mainloop_new() 34 | _mainloop_api = pa_threaded_mainloop_get_api(_mainloop) 35 | 36 | # context = pa_context_new(_mainloop_api, 'peak_demo') 37 | proplist = pa_proplist_new() 38 | pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, b"org.PulseAudio.pavucontrol") 39 | context = pa_context_new_with_proplist(_mainloop_api, None, proplist) 40 | 41 | pa_context_set_state_callback(context, self._context_notify_cb, None) 42 | pa_context_connect(context, None, 0, None) 43 | pa_threaded_mainloop_start(_mainloop) 44 | 45 | def __iter__(self): 46 | while True: 47 | yield self._samples.get() 48 | 49 | def context_notify_cb(self, context, _): 50 | state = pa_context_get_state(context) 51 | 52 | if state == PA_CONTEXT_READY: 53 | # Connected to Pulseaudio. Register the peak monitor. 54 | self.register_peak_monitor(context) 55 | 56 | elif state == PA_CONTEXT_FAILED : 57 | print("Connection failed") 58 | 59 | elif state == PA_CONTEXT_TERMINATED: 60 | print("Connection terminated") 61 | 62 | def register_peak_monitor(self, context): 63 | # Tell PA to call stream_read_cb with peak samples. 64 | # Eg: https://github.com/pulseaudio/pavucontrol/blob/574139c10e70b63874bcb75fe4cdfd1f4644ad68/src/mainwindow.cc#L574 65 | samplespec = pa_sample_spec() 66 | samplespec.channels = 1 67 | samplespec.format = PA_SAMPLE_U8 68 | samplespec.rate = self.rate 69 | 70 | pa_stream = pa_stream_new(context, b"Peak detect (plasma-pa-feedback)", samplespec, None) 71 | if not pa_stream: 72 | print("Failed to create monitoring stream") 73 | return 74 | 75 | pa_stream_set_read_callback(pa_stream, self._stream_read_cb, self.device_index) 76 | pa_stream_set_suspended_callback(pa_stream, self._stream_suspended_cb, self.device_index) 77 | 78 | # sinkinput and sourceoutput 79 | if self.stream_index != -1: 80 | pa_stream_set_monitor_stream(pa_stream, self.stream_index) 81 | 82 | flags = PA_STREAM_DONT_MOVE | PA_STREAM_PEAK_DETECT | PA_STREAM_ADJUST_LATENCY 83 | dev = STRING(str(self.device_index).encode('utf8')) 84 | attr = None 85 | result = pa_stream_connect_record(pa_stream, dev, attr, flags) 86 | if result < 0: 87 | print("Failed to connect monitoring stream") 88 | pa_stream_unref(pa_stream) 89 | return 90 | 91 | def stream_read_cb(self, stream, length, index_incr): 92 | data = c_void_p() 93 | 94 | if pa_stream_peek(stream, data, c_ulong(length)) < 0: 95 | print("Failed to read data from stream") 96 | return 97 | 98 | 99 | if not data: 100 | # NULL data means either a hole or empty buffer 101 | # Only drop the stream when there is a hole (length > 0) 102 | if length: 103 | pa_stream_drop(stream) 104 | return 105 | 106 | assert(length > 0) 107 | assert(length % sizeof(c_ubyte) == 0) 108 | 109 | data = cast(data, POINTER(c_ubyte)) 110 | for i in range(length): 111 | # When PA_SAMPLE_U8 is used, samples values range from 128 112 | # to 255 because the underlying audio data is signed but 113 | # it doesn't make sense to return signed peaks. 114 | self._samples.put(data[i] - 128) 115 | pa_stream_drop(stream) 116 | 117 | def stream_suspended_cb(self, stream, userdata): 118 | if pa_stream_is_suspended(stream): 119 | print("stream suspended") 120 | # w->updateVolumeMeter(pa_stream_get_device_index(s), PA_INVALID_INDEX, -1); 121 | 122 | 123 | 124 | if __name__ == '__main__': 125 | import sys 126 | if len(sys.argv) < 3: 127 | print('Usage: python3 peak_monitor.py [Sink|SinkInput|Source|SourceOutput] [index]') 128 | print('Eg: python3 peak_monitor.py Sink 0') 129 | sys.exit(1) 130 | 131 | stream_type = sys.argv[1].lower() 132 | device_index = int(sys.argv[2]) 133 | stream_index = int(sys.argv[3]) if len(sys.argv) >= 4 else -1 134 | 135 | peak = PeakMonitor(stream_type, device_index, stream_index=stream_index, rate=30) 136 | for sample in peak: 137 | # samples = 0..127 138 | # 65536 = PulseAudio.NormalVolume = 100% 139 | # 128 = 2^7 140 | # 65536 = 2^16 141 | # 65536 = 2^7 * 2^9 142 | # 512 = 2^9 143 | sys.stdout.write(str(sample * 512) + "\n") 144 | sys.stdout.flush() 145 | -------------------------------------------------------------------------------- /package/contents/config/config.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.configuration 2.0 3 | 4 | ConfigModel { 5 | ConfigCategory { 6 | name: i18nd("plasma_applet_org.kde.plasma.volume", "General") 7 | icon: "plasma" 8 | source: "config/ConfigApplet.qml" 9 | } 10 | // ConfigCategory { 11 | // name: "Stream Restore" 12 | // icon: "document-save-symbolic" 13 | // source: "config/ConfigStreamRestore.qml" 14 | // } 15 | } 16 | -------------------------------------------------------------------------------- /package/contents/config/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | true 17 | 18 | 19 | false 20 | 21 | 22 | true 23 | 24 | 25 | true 26 | 27 | 28 | false 29 | 30 | 31 | true 32 | 33 | 34 | bottom 35 | 36 | 37 | true 38 | 39 | 40 | desktoptheme 41 | 42 | 43 | true 44 | 45 | 46 | true 47 | 48 | 49 | false 50 | 51 | 52 | true 53 | 54 | 55 | true 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /package/contents/images/buildskins: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | def buildTheme(template, palette): 4 | overrides = 'id="defs4">\n' 5 | overrides += """ 6 | ' 16 | theme = template.replace('id="defs4">', overrides) 17 | return theme 18 | 19 | themes = { 20 | 'default': { 21 | 'ColorScheme-Text': '#eeeeee', 22 | 'ColorScheme-Background': '#31363b', 23 | 'ColorScheme-Highlight': '#93cee9', 24 | 'ColorScheme-ButtonText': '#31363b', 25 | 'ColorScheme-ButtonBackground': '#eff0f1', 26 | 'ColorScheme-ButtonHover': '#93cee9', 27 | 'ColorScheme-ButtonFocus': '#3daee9', 28 | } 29 | } 30 | 31 | with open("volumeslider.svg", "r") as f: 32 | template = f.read() 33 | 34 | for themeName, palette in themes.items(): 35 | theme = buildTheme(template, palette) 36 | filename = "volumeslider-{}.svg".format(themeName) 37 | with open(filename, "w") as f: 38 | f.write(theme) 39 | -------------------------------------------------------------------------------- /package/contents/images/volumeslider-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 51 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 95 | 97 | 98 | 113 | 163 | 172 | 173 | 175 | 176 | 178 | image/svg+xml 179 | 181 | 182 | 183 | 184 | 188 | 194 | 201 | 210 | 217 | 223 | 230 | 237 | 243 | 249 | 255 | 261 | 268 | 275 | 281 | 288 | 295 | 301 | 307 | 313 | 316 | 323 | 329 | 335 | 336 | 339 | 346 | 352 | 358 | 359 | 363 | 371 | 382 | 390 | 391 | 394 | 401 | 408 | 415 | 416 | 419 | 426 | 433 | 440 | 441 | 444 | 451 | 458 | 465 | 466 | 469 | 476 | 483 | 488 | 489 | 492 | 499 | 504 | 509 | 510 | 513 | 520 | 521 | 524 | 531 | 532 | 535 | 542 | 543 | 550 | 558 | 565 | 572 | 580 | 588 | 594 | 600 | 607 | 613 | 620 | 627 | 633 | 640 | 647 | 653 | 659 | 665 | 668 | 675 | 676 | 677 | 678 | -------------------------------------------------------------------------------- /package/contents/images/volumeslider.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 51 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 95 | 97 | 147 | 156 | 157 | 159 | 160 | 162 | image/svg+xml 163 | 165 | 166 | 167 | 168 | 172 | 178 | 185 | 194 | 201 | 207 | 214 | 221 | 227 | 233 | 239 | 245 | 252 | 259 | 265 | 272 | 279 | 285 | 291 | 297 | 300 | 307 | 313 | 319 | 320 | 323 | 330 | 336 | 342 | 343 | 347 | 355 | 366 | 374 | 375 | 378 | 385 | 392 | 399 | 400 | 403 | 410 | 417 | 424 | 425 | 428 | 435 | 442 | 449 | 450 | 453 | 460 | 467 | 472 | 473 | 476 | 483 | 488 | 493 | 494 | 497 | 504 | 505 | 508 | 515 | 516 | 519 | 526 | 527 | 534 | 542 | 549 | 556 | 564 | 572 | 578 | 584 | 591 | 597 | 604 | 611 | 617 | 624 | 631 | 637 | 643 | 649 | 652 | 659 | 660 | 661 | 662 | -------------------------------------------------------------------------------- /package/contents/ui/AppletConfig.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | property int mediaControllerSliderHeight: 16 * units.devicePixelRatio 5 | // property int mediaControllerButtonHeight: 48 * units.devicePixelRatio 6 | property int mediaControllerHeight: 64 * units.devicePixelRatio 7 | property int mixerGroupHeight: units.gridUnit * 24 8 | property int mixerItemWidth: 100 * units.devicePixelRatio 9 | property int volumeSliderWidth: 48 * units.devicePixelRatio 10 | 11 | property string volumeSliderDesktopThemeId: "widgets/volumeslider" 12 | property string volumeSliderUrl: { 13 | if (plasmoid.configuration.volumeSliderTheme == "desktoptheme") { 14 | if (false) { // svg exists 15 | return volumeSliderDesktopThemeId 16 | } else { 17 | return plasmoid.file("images", "volumeslider.svg") // colortheme 18 | } 19 | } else if (plasmoid.configuration.volumeSliderTheme == "colortheme") { 20 | return plasmoid.file("images", "volumeslider.svg") 21 | } else { // default 22 | return plasmoid.file("images", "volumeslider-default.svg") 23 | } 24 | } 25 | 26 | property color selectedStreamOutline: config.withAlpha(theme.textColor, 0.25) 27 | property color selectedStreamOutlinePulse: theme.textColor 28 | 29 | function withAlpha(c1, alpha) { 30 | var c2 = Qt.darker(c1, 1) 31 | c2.a = alpha 32 | return c2 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package/contents/ui/DialogApplet.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.plasmoid 2.0 6 | 7 | // Based roughly on: 8 | // https://github.com/KDE/plasma-desktop/blob/master/desktoppackage/contents/applet/CompactApplet.qml 9 | 10 | Item { 11 | id: main 12 | 13 | property var compactItemIcon: 'plasma' 14 | 15 | signal compactItemPressed(var mouse) 16 | signal compactItemClicked(var mouse) 17 | signal compactItemWheel(var wheel) 18 | 19 | Plasmoid.onCompactRepresentationItemChanged: { 20 | Plasmoid.compactRepresentationItem.compactItemPressed.connect(main.compactItemPressed) 21 | Plasmoid.compactRepresentationItem.compactItemClicked.connect(main.compactItemClicked) 22 | Plasmoid.compactRepresentationItem.compactItemWheel.connect(main.compactItemWheel) 23 | dialog.visualParent = Plasmoid.compactRepresentationItem 24 | } 25 | Plasmoid.compactRepresentation: Item { 26 | id: compactItem 27 | 28 | PlasmaCore.FrameSvgItem { 29 | id: expandedItem 30 | anchors.fill: parent 31 | imagePath: "widgets/tabbar" 32 | visible: fromCurrentTheme && opacity > 0 33 | prefix: { 34 | var prefix; 35 | switch (plasmoid.location) { 36 | case PlasmaCore.Types.LeftEdge: 37 | prefix = "west-active-tab"; 38 | break; 39 | case PlasmaCore.Types.TopEdge: 40 | prefix = "north-active-tab"; 41 | break; 42 | case PlasmaCore.Types.RightEdge: 43 | prefix = "east-active-tab"; 44 | break; 45 | default: 46 | prefix = "south-active-tab"; 47 | } 48 | if (!hasElementPrefix(prefix)) { 49 | prefix = "active-tab"; 50 | } 51 | return prefix; 52 | } 53 | opacity: main.dialogVisible ? 1 : 0 54 | Behavior on opacity { 55 | NumberAnimation { 56 | duration: units.shortDuration 57 | easing.type: Easing.InOutQuad 58 | } 59 | } 60 | } 61 | 62 | PlasmaCore.IconItem { 63 | anchors.fill: parent 64 | source: main.compactItemIcon 65 | active: mouseArea.containsMouse 66 | colorGroup: PlasmaCore.ColorScope.colorGroup 67 | } 68 | 69 | readonly property bool inPanel: (plasmoid.location == PlasmaCore.Types.TopEdge 70 | || plasmoid.location == PlasmaCore.Types.RightEdge 71 | || plasmoid.location == PlasmaCore.Types.BottomEdge 72 | || plasmoid.location == PlasmaCore.Types.LeftEdge) 73 | 74 | Layout.minimumWidth: { 75 | switch (plasmoid.formFactor) { 76 | case PlasmaCore.Types.Vertical: 77 | return 0; 78 | case PlasmaCore.Types.Horizontal: 79 | return height; 80 | default: 81 | return units.gridUnit * 3; 82 | } 83 | } 84 | 85 | Layout.minimumHeight: { 86 | switch (plasmoid.formFactor) { 87 | case PlasmaCore.Types.Vertical: 88 | return width; 89 | case PlasmaCore.Types.Horizontal: 90 | return 0; 91 | default: 92 | return units.gridUnit * 3; 93 | } 94 | } 95 | 96 | Layout.maximumWidth: inPanel ? units.iconSizeHints.panel : -1 97 | Layout.maximumHeight: inPanel ? units.iconSizeHints.panel : -1 98 | 99 | signal compactItemPressed(var mouse) 100 | signal compactItemClicked(var mouse) 101 | signal compactItemWheel(var wheel) 102 | MouseArea { 103 | id: mouseArea 104 | anchors.fill: parent 105 | hoverEnabled: true 106 | acceptedButtons: Qt.LeftButton | Qt.MiddleButton 107 | onPressed: compactItem.compactItemPressed(mouse) 108 | onClicked: compactItem.compactItemClicked(mouse) 109 | onWheel: compactItem.compactItemWheel(wheel) 110 | } 111 | } 112 | 113 | 114 | 115 | property alias dialog: dialog 116 | property alias dialogVisible: dialog.visible 117 | property alias dialogContents: dialog.mainItem 118 | 119 | signal dialogOpened(bool usedKeyboard) 120 | signal dialogClosed(bool usedKeyboard) 121 | 122 | PlasmaCore.Dialog { 123 | id: dialog 124 | flags: Qt.WindowStaysOnTopHint 125 | location: plasmoid.location 126 | hideOnWindowDeactivate: plasmoid.hideOnWindowDeactivate 127 | 128 | onMainItemChanged: { 129 | mainItem.Keys.onEscapePressed.connect(main.escapePressed) 130 | } 131 | } 132 | function escapePressed() { 133 | main.closeDialog(true) 134 | } 135 | function openDialog(usedKeyboard) { 136 | plasmoid.expanded = true 137 | delayedToggleTimer.opening = true 138 | delayedToggleTimer.usedKeyboard = usedKeyboard 139 | delayedToggleTimer.start() 140 | } 141 | function closeDialog(usedKeyboard) { 142 | delayedToggleTimer.opening = false 143 | delayedToggleTimer.usedKeyboard = usedKeyboard 144 | delayedToggleTimer.start() 145 | } 146 | function toggleDialog(usedKeyboard) { 147 | if (dialog.visible) { 148 | closeDialog(usedKeyboard) 149 | } else { 150 | openDialog(usedKeyboard) 151 | } 152 | } 153 | 154 | 155 | 156 | // NOTE: Taken from redshift plasmoid (which took it from the colorPicker). 157 | // This prevents the popup from actually opening, needs to be queued. 158 | Timer { 159 | id: delayedToggleTimer 160 | interval: 0 161 | property bool opening: false 162 | property bool usedKeyboard: false 163 | onTriggered: { 164 | plasmoid.expanded = false 165 | if (opening) { 166 | dialog.visible = true 167 | main.dialogOpened(usedKeyboard) 168 | } else { 169 | dialog.visible = false 170 | main.dialogClosed(usedKeyboard) 171 | } 172 | } 173 | } 174 | 175 | Plasmoid.onActivated: { 176 | toggleDialog(true) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /package/contents/ui/DynamicFilterModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.1 as PlasmaCore 3 | 4 | PlasmaCore.SortFilterModel { 5 | id: dynamicFilterModel 6 | 7 | function doFiltering(source_row, value) { 8 | // console.log('filterCallback.doFiltering', source_row, value) 9 | var idx = sourceModel.index(source_row, 0); 10 | var virtualStream = sourceModel.data(idx, sourceModel.role("VirtualStream")); 11 | // console.log('\t', 'virtualStream', virtualStream) 12 | if (virtualStream && !plasmoid.configuration.showVirtualStreams) 13 | return false; 14 | 15 | // var name = sourceModel.data(idx, sourceModel.role("Name")); 16 | // console.log('filterCallback', source_row, value, name) 17 | // if (name == "Echo-Cancel Source Stream" || name == "Echo-Cancel Sink Stream") // not localized 18 | // return false; 19 | 20 | return true; 21 | } 22 | 23 | function emptyFilter(source_row, value) { 24 | // console.log('filterCallback.emptyFilter', source_row, value) 25 | return false 26 | } 27 | 28 | filterCallback: dynamicFilterModel.doFiltering 29 | 30 | property var configConnnection: Connections { 31 | target: plasmoid.configuration 32 | onShowVirtualStreamsChanged: { 33 | // console.log('onShowVirtualStreamsChanged', plasmoid.configuration.showVirtualStreams) 34 | 35 | // Manually trigger setFilterCallback() which will invalidate the filter. 36 | dynamicFilterModel.filterCallback = dynamicFilterModel.emptyFilter 37 | dynamicFilterModel.filterCallback = dynamicFilterModel.doFiltering 38 | // console.log('dynamicFilterModel.filterCallback', dynamicFilterModel.filterCallback) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package/contents/ui/IconLabelButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | import QtQuick.Controls 1.0 4 | 5 | import org.kde.plasma.core 2.0 as PlasmaCore 6 | import org.kde.plasma.components 2.0 as PlasmaComponents 7 | 8 | PlasmaComponents.ToolButton { 9 | id: iconLabelButton 10 | height: childrenRect.height 11 | property alias labelText: textLabel.rawText 12 | property alias iconItemSource: icon.source 13 | property alias iconItemOverlays: icon.overlays 14 | property alias iconItemHeight: icon.height 15 | 16 | // ColumnLayout { 17 | Column { 18 | id: iconLabelButtonRow 19 | width: parent.width 20 | // spacing: 0 21 | 22 | PlasmaCore.IconItem { 23 | id: icon 24 | width: parent.width 25 | 26 | // From ToolButtonStyle: 27 | active: iconLabelButton.hovered 28 | colorGroup: iconLabelButton.hovered || !iconLabelButton.flat ? PlasmaCore.Theme.ButtonColorGroup : PlasmaCore.ColorScope.colorGroup 29 | } 30 | 31 | Label { 32 | id: textLabel 33 | width: parent.width 34 | // Layout.fillWidth: true 35 | 36 | property string rawText: '' 37 | text: rawText + '\n' 38 | function updateLineCount() { 39 | if (lineCount == 1) { 40 | text = rawText + '\n' 41 | } else if (truncated) { 42 | text = rawText 43 | } 44 | } 45 | onLineCountChanged: updateLineCount() 46 | onTruncatedChanged: updateLineCount() 47 | color: iconLabelButton.hovered ? theme.buttonTextColor : PlasmaCore.ColorScope.textColor 48 | opacity: iconLabelButton.hovered ? 1 : 0.6 49 | wrapMode: Text.Wrap 50 | elide: Text.ElideRight 51 | maximumLineCount: 2 52 | horizontalAlignment: Text.AlignHCenter 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package/contents/ui/IconToolButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import QtQuick.Controls.Private 1.0 as QtQuickControlsPrivate 3 | 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 2.0 as PlasmaComponents 6 | 7 | PlasmaComponents.ToolButton { 8 | property alias source: icon.source 9 | property alias iconOpacity: icon.opacity 10 | property bool controlHovered: hovered && !(QtQuickControlsPrivate.Settings.hasTouchScreen && QtQuickControlsPrivate.Settings.isMobile) 11 | PlasmaCore.IconItem { 12 | id: icon 13 | anchors.fill: parent 14 | visible: valid 15 | active: parent.controlHovered 16 | colorGroup: parent.controlHovered || !parent.flat ? PlasmaCore.Theme.ButtonColorGroup : PlasmaCore.ColorScope.colorGroup 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package/contents/ui/InputManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: inputMananger 5 | 6 | Connections { 7 | target: main 8 | onDialogOpened: { 9 | if (usedKeyboard) { 10 | inputMananger.selectDefault() 11 | } 12 | } 13 | onDialogClosed: { 14 | inputMananger.selectNone() 15 | } 16 | } 17 | 18 | property var mixerItemGroupList: [ 19 | sourceOutputMixerItemGroup, 20 | sinkInputMixerItemGroup, 21 | sourceMixerItemGroup, 22 | sinkMixerItemGroup, 23 | ] 24 | property int selectedGroupIndex: -1 25 | readonly property var selectedListView: selectedGroupIndex >= 0 ? mixerItemGroupList[selectedGroupIndex].view : null 26 | readonly property var selectedGroupModel: selectedListView ? selectedListView.model : null 27 | readonly property int selectedStreamIndex: selectedListView ? selectedListView.currentIndex : -1 28 | readonly property bool hasSelection: selectedStreamIndex >= 0 29 | readonly property var selectedMixerItem: hasSelection ? selectedListView.currentItem : null 30 | 31 | 32 | function setCurrentGroupStreamIndex(streamIndex) { 33 | if (selectedGroupIndex >= 0) { 34 | mixerItemGroupList[selectedGroupIndex].view.currentIndex = streamIndex 35 | } 36 | } 37 | function select(groupIndex, streamIndex) { 38 | // console.log('select', groupIndex, streamIndex) 39 | if (selectedGroupIndex != groupIndex) { 40 | setCurrentGroupStreamIndex(-1) 41 | } 42 | selectedGroupIndex = groupIndex 43 | setCurrentGroupStreamIndex(streamIndex) 44 | } 45 | function selectDefaultSink() { 46 | // console.log('selectDefaultSink') 47 | var defaultSinkIndex = main.findStream(sinkMixerItemGroup.model, function(stream) { return stream == sinkModel.defaultSink }) 48 | select(3, defaultSinkIndex) 49 | } 50 | function selectDefault() { 51 | // console.log('selectDefault') 52 | selectDefaultSink() 53 | } 54 | function selectNone() { 55 | // console.log('selectNone') 56 | select(-1, -1) 57 | } 58 | function modulo(n, l) { 59 | // qml returns a negative remainder, so make n positive first 60 | return (n + l) % l 61 | } 62 | function prevGroup() { 63 | for (var i = 1; i <= mixerItemGroupList.length; i++) { // Check each group 64 | var groupIndex = modulo(selectedGroupIndex - i, mixerItemGroupList.length) 65 | var mixerItemGroup = mixerItemGroupList[groupIndex] 66 | if (mixerItemGroup.model.count > 0) { 67 | select(groupIndex, mixerItemGroup.model.count - 1) // Select last item 68 | return 69 | } 70 | } 71 | } 72 | function selectLeft() { 73 | // console.log('selectLeft') 74 | if (hasSelection) { 75 | var streamIndex = selectedStreamIndex - 1 76 | if (streamIndex < 0) { 77 | prevGroup() 78 | } else { 79 | select(selectedGroupIndex, streamIndex) 80 | } 81 | } else { 82 | selectDefault() 83 | } 84 | } 85 | function nextGroup() { 86 | // console.log('nextGroup') 87 | for (var i = 1; i <= mixerItemGroupList.length; i++) { // Check each group 88 | var groupIndex = modulo(selectedGroupIndex + i, mixerItemGroupList.length) 89 | var mixerItemGroup = mixerItemGroupList[groupIndex] 90 | if (mixerItemGroup.model.count > 0) { 91 | select(groupIndex, 0) // Select first item 92 | return 93 | } 94 | } 95 | } 96 | function selectRight() { 97 | // console.log('selectRight') 98 | if (hasSelection) { 99 | var streamIndex = selectedStreamIndex + 1 100 | if (streamIndex >= selectedGroupModel.count) { 101 | nextGroup() 102 | } else { 103 | select(selectedGroupIndex, streamIndex) 104 | } 105 | } else { 106 | selectDefault() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /package/contents/ui/MediaController.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | import QtQuick.Layouts 1.0 3 | import QtQuick.Controls 1.0 4 | import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 5 | 6 | import org.kde.plasma.core 2.0 as PlasmaCore 7 | import org.kde.plasma.components 2.0 as PlasmaComponents 8 | import org.kde.plasma.extras 2.0 as PlasmaExtras 9 | import org.kde.kcoreaddons 1.0 as KCoreAddons 10 | 11 | Item { 12 | id: mediaController 13 | property bool disablePositionUpdate: false 14 | property bool keyPressed: false 15 | 16 | Item { 17 | anchors.fill: parent 18 | anchors.topMargin: seekRow.height 19 | 20 | Item { 21 | // PlasmaComponents.ToolButton { 22 | anchors.fill: parent 23 | anchors.rightMargin: rightSide.width 24 | // enabled: mpris2Source.canRaise 25 | // onClicked: { 26 | // mpris2Source.raise() 27 | // if (plasmoid.hideOnWindowDeactivate) { 28 | // plasmoid.expanded = false 29 | // } 30 | // } 31 | 32 | Item { 33 | id: albumArtContainer 34 | anchors.left: parent.left 35 | width: height 36 | height: parent.height 37 | 38 | PlasmaCore.IconItem { 39 | id: playerIcon 40 | anchors.fill: parent 41 | source: mpris2Source.playerIcon 42 | } 43 | 44 | Image { 45 | id: albumArt 46 | anchors.fill: parent 47 | source: mpris2Source.albumArt 48 | asynchronous: true 49 | fillMode: Image.PreserveAspectCrop 50 | sourceSize: Qt.size(width, height) 51 | visible: !!mpris2Source.track && status === Image.Ready 52 | } 53 | } 54 | 55 | Column { 56 | id: leftSide 57 | anchors.fill: parent 58 | anchors.leftMargin: albumArtContainer.width + (4 * PlasmaCore.Units.devicePixelRatio) 59 | // anchors.rightMargin: rightSide.width 60 | 61 | // MediaControllerCompact's style 62 | PlasmaComponents.Label { 63 | id: track 64 | width: parent.width 65 | opacity: 0.9 66 | height: parent.height / 2 67 | 68 | elide: Text.ElideRight 69 | text: mpris2Source.track 70 | } 71 | 72 | PlasmaComponents.Label { 73 | id: artist 74 | width: parent.width 75 | opacity: 0.7 76 | height: parent.height / 2 77 | 78 | elide: Text.ElideRight 79 | text: mpris2Source.artist 80 | } 81 | } 82 | } 83 | 84 | Row { 85 | id: rightSide 86 | width: childrenRect.width 87 | height: parent.height 88 | anchors.right: parent.right 89 | anchors.verticalCenter: parent.verticalCenter 90 | 91 | // Column { 92 | // width: height 93 | // height: parent.height 94 | // visible: mpris2Source.canGoNext 95 | 96 | // IconToolButton { 97 | // width: parent.height 98 | // height: parent.height / 2 99 | // enabled: mpris2Source.canLoop 100 | // source: { 101 | // if (mpris2Source.isLoopingTrack) { 102 | // return "media-repeat-single" 103 | // } else if (mpris2Source.isLoopingPlaylist) { 104 | // return "media-repeat-all" 105 | // } else { 106 | // return "media-repeat-none" 107 | // } 108 | // } 109 | // onClicked: mpris2Source.toggleLoopState() 110 | // } 111 | // IconToolButton { 112 | // width: parent.height 113 | // height: parent.height / 2 114 | 115 | // enabled: mpris2Source.canShuffle 116 | // source: "shuffle" 117 | // iconOpacity: mpris2Source.isShuffling ? 1 : 0.5 118 | // onClicked: mpris2Source.toggleShuffle() 119 | // } 120 | // } 121 | 122 | PlasmaComponents.ToolButton { 123 | iconSource: "media-skip-backward" 124 | width: height 125 | height: parent.height 126 | enabled: mpris2Source.canGoPrevious 127 | onClicked: { 128 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 (org.kde.plasma.mediacontroller) 129 | mpris2Source.previous() 130 | } 131 | } 132 | PlasmaComponents.ToolButton { 133 | iconSource: mpris2Source.isPlaying ? "media-playback-pause" : "media-playback-start" 134 | width: height 135 | height: parent.height 136 | enabled: mpris2Source.canControl 137 | onClicked: mpris2Source.playPause() 138 | } 139 | PlasmaComponents.ToolButton { 140 | iconSource: "media-skip-forward" 141 | width: height 142 | height: parent.height 143 | enabled: mpris2Source.canGoNext 144 | onClicked: { 145 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 (org.kde.plasma.mediacontroller) 146 | mpris2Source.next() 147 | } 148 | } 149 | } 150 | } 151 | 152 | RowLayout { 153 | id: seekRow 154 | anchors.left: parent.left 155 | anchors.top: parent.top 156 | anchors.right: parent.right 157 | height: config.mediaControllerSliderHeight 158 | 159 | // org.kde.plasma.mediacontroller 160 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song 161 | TextMetrics { 162 | id: timeMetrics 163 | text: i18ndc("plasma_applet_org.kde.plasma.mediacontroller", "Remaining time for song e.g -5:42", "-%1", 164 | KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, KCoreAddons.FormatTypes.FoldHours)) 165 | font: theme.smallestFont 166 | } 167 | 168 | PlasmaComponents.Label { 169 | visible: plasmoid.configuration.showMediaTimeElapsed 170 | Layout.preferredWidth: timeMetrics.width 171 | Layout.fillHeight: true 172 | verticalAlignment: Text.AlignVCenter 173 | horizontalAlignment: Text.AlignRight 174 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, KCoreAddons.FormatTypes.FoldHours) 175 | opacity: 0.6 176 | font: theme.smallestFont 177 | } 178 | 179 | PlasmaComponents.Slider { 180 | id: seekSlider 181 | Layout.fillWidth: true 182 | Layout.fillHeight: true 183 | enabled: mpris2Source.canSeek 184 | z: 999 185 | // style: PlasmaStyles.SliderStyle { 186 | // handle: Item {} 187 | // } 188 | 189 | // MouseArea { 190 | // id: seekSliderArea 191 | // anchors.fill: parent 192 | // hoverEnabled: true 193 | 194 | // acceptedButtons: Qt.NoButton 195 | // propagateComposedEvents: true 196 | // } 197 | opacity: hovered ? 1 : 0.75 198 | Behavior on opacity { 199 | NumberAnimation { duration: units.longDuration } 200 | } 201 | 202 | value: 0 203 | onValueChanged: { 204 | if (!mediaController.disablePositionUpdate) { 205 | // delay setting the position to avoid race conditions 206 | queuedPositionUpdate.restart() 207 | } else { 208 | // console.log('onValueChanged skipped') 209 | } 210 | } 211 | onMaximumValueChanged: mpris2Source.retrievePosition() 212 | 213 | Connections { 214 | target: mpris2Source 215 | 216 | onPositionChanged: { 217 | // we don't want to interrupt the user dragging the slider 218 | if (!seekSlider.pressed && !mediaController.keyPressed && !queuedPositionUpdate.running) { 219 | // we also don't want passive position updates 220 | mediaController.disablePositionUpdate = true 221 | // console.log('mpris2Source.position', mpris2Source.position) 222 | // console.log('\tmpris2Source.length', mpris2Source.length, seekSlider.maximumValue) 223 | if (seekSlider.maximumValue != mpris2Source.length) { // mpris2Source.onLengthChanged isn't always called. 224 | seekSlider.maximumValue = mpris2Source.length 225 | } 226 | seekSlider.value = mpris2Source.position 227 | mediaController.disablePositionUpdate = false 228 | } 229 | } 230 | onLengthChanged: { 231 | mediaController.disablePositionUpdate = true 232 | // console.log('mpris2Source.length', mpris2Source.length) 233 | seekSlider.maximumValue = mpris2Source.length 234 | mediaController.disablePositionUpdate = false 235 | } 236 | } 237 | 238 | 239 | Timer { 240 | id: queuedPositionUpdate 241 | interval: 100 242 | onTriggered: { 243 | if (!mediaController.disablePositionUpdate) { 244 | mpris2Source.setPosition(seekSlider.value) 245 | } else { 246 | // console.log('queuedPositionUpdate skipped') 247 | } 248 | } 249 | } 250 | 251 | Timer { 252 | id: seekTimer 253 | interval: 1000 254 | repeat: true 255 | running: mpris2Source.isPlaying && main.dialogVisible && !mediaController.keyPressed 256 | onTriggered: { 257 | // console.log(seekSlider.value, seekSlider.maximumValue, 258 | // seekSlider.pressed ? 'pressed' : '', 259 | // mediaController.disablePositionUpdate ? 'disablePositionUpdate' : '', 260 | // mpris2Source.canSeek ? 'canSeek': '') 261 | 262 | // some players don't continuously update the seek slider position via mpris 263 | // add one second; value in microseconds 264 | if (!seekSlider.pressed) { 265 | mediaController.disablePositionUpdate = true 266 | if (seekSlider.value == seekSlider.maximumValue) { 267 | mpris2Source.retrievePosition(); 268 | } else { 269 | seekSlider.value += 1000000 270 | } 271 | mediaController.disablePositionUpdate = false 272 | } 273 | } 274 | } 275 | } 276 | 277 | PlasmaComponents.Label { 278 | visible: plasmoid.configuration.showMediaTimeLeft 279 | Layout.preferredWidth: timeMetrics.width 280 | Layout.fillHeight: true 281 | verticalAlignment: Text.AlignVCenter 282 | text: i18nc("Remaining time for song e.g -5:42", "-%1", 283 | KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, KCoreAddons.FormatTypes.FoldHours)) 284 | opacity: 0.6 285 | font: theme.smallestFont 286 | } 287 | 288 | PlasmaComponents.Label { 289 | visible: plasmoid.configuration.showMediaTotalDuration 290 | Layout.preferredWidth: timeMetrics.width 291 | Layout.fillHeight: true 292 | verticalAlignment: Text.AlignVCenter 293 | horizontalAlignment: Text.AlignRight 294 | text: KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, KCoreAddons.FormatTypes.FoldHours) 295 | opacity: 0.6 296 | font: theme.smallestFont 297 | } 298 | 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /package/contents/ui/MixerItemGroup.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | import QtQuick.Controls 1.0 4 | import QtQuick.Controls.Private 1.0 as QtQuickControlsPrivate 5 | import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 6 | 7 | import org.kde.plasma.core 2.0 as PlasmaCore 8 | import org.kde.plasma.components 2.0 as PlasmaComponents 9 | import org.kde.plasma.extras 2.0 as PlasmaExtras 10 | 11 | import org.kde.plasma.private.volume 0.1 12 | 13 | import "lib" 14 | 15 | GroupBox { 16 | id: mixerItemGroup 17 | 18 | style: PlasmaStyles.GroupBoxStyle { 19 | id: groupBoxStyle 20 | 21 | panel: Item { 22 | anchors.fill: parent 23 | 24 | PlasmaComponents.ToolButton { 25 | id: label 26 | anchors.top: parent.top 27 | anchors.left: parent.left 28 | anchors.right: parent.right 29 | text: control.title 30 | // width: mixerItemGroup.mixerItemWidth 31 | height: Math.max(theme.defaultFont.pixelSize, pinButton.height) 32 | 33 | style: PlasmaStyles.ToolButtonStyle { 34 | label: PlasmaComponents.Label { 35 | id: label 36 | // anchors.verticalCenter: parent.verticalCenter 37 | Layout.minimumWidth: implicitWidth 38 | text: QtQuickControlsPrivate.StyleHelpers.stylizeMnemonics(control.text) 39 | font: control.font || theme.defaultFont 40 | visible: control.text != "" 41 | Layout.fillWidth: true 42 | color: control.hovered || !flat ? theme.buttonTextColor : PlasmaCore.ColorScope.textColor 43 | horizontalAlignment: Text.AlignLeft 44 | elide: Text.ElideRight 45 | } 46 | } 47 | 48 | 49 | onClicked: contextMenu.showRelative() 50 | ContextMenu { 51 | id: contextMenu 52 | visualParent: label 53 | placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup 54 | 55 | onBeforeOpen: { 56 | function filterStreamName(streamName) { 57 | return function() { 58 | console.log('menuItem.clicked', streamName) 59 | view.model.filters.push({ 60 | role: 'name', 61 | value: streamName, 62 | }) 63 | //TODO: Find function that will force the model to reparse the filterCallback 64 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/core/datamodel.h 65 | // view.model.invalidateFilter() // Not exposed 66 | // view.model.invalidate() // Just empties the model 67 | } 68 | } 69 | // console.log('onBeforeOpen', view.model, view.model.count) 70 | for (var i = 0; i < view.model.count; i++) { 71 | var stream = view.model.get(i) 72 | // console.log(mixerItemGroup.model, i, stream) 73 | var menuItem = menu.newMenuItem() 74 | menuItem.text = stream.PulseObject.name 75 | menuItem.checkable = true 76 | menuItem.checked = true 77 | menuItem.enabled = false 78 | // menuItem.clicked.connect(filterStreamName(stream.PulseObject.name)) 79 | } 80 | } 81 | } 82 | } 83 | 84 | PlasmaCore.FrameSvgItem { 85 | id: frame 86 | anchors.fill: parent 87 | imagePath: "widgets/frame" 88 | prefix: "plain" 89 | visible: !control.flat 90 | colorGroup: PlasmaCore.ColorScope.colorGroup 91 | Component.onCompleted: { 92 | groupBoxStyle.padding.left = frame.margins.left 93 | groupBoxStyle.padding.top = label.height 94 | groupBoxStyle.padding.right = frame.margins.right 95 | groupBoxStyle.padding.bottom = frame.margins.bottom 96 | } 97 | } 98 | } 99 | } 100 | property alias view: view 101 | property alias spacing: view.spacing 102 | property alias model: view.model 103 | property alias delegate: view.delegate 104 | property int mixerItemWidth: config.mixerItemWidth 105 | property int volumeSliderWidth: config.volumeSliderWidth 106 | property string mixerGroupType: '' 107 | visible: view.count > 0 108 | 109 | ListView { 110 | id: view 111 | width: Math.max(childrenRect.width, mixerItemGroup.mixerItemWidth) // At least 1 mixer item wide 112 | height: parent.height 113 | spacing: 0 114 | boundsBehavior: Flickable.StopAtBounds 115 | orientation: ListView.Horizontal 116 | 117 | delegate: MixerItem { 118 | // width: mixerItemWidth 119 | height: ListView.view.height 120 | mixerItemWidth: mixerItemGroup.mixerItemWidth 121 | volumeSliderWidth: mixerItemGroup.volumeSliderWidth 122 | mixerItemType: mixerItemGroup.mixerGroupType 123 | showDefaultDeviceIndicator: { 124 | if (isDevice) { 125 | return mixerItemGroup.model.count > 1 126 | } else { 127 | return false 128 | } 129 | } 130 | } 131 | 132 | currentIndex: -1 133 | 134 | highlight: Rectangle { 135 | color: "transparent" 136 | anchors.fill: view.currentItem 137 | border.width: 1 138 | border.color: config.selectedStreamOutline 139 | 140 | 141 | SequentialAnimation on border.color { 142 | loops: Animation.Infinite 143 | ColorAnimation { 144 | from: config.selectedStreamOutline 145 | to: config.selectedStreamOutlinePulse 146 | duration: 1000 147 | } 148 | ColorAnimation { 149 | from: config.selectedStreamOutlinePulse 150 | to: config.selectedStreamOutline 151 | duration: 1000 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /package/contents/ui/Mpris2DataSource.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | // https://github.com/KDE/plasma-workspace/tree/master/dataengines/mpris2 5 | PlasmaCore.DataSource { 6 | id: mpris2Source 7 | 8 | readonly property string multiplexSource: "@multiplex" 9 | property string current: multiplexSource 10 | 11 | engine: "mpris2" 12 | connectedSources: current 13 | 14 | onSourceRemoved: { 15 | // if player is closed, reset to multiplex source 16 | if (source === current) { 17 | current = multiplexSource 18 | } 19 | } 20 | 21 | // onNewData: logState() 22 | 23 | property bool hasPlayer: mpris2Source.sources.length >= 2 // We don't count @mutiplexSource 24 | property string playbackState: hasPlayer && mpris2Source.data[mpris2Source.current].PlaybackStatus 25 | property bool isPlaying: playbackState == "Playing" 26 | property bool isPaused: playbackState == "Paused" 27 | property bool isShuffling: canControl && mpris2Source.data[mpris2Source.current].Shuffle || false 28 | property string loopState: canControl && mpris2Source.data[mpris2Source.current].LoopStatus || "None" 29 | property bool isNotLooping: loopState == "None" 30 | property bool isLoopingTrack: loopState == "Track" 31 | property bool isLoopingPlaylist: loopState == "Playlist" 32 | 33 | property bool canControl: hasPlayer && mpris2Source.data[mpris2Source.current].CanControl 34 | property bool canGoPrevious: canControl && mpris2Source.data[mpris2Source.current].CanGoPrevious 35 | property bool canGoNext: canControl && mpris2Source.data[mpris2Source.current].CanGoNext 36 | property bool canRaise: hasPlayer && mpris2Source.data[mpris2Source.current].CanRaise 37 | property bool canShuffle: canControl 38 | property bool canLoop: canControl 39 | 40 | // if there's no "mpris:length" in teh metadata, we cannot seek, so hide it in that case (org.kde.plasma.mediacontroller) 41 | property bool canSeekMpris: hasPlayer && mpris2Source.data[mpris2Source.current].CanSeek 42 | property bool canSeek: canSeekMpris && /*track &&*/ length > 0 43 | 44 | property string playerIcon: hasPlayer && mpris2Source.data[mpris2Source.current]['Desktop Icon Name'] || '' 45 | 46 | property var currentMetadata: mpris2Source.data[mpris2Source.current] ? mpris2Source.data[mpris2Source.current].Metadata : null 47 | property string albumArt: currentMetadata ? currentMetadata["mpris:artUrl"] || "" : "" 48 | property string track: { 49 | if (!currentMetadata) { 50 | return "" 51 | } 52 | var xesamTitle = currentMetadata["xesam:title"] 53 | if (xesamTitle) { 54 | return xesamTitle 55 | } 56 | // if no track title is given, print out the file name 57 | var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" 58 | if (!xesamUrl) { 59 | return "" 60 | } 61 | var lastSlashPos = xesamUrl.lastIndexOf('/') 62 | if (lastSlashPos < 0) { 63 | return "" 64 | } 65 | var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) 66 | return decodeURIComponent(lastUrlPart) 67 | } 68 | property string artist: currentMetadata ? currentMetadata["xesam:artist"] || "" : "" 69 | // onTrackChanged: { 70 | // function logObj(obj) { 71 | // for (var key in obj) { 72 | // if (typeof obj[key] === 'function') continue 73 | // console.log(obj, key, obj[key]) 74 | // } 75 | // } 76 | // logObj(currentMetadata) 77 | // } 78 | 79 | property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 80 | property double position: hasPlayer ? mpris2Source.data[mpris2Source.current].Position || 0 : 0 81 | 82 | function logState() { 83 | console.log(JSON.stringify(mpris2Source.data, null, "\t")) 84 | console.log('hasPlayer', hasPlayer) 85 | console.log('currentMetadata', currentMetadata) 86 | console.log('position', position) 87 | console.log('length', length) 88 | console.log('canSeek', canSeek, 'canSeekMpris', canSeekMpris) 89 | } 90 | 91 | function retrievePosition() { 92 | serviceOp(mpris2Source.current, "GetPosition") 93 | } 94 | 95 | function setPosition(value) { 96 | var service = mpris2Source.serviceForSource(mpris2Source.current) 97 | var operation = service.operationDescription("SetPosition") 98 | operation.microseconds = value 99 | service.startOperationCall(operation) 100 | } 101 | 102 | function raisePlayer() { 103 | serviceOp(mpris2Source.current, "Raise") 104 | } 105 | 106 | function playPause() { 107 | serviceOp(mpris2Source.current, "PlayPause") 108 | } 109 | 110 | function previous() { 111 | serviceOp(mpris2Source.current, "Previous") 112 | } 113 | 114 | function next() { 115 | serviceOp(mpris2Source.current, "Next") 116 | } 117 | 118 | function stop() { 119 | serviceOp(mpris2Source.current, "Stop") 120 | } 121 | 122 | function raise() { 123 | serviceOp(mpris2Source.current, "Raise") 124 | } 125 | 126 | function setShuffle(value) { 127 | var service = mpris2Source.serviceForSource(mpris2Source.current) 128 | var operation = service.operationDescription("SetShuffle") 129 | operation.on = value 130 | service.startOperationCall(operation) 131 | } 132 | 133 | function toggleShuffle() { 134 | setShuffle(!isShuffling) 135 | } 136 | 137 | function setLoopState(value) { 138 | var service = mpris2Source.serviceForSource(mpris2Source.current) 139 | var operation = service.operationDescription("SetLoopStatus") 140 | operation.status = value 141 | service.startOperationCall(operation) 142 | } 143 | 144 | function toggleLoopState() { 145 | if (isNotLooping) { 146 | setLoopState("Track") 147 | } else if (isLoopingTrack) { 148 | setLoopState("Playlist") 149 | } else { 150 | setLoopState("None") 151 | } 152 | } 153 | 154 | function serviceOp(src, op) { 155 | var service = mpris2Source.serviceForSource(src) 156 | var operation = service.operationDescription(op) 157 | return service.startOperationCall(operation) 158 | } 159 | 160 | property var mainConnection: Connections { 161 | target: main 162 | onDialogVisibleChanged: { 163 | if (main.dialogVisible) { 164 | mpris2Source.retrievePosition() 165 | } 166 | } 167 | } 168 | 169 | Component.onCompleted: { 170 | // Ever since Plasma 5.10, the mediacontroller widget needs to manually bind the 171 | // Global Shortcuts. Unfortunately multiple widgets registering will cause a bug. 172 | // https://github.com/KDE/plasma-workspace/commit/7bd909fa3a4f70bf4c03c43b025f7ed65c2e5b5c 173 | // mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /package/contents/ui/PulseObjectDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import QtQuick.Window 2.1 3 | import QtQuick.Controls 1.2 4 | import QtQuick.Controls.Styles 1.2 5 | import QtQuick.Layouts 1.0 6 | import QtQuick.Dialogs 1.0 7 | 8 | Window { 9 | id: pulseObjectDialog 10 | 11 | property var pulseObject 12 | width: 600 * units.devicePixelRatio 13 | height: 600 * units.devicePixelRatio 14 | title: pulseObject.name + ' — ' + i18nd("plasma_applet_org.kde.plasma.volume", "Audio Volume") 15 | 16 | 17 | ColumnLayout { 18 | anchors.fill: parent 19 | 20 | // Label { 21 | // text: pulseObject.name 22 | // } 23 | 24 | TableView { 25 | id: tableView 26 | Layout.fillWidth: true 27 | Layout.fillHeight: true 28 | 29 | model: ListModel {} 30 | 31 | TableViewColumn { 32 | id: keyColumn 33 | role: "key" 34 | width: 200 * units.devicePixelRatio 35 | } 36 | TableViewColumn { 37 | id: valueColumn 38 | role: "value" 39 | width: 360 * units.devicePixelRatio 40 | } 41 | 42 | style: TableViewStyle {} // Ignore panel theme (which might be black bg) 43 | 44 | section.property: 'section' 45 | section.delegate: Label { 46 | text: section 47 | font.bold: true 48 | font.pixelSize: 16 * units.devicePixelRatio 49 | z: -1 // Make sure the section delegate is drawn under the column heading 50 | } 51 | } 52 | } 53 | 54 | function findEntry(section, key) { 55 | for (var i = 0; i < tableView.model.count; i++) { 56 | var item = tableView.model.get(i) 57 | if (item.section === section && item.key === key) { 58 | return i 59 | } 60 | } 61 | return -1 62 | } 63 | 64 | function addEntry(key, value, section) { 65 | tableView.model.append({ 66 | key: key, 67 | value: '' + value, 68 | section: ('' + section) || '', 69 | }) 70 | } 71 | 72 | function setEntry(key, value, section) { 73 | // Scan for existing property 74 | var entryIndex = findEntry(section, key) 75 | if (entryIndex >= 0) { 76 | var item = tableView.model.get(entryIndex) 77 | var newValueStr = '' + value 78 | if (item.value !== newValueStr) { 79 | // valueChanged 80 | console.log(key, value) 81 | tableView.model.setProperty(entryIndex, "value", newValueStr) 82 | } 83 | } else { 84 | // Property doesn't yet exist. 85 | console.log(key, value) 86 | addEntry(key, value, section) 87 | } 88 | } 89 | 90 | function addPulseObjectEntry(key, section) { 91 | if (typeof pulseObject[key] !== 'undefined') { 92 | setEntry(key, pulseObject[key], section) 93 | } 94 | } 95 | 96 | function addPortEntry(i, port, key, section) { 97 | if (typeof port[key] !== 'undefined') { 98 | setEntry('port[' + i + '].' + key, port[key], section) 99 | } 100 | } 101 | 102 | function addPropertiesEntries(obj, section) { 103 | if (typeof obj.properties !== 'undefined') { 104 | for (var key in obj.properties) { 105 | setEntry(key, obj.properties[key], section) 106 | } 107 | } 108 | } 109 | 110 | function update() { 111 | addPulseObjectEntry('name', '') 112 | 113 | // https://github.com/KDE/plasma-pa/blob/master/src/pulseobject.h 114 | addPulseObjectEntry('index', 'PulseObject') 115 | addPulseObjectEntry('iconName', 'PulseObject') 116 | // addPulseObjectEntry('properties', 'PulseObject') 117 | 118 | // https://github.com/KDE/plasma-pa/blob/master/src/volumeobject.h 119 | addPulseObjectEntry('volume', 'VolumeObject') 120 | addPulseObjectEntry('muted', 'VolumeObject') 121 | addPulseObjectEntry('hasVolume', 'VolumeObject') 122 | addPulseObjectEntry('volumeWriteable', 'VolumeObject') 123 | addPulseObjectEntry('channels', 'VolumeObject') 124 | addPulseObjectEntry('channelVolumes', 'VolumeObject') 125 | addPulseObjectEntry('rawChannels', 'VolumeObject') 126 | 127 | // if (typeof pulseObject.channelVolumes !== 'undefined') { 128 | // for (var i = 0; i < pulseObject.channels.length; i++) { 129 | // var section = 'Device.channels[' + i + ']' 130 | // addEntry('channels[' + i + '].name', pulseObject.channels[i], section) 131 | // // addEntry('channels[' + i + '].volume', pulseObject.channelVolumes[i], section) // Doesn't work since channelVolumes is a QVariant... 132 | // } 133 | // } 134 | 135 | // https://github.com/KDE/plasma-pa/blob/master/src/device.h 136 | addPulseObjectEntry('state', 'Device') 137 | // addPulseObjectEntry('name', 'Device') 138 | addPulseObjectEntry('description', 'Device') 139 | addPulseObjectEntry('cardIndex', 'Device') 140 | // addPulseObjectEntry('ports', 'Device') 141 | addPulseObjectEntry('activePortIndex', 'Device') 142 | addPulseObjectEntry('default', 'Device') 143 | 144 | if (typeof pulseObject.ports !== 'undefined') { 145 | for (var i = 0; i < pulseObject.ports.length; i++) { 146 | var port = pulseObject.ports[i]; 147 | var section = 'Device.ports[' + i + ']' 148 | // https://github.com/KDE/plasma-pa/blob/master/src/profile.h 149 | addPortEntry(i, port, 'name', section) 150 | addPortEntry(i, port, 'description', section) 151 | addPortEntry(i, port, 'priority', section) 152 | 153 | // https://github.com/KDE/plasma-pa/blob/master/src/port.h 154 | addPortEntry(i, port, 'available', section) 155 | 156 | // https://github.com/KDE/plasma-pa/blob/master/src/card.h 157 | addPropertiesEntries(port, section) 158 | } 159 | } 160 | 161 | 162 | // https://github.com/KDE/plasma-pa/blob/master/src/stream.h 163 | // addPulseObjectEntry('name', 'Stream') 164 | // addPulseObjectEntry('client', 'Stream') 165 | addPulseObjectEntry('virtualStream', 'Stream') 166 | addPulseObjectEntry('deviceIndex', 'Stream') 167 | addPulseObjectEntry('corked', 'Stream') 168 | 169 | // https://github.com/KDE/plasma-pa/blob/master/src/client.h 170 | // addPulseObjectEntry('name', 'Client') 171 | 172 | // 173 | addPropertiesEntries(pulseObject, 'PulseObject.properties') 174 | } 175 | 176 | Component.onCompleted: { 177 | update() 178 | } 179 | 180 | Timer { 181 | running: pulseObjectDialog.visible 182 | repeat: true 183 | interval: 1000 184 | onTriggered: update() 185 | } 186 | 187 | onVisibleChanged: { 188 | if (!visible) { 189 | destroy() 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /package/contents/ui/VerticalVolumeSlider.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Window 2.1 3 | import QtQuick.Controls 1.0 4 | import QtQuick.Controls.Styles 1.0 5 | import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 6 | import org.kde.plasma.core 2.0 as PlasmaCore 7 | import org.kde.plasma.components 2.0 as PlasmaComponents 8 | 9 | PlasmaComponents.Slider { 10 | id: slider 11 | anchors.fill: parent 12 | orientation: Qt.Vertical 13 | tickmarksEnabled: true 14 | property real hundredPercentValue: 65536 15 | maximumValue: hundredPercentValue * 1.05 16 | property bool isVolumeBoosted: value > hundredPercentValue // 100% is 65863.68, not 65536... Bleh. Just trigger at a round number. 17 | property bool isBoostable: maximumValue > hundredPercentValue 18 | readonly property int percentage: Math.round(value / hundredPercentValue * 100) 19 | readonly property int maxPercentage: Math.ceil(maximumValue / hundredPercentValue * 100) 20 | 21 | property bool showPercentageLabel: true 22 | property bool showVisualFeedback: plasmoid.configuration.showVisualFeedback 23 | readonly property bool isPeaking: volumePeakLoader.active && volumePeakLoader.item 24 | readonly property real peakValue: isPeaking ? volumePeakLoader.item.defaultSinkPeak : 65536 25 | readonly property real peakRatio: peakValue / 65536 26 | Loader { 27 | id: volumePeakLoader 28 | property bool validType: mixerItem.mixerItemType === 'Sink' || mixerItem.mixerItemType === 'Source' || mixerItem.mixerItemType === 'SinkInput' // || mixerItem.mixerItemType === 'SourceOutput' 29 | active: showVisualFeedback && validType 30 | source: "VolumePeaksManager.qml" 31 | 32 | onStatusChanged: { 33 | if (status == Loader.Error) { 34 | // Error loading. Disable it so we don't bother trying again. 35 | if (plasmoid.configuration.showVisualFeedback) { 36 | plasmoid.configuration.showVisualFeedback = false 37 | } 38 | } 39 | } 40 | } 41 | 42 | // Component.onCompleted: { 43 | // console.log('maxPercentage', maxPercentage) 44 | // console.log(Math.floor(maxPercentage / 10) + 1) 45 | // } 46 | 47 | property int grooveThickness: 5 * units.devicePixelRatio 48 | // property int handleHeight: 20 * units.devicePixelRatio 49 | 50 | property string svgUrl: config.volumeSliderUrl 51 | PlasmaCore.Svg { 52 | id: grooveSvg 53 | imagePath: slider.svgUrl 54 | colorGroup: PlasmaCore.ColorScope.colorGroup 55 | } 56 | 57 | property alias handleHeight: handleSize.naturalSize.height 58 | PlasmaCore.SvgItem { 59 | id: handleSize 60 | anchors.fill: parent 61 | svg: grooveSvg 62 | elementId: "vertical-slider-handle" 63 | visible: false 64 | } 65 | 66 | // http://api.kde.org/frameworks-api/frameworks5-apidocs/plasma-framework/html/SliderStyle_8qml_source.html 67 | style: PlasmaStyles.SliderStyle { 68 | id: style 69 | 70 | property int numTicks: Math.ceil(control.maxPercentage / 10) + 1 // 0% .. 100% by 10 = 11 ticks (or ...150% = 16 ticks) 71 | property real controlWidth: orientation == Qt.Vertical ? control.width : control.height 72 | property real controlLength: orientation == Qt.Vertical ? control.height : control.width 73 | property real tickAvailableHeight: (style.controlWidth - control.grooveThickness) / 2 74 | 75 | function calcTickWidth(tickIndex) { 76 | if (tickIndex == 0) { 77 | return 0 // 0% has no tick 78 | } else if (tickIndex % 5 == 0) { 79 | // 50%, 100%, 150% have medium length ticks 80 | // 50%: 2/10 81 | // 100%: 3/10 82 | // 150%: 4/10 83 | // >=200%: 5/10 84 | return tickAvailableHeight*(1+Math.min(tickIndex/5, 4))/5 85 | } else { 86 | return tickAvailableHeight*1/5 // 10%, 20%, ... have short ticks 87 | } 88 | } 89 | 90 | handle: Item { 91 | width: handle.naturalSize.width 92 | height: handle.naturalSize.height 93 | 94 | PlasmaCore.SvgItem { 95 | id: handle 96 | anchors.fill: parent 97 | svg: grooveSvg 98 | elementId: { 99 | if (control.focus || control.pressed) { 100 | return "vertical-slider-focus" 101 | } else if (control.hovered) { 102 | return "vertical-slider-hover" 103 | } else { 104 | return "vertical-slider-handle" 105 | } 106 | } 107 | } 108 | // Rectangle { anchors.fill: handle; border.color: "red"; color: "transparent"; border.width: 1; } 109 | 110 | PlasmaComponents.Label { 111 | id: percentageLabel 112 | visible: slider.showPercentageLabel 113 | text: control.percentage 114 | anchors.horizontalCenter: handle.horizontalCenter 115 | anchors.bottom: handle.top 116 | rotation: control.orientation == Qt.Vertical ? 90 : 0 117 | // horizontalAlignment: control.orientation == Qt.Vertical ? Text.AlignRight : Text.AlignHCenter 118 | verticalAlignment: control.orientation == Qt.Vertical ? Text.AlignVCenter : Text.AlignBottom 119 | } 120 | // Rectangle { anchors.fill: percentageLabel; border.color: "yellow"; color: "transparent"; border.width: 1; } 121 | } 122 | 123 | groove: Item { 124 | id: grooveItem 125 | anchors.fill: parent 126 | 127 | property real valuePosition: styleData.handlePosition - control.handleHeight/2 128 | property real peakPosition: valuePosition * control.peakRatio 129 | 130 | PlasmaCore.FrameSvgItem { 131 | id: groove 132 | imagePath: slider.svgUrl 133 | prefix: "groove" 134 | // height: 15 135 | height: control.grooveThickness 136 | colorGroup: PlasmaCore.ColorScope.colorGroup 137 | opacity: control.enabled ? 1 : 0.6 138 | // anchors.fill: parent 139 | // anchors.fill: parent 140 | 141 | anchors.leftMargin: control.handleHeight / 2 142 | anchors.rightMargin: control.handleHeight - control.handleHeight / 2 143 | // width: parent.width - styleData.handleWidth 144 | anchors.left: parent.left 145 | anchors.right: parent.right 146 | anchors.verticalCenter: parent.verticalCenter 147 | 148 | PlasmaCore.FrameSvgItem { 149 | id: highlight 150 | imagePath: slider.svgUrl 151 | prefix: control.percentage <= 100 ? "groove-highlight" : "groove-danger" 152 | height: groove.height 153 | width: grooveItem.valuePosition 154 | visible: width > 0 155 | anchors.verticalCenter: parent.verticalCenter 156 | colorGroup: PlasmaCore.ColorScope.colorGroup 157 | } 158 | 159 | PlasmaCore.FrameSvgItem { 160 | id: peakHighlight 161 | imagePath: slider.svgUrl 162 | prefix: "groove-peaking" 163 | height: groove.height 164 | width: grooveItem.peakPosition 165 | visible: control.isPeaking && width > 0 166 | anchors.verticalCenter: parent.verticalCenter 167 | colorGroup: PlasmaCore.ColorScope.colorGroup 168 | } 169 | 170 | PlasmaCore.SvgItem { 171 | id: grooveTriangle 172 | svg: grooveSvg 173 | elementId: "groove-triangle" 174 | height: style.calcTickWidth(style.numTicks - 1) 175 | anchors.left: parent.left 176 | anchors.top: groove.bottom 177 | anchors.right: parent.right 178 | 179 | Item { 180 | height: grooveTriangle.height 181 | width: grooveItem.valuePosition 182 | clip: true 183 | 184 | PlasmaCore.SvgItem { 185 | id: grooveHighlightTriangle 186 | svg: grooveSvg 187 | elementId: control.percentage <= 100 ? "groove-highlight-triangle" : "groove-danger-triangle" 188 | height: grooveTriangle.height 189 | width: grooveTriangle.width 190 | visible: control.value > 0 191 | } 192 | } 193 | 194 | Item { 195 | height: grooveTriangle.height 196 | width: grooveItem.peakPosition 197 | clip: true 198 | 199 | PlasmaCore.SvgItem { 200 | id: groovePeakHighlightTriangle 201 | svg: grooveSvg 202 | elementId: "groove-peaking-triangle" 203 | height: grooveTriangle.height 204 | width: grooveTriangle.width 205 | visible: control.isPeaking && control.value > 0 206 | } 207 | } 208 | 209 | } 210 | } 211 | } 212 | 213 | tickmarks: Repeater { 214 | // width/height and x/y is reversed since it's Vertical 215 | 216 | id: repeater 217 | model: style.numTicks 218 | // onModelChanged: console.log('model', model) 219 | // model: slider.tickmarkModel 220 | // width: control.height 221 | // height: control.width 222 | anchors.fill: parent 223 | 224 | Rectangle { 225 | function setAlpha(c, a) { 226 | var c2 = Qt.darker(c, 1) 227 | c2.a = a 228 | return c2 229 | } 230 | color: theme.textColor == theme.buttonBackgroundColor ? theme.backgroundColor : setAlpha(theme.textColor, 0.3) 231 | // opacity: 0.2 232 | // border.width: 1 233 | // border.color: theme.backgroundColor 234 | // width: 3 235 | width: 1 236 | height: style.calcTickWidth(index) 237 | y: { 238 | return style.controlWidth / 2 + control.grooveThickness / 2 239 | } 240 | x: { 241 | return styleData.handleWidth / 2 + index * ((style.controlLength - styleData.handleWidth) / (repeater.count>1 ? repeater.count-1 : 1)) - 1 242 | } 243 | 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /package/contents/ui/VolumePeaksManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.private.volumewin7mixer 1.0 3 | 4 | VolumePeaks { 5 | id: volumePeaks 6 | peaking: main.dialogVisible 7 | property real defaultSinkPeakRatio: defaultSinkPeak / 65536 8 | property int defaultSinkPeakPercent: Math.round(defaultSinkPeakRatio*100) 9 | property string filename: plasmoid.file("", "code/peak/peak_monitor.py") 10 | peakCommand: "python3" 11 | peakCommandArgs: { 12 | if (mixerItem.mixerItemType == 'Sink' || mixerItem.mixerItemType == 'Source') { 13 | return [filename, mixerItem.mixerItemType, ''+PulseObject.index] 14 | } else if (mixerItem.mixerItemType == 'SinkInput' || mixerItem.mixerItemType == 'SourceOutput') { 15 | return [filename, mixerItem.mixerItemType, ''+PulseObject.deviceIndex, ''+PulseObject.index] 16 | } else { 17 | return [] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package/contents/ui/code/Icon.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | /* 4 | Copyright 2014-2015 Harald Sitter 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) version 3, or any 10 | later version accepted by the membership of KDE e.V. (or its 11 | successor approved by the membership of KDE e.V.), which shall 12 | act as a proxy defined in Section 6 of version 3 of the license. 13 | 14 | This library is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | Lesser General Public License for more details. 18 | 19 | You should have received a copy of the GNU Lesser General Public 20 | License along with this library. If not, see . 21 | */ 22 | 23 | function name(volume, muted, prefix) { 24 | if (!prefix) { 25 | prefix = "audio-volume"; 26 | } 27 | // FIXME: hardcoded max value 28 | var split_base = 65536/3.0; 29 | var icon = null; 30 | if ((volume / split_base <= 0) || muted) { 31 | icon = prefix + "-muted"; 32 | } else if (volume / split_base <= 1) { 33 | icon = prefix + "-low"; 34 | } else if (volume / split_base <= 2) { 35 | icon = prefix + "-medium"; 36 | } else { 37 | icon = prefix + "-high"; 38 | } 39 | return icon; 40 | } 41 | -------------------------------------------------------------------------------- /package/contents/ui/code/PulseObjectCommands.js: -------------------------------------------------------------------------------- 1 | var maximumValue = 65536 2 | 3 | function bound(value, min, max) { 4 | return Math.max(min, Math.min(value, max)) 5 | } 6 | 7 | function volumePercent(volume) { 8 | return Math.round(volume / maximumValue * 100.0) 9 | } 10 | 11 | function toggleMute(pulseObject) { 12 | var toMute = !pulseObject.muted 13 | pulseObject.muted = toMute 14 | return toMute 15 | } 16 | 17 | function setPercent(pulseObject, percent) { 18 | var volume = maximumValue * percent/100 19 | return setVolume(pulseObject, volume) 20 | } 21 | 22 | function setVolume(pulseObject, volume) { 23 | // console.log('setVolume', pulseObject.volume, '=>', volume) 24 | if ((volume > 0 && pulseObject.muted) || (volume == 0 && !pulseObject.muted)) { 25 | toggleMute(pulseObject) 26 | } 27 | pulseObject.volume = volume 28 | return volume 29 | } 30 | 31 | function calcVolume(min, current, max, step) { 32 | step = Math.ceil(step) 33 | var volume = bound(current + step, min, max) 34 | if (max - volume < step * 0.5) { 35 | volume = max 36 | } else if (volume < step * 0.5) { 37 | volume = min 38 | } 39 | return volume 40 | } 41 | 42 | function addVolume(pulseObject, step) { 43 | // console.log('addVolume', pulseObject, step) 44 | var volume = calcVolume(0, pulseObject.volume, maximumValue, step) 45 | return setVolume(pulseObject, volume) 46 | } 47 | 48 | function increaseVolume(pulseObject) { 49 | // console.log('increaseVolume', pulseObject) 50 | var totalSteps = plasmoid.configuration.volumeUpDownSteps 51 | var step = maximumValue / totalSteps 52 | return addVolume(pulseObject, step) 53 | } 54 | 55 | 56 | function decreaseVolume(pulseObject) { 57 | // console.log('decreaseVolume', pulseObject) 58 | var totalSteps = plasmoid.configuration.volumeUpDownSteps 59 | var step = maximumValue / totalSteps 60 | return addVolume(pulseObject, -step) 61 | } 62 | 63 | function addChannelVolume(pulseObject, channelIndex, step) { 64 | var volume = calcVolume(0, pulseObject.channelVolumes[channelIndex], maximumValue, step) 65 | return pulseObject.setChannelVolume(channelIndex, volume) 66 | } 67 | 68 | function increaseChannelVolume(pulseObject, channelIndex) { 69 | var totalSteps = plasmoid.configuration.volumeUpDownSteps 70 | var step = maximumValue / totalSteps 71 | return addChannelVolume(pulseObject, channelIndex, step) 72 | } 73 | 74 | function decreaseChannelVolume(pulseObject, channelIndex) { 75 | var totalSteps = plasmoid.configuration.volumeUpDownSteps 76 | var step = maximumValue / totalSteps 77 | return addChannelVolume(pulseObject, channelIndex, -step) 78 | } 79 | 80 | 81 | // module toggle utils 82 | function getProperty(pulseObject, key, defaultValue) { 83 | // Not necessarily a Source 84 | if (typeof pulseObject.properties === "undefined") 85 | return defaultValue 86 | 87 | var value = pulseObject.properties[key] 88 | if (value) { 89 | return parseInt(value, 10) 90 | } else { 91 | return defaultValue 92 | } 93 | } 94 | 95 | function setSourceProperty(sourceId, key, value) { 96 | var command = 'pacmd update-source-proplist ' + sourceId + ' ' + key + '="' + value + '"' 97 | console.log('setSourceProperty.command', command) 98 | executable.exec(command) 99 | } 100 | 101 | function disableModule(moduleId) { 102 | var command = 'pactl unload-module ' + moduleId 103 | console.log('disableModule.command', command) 104 | executable.exec(command) 105 | } 106 | 107 | function hasIdProperty(pulseObject, key) { 108 | return getProperty(pulseObject, key, -1) >= 0 109 | } 110 | 111 | // module-loopback 112 | // https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-loopback 113 | // We use source.properties['loopback.module_id'] != -1 serialize the state. 114 | function getLoopbackModuleId(pulseObject) { 115 | return getProperty(pulseObject, 'loopback.module_id', -1) 116 | } 117 | function hasLoopbackModuleId(pulseObject) { 118 | return getLoopbackModuleId(pulseObject) >= 0 119 | } 120 | function toggleModuleLoopback(pulseObject) { 121 | var moduleId = getLoopbackModuleId(pulseObject) 122 | if (moduleId >= 0) { 123 | disableModule(moduleId) 124 | setSourceProperty(pulseObject.index, 'loopback.module_id', -1) 125 | } else { 126 | enableModuleLoopback(pulseObject.index) 127 | } 128 | } 129 | 130 | function enableModuleLoopback(sourceId) { 131 | var command = 'pactl load-module module-loopback' 132 | command += ' latency_msec=1' 133 | command += ' source=' + sourceId 134 | command += ' source_output_properties="loopback.source=' + sourceId + '"' 135 | command += ' sink_input_properties="loopback.source=' + sourceId + '"' 136 | console.log('enableModuleLoopback.command', command) 137 | var callback = loadModuleLoopbackCallback.bind(null, sourceId) 138 | executable.execAwait(command, callback) 139 | } 140 | 141 | function loadModuleLoopbackCallback(sourceId, command, exitCode, exitStatus, stdout, stderr) { 142 | console.log('LoopbackCallback.sourceId', sourceId) 143 | var moduleId = executable.trimOutput(stdout) 144 | console.log('LoopbackCallback.moduleId', moduleId) 145 | setSourceProperty(sourceId, 'loopback.module_id', moduleId) 146 | } 147 | 148 | 149 | // module-echo-cancel 150 | // https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-echo-cancel 151 | // https://github.com/pulseaudio/pulseaudio/blob/master/src/modules/echo-cancel/module-echo-cancel.c 152 | // We use source.properties['echo_cancel.module_id'] != -1 serialize the state. 153 | function getEchoCancelModuleId(pulseObject) { 154 | return getProperty(pulseObject, 'echo_cancel.module_id', -1) 155 | } 156 | function hasEchoCancelModuleId(pulseObject) { 157 | return getEchoCancelModuleId(pulseObject) >= 0 158 | } 159 | function toggleModuleEchoCancel(pulseObject) { 160 | var moduleId = getEchoCancelModuleId(pulseObject) 161 | console.log('toggleModuleEchoCancel.moduleId', moduleId) 162 | if (moduleId >= 0) { 163 | 164 | // If the generated stream has loopback enabled, we need to... 165 | if (true) { 166 | // ... disable the other stream first. 167 | var loopbackedStream = main.getStream(filteredSourceModel, function(stream) { 168 | // console.log('findStream', getProperty(stream, 'echo_cancel.source', -1), pulseObject.index, hasLoopbackModuleId(stream)) 169 | return getProperty(stream, 'echo_cancel.source', -1) == pulseObject.index // The generated echo cancelled source (microphone) 170 | && hasLoopbackModuleId(stream) // which also has loopback enabled 171 | }) 172 | console.log('toggleModuleEchoCancel.loopbackedStream', loopbackedStream) 173 | if (loopbackedStream) { 174 | var loopbackModuleId = getLoopbackModuleId(loopbackedStream) 175 | console.log('toggleModuleEchoCancel.loopbackModuleId', loopbackModuleId) 176 | if (loopbackModuleId >= 0) { 177 | disableModule(loopbackModuleId) 178 | // We don't need to block execution, since if echo cancel is disabled first 179 | // the loopback will attach itself to the microphone directly. 180 | // We should block execution if someone complains a noise when cancelling both. 181 | } 182 | } 183 | } else { 184 | // ... move the "loopback.module_id" to the current stream 185 | // Since the loopback will automatically attach itself to the echo cancelled source (this stream) 186 | // TODO: 187 | } 188 | 189 | 190 | disableModule(moduleId) 191 | setSourceProperty(pulseObject.index, 'echo_cancel.module_id', -1) 192 | } else { 193 | enableModuleEchoCancel(pulseObject.index) 194 | } 195 | } 196 | 197 | function enableModuleEchoCancel(sourceId) { 198 | var command = 'pactl load-module module-echo-cancel' 199 | command += ' source_master=' + sourceId 200 | command += ' source_properties="echo_cancel.source=' + sourceId + '"' 201 | command += ' sink_properties="echo_cancel.source=' + sourceId + '"' 202 | // command += ' adjust_threshold="0"' 203 | command += ' aec_method="webrtc"' 204 | // command += ' aec_args="drift_compensation=0"' 205 | 206 | // command += " source_properties=echo_cancel.source=\\'" + sourceId + "\\'application.id=\\'org.PulseAudio.pavucontrol\\'" 207 | // command += " sink_properties=echo_cancel.source=\\'" + sourceId + "\\'application.id=\\'org.PulseAudio.pavucontrol\\'" 208 | 209 | console.log('enableModuleEchoCancel.command', command) 210 | var callback = loadModuleEchoCancelCallback.bind(null, sourceId) 211 | executable.execAwait(command, callback) 212 | } 213 | 214 | function loadModuleEchoCancelCallback(sourceId, command, exitCode, exitStatus, stdout, stderr) { 215 | console.log('EchoCancelCallback.sourceId', sourceId) 216 | console.log('EchoCancelCallback.stdout', stdout) 217 | var moduleId = executable.trimOutput(stdout) 218 | console.log('EchoCancelCallback.moduleId', moduleId) 219 | if (moduleId) { 220 | setSourceProperty(sourceId, 'echo_cancel.module_id', moduleId) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /package/contents/ui/code/Utils.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | .import "./Icon.js" as Icon 3 | 4 | function isDummyOutput(output) { 5 | // DEFAULT_SINK_NAME in module-always-sink.c 6 | return output && output.name === "auto_null" 7 | } 8 | 9 | function iconNameForStream(pulseObject) { 10 | return pulseObject ? Icon.name(pulseObject.volume, pulseObject.muted) : Icon.name(0, true) 11 | } 12 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigApplet.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 2.0 as PlasmaComponents 6 | import org.kde.plasma.extras 2.0 as PlasmaExtras 7 | 8 | import "../lib" 9 | 10 | ConfigPage { 11 | id: page 12 | showAppletVersion: true 13 | 14 | property alias cfg_volumeUpDownSteps: volumeUpDownSteps.value 15 | property alias cfg_showVolumeTickmarks: showVolumeTickmarks.checked 16 | // property alias cfg_showOpenKcmAudioVolume: showOpenKcmAudioVolume.checked 17 | // property alias cfg_showOpenPavucontrol: showOpenPavucontrol.checked 18 | property alias cfg_moveAllAppsOnSetDefault: moveAllAppsOnSetDefault.checked 19 | property alias cfg_closeOnSetDefault: closeOnSetDefault.checked 20 | property alias cfg_setDefaultOnClickIcon: setDefaultOnClickIcon.checked 21 | property alias cfg_showMediaController: showMediaController.checked 22 | property alias cfg_showMediaTimeElapsed: showMediaTimeElapsed.checked 23 | property alias cfg_showMediaTimeLeft: showMediaTimeLeft.checked 24 | property alias cfg_showMediaTotalDuration: showMediaTotalDuration.checked 25 | property alias cfg_showOsd: showOsd.checked 26 | property alias cfg_volumeChangeFeedback: volumeChangeFeedback.checked 27 | property alias cfg_showVisualFeedback: showVisualFeedback.checked 28 | property alias cfg_showVirtualStreams: showVirtualStreams.checked 29 | 30 | GroupBox { 31 | Layout.fillWidth: true 32 | title: i18n("Media Keys") 33 | 34 | ColumnLayout { 35 | 36 | RowLayout { 37 | Label { 38 | text: i18n("Volume Up/Down Steps:") 39 | } 40 | SpinBox { 41 | id: volumeUpDownSteps 42 | minimumValue: 1 43 | maximumValue: 1000 44 | } 45 | Label { 46 | text: i18n("One step = %1%", Math.round(1/volumeUpDownSteps.value * 100)) 47 | } 48 | } 49 | 50 | } 51 | } 52 | 53 | GroupBox { 54 | Layout.fillWidth: true 55 | title: i18n("Mixer") 56 | 57 | ColumnLayout { 58 | 59 | CheckBox { 60 | enabled: false 61 | id: showVolumeTickmarks 62 | checked: true 63 | text: i18n("Show Ticks every 10%") 64 | } 65 | 66 | RowLayout { 67 | Label { 68 | text: i18n("Volume Boost") 69 | } 70 | SpinBox { 71 | enabled: false 72 | id: volumeBoostMaxVolume 73 | minimumValue: 100 74 | value: 150 75 | maximumValue: 1000 76 | stepSize: 10 77 | suffix: i18nd("plasma_applet_org.kde.plasma.volume", "%") 78 | } 79 | } 80 | 81 | 82 | 83 | } 84 | } 85 | 86 | ExclusiveGroup { id: volumeSliderThemeGroup } 87 | GroupBox { 88 | Layout.fillWidth: true 89 | title: i18n("Volume Slider Theme") 90 | 91 | ColumnLayout { 92 | RadioButton { 93 | text: i18n("Desktop Theme (%1)", theme.themeName) 94 | exclusiveGroup: volumeSliderThemeGroup 95 | enabled: false 96 | // checked: plasmoid.configuration.volumeSliderTheme == "desktoptheme" 97 | // onClicked: plasmoid.configuration.volumeSliderTheme = "desktoptheme" 98 | } 99 | RadioButton { 100 | text: i18n("Color Theme (Default Look)") 101 | exclusiveGroup: volumeSliderThemeGroup 102 | // checked: plasmoid.configuration.volumeSliderTheme == "colortheme" 103 | // onClicked: plasmoid.configuration.volumeSliderTheme = "colortheme" 104 | checked: plasmoid.configuration.volumeSliderTheme == "desktoptheme" 105 | onClicked: plasmoid.configuration.volumeSliderTheme = "desktoptheme" 106 | } 107 | 108 | RadioButton { 109 | text: i18n("Light Blue on Grey (Default Look)") 110 | exclusiveGroup: volumeSliderThemeGroup 111 | checked: plasmoid.configuration.volumeSliderTheme == "default" 112 | onClicked: plasmoid.configuration.volumeSliderTheme = "default" 113 | } 114 | } 115 | } 116 | 117 | // GroupBox { 118 | // Layout.fillWidth: true 119 | // title: 'Context Menu' 120 | 121 | // ColumnLayout { 122 | 123 | // CheckBox { 124 | // id: showOpenKcmAudioVolume 125 | // text: 'KDE Audio Volume' 126 | // } 127 | 128 | // CheckBox { 129 | // id: showOpenPavucontrol 130 | // text: 'pavucontrol (PulseAudio Control) (Can do Audio Boost)' 131 | // } 132 | 133 | // RowLayout { 134 | // Text { width: 24 } // indent 135 | // Text { 136 | // font.family: 'monospace' 137 | // text: 'sudo apt-get install pavucontrol' 138 | // } 139 | // } 140 | 141 | // } 142 | // } 143 | 144 | GroupBox { 145 | Layout.fillWidth: true 146 | title: i18n("Options") 147 | 148 | ColumnLayout { 149 | 150 | CheckBox { 151 | id: moveAllAppsOnSetDefault 152 | text: i18n("Move all Apps to device when setting default device (when set in with the context menu)") 153 | } 154 | 155 | CheckBox { 156 | id: closeOnSetDefault 157 | text: i18n("Close the popup after setting a default device") 158 | } 159 | 160 | CheckBox { 161 | id: setDefaultOnClickIcon 162 | text: i18n("Set default device after clicking a speaker/mic icon") 163 | } 164 | 165 | CheckBox { 166 | id: showOsd 167 | text: i18n("Show OSD on when changing the volume.") 168 | } 169 | 170 | CheckBox { 171 | id: volumeChangeFeedback 172 | text: i18n("Volume Feedback: Play popping noise when changing the volume.") 173 | } 174 | 175 | CheckBox { 176 | id: showVisualFeedback 177 | enabled: false 178 | text: i18n("Visual Feedback: Visualize current sound.") 179 | 180 | Component.onCompleted: { 181 | var mixerPluginTest = Qt.createQmlObject('import org.kde.plasma.private.volumewin7mixer 1.0; import QtQuick 2.0; QtObject {}', volumeChangeFeedback) 182 | if (mixerPluginTest) { 183 | enabled = true 184 | } 185 | } 186 | } 187 | 188 | CheckBox { 189 | id: showVirtualStreams 190 | text: i18n("Show virtual streams.") 191 | } 192 | 193 | } 194 | } 195 | 196 | GroupBox { 197 | Layout.fillWidth: true 198 | title: i18n("Media Controller") 199 | 200 | ColumnLayout { 201 | 202 | CheckBox { 203 | id: showMediaController 204 | text: i18n("Show Media Controller") 205 | } 206 | 207 | ConfigComboBox { 208 | id: appDescriptionControl 209 | configKey: "mediaControllerLocation" 210 | label: i18n("Position") 211 | model: [ 212 | { value: "top", text: i18n("Top") }, 213 | { value: "bottom", text: i18n("Bottom") }, 214 | ] 215 | } 216 | 217 | CheckBox { 218 | id: showMediaTimeElapsed 219 | text: i18n("Show Time Elapsed") 220 | } 221 | 222 | CheckBox { 223 | id: showMediaTimeLeft 224 | text: i18n("Show Time Left") 225 | } 226 | 227 | CheckBox { 228 | id: showMediaTotalDuration 229 | text: i18n("Show Total Duration") 230 | } 231 | 232 | } 233 | } 234 | 235 | GroupBox { 236 | Layout.fillWidth: true 237 | title: i18n("Keyboard Shortcuts") 238 | 239 | ColumnLayout { 240 | id: shortcutsTable 241 | Layout.fillWidth: true 242 | 243 | Label { 244 | text: i18n("Set the Global Shortcut in the Keyboard Shortcuts tab.") 245 | wrapMode: Text.Wrap 246 | } 247 | 248 | Label {} // Whitespace 249 | 250 | Repeater { 251 | property var shortcuts: [ 252 | { 253 | "label": i18n("Global Shortcut"), 254 | "keySequence": plasmoid.globalShortcut, 255 | }, 256 | { 257 | "label": i18n("Selection: Select Previous Stream"), 258 | "keySequence": "Left", 259 | }, 260 | { 261 | "label": i18n("Selection: Select Next Stream"), 262 | "keySequence": "Right", 263 | }, 264 | { 265 | "label": i18n("Selection: Increase Volume"), 266 | "keySequence": "Up", 267 | }, 268 | { 269 | "label": i18n("Selection: Decrease Volume"), 270 | "keySequence": "Down", 271 | }, 272 | { 273 | "label": i18n("Selection: Make Default Device"), 274 | "keySequence": "Enter", 275 | }, 276 | { 277 | "label": i18n("Selection: Toggle Mute"), 278 | "keySequence": "M", 279 | }, 280 | { 281 | "label": i18n("Selection: Open Context Menu"), 282 | "keySequence": "Menu", 283 | }, 284 | ] 285 | 286 | Component.onCompleted: { 287 | for (var i = 0; i <= 10; i++) { 288 | shortcuts.push({ 289 | "label": i18n("Selection: Set Volume to %1%", i*10), 290 | "keySequence": i < 10 ? "" + i : "", 291 | }) 292 | model = shortcuts 293 | } 294 | } 295 | 296 | 297 | RowLayout { 298 | Layout.fillWidth: true 299 | Label { 300 | text: modelData.keySequence 301 | 302 | Layout.minimumWidth: 100 * units.devicePixelRatio 303 | } 304 | Label { 305 | text: modelData.label 306 | font.bold: true 307 | } 308 | } 309 | 310 | } 311 | } 312 | } 313 | 314 | 315 | } 316 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigComboBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | import org.kde.plasma.core 2.0 as PlasmaCore 6 | import org.kde.plasma.components 2.0 as PlasmaComponents 7 | 8 | import ".." 9 | 10 | /* 11 | ** Example: 12 | ** 13 | ConfigComboBox { 14 | configKey: "appDescription" 15 | model: [ 16 | { value: "hidden", text: i18n("Hidden") }, 17 | { value: "after", text: i18n("After") }, 18 | { value: "below", text: i18n("Below") }, 19 | ] 20 | } 21 | */ 22 | RowLayout { 23 | id: configComboBox 24 | spacing: 2 25 | // Layout.fillWidth: true 26 | Layout.maximumWidth: 300 27 | 28 | property alias label: label.text 29 | property alias horizontalAlignment: label.horizontalAlignment 30 | 31 | property string configKey: '' 32 | readonly property string value: configKey ? plasmoid.configuration[configKey] : "" 33 | onValueChanged: comboBox.selectValue(value) 34 | function setValue(val) { comboBox.selectValue(val) } 35 | 36 | property alias model: comboBox.model 37 | 38 | signal populate() 39 | Component.onCompleted: populate() 40 | 41 | Label { 42 | id: label 43 | text: "Label" 44 | Layout.fillWidth: horizontalAlignment == Text.AlignRight 45 | horizontalAlignment: Text.AlignLeft 46 | } 47 | 48 | ComboBox { 49 | id: comboBox 50 | Layout.fillWidth: label.horizontalAlignment == Text.AlignLeft 51 | 52 | onCurrentIndexChanged: { 53 | if (currentIndex >= 0 && typeof model !== 'number') { 54 | var val = model[currentIndex].value 55 | if (configKey && val) { 56 | plasmoid.configuration[configKey] = val 57 | } 58 | } 59 | } 60 | 61 | function size() { 62 | if (typeof model === "number") { 63 | return model 64 | } else if (typeof model.count === "number") { 65 | return model.count 66 | } else if (typeof model.length === "number") { 67 | return model.length 68 | } else { 69 | return 0 70 | } 71 | } 72 | 73 | function findValue(val) { 74 | for (var i = 0; i < size(); i++) { 75 | if (model[i].value == val) { 76 | return i 77 | } 78 | } 79 | return -1 80 | } 81 | 82 | function selectValue(val) { 83 | var index = comboBox.findValue(val) 84 | if (index >= 0) { 85 | comboBox.currentIndex = index 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigStreamRestore.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 2.0 as PlasmaComponents 6 | import org.kde.plasma.extras 2.0 as PlasmaExtras 7 | 8 | import org.kde.plasma.private.volume 0.1 9 | 10 | Item { 11 | id: page 12 | 13 | SinkModel { id: sinkModel } 14 | StreamRestoreModel { id: streamRestoreModel } 15 | 16 | TableView { 17 | anchors.fill: parent 18 | 19 | model: streamRestoreModel 20 | 21 | TableViewColumn { 22 | role: "Name" 23 | title: "Name" 24 | } 25 | 26 | TableViewColumn { 27 | role: "Device" 28 | title: "Device" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package/contents/ui/lib/AppletIcon.qml: -------------------------------------------------------------------------------- 1 | // Version: 3 2 | 3 | import QtQuick 2.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | 6 | Item { 7 | id: appletIcon 8 | property string source: '' 9 | property bool active: false 10 | readonly property bool usingPackageSvg: filename // plasmoid.file() returns "" if file doesn't exist. 11 | readonly property string filename: source ? plasmoid.file("", "icons/" + source + '.svg') : "" 12 | readonly property int minSize: Math.min(width, height) 13 | property bool smooth: true 14 | property var overlays: [] 15 | property var colorGroup: PlasmaCore.ColorScope.colorGroup 16 | 17 | 18 | PlasmaCore.IconItem { 19 | id: iconItem 20 | anchors.fill: parent 21 | visible: !appletIcon.usingPackageSvg 22 | source: appletIcon.usingPackageSvg ? '' : appletIcon.source 23 | active: appletIcon.active 24 | smooth: appletIcon.smooth 25 | overlays: appletIcon.overlays 26 | colorGroup: appletIcon.colorGroup 27 | } 28 | 29 | PlasmaCore.SvgItem { 30 | id: svgItem 31 | anchors.centerIn: parent 32 | readonly property real maxSize: Math.min(naturalSize.width, naturalSize.height) 33 | readonly property real widthRatio: naturalSize.width / maxSize 34 | readonly property real heightRatio: naturalSize.height / maxSize 35 | width: appletIcon.minSize * widthRatio 36 | height: appletIcon.minSize * heightRatio 37 | 38 | smooth: appletIcon.smooth 39 | 40 | visible: appletIcon.usingPackageSvg 41 | svg: PlasmaCore.Svg { 42 | id: svg 43 | imagePath: appletIcon.filename 44 | } 45 | 46 | PlasmaCore.IconItem { 47 | id: emblemItem 48 | anchors.fill: parent 49 | visible: parent.visible 50 | overlays: appletIcon.overlays 51 | colorGroup: appletIcon.colorGroup 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package/contents/ui/lib/AppletVersion.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.plasmoid 2.0 6 | 7 | Item { 8 | implicitWidth: label.implicitWidth 9 | implicitHeight: label.implicitHeight 10 | 11 | property string version: "?" 12 | property string metadataFilepath: plasmoid.file("", "../metadata.desktop") 13 | 14 | PlasmaCore.DataSource { 15 | id: executable 16 | engine: "executable" 17 | connectedSources: [] 18 | onNewData: { 19 | var exitCode = data["exit code"] 20 | var exitStatus = data["exit status"] 21 | var stdout = data["stdout"] 22 | var stderr = data["stderr"] 23 | exited(exitCode, exitStatus, stdout, stderr) 24 | disconnectSource(sourceName) // cmd finished 25 | } 26 | function exec(cmd) { 27 | connectSource(cmd) 28 | } 29 | signal exited(int exitCode, int exitStatus, string stdout, string stderr) 30 | } 31 | 32 | Connections { 33 | target: executable 34 | onExited: { 35 | version = stdout.replace('\n', ' ').trim() 36 | } 37 | } 38 | 39 | Label { 40 | id: label 41 | text: i18n("Version: %1", version) 42 | } 43 | 44 | Component.onCompleted: { 45 | var cmd = 'kreadconfig5 --file "' + metadataFilepath + '" --group "Desktop Entry" --key "X-KDE-PluginInfo-Version"' 46 | executable.exec(cmd) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | 4 | ColumnLayout { 5 | id: page 6 | Layout.fillWidth: true 7 | default property alias _contentChildren: content.data 8 | 9 | ColumnLayout { 10 | id: content 11 | Layout.fillWidth: true 12 | Layout.alignment: Qt.AlignTop 13 | 14 | // Workaround for crash when using default on a Layout. 15 | // https://bugreports.qt.io/browse/QTBUG-52490 16 | // Still affecting Qt 5.7.0 17 | Component.onDestruction: { 18 | while (children.length > 0) { 19 | children[children.length - 1].parent = page; 20 | } 21 | } 22 | } 23 | 24 | property alias showAppletVersion: appletVersionLoader.active 25 | Loader { 26 | id: appletVersionLoader 27 | active: false 28 | visible: active 29 | source: "AppletVersion.qml" 30 | anchors.right: parent.right 31 | anchors.bottom: parent.top 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ContextMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | import org.kde.plasma.components 2.0 as PlasmaComponents 5 | 6 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/plasmacomponents/qmenu.cpp 7 | // Example: https://github.com/KDE/plasma-desktop/blob/master/applets/taskmanager/package/contents/ui/ContextMenu.qml 8 | PlasmaComponents.ContextMenu { 9 | id: contextMenu 10 | 11 | function newSeperator() { 12 | return Qt.createQmlObject("ContextMenuItem { separator: true }", contextMenu); 13 | } 14 | function newMenuItem() { 15 | return Qt.createQmlObject("ContextMenuItem {}", contextMenu); 16 | } 17 | function newSubMenu() { 18 | return Qt.createQmlObject("ContextSubMenu {}", contextMenu); 19 | } 20 | 21 | property bool clearBeforeOpen: true 22 | signal beforeOpen(var menu) 23 | 24 | function removeAllItems() { 25 | // console.log('removeAllItems', contextMenu) 26 | 27 | // clearMenuItems() causes a segfault when trying to destroy a submenu. 28 | // So we need to manually destroy it as a workaround. 29 | for (var i = content.length-1; i >= 0; i--) { 30 | var item = content[i] 31 | var isSubMenu = item.hasOwnProperty("subContextMenu") 32 | // console.log(contextMenu, i, 'destroy', isSubMenu, item.text) 33 | if (isSubMenu) { 34 | item.subContextMenu.removeAllItems() // Probably only necessary for a sub-sub-menu. 35 | item.subContextMenu.destroy() // We need this or it will segfault on the 2nd open. 36 | } 37 | removeMenuItem(item) // We need this or it will segfault on the 3rd open. 38 | item.destroy() 39 | } 40 | } 41 | 42 | function doBeforeOpen() { 43 | // console.log('doBeforeOpen') 44 | // console.log('doBeforeOpen.content.length', content.length) 45 | if (clearBeforeOpen) { 46 | removeAllItems() 47 | // console.log('doBeforeOpen.clearMenuItems.done') 48 | } 49 | beforeOpen(contextMenu) 50 | } 51 | 52 | function show(x, y) { 53 | doBeforeOpen() 54 | open(x, y) 55 | } 56 | 57 | function showRelative() { 58 | doBeforeOpen() 59 | openRelative() 60 | } 61 | 62 | function showBelow(item) { 63 | visualParent = item 64 | placement = PlasmaCore.Types.BottomPosedLeftAlignedPopup 65 | showRelative() 66 | } 67 | 68 | Component.onDestruction: { 69 | // console.log('contextMenu.onDestruction', contextMenu) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ContextMenuItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import org.kde.plasma.components 2.0 as PlasmaComponents 4 | 5 | PlasmaComponents.MenuItem { 6 | id: contextMenuItem 7 | 8 | Component.onDestruction: { 9 | // console.log('contextMenuItem.onDestruction', contextMenuItem, contextMenuItem.visualParent) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ContextSubMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | import org.kde.plasma.components 2.0 as PlasmaComponents 5 | 6 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/plasmacomponents/qmenu.cpp 7 | // Example: https://github.com/KDE/plasma-desktop/blob/master/applets/taskmanager/package/contents/ui/ContextMenu.qml 8 | ContextMenuItem { 9 | id: subMenuItem 10 | 11 | property var subContextMenu: ContextMenu { 12 | id: subContextMenu 13 | 14 | visualParent: subMenuItem.action 15 | 16 | Component.onDestruction: { 17 | // console.log('subContextMenu.onDestruction', subContextMenu, subContextMenu.visualParent) 18 | } 19 | } 20 | Component.onDestruction: { 21 | // console.log('subMenuItem.onDestruction', subMenuItem) 22 | } 23 | 24 | function newSeperator() { 25 | return Qt.createQmlObject("ContextMenuItem { separator: true }", subContextMenu); 26 | } 27 | function newMenuItem() { 28 | return Qt.createQmlObject("ContextMenuItem {}", subContextMenu); 29 | } 30 | function newSubMenu() { 31 | return Qt.createQmlObject("ContextSubMenu {}", subContextMenu); 32 | } 33 | 34 | function addMenuItem(menuItem) { 35 | // console.log('addMenuItem', menuItem, menuItem.text) 36 | subContextMenu.addMenuItem(menuItem) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ExecUtil.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | PlasmaCore.DataSource { 5 | id: executable 6 | engine: "executable" 7 | connectedSources: [] 8 | onNewData: { 9 | var exitCode = data["exit code"] 10 | var exitStatus = data["exit status"] 11 | var stdout = data["stdout"] 12 | var stderr = data["stderr"] 13 | exited(sourceName, exitCode, exitStatus, stdout, stderr) 14 | disconnectSource(sourceName) // cmd finished 15 | } 16 | function exec(cmd) { 17 | connectSource(cmd) 18 | } 19 | signal exited(string command, int exitCode, int exitStatus, string stdout, string stderr) 20 | 21 | function trimOutput(stdout) { 22 | return stdout.replace('\n', ' ').trim() 23 | } 24 | 25 | property var callbacks: { return {} } 26 | function execAwait(cmd, callback) { 27 | connectSource(cmd) 28 | if (typeof callback === "function") { 29 | if (callbacks[cmd]) { 30 | console.log('ExecUtil.callbacks[cmd] already registered', cmd) 31 | } else { 32 | callbacks[cmd] = callback 33 | } 34 | } 35 | } 36 | onExited: { 37 | if (callbacks[command]) { 38 | callbacks[command](command, exitCode, exitStatus, stdout, stderr) 39 | delete callbacks[command] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package/contents/ui/main.qml: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014-2015 Harald Sitter 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License as 6 | published by the Free Software Foundation; either version 2 of 7 | the License or (at your option) version 3 or any later version 8 | accepted by the membership of KDE e.V. (or its successor approved 9 | by the membership of KDE e.V.), which shall act as a proxy 10 | defined in Section 14 of version 3 of the license. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | import QtQuick 2.0 22 | import QtQuick.Layouts 1.0 23 | import QtQuick.Controls 1.0 24 | import QtQuick.Controls.Styles.Plasma 2.0 as PlasmaStyles 25 | 26 | import org.kde.plasma.core 2.0 as PlasmaCore 27 | import org.kde.plasma.components 2.0 as PlasmaComponents 28 | import org.kde.plasma.extras 2.0 as PlasmaExtras 29 | import org.kde.plasma.plasmoid 2.0 30 | 31 | import org.kde.plasma.private.volume 0.1 as PlasmaVolume 32 | 33 | import "./code/Utils.js" as Utils 34 | import "./code/PulseObjectCommands.js" as PulseObjectCommands 35 | import "lib" 36 | 37 | DialogApplet { 38 | id: main 39 | 40 | AppletConfig { id: config } 41 | 42 | property string draggedStreamType: '' 43 | property QtObject draggedStream: null 44 | function startDrag(pulseObject, type) { 45 | draggedStreamType = type 46 | draggedStream = pulseObject 47 | } 48 | function clearDrag() { 49 | draggedStream = null 50 | draggedStreamType = '' 51 | } 52 | 53 | property string displayName: i18nd("plasma_applet_org.kde.plasma.volume", "Audio Volume") 54 | property string speakerIcon: Utils.iconNameForStream(sinkModel.defaultSink) 55 | 56 | compactItemIcon: speakerIcon 57 | onCompactItemClicked: { 58 | if (mouse.button == Qt.LeftButton) { 59 | main.toggleDialog(false) 60 | } else if (mouse.button == Qt.MiddleButton) { 61 | toggleDefaultSinksMute() 62 | } 63 | } 64 | onCompactItemWheel: { 65 | var delta = wheel.angleDelta.y || wheel.angleDelta.x 66 | if (delta > 0) { 67 | increaseDefaultSinkVolume() 68 | } else if (delta < 0) { 69 | decreaseDefaultSinkVolume() 70 | } 71 | } 72 | 73 | Plasmoid.icon: { 74 | if (mpris2Source.hasPlayer && mpris2Source.albumArt) { 75 | return mpris2Source.albumArt 76 | } else { 77 | return speakerIcon 78 | } 79 | } 80 | Plasmoid.toolTipMainText: { 81 | if (mpris2Source.hasPlayer && mpris2Source.track) { 82 | return mpris2Source.track 83 | } else { 84 | return displayName 85 | } 86 | } 87 | Plasmoid.toolTipSubText: { 88 | var lines = [] 89 | if (mpris2Source.hasPlayer && mpris2Source.artist) { 90 | if (mpris2Source.isPaused) { 91 | lines.push(mpris2Source.artist ? i18ndc("plasma_applet_org.kde.plasma.mediacontroller", "Artist of the song", "by %1 (paused)", mpris2Source.artist) : i18nd("plasma_applet_org.kde.plasma.mediacontroller", "Paused")) 92 | } else if (mpris2Source.artist) { 93 | lines.push(i18ndc("plasma_applet_org.kde.plasma.mediacontroller", "Artist of the song", "by %1", mpris2Source.artist)) 94 | } 95 | } 96 | if (sinkModel.defaultSink) { 97 | var sinkVolumePercent = Math.round(PulseObjectCommands.volumePercent(sinkModel.defaultSink.volume)) 98 | lines.push(i18nd("plasma_applet_org.kde.plasma.volume", "Volume at %1%", sinkVolumePercent)) 99 | lines.push(sinkModel.defaultSink.description) 100 | } 101 | return lines.join('\n') 102 | } 103 | 104 | 105 | property bool showMediaController: plasmoid.configuration.showMediaController 106 | property string mediaControllerLocation: plasmoid.configuration.mediaControllerLocation || 'bottom' 107 | property bool mediaControllerVisible: showMediaController && mpris2Source.hasPlayer 108 | // property int mediaControllerHeight: 56 // = 48px albumArt + 8px seekbar 109 | 110 | dialogContents: Item { 111 | id: dialogContents 112 | 113 | width: mixerItemRow.width 114 | height: config.mixerGroupHeight + (mediaControllerVisible ? config.mediaControllerHeight : 0) 115 | 116 | 117 | // Keyboard Navigation/Controls 118 | InputManager { id: inputManager } 119 | focus: true 120 | Keys.forwardTo: inputManager.hasSelection ? [inputManager.selectedMixerItem] : [] 121 | Keys.onLeftPressed: inputManager.selectLeft() 122 | Keys.onRightPressed: inputManager.selectRight() 123 | function fireKeyOnDefault(keyName, event) { 124 | if (!inputManager.hasSelection) { 125 | inputManager.selectDefault() 126 | var fnName = 'on' + keyName + 'Pressed' 127 | inputManager.selectedMixerItem.Keys[fnName](event) // Manually trigger since it hasn't been forwarded yet. 128 | } 129 | } 130 | Keys.onUpPressed: fireKeyOnDefault('Up', event) 131 | Keys.onDownPressed: fireKeyOnDefault('Down', event) 132 | Keys.onPressed: fireKeyOnDefault('', event) 133 | 134 | Row { 135 | id: mixerItemRow 136 | anchors.right: parent.right 137 | width: childrenRect.width 138 | height: parent.height - (mediaControllerVisible ? config.mediaControllerHeight : 0) 139 | spacing: 10 140 | 141 | MixerItemGroup { 142 | id: sourceOutputMixerItemGroup 143 | height: parent.height 144 | title: i18n("Recording Apps") 145 | 146 | model: appOutputsModel 147 | mixerGroupType: 'SourceOutput' 148 | } 149 | 150 | MixerItemGroup { 151 | id: sinkInputMixerItemGroup 152 | height: parent.height 153 | title: i18n("Apps") 154 | 155 | model: appsModel 156 | mixerGroupType: 'SinkInput' 157 | } 158 | 159 | MixerItemGroup { 160 | id: sourceMixerItemGroup 161 | height: parent.height 162 | title: i18n("Mics") 163 | 164 | model: filteredSourceModel 165 | mixerGroupType: 'Source' 166 | } 167 | 168 | MixerItemGroup { 169 | id: sinkMixerItemGroup 170 | height: parent.height 171 | title: i18n("Speakers") 172 | 173 | model: filteredSinkModel 174 | mixerGroupType: 'Sink' 175 | } 176 | 177 | } 178 | 179 | MediaController { 180 | id: mediaController 181 | width: mixerItemRow.width 182 | height: config.mediaControllerHeight 183 | } 184 | 185 | PlasmaComponents.ToolButton { 186 | id: pinButton 187 | anchors.top: parent.top 188 | anchors.right: parent.right 189 | width: Math.round(units.gridUnit * 1.25) 190 | height: width 191 | checkable: true 192 | iconSource: "window-pin" 193 | onCheckedChanged: plasmoid.hideOnWindowDeactivate = !checked 194 | } 195 | 196 | states: [ 197 | State { 198 | name: "mediaControllerHidden" 199 | when: !mediaControllerVisible 200 | PropertyChanges { 201 | target: mixerItemRow 202 | anchors.top: mixerItemRow.parent.top 203 | anchors.bottom: mixerItemRow.parent.bottom 204 | } 205 | PropertyChanges { 206 | target: mediaController 207 | visible: false 208 | } 209 | }, 210 | State { 211 | name: "mediaControllerTop" 212 | when: mediaControllerVisible && mediaControllerLocation == 'top' 213 | PropertyChanges { 214 | target: mixerItemRow 215 | // anchors.top: undefined 216 | anchors.topMargin: config.mediaControllerHeight 217 | anchors.bottom: mixerItemRow.parent.bottom 218 | } 219 | PropertyChanges { 220 | target: mediaController 221 | visible: true 222 | anchors.left: mediaController.parent.left 223 | anchors.top: mediaController.parent.top 224 | anchors.bottom: mixerItemRow.top 225 | } 226 | PropertyChanges { 227 | target: pinButton 228 | anchors.topMargin: config.mediaControllerHeight 229 | } 230 | }, 231 | State { 232 | name: "mediaControllerBottom" 233 | when: mediaControllerVisible && mediaControllerLocation == 'bottom' 234 | PropertyChanges { 235 | target: mixerItemRow 236 | anchors.top: mixerItemRow.parent.top 237 | // anchors.bottom: undefined 238 | anchors.bottomMargin: config.mediaControllerHeight 239 | } 240 | PropertyChanges { 241 | target: mediaController 242 | visible: true 243 | anchors.left: mediaController.parent.left 244 | anchors.top: mixerItemRow.bottom 245 | anchors.right: mediaController.parent.right 246 | anchors.bottom: mediaController.parent.bottom 247 | } 248 | } 249 | ] 250 | } 251 | 252 | function increaseDefaultSinkVolume() { 253 | if (!sinkModel.defaultSink) { 254 | return 255 | } 256 | sinkModel.defaultSink.muted = false 257 | var volume = PulseObjectCommands.increaseVolume(sinkModel.defaultSink) 258 | osd.showVolume(volume) 259 | playFeedback() 260 | } 261 | 262 | function decreaseDefaultSinkVolume() { 263 | if (!sinkModel.defaultSink) { 264 | return 265 | } 266 | sinkModel.defaultSink.muted = false 267 | var volume = PulseObjectCommands.decreaseVolume(sinkModel.defaultSink) 268 | osd.showVolume(volume) 269 | playFeedback() 270 | } 271 | 272 | function toggleDefaultSinksMute() { 273 | if (!sinkModel.defaultSink) { 274 | return 275 | } 276 | var toMute = PulseObjectCommands.toggleMute(sinkModel.defaultSink) 277 | osd.showVolume(toMute ? 0 : sinkModel.defaultSink.volume) 278 | playFeedback() 279 | } 280 | 281 | function increaseDefaultSourceVolume() { 282 | if (!sourceModel.defaultSource) { 283 | return 284 | } 285 | sourceModel.defaultSource.muted = false 286 | var volume = PulseObjectCommands.increaseVolume(sourceModel.defaultSource) 287 | osd.showMicVolume(volume) 288 | } 289 | 290 | function decreaseDefaultSourceVolume() { 291 | if (!sourceModel.defaultSource) { 292 | return 293 | } 294 | sourceModel.defaultSource.muted = false 295 | var volume = PulseObjectCommands.decreaseVolume(sourceModel.defaultSource) 296 | osd.showMicVolume(volume) 297 | } 298 | 299 | function toggleDefaultSourceMute() { 300 | if (!sourceModel.defaultSource) { 301 | return 302 | } 303 | var toMute = PulseObjectCommands.toggleMute(sourceModel.defaultSource) 304 | osd.showMicVolume(toMute ? 0 : sourceModel.defaultSource.volume) 305 | } 306 | 307 | // Connections { 308 | // target: sinkModel 309 | // onDefaultSinkChanged: { 310 | // // console.log('sinkModel.onDefaultSinkChanged', sinkModel.defaultSink) 311 | // if (!sinkModel.defaultSink) { 312 | // return 313 | // } 314 | // if (plasmoid.configuration.moveAllAppsOnSetDefault) { 315 | // // console.log(appsModel, appsModel.count) 316 | // for (var i = 0; i < appsModel.count; i++) { 317 | // var stream = appsModel.get(i) 318 | // stream = stream.PulseObject 319 | // // console.log(i, stream, stream.name, stream.deviceIndex, sinkModel.defaultSink.index) 320 | // stream.deviceIndex = sinkModel.defaultSink.index 321 | // } 322 | // } 323 | // } 324 | // } 325 | 326 | PlasmaVolume.GlobalActionCollection { 327 | // KGlobalAccel cannot transition from kmix to something else, so if 328 | // the user had a custom shortcut set for kmix those would get lost. 329 | // To avoid this we hijack kmix name and actions. Entirely mental but 330 | // best we can do to not cause annoyance for the user. 331 | // The display name actually is updated to whatever registered last 332 | // though, so as far as user visible strings go we should be fine. 333 | // As of 2015-07-21: 334 | // componentName: kmix 335 | // actions: increase_volume, decrease_volume, mute 336 | name: "kmix" 337 | displayName: main.displayName 338 | PlasmaVolume.GlobalAction { 339 | objectName: "increase_volume" 340 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Increase Volume") 341 | shortcut: Qt.Key_VolumeUp 342 | onTriggered: increaseDefaultSinkVolume() 343 | } 344 | PlasmaVolume.GlobalAction { 345 | objectName: "decrease_volume" 346 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Decrease Volume") 347 | shortcut: Qt.Key_VolumeDown 348 | onTriggered: decreaseDefaultSinkVolume() 349 | } 350 | PlasmaVolume.GlobalAction { 351 | objectName: "mute" 352 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Mute") 353 | shortcut: Qt.Key_VolumeMute 354 | onTriggered: toggleDefaultSinksMute() 355 | } 356 | PlasmaVolume.GlobalAction { 357 | objectName: "increase_microphone_volume" 358 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Increase Microphone Volume") 359 | shortcut: Qt.Key_MicVolumeUp 360 | onTriggered: increaseDefaultSourceVolume() 361 | } 362 | PlasmaVolume.GlobalAction { 363 | objectName: "decrease_microphone_volume" 364 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Decrease Microphone Volume") 365 | shortcut: Qt.Key_MicVolumeDown 366 | onTriggered: decreaseDefaultSourceVolume() 367 | } 368 | PlasmaVolume.GlobalAction { 369 | objectName: "mic_mute" 370 | text: i18nd("plasma_applet_org.kde.plasma.volume", "Mute Microphone") 371 | shortcut: Qt.Key_MicMute 372 | onTriggered: toggleDefaultSourceMute() 373 | } 374 | } 375 | 376 | ExecUtil { 377 | id: executable 378 | } 379 | 380 | PlasmaVolume.VolumeOSD { 381 | id: osd 382 | 383 | function showVolume(volume) { 384 | if (plasmoid.configuration.showOsd) { 385 | var volPercent = PulseObjectCommands.volumePercent(volume) 386 | try { 387 | // Plasma 5.19 and below 388 | osd.show(volPercent) 389 | } catch (e) { // invalid number of arguments 390 | // Plasma 5.20 391 | var maxPercent = volPercent > 100 ? 150 : 100 392 | osd.show(volPercent, maxPercent) 393 | } 394 | } 395 | } 396 | 397 | function showMicVolume(volume) { 398 | if (plasmoid.configuration.showOsd) { 399 | var volPercent = PulseObjectCommands.volumePercent(volume) 400 | osd.showMicrophone(volPercent) 401 | } 402 | } 403 | } 404 | 405 | PlasmaVolume.VolumeFeedback { 406 | id: feedback 407 | } 408 | 409 | function playFeedback(sinkIndex) { 410 | if (!plasmoid.configuration.volumeChangeFeedback) { 411 | return 412 | } 413 | if (sinkIndex == undefined) { 414 | sinkIndex = sinkModel.defaultSink.index 415 | } 416 | feedback.play(sinkIndex) 417 | } 418 | 419 | Mpris2DataSource { 420 | id: mpris2Source 421 | } 422 | 423 | // https://github.com/KDE/plasma-pa/tree/master/src/kcm/package/contents/ui 424 | DynamicFilterModel { 425 | id: appsModel 426 | sourceModel: PlasmaVolume.SinkInputModel {} 427 | } 428 | DynamicFilterModel { 429 | id: appOutputsModel 430 | sourceModel: PlasmaVolume.SourceOutputModel {} 431 | } 432 | DynamicFilterModel { 433 | id: filteredSourceModel 434 | sourceModel: PlasmaVolume.SourceModel { 435 | id: sourceModel 436 | } 437 | } 438 | DynamicFilterModel { 439 | id: filteredSinkModel 440 | sourceModel: PlasmaVolume.SinkModel { 441 | id: sinkModel 442 | } 443 | } 444 | // DynamicFilterModel { 445 | // id: filteredStreamRestoreModel 446 | // sourceModel: PlasmaVolume.StreamRestoreModel { 447 | // id: streamRestoreModel 448 | // } 449 | // } 450 | DynamicFilterModel { 451 | id: filteredCardModel 452 | sourceModel: PlasmaVolume.CardModel { 453 | id: cardModel 454 | } 455 | } 456 | function findStream(model, predicate) { 457 | for (var i = 0; i < model.count; i++) { 458 | var stream = model.get(i) 459 | stream = stream.PulseObject 460 | // console.log(i, stream, predicate(stream, i)) 461 | if (predicate(stream, i)) { 462 | return i 463 | } 464 | } 465 | return -1 466 | } 467 | function getStream(model, predicate) { 468 | for (var i = 0; i < model.count; i++) { 469 | var stream = model.get(i) 470 | stream = stream.PulseObject 471 | // console.log(i, stream, predicate(stream, i)) 472 | if (predicate(stream, i)) { 473 | return stream 474 | } 475 | } 476 | return null 477 | } 478 | 479 | function action_alsamixer() { 480 | executable.exec("konsole -e alsamixer") 481 | } 482 | 483 | function action_pavucontrol() { 484 | executable.exec("pavucontrol") 485 | } 486 | 487 | Component.onCompleted: { 488 | if (plasmoid.hasOwnProperty("activationTogglesExpanded")) { 489 | plasmoid.activationTogglesExpanded = true 490 | } 491 | 492 | plasmoid.setAction("pavucontrol", i18n("PulseAudio Control"), "configure") 493 | plasmoid.setAction("alsamixer", i18n("AlsaMixer"), "configure") 494 | 495 | var widgetName = i18nd("plasma_applet_org.kde.plasma.volume", "Audio Volume") 496 | var configureText = i18ndc("libplasma5", "%1 is the name of the applet", "%1 Settings...", widgetName) // plasma-framework 497 | plasmoid.setAction("configure", configureText, "configure") 498 | 499 | // plasmoid.action("configure").trigger() 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /package/metadata.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Audio Volume (Win7 Mixer) 3 | Comment=Adjust the volume of devices and applications 4 | 5 | Icon=org.kde.plasma.volume 6 | Type=Service 7 | X-KDE-ServiceTypes=Plasma/Applet 8 | 9 | X-Plasma-API=declarativeappletscript 10 | X-Plasma-MainScript=ui/main.qml 11 | 12 | X-Plasma-ConfigPlugins=kcm_pulseaudio 13 | 14 | X-KDE-PluginInfo-Name=org.kde.plasma.volumewin7mixer 15 | X-KDE-PluginInfo-Category=Multimedia 16 | X-KDE-PluginInfo-Author=Harald Sitter + Chris Holland 17 | X-KDE-PluginInfo-Email=zrenfire@gmail.com 18 | X-KDE-PluginInfo-Version=26 19 | X-KDE-PluginInfo-Website=https://github.com/Zren/plasma-applet-volumewin7mixer 20 | X-KDE-PluginInfo-KdeStoreId=1100894 21 | X-KDE-PluginInfo-License=GPL 22 | 23 | X-Plasma-NotificationArea=true 24 | X-Plasma-NotificationAreaCategory=Hardware 25 | X-KDE-PluginInfo-EnabledByDefault=true 26 | 27 | Name[fr]=Volume audio (Win7 Mixer) 28 | Name[nl]=Geluidsvolume (Win7 Mixer) 29 | Comment[fr]=Ajuster le volume des périphériques et des applications 30 | Comment[nl]=Geluidsniveau van apparaten en toepassingen aanpassen 31 | -------------------------------------------------------------------------------- /package/translate/ReadMe.md: -------------------------------------------------------------------------------- 1 | > Version 7 of Zren's i18n scripts. 2 | 3 | With KDE Frameworks v5.37 and above, translations are bundled with the `*.plasmoid` file downloaded from the store. 4 | 5 | ## Install Translations 6 | 7 | Go to `~/.local/share/plasma/plasmoids/org.kde.plasma.volumewin7mixer/translate/` and run `sh ./build --restartplasma`. 8 | 9 | ## New Translations 10 | 11 | 1. Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/Zren/plasma-applet-volumewin7mixer/issues/new), name the file `spanish.txt`, attach the txt file to the issue (drag and drop). 12 | 13 | Or if you know how to make a pull request 14 | 15 | 1. Copy the `template.pot` file and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`. 16 | 17 | ## Scripts 18 | 19 | * `sh ./merge` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files. 20 | * `sh ./build` will convert the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...` which will bundle the translations in the `*.plasmoid` without needing the user to manually install them. 21 | * `sh ./plasmoidlocaletest` will run `./build` then `plasmoidviewer` (part of `plasma-sdk`). 22 | 23 | ## Links 24 | 25 | * https://zren.github.io/kde/docs/widget/#translations-i18n 26 | * https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems 27 | * https://api.kde.org/frameworks/ki18n/html/prg_guide.html 28 | 29 | ## Examples 30 | 31 | * https://l10n.kde.org/stats/gui/trunk-kf5/team/fr/plasma-desktop/ 32 | * https://github.com/psifidotos/nowdock-plasmoid/tree/master/po 33 | * https://github.com/kotelnik/plasma-applet-redshift-control/tree/master/translations 34 | 35 | ## Status 36 | | Locale | Lines | % Done| 37 | |----------|---------|-------| 38 | | Template | 64 | | 39 | | fr | 61/64 | 95% | 40 | | nl | 61/64 | 95% | 41 | -------------------------------------------------------------------------------- /package/translate/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Version: 6 3 | 4 | # This script will convert the *.po files to *.mo files, rebuilding the package/contents/locale folder. 5 | # Feature discussion: https://phabricator.kde.org/D5209 6 | # Eg: contents/locale/fr_CA/LC_MESSAGES/plasma_applet_org.kde.plasma.eventcalendar.mo 7 | 8 | DIR=`cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd` 9 | plasmoidName=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 10 | website=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Website"` 11 | bugAddress="$website" 12 | packageRoot=".." # Root of translatable sources 13 | projectName="plasma_applet_${plasmoidName}" # project name 14 | 15 | #--- 16 | if [ -z "$plasmoidName" ]; then 17 | echo "[build] Error: Couldn't read plasmoidName." 18 | exit 19 | fi 20 | 21 | if [ -z "$(which msgfmt)" ]; then 22 | echo "[build] Error: msgfmt command not found. Need to install gettext" 23 | echo "[build] Running 'sudo apt install gettext'" 24 | sudo apt install gettext 25 | echo "[build] gettext installation should be finished. Going back to installing translations." 26 | fi 27 | 28 | #--- 29 | echo "[build] Compiling messages" 30 | 31 | catalogs=`find . -name '*.po' | sort` 32 | for cat in $catalogs; do 33 | echo "$cat" 34 | catLocale=`basename ${cat%.*}` 35 | msgfmt -o "${catLocale}.mo" "$cat" 36 | 37 | installPath="$DIR/../contents/locale/${catLocale}/LC_MESSAGES/${projectName}.mo" 38 | 39 | echo "[build] Install to ${installPath}" 40 | mkdir -p "$(dirname "$installPath")" 41 | mv "${catLocale}.mo" "${installPath}" 42 | done 43 | 44 | echo "[build] Done building messages" 45 | 46 | if [ "$1" = "--restartplasma" ]; then 47 | echo "[build] Restarting plasmashell" 48 | killall plasmashell 49 | kstart5 plasmashell 50 | echo "[build] Done restarting plasmashell" 51 | else 52 | echo "[build] (re)install the plasmoid and restart plasmashell to test." 53 | fi 54 | -------------------------------------------------------------------------------- /package/translate/fr.po: -------------------------------------------------------------------------------- 1 | # Translation of volumewin7mixer in fr 2 | # Copyright (C) 2019 3 | # This file is distributed under the same license as the volumewin7mixer package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: volumewin7mixer\n" 10 | "Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-volumewin7mixer\n" 11 | "POT-Creation-Date: 2020-10-23 20:12-0400\n" 12 | "PO-Revision-Date: 2019-01-10 00:00+0100\n" 13 | "Last-Translator: ROMAIN VALEYE \n" 14 | "Language-Team: French \n" 15 | "Language: fr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: ../metadata.desktop 22 | msgid "Audio Volume (Win7 Mixer)" 23 | msgstr "Volume audio (Win7 Mixer)" 24 | 25 | #: ../metadata.desktop 26 | msgid "Adjust the volume of devices and applications" 27 | msgstr "Ajuster le volume des périphériques et des applications" 28 | 29 | #: ../contents/ui/config/ConfigApplet.qml 30 | msgid "Media Keys" 31 | msgstr "Touches de média" 32 | 33 | #: ../contents/ui/config/ConfigApplet.qml 34 | msgid "Volume Up/Down Steps:" 35 | msgstr "Pas de volume:" 36 | 37 | #: ../contents/ui/config/ConfigApplet.qml 38 | msgid "One step = %1%" 39 | msgstr "Un pas = %1%" 40 | 41 | #: ../contents/ui/config/ConfigApplet.qml 42 | msgid "Mixer" 43 | msgstr "Mixeur" 44 | 45 | #: ../contents/ui/config/ConfigApplet.qml 46 | msgid "Show Ticks every 10%" 47 | msgstr "Afficher les marques tous les 10%" 48 | 49 | #: ../contents/ui/config/ConfigApplet.qml 50 | msgid "Volume Boost" 51 | msgstr "Boost de volume" 52 | 53 | #: ../contents/ui/config/ConfigApplet.qml 54 | msgid "Volume Slider Theme" 55 | msgstr "Thème du curseur de volume" 56 | 57 | #: ../contents/ui/config/ConfigApplet.qml 58 | msgid "Desktop Theme (%1)" 59 | msgstr "Thème de bureau (%1)" 60 | 61 | #: ../contents/ui/config/ConfigApplet.qml 62 | msgid "Color Theme (Default Look)" 63 | msgstr "Thème de couleur (Look par défaut)" 64 | 65 | #: ../contents/ui/config/ConfigApplet.qml 66 | msgid "Light Blue on Grey (Default Look)" 67 | msgstr "Bleu ciel sur gris (Look par défaut)" 68 | 69 | #: ../contents/ui/config/ConfigApplet.qml 70 | msgid "Options" 71 | msgstr "Options" 72 | 73 | #: ../contents/ui/config/ConfigApplet.qml 74 | msgid "Move all Apps to device when setting default device (when set in with the context menu)" 75 | msgstr "Déplacer les applications vers le nouveau périphérique par défaut (quand sélectionné depuis le menu contextuel)" 76 | 77 | #: ../contents/ui/config/ConfigApplet.qml 78 | msgid "Close the popup after setting a default device" 79 | msgstr "Fermer la popup après avoir choisi un périphérique par défaut." 80 | 81 | #: ../contents/ui/config/ConfigApplet.qml 82 | msgid "Set default device after clicking a speaker/mic icon" 83 | msgstr "" 84 | 85 | #: ../contents/ui/config/ConfigApplet.qml 86 | msgid "Show OSD on when changing the volume." 87 | msgstr "Afficher l'OSD lors du changement de volume." 88 | 89 | #: ../contents/ui/config/ConfigApplet.qml 90 | msgid "Volume Feedback: Play popping noise when changing the volume." 91 | msgstr "Retour audio: Jouer un bruit lors du changement de volume." 92 | 93 | #: ../contents/ui/config/ConfigApplet.qml 94 | msgid "Visual Feedback: Visualize current sound." 95 | msgstr "Retour visuel: Visualiser le son en cours." 96 | 97 | #: ../contents/ui/config/ConfigApplet.qml 98 | msgid "Show virtual streams." 99 | msgstr "Afficher les flux virtuels." 100 | 101 | #: ../contents/ui/config/ConfigApplet.qml 102 | msgid "Media Controller" 103 | msgstr "Contrôleur de média" 104 | 105 | #: ../contents/ui/config/ConfigApplet.qml 106 | msgid "Show Media Controller" 107 | msgstr "Afficher le contrôleur de média" 108 | 109 | #: ../contents/ui/config/ConfigApplet.qml 110 | msgid "Position" 111 | msgstr "Position" 112 | 113 | #: ../contents/ui/config/ConfigApplet.qml 114 | msgid "Top" 115 | msgstr "Haut" 116 | 117 | #: ../contents/ui/config/ConfigApplet.qml 118 | msgid "Bottom" 119 | msgstr "Bas" 120 | 121 | #: ../contents/ui/config/ConfigApplet.qml 122 | msgid "Show Time Elapsed" 123 | msgstr "Afficher le temps passé" 124 | 125 | #: ../contents/ui/config/ConfigApplet.qml 126 | msgid "Show Time Left" 127 | msgstr "Afficher le temps restant" 128 | 129 | #: ../contents/ui/config/ConfigApplet.qml 130 | msgid "Show Total Duration" 131 | msgstr "Afficher la durée totale" 132 | 133 | #: ../contents/ui/config/ConfigApplet.qml 134 | msgid "Keyboard Shortcuts" 135 | msgstr "Raccourcis clavier" 136 | 137 | #: ../contents/ui/config/ConfigApplet.qml 138 | msgid "Set the Global Shortcut in the Keyboard Shortcuts tab." 139 | msgstr "Définit le raccourci clavier global dans l'onglet des raccourcis clavier." 140 | 141 | #: ../contents/ui/config/ConfigApplet.qml 142 | msgid "Global Shortcut" 143 | msgstr "Raccourci clavier global" 144 | 145 | #: ../contents/ui/config/ConfigApplet.qml 146 | msgid "Selection: Select Previous Stream" 147 | msgstr "Sélection: Sélectionner le flux précédent" 148 | 149 | #: ../contents/ui/config/ConfigApplet.qml 150 | msgid "Selection: Select Next Stream" 151 | msgstr "Sélection: Sélectionner le flux suivant" 152 | 153 | #: ../contents/ui/config/ConfigApplet.qml 154 | msgid "Selection: Increase Volume" 155 | msgstr "Sélection: Augmenter le volume" 156 | 157 | #: ../contents/ui/config/ConfigApplet.qml 158 | msgid "Selection: Decrease Volume" 159 | msgstr "Sélection: Baisser le volume" 160 | 161 | #: ../contents/ui/config/ConfigApplet.qml 162 | msgid "Selection: Make Default Device" 163 | msgstr "Sélection: Définir comme périphérique par défaut" 164 | 165 | #: ../contents/ui/config/ConfigApplet.qml 166 | msgid "Selection: Toggle Mute" 167 | msgstr "Sélection: Permuter le mode muet" 168 | 169 | #: ../contents/ui/config/ConfigApplet.qml 170 | msgid "Selection: Open Context Menu" 171 | msgstr "Sélection: Ouvrir le menu contextuel" 172 | 173 | #: ../contents/ui/config/ConfigApplet.qml 174 | msgid "Selection: Set Volume to %1%" 175 | msgstr "Sélection: Mettre le volume à %1%" 176 | 177 | #: ../contents/ui/lib/AppletVersion.qml 178 | msgid "Version: %1" 179 | msgstr "Version: %1" 180 | 181 | #: ../contents/ui/main.qml 182 | msgid "Recording Apps" 183 | msgstr "Applications d'enregistrement" 184 | 185 | #: ../contents/ui/main.qml 186 | msgid "Apps" 187 | msgstr "Applications" 188 | 189 | #: ../contents/ui/main.qml 190 | msgid "Mics" 191 | msgstr "Microphones" 192 | 193 | #: ../contents/ui/main.qml 194 | msgid "Speakers" 195 | msgstr "Hauts-parleurs" 196 | 197 | #: ../contents/ui/main.qml 198 | msgid "PulseAudio Control" 199 | msgstr "Contrôleur PulseAudio" 200 | 201 | #: ../contents/ui/main.qml 202 | msgid "AlsaMixer" 203 | msgstr "AlsaMixer" 204 | 205 | #: ../contents/ui/MediaController.qml 206 | msgctxt "Remaining time for song e.g -5:42" 207 | msgid "-%1" 208 | msgstr "-%1" 209 | 210 | #: ../contents/ui/MixerItem.qml 211 | msgid "%1 (Echo Cancelled)" 212 | msgstr "%1 (Écho Annulé)" 213 | 214 | #: ../contents/ui/MixerItem.qml 215 | msgid "Mic" 216 | msgstr "Microphone" 217 | 218 | #: ../contents/ui/MixerItem.qml 219 | msgid "Speaker" 220 | msgstr "Haut-Parleur" 221 | 222 | #: ../contents/ui/MixerItem.qml 223 | msgid "HDMI" 224 | msgstr "HDMI" 225 | 226 | #: ../contents/ui/MixerItem.qml 227 | msgid "Name" 228 | msgstr "Nom" 229 | 230 | #: ../contents/ui/MixerItem.qml 231 | msgid "Description" 232 | msgstr "Description" 233 | 234 | #: ../contents/ui/MixerItem.qml 235 | msgid "Volume" 236 | msgstr "Volume" 237 | 238 | #: ../contents/ui/MixerItem.qml 239 | msgid "Port" 240 | msgstr "Port" 241 | 242 | #: ../contents/ui/MixerItem.qml 243 | msgid "Device" 244 | msgstr "Périphérique" 245 | 246 | #: ../contents/ui/MixerItem.qml 247 | msgid "Is default device" 248 | msgstr "" 249 | 250 | #: ../contents/ui/MixerItem.qml 251 | msgid "Make default device" 252 | msgstr "" 253 | 254 | #: ../contents/ui/MixerItem.qml 255 | msgid "Volume Boost (150% Volume)" 256 | msgstr "Boost de volume (150%)" 257 | 258 | #: ../contents/ui/MixerItem.qml 259 | msgid "Show Channels" 260 | msgstr "Afficher les canaux" 261 | 262 | #: ../contents/ui/MixerItem.qml 263 | msgid "Profile" 264 | msgstr "Profil" 265 | 266 | #: ../contents/ui/MixerItem.qml 267 | msgid "Echo Cancellation" 268 | msgstr "Annulation de l'écho" 269 | 270 | #: ../contents/ui/MixerItem.qml 271 | msgid "Listen to Device" 272 | msgstr "Écouter le périphérique" 273 | 274 | #: ../contents/ui/MixerItem.qml 275 | msgid "Properties" 276 | msgstr "Propriétés" 277 | -------------------------------------------------------------------------------- /package/translate/merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Version: 17 3 | 4 | # https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems 5 | # Based on: https://github.com/psifidotos/nowdock-plasmoid/blob/master/po/Messages.sh 6 | 7 | DIR=`cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd` 8 | plasmoidName=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 9 | widgetName="${plasmoidName##*.}" # Strip namespace 10 | website=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Website"` 11 | bugAddress="$website" 12 | packageRoot=".." # Root of translatable sources 13 | projectName="plasma_applet_${plasmoidName}" # project name 14 | 15 | #--- 16 | if [ -z "$plasmoidName" ]; then 17 | echo "[merge] Error: Couldn't read plasmoidName." 18 | exit 19 | fi 20 | 21 | if [ -z "$(which xgettext)" ]; then 22 | echo "[merge] Error: xgettext command not found. Need to install gettext" 23 | echo "[merge] Running 'sudo apt install gettext'" 24 | sudo apt install gettext 25 | echo "[merge] gettext installation should be finished. Going back to merging translations." 26 | fi 27 | 28 | #--- 29 | echo "[merge] Extracting messages" 30 | potArgs="--from-code=UTF-8 --width=200 --add-location=file" 31 | 32 | find "${packageRoot}" -name '*.desktop' | sort > "${DIR}/infiles.list" 33 | xgettext \ 34 | ${potArgs} \ 35 | --files-from="${DIR}/infiles.list" \ 36 | --language=Desktop \ 37 | -D "${packageRoot}" \ 38 | -D "${DIR}" \ 39 | -o "template.pot.new" \ 40 | || \ 41 | { echo "[merge] error while calling xgettext. aborting."; exit 1; } 42 | 43 | sed -i 's/"Content-Type: text\/plain; charset=CHARSET\\n"/"Content-Type: text\/plain; charset=UTF-8\\n"/' "template.pot.new" 44 | 45 | find "${packageRoot}" -name '*.cpp' -o -name '*.h' -o -name '*.c' -o -name '*.qml' -o -name '*.js' | sort > "${DIR}/infiles.list" 46 | xgettext \ 47 | ${potArgs} \ 48 | --files-from="${DIR}/infiles.list" \ 49 | -C -kde -ci18n -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -ktr2i18n:1 -kI18N_NOOP:1 \ 50 | -kI18N_NOOP2:1c,2 -kN_:1 -kaliasLocale -kki18n:1 -kki18nc:1c,2 -kki18np:1,2 -kki18ncp:1c,2,3 \ 51 | --package-name="${widgetName}" \ 52 | --msgid-bugs-address="${bugAddress}" \ 53 | -D "${packageRoot}" \ 54 | -D "${DIR}" \ 55 | --join-existing \ 56 | -o "template.pot.new" \ 57 | || \ 58 | { echo "[merge] error while calling xgettext. aborting."; exit 1; } 59 | 60 | sed -i 's/# SOME DESCRIPTIVE TITLE./'"# Translation of ${widgetName} in LANGUAGE"'/' "template.pot.new" 61 | sed -i 's/# Copyright (C) YEAR THE PACKAGE'"'"'S COPYRIGHT HOLDER/'"# Copyright (C) $(date +%Y)"'/' "template.pot.new" 62 | 63 | if [ -f "template.pot" ]; then 64 | newPotDate=`grep "POT-Creation-Date:" template.pot.new | sed 's/.\{3\}$//'` 65 | oldPotDate=`grep "POT-Creation-Date:" template.pot | sed 's/.\{3\}$//'` 66 | sed -i 's/'"${newPotDate}"'/'"${oldPotDate}"'/' "template.pot.new" 67 | changes=`diff "template.pot" "template.pot.new"` 68 | if [ ! -z "$changes" ]; then 69 | # There's been changes 70 | sed -i 's/'"${oldPotDate}"'/'"${newPotDate}"'/' "template.pot.new" 71 | mv "template.pot.new" "template.pot" 72 | 73 | addedKeys=`echo "$changes" | grep "> msgid" | cut -c 9- | sort` 74 | removedKeys=`echo "$changes" | grep "< msgid" | cut -c 9- | sort` 75 | echo "" 76 | echo "Added Keys:" 77 | echo "$addedKeys" 78 | echo "" 79 | echo "Removed Keys:" 80 | echo "$removedKeys" 81 | echo "" 82 | 83 | else 84 | # No changes 85 | rm "template.pot.new" 86 | fi 87 | else 88 | # template.pot didn't already exist 89 | mv "template.pot.new" "template.pot" 90 | fi 91 | 92 | potMessageCount=`expr $(grep -Pzo 'msgstr ""\n(\n|$)' "template.pot" | grep -c 'msgstr ""')` 93 | echo "| Locale | Lines | % Done|" > "./Status.md" 94 | echo "|----------|---------|-------|" >> "./Status.md" 95 | entryFormat="| %-8s | %7s | %5s |" 96 | templateLine=`perl -e "printf(\"$entryFormat\", \"Template\", \"${potMessageCount}\", \"\")"` 97 | echo "$templateLine" >> "./Status.md" 98 | 99 | rm "${DIR}/infiles.list" 100 | echo "[merge] Done extracting messages" 101 | 102 | #--- 103 | echo "[merge] Merging messages" 104 | catalogs=`find . -name '*.po' | sort` 105 | for cat in $catalogs; do 106 | echo "[merge] $cat" 107 | catLocale=`basename ${cat%.*}` 108 | 109 | widthArg="" 110 | catUsesGenerator=`grep "X-Generator:" "$cat"` 111 | if [ -z "$catUsesGenerator" ]; then 112 | widthArg="--width=400" 113 | fi 114 | 115 | cp "$cat" "$cat.new" 116 | sed -i 's/"Content-Type: text\/plain; charset=CHARSET\\n"/"Content-Type: text\/plain; charset=UTF-8\\n"/' "$cat.new" 117 | 118 | msgmerge \ 119 | ${widthArg} \ 120 | --add-location=file \ 121 | --no-fuzzy-matching \ 122 | -o "$cat.new" \ 123 | "$cat.new" "${DIR}/template.pot" 124 | 125 | sed -i 's/# SOME DESCRIPTIVE TITLE./'"# Translation of ${widgetName} in ${catLocale}"'/' "$cat.new" 126 | sed -i 's/# Translation of '"${widgetName}"' in LANGUAGE/'"# Translation of ${widgetName} in ${catLocale}"'/' "$cat.new" 127 | sed -i 's/# Copyright (C) YEAR THE PACKAGE'"'"'S COPYRIGHT HOLDER/'"# Copyright (C) $(date +%Y)"'/' "$cat.new" 128 | 129 | poEmptyMessageCount=`expr $(grep -Pzo 'msgstr ""\n(\n|$)' "$cat.new" | grep -c 'msgstr ""')` 130 | poMessagesDoneCount=`expr $potMessageCount - $poEmptyMessageCount` 131 | poCompletion=`perl -e "printf(\"%d\", $poMessagesDoneCount * 100 / $potMessageCount)"` 132 | poLine=`perl -e "printf(\"$entryFormat\", \"$catLocale\", \"${poMessagesDoneCount}/${potMessageCount}\", \"${poCompletion}%\")"` 133 | echo "$poLine" >> "./Status.md" 134 | 135 | # mv "$cat" "$cat.old" 136 | mv "$cat.new" "$cat" 137 | done 138 | echo "[merge] Done merging messages" 139 | 140 | #--- 141 | echo "[merge] Updating .desktop file" 142 | 143 | # Generate LINGUAS for msgfmt 144 | if [ -f "$DIR/LINGUAS" ]; then 145 | rm "$DIR/LINGUAS" 146 | fi 147 | for cat in $catalogs; do 148 | catLocale=`basename ${cat%.*}` 149 | echo "${catLocale}" >> "$DIR/LINGUAS" 150 | done 151 | 152 | cp -f "$DIR/../metadata.desktop" "$DIR/template.desktop" 153 | sed -i '/^Name\[/ d; /^GenericName\[/ d; /^Comment\[/ d; /^Keywords\[/ d' "$DIR/template.desktop" 154 | 155 | msgfmt \ 156 | --desktop \ 157 | --template="$DIR/template.desktop" \ 158 | -d "$DIR/" \ 159 | -o "$DIR/new.desktop" 160 | 161 | # Delete empty msgid messages that used the po header 162 | if [ ! -z "$(grep '^Name=$' "$DIR/new.desktop")" ]; then 163 | echo "[merge] Name in metadata.desktop is empty!" 164 | sed -i '/^Name\[/ d' "$DIR/new.desktop" 165 | fi 166 | if [ ! -z "$(grep '^GenericName=$' "$DIR/new.desktop")" ]; then 167 | echo "[merge] GenericName in metadata.desktop is empty!" 168 | sed -i '/^GenericName\[/ d' "$DIR/new.desktop" 169 | fi 170 | if [ ! -z "$(grep '^Comment=$' "$DIR/new.desktop")" ]; then 171 | echo "[merge] Comment in metadata.desktop is empty!" 172 | sed -i '/^Comment\[/ d' "$DIR/new.desktop" 173 | fi 174 | if [ ! -z "$(grep '^Keywords=$' "$DIR/new.desktop")" ]; then 175 | echo "[merge] Keywords in metadata.desktop is empty!" 176 | sed -i '/^Keywords\[/ d' "$DIR/new.desktop" 177 | fi 178 | 179 | # Place translations at the bottom of the desktop file. 180 | translatedLines=`cat "$DIR/new.desktop" | grep "]="` 181 | if [ ! -z "${translatedLines}" ]; then 182 | sed -i '/^Name\[/ d; /^GenericName\[/ d; /^Comment\[/ d; /^Keywords\[/ d' "$DIR/new.desktop" 183 | if [ "$(tail -c 2 "$DIR/new.desktop" | wc -l)" != "2" ]; then 184 | # Does not end with 2 empty lines, so add an empty line. 185 | echo "" >> "$DIR/new.desktop" 186 | fi 187 | echo "${translatedLines}" >> "$DIR/new.desktop" 188 | fi 189 | 190 | # Cleanup 191 | mv "$DIR/new.desktop" "$DIR/../metadata.desktop" 192 | rm "$DIR/template.desktop" 193 | rm "$DIR/LINGUAS" 194 | 195 | #--- 196 | # Populate ReadMe.md 197 | echo "[merge] Updating translate/ReadMe.md" 198 | sed -i -E 's`share\/plasma\/plasmoids\/(.+)\/translate`share/plasma/plasmoids/'"${plasmoidName}"'/translate`' ./ReadMe.md 199 | if [[ "$website" == *"github.com"* ]]; then 200 | sed -i -E 's`\[new issue\]\(https:\/\/github\.com\/(.+)\/(.+)\/issues\/new\)`[new issue]('"${website}"'/issues/new)`' ./ReadMe.md 201 | fi 202 | sed -i '/^|/ d' ./ReadMe.md # Remove status table from ReadMe 203 | cat ./Status.md >> ./ReadMe.md 204 | rm ./Status.md 205 | 206 | echo "[merge] Done" 207 | -------------------------------------------------------------------------------- /package/translate/nl.po: -------------------------------------------------------------------------------- 1 | # Translation of volumewin7mixer in nl 2 | # Copyright (C) 2019 3 | # This file is distributed under the same license as the volumewin7mixer package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: volumewin7mixer\n" 9 | "Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-volumewin7mixer\n" 10 | "POT-Creation-Date: 2020-10-23 20:12-0400\n" 11 | "PO-Revision-Date: 2019-01-25 14:39+0100\n" 12 | "Last-Translator: Heimen Stoffels \n" 13 | "Language-Team: Dutch Version: %1" 183 | msgstr "Versie: %1" 184 | 185 | #: ../contents/ui/main.qml 186 | msgid "Recording Apps" 187 | msgstr "Opname-applicaties" 188 | 189 | #: ../contents/ui/main.qml 190 | msgid "Apps" 191 | msgstr "Apps" 192 | 193 | #: ../contents/ui/main.qml 194 | msgid "Mics" 195 | msgstr "Microfoons" 196 | 197 | #: ../contents/ui/main.qml 198 | msgid "Speakers" 199 | msgstr "Luidsprekers" 200 | 201 | #: ../contents/ui/main.qml 202 | msgid "PulseAudio Control" 203 | msgstr "PulseAudio-bediening" 204 | 205 | #: ../contents/ui/main.qml 206 | msgid "AlsaMixer" 207 | msgstr "AlsaMixer" 208 | 209 | #: ../contents/ui/MediaController.qml 210 | msgctxt "Remaining time for song e.g -5:42" 211 | msgid "-%1" 212 | msgstr "-%1" 213 | 214 | #: ../contents/ui/MixerItem.qml 215 | msgid "%1 (Echo Cancelled)" 216 | msgstr "%1 (echo onderdrukt)" 217 | 218 | #: ../contents/ui/MixerItem.qml 219 | msgid "Mic" 220 | msgstr "Microfoon" 221 | 222 | #: ../contents/ui/MixerItem.qml 223 | msgid "Speaker" 224 | msgstr "Luidspreker" 225 | 226 | #: ../contents/ui/MixerItem.qml 227 | msgid "HDMI" 228 | msgstr "HDMI" 229 | 230 | #: ../contents/ui/MixerItem.qml 231 | msgid "Name" 232 | msgstr "Naam" 233 | 234 | #: ../contents/ui/MixerItem.qml 235 | msgid "Description" 236 | msgstr "Omschrijving" 237 | 238 | #: ../contents/ui/MixerItem.qml 239 | msgid "Volume" 240 | msgstr "Volume" 241 | 242 | #: ../contents/ui/MixerItem.qml 243 | msgid "Port" 244 | msgstr "Poort" 245 | 246 | #: ../contents/ui/MixerItem.qml 247 | msgid "Device" 248 | msgstr "Apparaat" 249 | 250 | #: ../contents/ui/MixerItem.qml 251 | msgid "Is default device" 252 | msgstr "" 253 | 254 | #: ../contents/ui/MixerItem.qml 255 | msgid "Make default device" 256 | msgstr "" 257 | 258 | #: ../contents/ui/MixerItem.qml 259 | msgid "Volume Boost (150% Volume)" 260 | msgstr "Volumeboost (150% volume)" 261 | 262 | #: ../contents/ui/MixerItem.qml 263 | msgid "Show Channels" 264 | msgstr "Kanalen tonen" 265 | 266 | #: ../contents/ui/MixerItem.qml 267 | msgid "Profile" 268 | msgstr "Profiel" 269 | 270 | #: ../contents/ui/MixerItem.qml 271 | msgid "Echo Cancellation" 272 | msgstr "Echo-onderdrukking" 273 | 274 | #: ../contents/ui/MixerItem.qml 275 | msgid "Listen to Device" 276 | msgstr "Luisteren naar apparaat" 277 | 278 | #: ../contents/ui/MixerItem.qml 279 | msgid "Properties" 280 | msgstr "Eigenschappen" 281 | -------------------------------------------------------------------------------- /package/translate/plasmoidlocaletest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 9 3 | # Requires plasmoidviewer v5.13.0 4 | 5 | function checkIfLangInstalled { 6 | if [ -x "$(command -v dpkg)" ]; then 7 | dpkg -l ${1} >/dev/null 2>&1 || ( \ 8 | echo -e "${1} not installed.\nInstalling now before continuing.\n" \ 9 | ; sudo apt install ${1} \ 10 | ) || ( \ 11 | echo -e "\nError trying to install ${1}\nPlease run 'sudo apt install ${1}'\n" \ 12 | ; exit 1 \ 13 | ) 14 | elif [ -x "$(command -v pacman)" ]; then 15 | # TODO: run `locale -a` and check if the locale is enabled. 16 | if false; then 17 | # https://wiki.archlinux.org/index.php/Locale 18 | # Uncomment the locale in /etc/locale.gen 19 | # Then run `locale-gen` 20 | echo -e "\nPlease install this locale in System Settings first.\n" 21 | exit 1 22 | else 23 | echo "" 24 | fi 25 | else 26 | echo -e "\nPackage manager not recognized. If the widget is not translated, please install the package '${1}'\n" 27 | fi 28 | } 29 | 30 | langInput="${1}" 31 | lang="" 32 | languagePack="" 33 | 34 | if [[ "$langInput" =~ ":" ]]; then # String contains a colon so assume it's a locale code. 35 | lang="${langInput}" 36 | IFS=: read -r l1 l2 <<< "${lang}" 37 | languagePack="language-pack-${l2}" 38 | fi 39 | 40 | # https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes/28357857#28357857 41 | declare -a langArr=( 42 | "af_ZA:af:Afrikaans (South Africa)" 43 | "ak_GH:ak:Akan (Ghana)" 44 | "am_ET:am:Amharic (Ethiopia)" 45 | "ar_EG:ar:Arabic (Egypt)" 46 | "as_IN:as:Assamese (India)" 47 | "az_AZ:az:Azerbaijani (Azerbaijan)" 48 | "be_BY:be:Belarusian (Belarus)" 49 | "bem_ZM:bem:Bemba (Zambia)" 50 | "bg_BG:bg:Bulgarian (Bulgaria)" 51 | "bo_IN:bo:Tibetan (India)" 52 | "bs_BA:bs:Bosnian (Bosnia and Herzegovina)" 53 | "ca_ES:ca:Catalan (Spain)" 54 | "chr_US:ch:Cherokee (United States)" 55 | "cs_CZ:cs:Czech (Czech Republic)" 56 | "cy_GB:cy:Welsh (United Kingdom)" 57 | "da_DK:da:Danish (Denmark)" 58 | "de_DE:de:German (Germany)" 59 | "el_GR:el:Greek (Greece)" 60 | "es_MX:es:Spanish (Mexico)" 61 | "et_EE:et:Estonian (Estonia)" 62 | "eu_ES:eu:Basque (Spain)" 63 | "fa_IR:fa:Persian (Iran)" 64 | "ff_SN:ff:Fulah (Senegal)" 65 | "fi_FI:fi:Finnish (Finland)" 66 | "fo_FO:fo:Faroese (Faroe Islands)" 67 | "fr_CA:fr:French (Canada)" 68 | "ga_IE:ga:Irish (Ireland)" 69 | "gl_ES:gl:Galician (Spain)" 70 | "gu_IN:gu:Gujarati (India)" 71 | "gv_GB:gv:Manx (United Kingdom)" 72 | "ha_NG:ha:Hausa (Nigeria)" 73 | "he_IL:he:Hebrew (Israel)" 74 | "hi_IN:hi:Hindi (India)" 75 | "hr_HR:hr:Croatian (Croatia)" 76 | "hu_HU:hu:Hungarian (Hungary)" 77 | "hy_AM:hy:Armenian (Armenia)" 78 | "id_ID:id:Indonesian (Indonesia)" 79 | "ig_NG:ig:Igbo (Nigeria)" 80 | "is_IS:is:Icelandic (Iceland)" 81 | "it_IT:it:Italian (Italy)" 82 | "ja_JP:ja:Japanese (Japan)" 83 | "ka_GE:ka:Georgian (Georgia)" 84 | "kk_KZ:kk:Kazakh (Kazakhstan)" 85 | "kl_GL:kl:Kalaallisut (Greenland)" 86 | "km_KH:km:Khmer (Cambodia)" 87 | "kn_IN:kn:Kannada (India)" 88 | "ko_KR:ko:Korean (South Korea)" 89 | "ko_KR:ko:Korean (South Korea)" 90 | "lg_UG:lg:Ganda (Uganda)" 91 | "lt_LT:lt:Lithuanian (Lithuania)" 92 | "lv_LV:lv:Latvian (Latvia)" 93 | "mg_MG:mg:Malagasy (Madagascar)" 94 | "mk_MK:mk:Macedonian (Macedonia)" 95 | "ml_IN:ml:Malayalam (India)" 96 | "mr_IN:mr:Marathi (India)" 97 | "ms_MY:ms:Malay (Malaysia)" 98 | "mt_MT:mt:Maltese (Malta)" 99 | "my_MM:my:Burmese (Myanmar [Burma])" 100 | "nb_NO:nb:Norwegian Bokmål (Norway)" 101 | "ne_NP:ne:Nepali (Nepal)" 102 | "nl_NL:nl:Dutch (Netherlands)" 103 | "nn_NO:nn:Norwegian Nynorsk (Norway)" 104 | "om_ET:om:Oromo (Ethiopia)" 105 | "or_IN:or:Oriya (India)" 106 | "pa_PK:pa:Punjabi (Pakistan)" 107 | "pl_PL:pl:Polish (Poland)" 108 | "ps_AF:ps:Pashto (Afghanistan)" 109 | "pt_BR:pt:Portuguese (Brazil)" 110 | "ro_RO:ro:Romanian (Romania)" 111 | "ru_RU:ru:Russian (Russia)" 112 | "rw_RW:rw:Kinyarwanda (Rwanda)" 113 | "si_LK:si:Sinhala (Sri Lanka)" 114 | "sk_SK:sk:Slovak (Slovakia)" 115 | "sl_SI:sl:Slovenian (Slovenia)" 116 | "so_SO:so:Somali (Somalia)" 117 | "sq_AL:sq:Albanian (Albania)" 118 | "sr_RS:sr:Serbian (Serbia)" 119 | "sv_SE:sv:Swedish (Sweden)" 120 | "sw_KE:sw:Swahili (Kenya)" 121 | "ta_IN:ta:Tamil (India)" 122 | "te_IN:te:Telugu (India)" 123 | "th_TH:th:Thai (Thailand)" 124 | "ti_ER:ti:Tigrinya (Eritrea)" 125 | "to_TO:to:Tonga (Tonga)" 126 | "tr_TR:tr:Turkish (Turkey)" 127 | "uk_UA:uk:Ukrainian (Ukraine)" 128 | "ur_IN:ur:Urdu (India)" 129 | "uz_UZ:uz:Uzbek (Uzbekistan)" 130 | "vi_VN:vi:Vietnamese (Vietnam)" 131 | "yo_NG:yo:Yoruba (Nigeria)" 132 | "yo_NG:yo:Yoruba (Nigeria)" 133 | "yue_HK:yu:Cantonese (Hong Kong)" 134 | "zh_CN:zh:Chinese (China)" 135 | "zu_ZA:zu:Zulu (South Africa)" 136 | ) 137 | 138 | for i in "${langArr[@]}"; do 139 | IFS=: read -r l1 l2 l3 <<< "$i" 140 | if [ "$langInput" == "$l2" ]; then 141 | lang="${l1}:${l2}" 142 | languagePack="language-pack-${l2}" 143 | fi 144 | done 145 | 146 | if [ -z "$lang" ]; then 147 | echo "plasmoidlocaletest doesn't recognize the language '$lang'" 148 | echo "Eg:" 149 | scriptcmd='sh ./plasmoidlocaletest' 150 | for i in "${langArr[@]}"; do 151 | IFS=: read -r l1 l2 l3 <<< "$i" 152 | echo " ${scriptcmd} ${l2} | ${l3}" 153 | done 154 | echo "" 155 | echo "Or use a the full locale code:" 156 | echo " ${scriptcmd} ar_EG:ar" 157 | exit 1 158 | fi 159 | 160 | IFS=: read -r l1 l2 <<< "${lang}" 161 | l1="${l1}.UTF-8" 162 | 163 | # Check if language is installed 164 | if [ ! -z "$languagePack" ]; then 165 | if [ "$lang" == "zh_CN:zh" ]; then languagePack="language-pack-zh-hans" 166 | fi 167 | 168 | checkIfLangInstalled "$languagePack" || exit 1 169 | fi 170 | 171 | 172 | echo "LANGUAGE=\"${lang}\"" 173 | echo "LANG=\"${l1}\"" 174 | 175 | scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 176 | packageDir="${scriptDir}/.." 177 | 178 | # Build local translations for plasmoidviewer 179 | sh "${scriptDir}/build" 180 | 181 | LANGUAGE="${lang}" LANG="${l1}" LC_TIME="${l1}" QML_DISABLE_DISK_CACHE=true plasmoidviewer -a "$packageDir" -l topedge -f horizontal -x 0 -y 0 182 | -------------------------------------------------------------------------------- /package/translate/template.pot: -------------------------------------------------------------------------------- 1 | # Translation of volumewin7mixer in LANGUAGE 2 | # Copyright (C) 2020 3 | # This file is distributed under the same license as the volumewin7mixer package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: volumewin7mixer\n" 10 | "Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-volumewin7mixer\n" 11 | "POT-Creation-Date: 2020-10-23 20:12-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../metadata.desktop 21 | msgid "Audio Volume (Win7 Mixer)" 22 | msgstr "" 23 | 24 | #: ../metadata.desktop 25 | msgid "Adjust the volume of devices and applications" 26 | msgstr "" 27 | 28 | #: ../contents/ui/config/ConfigApplet.qml 29 | msgid "Media Keys" 30 | msgstr "" 31 | 32 | #: ../contents/ui/config/ConfigApplet.qml 33 | msgid "Volume Up/Down Steps:" 34 | msgstr "" 35 | 36 | #: ../contents/ui/config/ConfigApplet.qml 37 | msgid "One step = %1%" 38 | msgstr "" 39 | 40 | #: ../contents/ui/config/ConfigApplet.qml 41 | msgid "Mixer" 42 | msgstr "" 43 | 44 | #: ../contents/ui/config/ConfigApplet.qml 45 | msgid "Show Ticks every 10%" 46 | msgstr "" 47 | 48 | #: ../contents/ui/config/ConfigApplet.qml 49 | msgid "Volume Boost" 50 | msgstr "" 51 | 52 | #: ../contents/ui/config/ConfigApplet.qml 53 | msgid "Volume Slider Theme" 54 | msgstr "" 55 | 56 | #: ../contents/ui/config/ConfigApplet.qml 57 | msgid "Desktop Theme (%1)" 58 | msgstr "" 59 | 60 | #: ../contents/ui/config/ConfigApplet.qml 61 | msgid "Color Theme (Default Look)" 62 | msgstr "" 63 | 64 | #: ../contents/ui/config/ConfigApplet.qml 65 | msgid "Light Blue on Grey (Default Look)" 66 | msgstr "" 67 | 68 | #: ../contents/ui/config/ConfigApplet.qml 69 | msgid "Options" 70 | msgstr "" 71 | 72 | #: ../contents/ui/config/ConfigApplet.qml 73 | msgid "Move all Apps to device when setting default device (when set in with the context menu)" 74 | msgstr "" 75 | 76 | #: ../contents/ui/config/ConfigApplet.qml 77 | msgid "Close the popup after setting a default device" 78 | msgstr "" 79 | 80 | #: ../contents/ui/config/ConfigApplet.qml 81 | msgid "Set default device after clicking a speaker/mic icon" 82 | msgstr "" 83 | 84 | #: ../contents/ui/config/ConfigApplet.qml 85 | msgid "Show OSD on when changing the volume." 86 | msgstr "" 87 | 88 | #: ../contents/ui/config/ConfigApplet.qml 89 | msgid "Volume Feedback: Play popping noise when changing the volume." 90 | msgstr "" 91 | 92 | #: ../contents/ui/config/ConfigApplet.qml 93 | msgid "Visual Feedback: Visualize current sound." 94 | msgstr "" 95 | 96 | #: ../contents/ui/config/ConfigApplet.qml 97 | msgid "Show virtual streams." 98 | msgstr "" 99 | 100 | #: ../contents/ui/config/ConfigApplet.qml 101 | msgid "Media Controller" 102 | msgstr "" 103 | 104 | #: ../contents/ui/config/ConfigApplet.qml 105 | msgid "Show Media Controller" 106 | msgstr "" 107 | 108 | #: ../contents/ui/config/ConfigApplet.qml 109 | msgid "Position" 110 | msgstr "" 111 | 112 | #: ../contents/ui/config/ConfigApplet.qml 113 | msgid "Top" 114 | msgstr "" 115 | 116 | #: ../contents/ui/config/ConfigApplet.qml 117 | msgid "Bottom" 118 | msgstr "" 119 | 120 | #: ../contents/ui/config/ConfigApplet.qml 121 | msgid "Show Time Elapsed" 122 | msgstr "" 123 | 124 | #: ../contents/ui/config/ConfigApplet.qml 125 | msgid "Show Time Left" 126 | msgstr "" 127 | 128 | #: ../contents/ui/config/ConfigApplet.qml 129 | msgid "Show Total Duration" 130 | msgstr "" 131 | 132 | #: ../contents/ui/config/ConfigApplet.qml 133 | msgid "Keyboard Shortcuts" 134 | msgstr "" 135 | 136 | #: ../contents/ui/config/ConfigApplet.qml 137 | msgid "Set the Global Shortcut in the Keyboard Shortcuts tab." 138 | msgstr "" 139 | 140 | #: ../contents/ui/config/ConfigApplet.qml 141 | msgid "Global Shortcut" 142 | msgstr "" 143 | 144 | #: ../contents/ui/config/ConfigApplet.qml 145 | msgid "Selection: Select Previous Stream" 146 | msgstr "" 147 | 148 | #: ../contents/ui/config/ConfigApplet.qml 149 | msgid "Selection: Select Next Stream" 150 | msgstr "" 151 | 152 | #: ../contents/ui/config/ConfigApplet.qml 153 | msgid "Selection: Increase Volume" 154 | msgstr "" 155 | 156 | #: ../contents/ui/config/ConfigApplet.qml 157 | msgid "Selection: Decrease Volume" 158 | msgstr "" 159 | 160 | #: ../contents/ui/config/ConfigApplet.qml 161 | msgid "Selection: Make Default Device" 162 | msgstr "" 163 | 164 | #: ../contents/ui/config/ConfigApplet.qml 165 | msgid "Selection: Toggle Mute" 166 | msgstr "" 167 | 168 | #: ../contents/ui/config/ConfigApplet.qml 169 | msgid "Selection: Open Context Menu" 170 | msgstr "" 171 | 172 | #: ../contents/ui/config/ConfigApplet.qml 173 | msgid "Selection: Set Volume to %1%" 174 | msgstr "" 175 | 176 | #: ../contents/ui/lib/AppletVersion.qml 177 | msgid "Version: %1" 178 | msgstr "" 179 | 180 | #: ../contents/ui/main.qml 181 | msgid "Recording Apps" 182 | msgstr "" 183 | 184 | #: ../contents/ui/main.qml 185 | msgid "Apps" 186 | msgstr "" 187 | 188 | #: ../contents/ui/main.qml 189 | msgid "Mics" 190 | msgstr "" 191 | 192 | #: ../contents/ui/main.qml 193 | msgid "Speakers" 194 | msgstr "" 195 | 196 | #: ../contents/ui/main.qml 197 | msgid "PulseAudio Control" 198 | msgstr "" 199 | 200 | #: ../contents/ui/main.qml 201 | msgid "AlsaMixer" 202 | msgstr "" 203 | 204 | #: ../contents/ui/MediaController.qml 205 | msgctxt "Remaining time for song e.g -5:42" 206 | msgid "-%1" 207 | msgstr "" 208 | 209 | #: ../contents/ui/MixerItem.qml 210 | msgid "%1 (Echo Cancelled)" 211 | msgstr "" 212 | 213 | #: ../contents/ui/MixerItem.qml 214 | msgid "Mic" 215 | msgstr "" 216 | 217 | #: ../contents/ui/MixerItem.qml 218 | msgid "Speaker" 219 | msgstr "" 220 | 221 | #: ../contents/ui/MixerItem.qml 222 | msgid "HDMI" 223 | msgstr "" 224 | 225 | #: ../contents/ui/MixerItem.qml 226 | msgid "Name" 227 | msgstr "" 228 | 229 | #: ../contents/ui/MixerItem.qml 230 | msgid "Description" 231 | msgstr "" 232 | 233 | #: ../contents/ui/MixerItem.qml 234 | msgid "Volume" 235 | msgstr "" 236 | 237 | #: ../contents/ui/MixerItem.qml 238 | msgid "Port" 239 | msgstr "" 240 | 241 | #: ../contents/ui/MixerItem.qml 242 | msgid "Device" 243 | msgstr "" 244 | 245 | #: ../contents/ui/MixerItem.qml 246 | msgid "Is default device" 247 | msgstr "" 248 | 249 | #: ../contents/ui/MixerItem.qml 250 | msgid "Make default device" 251 | msgstr "" 252 | 253 | #: ../contents/ui/MixerItem.qml 254 | msgid "Volume Boost (150% Volume)" 255 | msgstr "" 256 | 257 | #: ../contents/ui/MixerItem.qml 258 | msgid "Show Channels" 259 | msgstr "" 260 | 261 | #: ../contents/ui/MixerItem.qml 262 | msgid "Profile" 263 | msgstr "" 264 | 265 | #: ../contents/ui/MixerItem.qml 266 | msgid "Echo Cancellation" 267 | msgstr "" 268 | 269 | #: ../contents/ui/MixerItem.qml 270 | msgid "Listen to Device" 271 | msgstr "" 272 | 273 | #: ../contents/ui/MixerItem.qml 274 | msgid "Properties" 275 | msgstr "" 276 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | *.pyc 3 | -------------------------------------------------------------------------------- /plugin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) 2 | 3 | set(QT_MIN_VERSION "5.4.0") 4 | set(KF5_MIN_VERSION "5.0.0") 5 | 6 | find_package(ECM 0.0.11 REQUIRED NO_MODULE) 7 | set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) 8 | 9 | include(KDEInstallDirs) 10 | include(KDECMakeSettings) 11 | include(KDECompilerSettings) 12 | 13 | find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Quick) 14 | find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma) 15 | 16 | 17 | 18 | 19 | 20 | 21 | set(qml_SRCS 22 | qmldir 23 | ) 24 | 25 | set(cpp_SRCS 26 | plugin.cpp 27 | volumepeaks.cpp 28 | ) 29 | 30 | add_library(plasma-volumewin7mixer-declarative SHARED ${cpp_SRCS}) 31 | target_link_libraries(plasma-volumewin7mixer-declarative 32 | Qt5::Quick 33 | KF5::Plasma 34 | ) 35 | 36 | set(PRIVATE_QML_INSTALL_DIR ${QML_INSTALL_DIR}/org/kde/plasma/private/volumewin7mixer) 37 | install(TARGETS plasma-volumewin7mixer-declarative DESTINATION ${PRIVATE_QML_INSTALL_DIR}) 38 | install(FILES ${qml_SRCS} DESTINATION ${PRIVATE_QML_INSTALL_DIR}) 39 | -------------------------------------------------------------------------------- /plugin/ReadMe.md: -------------------------------------------------------------------------------- 1 | This optional plugin is not ready yet. **Do not package it.** 2 | 3 | The widget will work fine without the plugin, but if installed, will show the current sound level in the slider similar to Windows 7. Currently only shows the effect for "sinks" (aka the speakers). 4 | 5 | Run `sh ./install` to install dependencies and compile this optional plugin. Ideally, the config window will detect if the plugin is installed and disable unavailable settings. I plan having a button that will launch the install script to make it easier for users who install via the store. 6 | -------------------------------------------------------------------------------- /plugin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # sudo apt install extra-cmake-modules 4 | # sudo apt install plasma-framework-dev 5 | 6 | # https://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe 7 | if [ -t 1 ]; then 8 | sudoCmd="sudo" 9 | else 10 | sudoCmd="kdesudo" 11 | fi 12 | 13 | 14 | buildDir="out" 15 | 16 | rm -r $buildDir 17 | (mkdir $buildDir && cd $buildDir \ 18 | && cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DKDE_INSTALL_LIBDIR=lib -DKDE_INSTALL_USE_QT_SYS_PATHS=ON \ 19 | && make \ 20 | && $sudoCmd make install \ 21 | ) 22 | -------------------------------------------------------------------------------- /plugin/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt install extra-cmake-modules 4 | sudo apt install plasma-framework-dev 5 | 6 | sh ./build 7 | -------------------------------------------------------------------------------- /plugin/plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin.h" 2 | #include "volumepeaks.h" 3 | 4 | #include 5 | 6 | void Plugin::registerTypes(const char* uri) { 7 | qmlRegisterType(uri, 1, 0, "VolumePeaks"); 8 | } 9 | -------------------------------------------------------------------------------- /plugin/plugin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Plugin : public QQmlExtensionPlugin 6 | { 7 | Q_OBJECT 8 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") 9 | public: 10 | void registerTypes(const char * uri) override; 11 | }; 12 | -------------------------------------------------------------------------------- /plugin/qmldir: -------------------------------------------------------------------------------- 1 | module org.kde.plasma.private.volumewin7mixer 2 | plugin plasma-volumewin7mixer-declarative 3 | -------------------------------------------------------------------------------- /plugin/volumepeaks.cpp: -------------------------------------------------------------------------------- 1 | #include "volumepeaks.h" 2 | 3 | VolumePeaks::VolumePeaks(QObject *parent) 4 | : QObject(parent) 5 | , m_process(nullptr) 6 | , m_peaking(false) 7 | , m_defaultSinkPeak(0) 8 | , m_peakCommand("") 9 | , m_peakCommandArgs() 10 | { 11 | 12 | } 13 | 14 | VolumePeaks::~VolumePeaks() { 15 | stop(); 16 | } 17 | 18 | bool VolumePeaks::peaking() const { 19 | return m_peaking; 20 | } 21 | void VolumePeaks::setPeaking(bool b) { 22 | if (b != m_peaking) { 23 | m_peaking = b; 24 | emit peakingChanged(); 25 | if (m_peaking) { 26 | run(); 27 | } else { 28 | stop(); 29 | } 30 | } 31 | } 32 | 33 | int VolumePeaks::defaultSinkPeak() const { 34 | return m_defaultSinkPeak; 35 | } 36 | void VolumePeaks::setDefaultSinkPeak(int peak) { 37 | if (peak != m_defaultSinkPeak) { 38 | m_defaultSinkPeak = peak; 39 | emit defaultSinkPeakChanged(); 40 | } 41 | } 42 | 43 | QString VolumePeaks::peakCommand() const { 44 | return m_peakCommand; 45 | } 46 | void VolumePeaks::setPeakCommand(const QString &command) { 47 | if (command != m_peakCommand) { 48 | m_peakCommand = command; 49 | emit peakCommandChanged(); 50 | restart(); 51 | } 52 | } 53 | 54 | QStringList VolumePeaks::peakCommandArgs() const { 55 | return m_peakCommandArgs; 56 | } 57 | void VolumePeaks::setPeakCommandArgs(const QStringList &args) { 58 | if (args != m_peakCommandArgs) { 59 | m_peakCommandArgs = args; 60 | emit peakCommandArgsChanged(); 61 | restart(); 62 | } 63 | } 64 | 65 | void VolumePeaks::readyReadStandardOutput() { 66 | QByteArray data = m_process->readAllStandardOutput(); 67 | QList tokens = data.split('\n'); 68 | 69 | // TODO: Maybe just asign the last token? 70 | // If it's running behind, we shouldn't cause excess UI updates. 71 | for (int i = 0; i < tokens.size(); ++i) { 72 | QByteArray token = tokens.at(i); 73 | if (!token.isEmpty()) { 74 | bool ok; 75 | int peak = token.toInt(&ok); 76 | if (ok) { 77 | setDefaultSinkPeak(peak); 78 | } 79 | } 80 | } 81 | } 82 | 83 | void VolumePeaks::run() { 84 | if (m_peakCommand.isEmpty()) 85 | return; 86 | 87 | m_process = new QProcess(this); 88 | connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput())); 89 | m_process->start(m_peakCommand, m_peakCommandArgs); 90 | } 91 | 92 | 93 | void VolumePeaks::stop() { 94 | if (m_process) { 95 | m_process->close(); 96 | disconnect(m_process, nullptr, this, nullptr); 97 | delete m_process; 98 | } 99 | } 100 | 101 | void VolumePeaks::restart() { 102 | stop(); 103 | run(); 104 | } 105 | -------------------------------------------------------------------------------- /plugin/volumepeaks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class VolumePeaks : public QObject { 8 | Q_OBJECT 9 | 10 | Q_PROPERTY(bool peaking READ peaking WRITE setPeaking NOTIFY peakingChanged) 11 | Q_PROPERTY(int defaultSinkPeak READ defaultSinkPeak WRITE setDefaultSinkPeak NOTIFY defaultSinkPeakChanged) 12 | Q_PROPERTY(QString peakCommand READ peakCommand WRITE setPeakCommand NOTIFY peakCommandChanged) 13 | Q_PROPERTY(QStringList peakCommandArgs READ peakCommandArgs WRITE setPeakCommandArgs NOTIFY peakCommandArgsChanged) 14 | 15 | public: 16 | explicit VolumePeaks(QObject *parent = nullptr); 17 | ~VolumePeaks(); 18 | 19 | bool peaking() const; 20 | void setPeaking(bool b); 21 | 22 | int defaultSinkPeak() const; 23 | void setDefaultSinkPeak(int peak); 24 | 25 | QString peakCommand() const; 26 | void setPeakCommand(const QString &command); 27 | 28 | QStringList peakCommandArgs() const; 29 | void setPeakCommandArgs(const QStringList &args); 30 | 31 | Q_SIGNALS: 32 | void peakingChanged() const; 33 | void defaultSinkPeakChanged() const; 34 | void peakCommandChanged() const; 35 | void peakCommandArgsChanged() const; 36 | 37 | public slots: 38 | void readyReadStandardOutput(); 39 | 40 | private: 41 | void run(); 42 | void stop(); 43 | void restart(); 44 | 45 | QProcess* m_process; 46 | 47 | bool m_peaking; 48 | int m_defaultSinkPeak; 49 | QString m_peakCommand; 50 | QStringList m_peakCommandArgs; 51 | }; 52 | -------------------------------------------------------------------------------- /reinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 2 3 | 4 | kpackagetool5 -t Plasma/Applet -u package 5 | killall plasmashell 6 | kstart5 plasmashell 7 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 5 3 | 4 | ### Clear SVG cache 5 | rm ~/.cache/plasma-svgelements-* 6 | 7 | killall plasmoidviewer 8 | QML_DISABLE_DISK_CACHE=true plasmoidviewer -a package -l topedge -f horizontal -x 0 -y 0 9 | 10 | ### 2x DPI Test 11 | # QT_DEVICE_PIXEL_RATIO=2 QML_DISABLE_DISK_CACHE=true plasmoidviewer -a package -l topedge -f horizontal -x 0 -y 0 12 | 13 | ### Test French Locale 14 | # LANG=fr_FR.UTF-8 QML_DISABLE_DISK_CACHE=true plasmoidviewer -a package -l topedge -f horizontal -x 0 -y 0 15 | --------------------------------------------------------------------------------