From ed851b0eedf8e7aaf176fd0b676ade83ef45af09 Mon Sep 17 00:00:00 2001 From: Daniel Weschke Date: Sat, 23 Dec 2023 14:36:36 +0100 Subject: [PATCH] add source volume slider, add gui config flags, add gui menu --- controldeck.py | 26 +++++-- controldeck_gui.py | 151 ++++++++++++++++++++++++++++++--------- example/controldeck.conf | 104 +++++++++++++++------------ 3 files changed, 196 insertions(+), 85 deletions(-) diff --git a/controldeck.py b/controldeck.py index 07f8488..45c5ec8 100755 --- a/controldeck.py +++ b/controldeck.py @@ -3,6 +3,12 @@ HTML style powered by Quasar NOTE: currently buttons only updated on page reload + +Icon string +https://quasar.dev/vue-components/icon#webfont-usagehttps://quasar.dev/vue-components/icon#webfont-usage +for example: + without prefix uses material-icons https://fonts.google.com/icons?icon.set=Material+Icons + "fas fa-" uses fontawesome-v5 https://fontawesome.com/icons """ import sys @@ -321,7 +327,7 @@ class Slider(Div): class Volume(Div): # class variables data = {} # pulseaudio info for all sinks - icon_muted = 'volume_mute' # default icon for muted state, 'volume_off' better for disabled or not found? + icon_muted = 'volume_mute' # default icon for muted state, 'volume_off' better for disabled? icon_unmuted = 'volume_up' # default icon for unmuted state last_update = 0 # used for updates. init set to zero so the 1st diff is large to go into update at startup @@ -330,8 +336,8 @@ class Volume(Div): self.slider = None # for handle methods to access slider # default **kwargs - self.wtype = 'sink' # sink (loudspeaker) or sink-input (app output) - self.name = '' # pulseaudio sink name + self.wtype = 'sink' # sink (loudspeaker), source (microphone) or sink-input (app output) + self.name = '' # pulseaudio sink or source name self.description = '' # badge name super().__init__(**kwargs) @@ -342,6 +348,11 @@ class Volume(Div): if self.wtype == 'sink': cmdl_toggle = 'pactl set-sink-mute {name} toggle' cmdl_value = 'pactl set-sink-volume {name} {value}%' + if self.wtype == 'source': + cmdl_toggle = 'pactl set-source-mute {name} toggle' + cmdl_value = 'pactl set-source-volume {name} {value}%' + self.icon_muted = 'mic_none' # default icon for muted state, 'mic_off' better for disabled? + self.icon_unmuted = 'mic' # default icon for unmuted state elif self.wtype == 'sink-input': cmdl_toggle = 'pactl set-sink-input-mute {name} toggle' cmdl_value = 'pactl set-sink-input-volume {name} {value}%' @@ -539,7 +550,7 @@ def widget_load(config) -> dict: # TODO: empty using label class, like an alias? args = [{'widget-class': 'Empty', 'type': wid_type}] - if wid_type == 'label': + elif wid_type == 'label': args = [{'widget-class': Label, 'type': wid_type, 'text': wid_name}] @@ -574,6 +585,13 @@ def widget_load(config) -> dict: 'name': wid_name, 'description': config.get(i, 'description', fallback=''), }] + elif wid_type == 'source': + # volume sliders + args = [{'widget-class': Volume, + 'type': wid_type, + 'name': wid_name, + 'description': config.get(i, 'description', fallback=''), + }] elif wid_type == 'sink-inputs': # multiple volume sliders args = [{'widget-class': VolumeGroup, diff --git a/controldeck_gui.py b/controldeck_gui.py index 6114801..9d41206 100755 --- a/controldeck_gui.py +++ b/controldeck_gui.py @@ -3,7 +3,7 @@ import sys import os import argparse from tkinter import Tk, messagebox -from webview import create_window, start +import webview from controldeck import config_load, process import threading import time @@ -28,41 +28,50 @@ def main(args, pid=-1): port = config.get('default', 'port', fallback='8000') url = f"http://{host}:{port}/?gui&pid={str(pid)}" try: - width = int(config.get('gui', 'width', fallback=800)) + width = config.getint('gui', 'width', fallback=800) except ValueError as e: - print(f"{e}") width = 800 + print(f"Error width: {e}. fallback to: {width}") try: - height = int(config.get('gui', 'height', fallback=600)) + height = config.getint('gui', 'height', fallback=600) except ValueError as e: - print(f"{e}") width = 600 + print(f"Error height: {e}. fallback to {width}") try: - x = int(config.get('gui', 'x', fallback='')) + x = config.getint('gui', 'x', fallback='') except ValueError as e: - print(f"{e}") x = None + print(f"Error x: {e}. fallback to {x}") try: - y = int(config.get('gui', 'y', fallback='')) + y = config.getint('gui', 'y', fallback='') except ValueError as e: - print(f"{e}") y = None + print(f"Error y: {e}. fallback to: {y}") resizable = config.get('gui', 'resizable', fallback='True').title() == 'True' fullscreen = config.get('gui', 'fullscreen', fallback='False').title() == 'True' try: - min_width = int(config.get('gui', 'min_width', fallback=200)) + min_width = config.getint('gui', 'min_width', fallback=200) except ValueError as e: - print(f"{e}") min_width = 200 + print(f"Error min_width: {e}. fallback to: {min_width}") try: - min_height = int(config.get('gui', 'min_height', fallback=100)) + min_height = config.getint('gui', 'min_height', fallback=100) except ValueError as e: - print(f"{e}") - min_width = 100 + min_height = 100 + print(f"Error min_height: {e}. fallback to: {min_height}") min_size = (min_width, min_height) frameless = config.get('gui', 'frameless', fallback='False').title() == 'True' minimized = config.get('gui', 'minimized', fallback='False').title() == 'True' + maximized = config.get('gui', 'maximized', fallback='False').title() == 'True' on_top = config.get('gui', 'always_on_top', fallback='False').title() == 'True' + confirm_close = config.get('gui', 'confirm_close', fallback='False').title() == 'True' + transparent = config.get('gui', 'transparent', fallback='True').title() == 'True' + gui_type = config.get('gui', 'gui_type', fallback=None) + gui_type = gui_type if gui_type != "" else None + menu = config.get('gui', 'menu', fallback='True').title() == 'True' + if args.debug: + print(f"config file [default]: {config.items('default')}") + print(f"config file [gui]: {config.items('gui')}") #controldeck_process = process("ps --no-headers -C controldeck") controldeck_process = process("ps --no-headers -C controldeck || ps aux | grep -e 'python.*controldeck.py' | grep -v grep", shell=True, output=True) @@ -86,29 +95,101 @@ def main(args, pid=-1): sys.exit(2) - create_window("ControlDeck", - url=url, - html=None, - js_api=None, - width=width, - height=height, - x=x, - y=y, - resizable=resizable, - fullscreen=fullscreen, - min_size=min_size, - hidden=False, - frameless=frameless, - easy_drag=True, - minimized=minimized, - on_top=on_top, - confirm_close=False, - background_color='#000000', - transparent=True, - text_select=False) + window = webview.create_window( + title="ControlDeck", + url=url, + html=None, + js_api=None, + width=width, + height=height, + x=x, + y=y, + screen=None, + resizable=resizable, + fullscreen=fullscreen, + min_size=min_size, + hidden=False, + frameless=frameless, + easy_drag=True, + focus=True, + minimized=minimized, + maximized=maximized, + on_top=on_top, + confirm_close=confirm_close, + background_color='#000000', + transparent=transparent, # TODO: bug in qt; menu bar is transparent + text_select=False, + zoomable=False, # zoom via js + draggable=False, + vibrancy=False, + localization=None, + ) x = threading.Thread(target=thread_function, args=(1,)) x.start() - start() + + def menu_reload(): + window = webview.active_window() + if window: + url = window.get_current_url() + window.load_url(url) + print(window.get_current_url()) + print(window) + print(dir(window)) + def menu_zoomin(): + window = webview.active_window() + if window: + zoom = window.evaluate_js('document.documentElement.style.zoom') + if zoom == "": + zoom = 1.0 + else: + zoom = float(zoom) + zoom += 0.1 + print(f"zoom-in: {zoom}") + window.evaluate_js(f'document.documentElement.style.zoom = {zoom}') + print(f"set document.documentElement.style.zoom = {zoom}") + def menu_zoomout(): + window = webview.active_window() + if window: + zoom = window.evaluate_js('document.documentElement.style.zoom') + if zoom == "": + zoom = 1.0 + else: + zoom = float(zoom) + zoom -= 0.1 + print(f"zoom-out: {zoom}") + window.evaluate_js(f'document.documentElement.style.zoom = {zoom}') + print(f"set document.documentElement.style.zoom = {zoom}") + def menu_zoomreset(): + window = webview.active_window() + if window: + zoom = 1.0 + print(f"zoom-reset: {zoom}") + window.evaluate_js(f'document.documentElement.style.zoom = {zoom}') + print(f"set document.documentElement.style.zoom = {zoom}") + menu_items = [] + if menu: + menu_items = [webview.menu.Menu( + 'Main', [ + webview.menu.MenuAction('Reload', menu_reload), + webview.menu.MenuAction('zoom +', menu_zoomin), + webview.menu.MenuAction('zoom -', menu_zoomout), + webview.menu.MenuAction('zoom reset', menu_zoomreset), + ] + )] + # TODO: zoom reset on reload (both from menu and within justpy) + # TODO: add zoom in config + # TODO: move zoom logic to justpy but then it is fix for all, + # maybe better a zoom argument in url address + + def win_func(window): + print(window.get_current_url()) + webview.start( + func=win_func, + args=window, + gui=gui_type, # TODO: bug in qt; any menu action is always the last action + debug=args.debug, + menu=menu_items, + ) def cli(): parser = argparse.ArgumentParser( diff --git a/example/controldeck.conf b/example/controldeck.conf index a3140df..0ad26f4 100644 --- a/example/controldeck.conf +++ b/example/controldeck.conf @@ -1,59 +1,66 @@ # Examples: # -# [N.volume.NAME] -# name = sink_name -# color-fg = hex color code -# color-bg = hex color code -# -# : N. optional number to specify group/row -# : NAME id, name of the button -# : name sink name, see name with either: -# pactl list sinks short -# pamixer --list-sinks -# : color-bg background color -# : color-bg forground color -# -# [N.button.NAME] -# text-alt = name -# color-fg = hex color code -# color-bg = hex color code -# command = shell command -# second command -# ... -# command-alt = shell command ... -# state = normal state -# state-command = shell command ... -# icon = Font Awesome -# icon-alt = Font Awesom -# image = path to svg file -# image-alt = path to svg file -# -# : N. optional group/row specification -# : NAME id, name of the button -# : text-alt optional alternative button text -# : color-bg background color -# : color-bg forground color -# : command command(s) to run -# : command-alt optional back-switch command(s) to run -# : state string to define the normal state -# : state-command command to get the state -# : icon use icon instead of NAME (Font Awesome), e.g.: fas fa-play -# : icon-alt optional alternative icon -# : image absolute path to svg file -# : image-alt optional alternative image -# -# [N.empty.NAME] -# +# [TAB:N.empty.NAME] +# : TAB optional tab name to specify tab group # : N. optional number to specify group/row +# : NAME id of the empty spot +# +# [TAB:N.label.NAME] +# : TAB optional tab name to specify tab group +# : N. optional number to specify group/row +# : NAME id, name of the label +# +# [TAB:N.sink.NAME] +# : TAB optional tab name to specify tab group +# : N. optional number to specify group/row +# : NAME sink id name, see name with either: +# pactl list sinks short +# pamixer --list-sinks +# description = text for the sink +# +# [TAB:N.source.NAME] +# : TAB optional tab name to specify tab group +# : N. optional number to specify group/row +# : NAME source id name, see name with either: +# pactl list sources short +# pamixer --list-sources +# description = text for the source +# +# [TAB:N.sink-inputs] +# : TAB optional tab name to specify tab group +# : N. optional number to specify group/row +# +# [TAB:N.button.NAME] +# : TAB optional tab name to specify tab group +# : N. optional group/row specification # : NAME id, name of the button +# text-alt = optional alternative burron text +# color-fg = foreground color as hex color code, e.g. \#ff0000 +# color-bg = background color as hex color code, e.g. \#ff0000 +# command = command(s) to run, seperated by new lines: shell command +# second command ... +# command-alt = optinal back-switch command(s) to run: shell command ... +# state-alt = string to define the alternative state (pressed) +# state-command = command to get the state: shell command ... +# icon = add icon in front of NAME (Font Awesome), e.g. fas fa-play +# icon-alt = optional alternative icon +# image = absolte path to image file (svg, png) +# image-alt = optional alternative absolue path to image file + +# [TAB:N.slider.NAME] +# : TAB optional tab name to specify tab group +# : N. optional group/row specification +# : NAME id, name of the button +# description = text for the slider [default] +host = 0.0.0.0 +port = 8000 # status = False # volume-decrease-icon = fas fa-volume-down # volume-increase-icon = fas fa-volume-up # volume-mute-icon = fas fa-volume-off # volume-mute-icon-alt = fas fa-volume-mute -# volume-mute-icon-alt = # volume-decrease-image = # volume-increase-image = # volume-mute-image = @@ -68,7 +75,6 @@ # mic-mute-image-alt = [gui] -url = http://0.0.0.0:8000 width = 800 height = 600 # x and y specifying the window coordinate (empty = centered) @@ -80,7 +86,13 @@ min_width = 200 min_height = 100 frameless = False minimized = False +maximized = False always_on_top = False +confirm_close = False +transparent = True +# gui_type: qt, gtk, cef, mshtml, edgechromium. or set env PYWEBVIEW_GUI +gui_type = +menu = True [4.button.Test] command = notify-send -a foo baz