├── .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 | 
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 |
678 |
--------------------------------------------------------------------------------
/package/contents/images/volumeslider.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------