diff --git a/AUTHORS b/AUTHORS index 8c87bf7..66c321e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Frédéric Péters * Frédéric Péters * Daniel Sheeler * Athanasios Silis +* Ryno Kotze # Translation diff --git a/jack_mixer/app.py b/jack_mixer/app.py index 7328b5b..743a05f 100644 --- a/jack_mixer/app.py +++ b/jack_mixer/app.py @@ -34,7 +34,7 @@ from .serialization import SerializedObject, Serializator from .styling import load_css_styles from .version import __version__ - +from .indicator import Indicator __program__ = "jack_mixer" # A "locale" directory present within the package take precedence @@ -92,7 +92,8 @@ class JackMixer(SerializedObject): # scales suitable as volume slider scales slider_scales = [scale.Linear30dB(), scale.Linear70dB()] - def __init__(self, client_name=__program__): + def __init__(self, client_name=__program__, minimized=False): + self.minimize_on_start = minimized self.visible = False self.nsm_client = None # name of project file that is currently open @@ -103,6 +104,9 @@ def __init__(self, client_name=__program__): self.last_xml_serialization = None self.cached_xml_serialization = None + log.info("Starting %s %s", __program__, __version__) + self.indicator = Indicator(self) + if os.environ.get("NSM_URL"): self.nsm_client = NSMClient( prettyName=__program__, @@ -719,7 +723,14 @@ def on_save_as_cb(self, *args): dlg.destroy() def on_quit_cb(self, *args, on_delete=False): - if not self.nsm_client and self.gui_factory.get_confirm_quit(): + log.info("on_quit_cb") + if self.indicator.available and self.gui_factory.get_tray_minimized(): + log.info("on_quit_cb: hiding window") + self.window.set_visible(False) + return True + + elif not self.nsm_client and self.gui_factory.get_confirm_quit(): + log.info("on_quit_cb: confirm quit") dlg = Gtk.MessageDialog( parent=self.window, message_type=Gtk.MessageType.QUESTION, @@ -741,6 +752,7 @@ def on_quit_cb(self, *args, on_delete=False): if response != Gtk.ResponseType.OK: return on_delete + log.info("on_quit_cb: quitting") Gtk.main_quit() def on_shrink_channels_cb(self, widget): @@ -1049,7 +1061,7 @@ def save_to_xml(self, file): b.save(file) def load_from_xml(self, file, silence_errors=False, from_nsm=False): - log.debug("Loading from XML...") + log.debug("Loading from XML... YES WE ARE!") self.unserialized_channels = [] b = XmlSerialization() try: @@ -1146,11 +1158,17 @@ def main(self): if not self.mixer: return - if self.visible or self.nsm_client is None: - width, height = self.window.get_size() - self.window.show_all() - if hasattr(self, "paned_position"): - self.paned.set_position(self.paned_position / self.width * width) + if not self.minimize_on_start: + log.info("Showing main window") + if self.visible or self.nsm_client is None: + width, height = self.window.get_size() + self.window.show_all() + if hasattr(self, "paned_position"): + self.paned.set_position(self.paned_position / self.width * width) + else: + if self.indicator.available: + log.info("Minimizing main window") + self.window.hide() signal.signal(signal.SIGUSR1, self.sighandler) signal.signal(signal.SIGTERM, self.sighandler) @@ -1176,7 +1194,16 @@ def error_dialog(parent, msg, *args, **kw): def main(): + log.debug("JACK Mixer version %s" % __version__) + sys.stdout.flush() parser = argparse.ArgumentParser(prog=__program__, description=_(__doc__.splitlines()[0])) + parser.add_argument( + "-m", + "--minimized", + action="store_true", + default=False, + help=_("start JACK Mixer minimized to system tray (default: %(default)s) If system tray is available."), + ) parser.add_argument( "-c", "--config", @@ -1204,7 +1231,7 @@ def main(): ) try: - mixer = JackMixer(args.client_name) + mixer = JackMixer(args.client_name, args.minimized) except Exception as e: error_dialog(None, _("Mixer creation failed:\n\n{}"), e, debug=args.debug) sys.exit(1) @@ -1230,6 +1257,5 @@ def main(): mixer.main() mixer.cleanup() - if __name__ == "__main__": main() diff --git a/jack_mixer/gui.py b/jack_mixer/gui.py index 855d9f5..2ad78db 100644 --- a/jack_mixer/gui.py +++ b/jack_mixer/gui.py @@ -74,6 +74,7 @@ def __init__(self, topwindow, meter_scales, slider_scales): ) def set_default_preferences(self): + self.tray_minimized = False self.confirm_quit = False self.default_meter_scale = self.meter_scales[0] self.default_project_path = None @@ -93,6 +94,10 @@ def read_preferences(self): "Preferences", "confirm_quit", fallback=self.confirm_quit ) + self.tray_minimized = self.config.getboolean( + "Preferences", "tray_minimized", fallback=self.tray_minimized + ) + scale_id = self.config["Preferences"]["default_meter_scale"] self.default_meter_scale = lookup_scale(self.meter_scales, scale_id) if not self.default_meter_scale: @@ -137,6 +142,7 @@ def read_preferences(self): def write_preferences(self): self.config["Preferences"] = {} + self.config["Preferences"]["tray_minimized"] = str(self.tray_minimized) self.config["Preferences"]["confirm_quit"] = str(self.confirm_quit) self.config["Preferences"]["default_meter_scale"] = self.default_meter_scale.scale_id self.config["Preferences"]["default_project_path"] = self.default_project_path or "" @@ -164,6 +170,9 @@ def _update_setting(self, name, value): signal = "{}-changed".format(name.replace("_", "-")) self.emit(signal, value) + def set_tray_minimized(self, tray_minimized): + self._update_setting("tray_minimized", tray_minimized) + def set_confirm_quit(self, confirm_quit): self._update_setting("confirm_quit", confirm_quit) @@ -210,6 +219,9 @@ def set_auto_reset_peak_meters_time_seconds(self, time): def set_meter_refresh_period_milliseconds(self, period): self._update_setting("meter_refresh_period_milliseconds", period) + def get_tray_minimized(self): + return self.tray_minimized + def get_confirm_quit(self): return self.confirm_quit @@ -284,7 +296,10 @@ def serialize(self, object_backend): ) def unserialize_property(self, name, value): - if name == "confirm_quit": + if name == "tray_minimized": + self.set_tray_minimized(value == "True") + return True + elif name == "confirm_quit": self.set_confirm_quit(value == "True") return True elif name == "default_meter_scale": @@ -319,6 +334,13 @@ def unserialize_property(self, name, value): return False +GObject.signal_new( + "tray-minimized-changed", + Factory, + GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, + None, + [bool], +) GObject.signal_new( "confirm-quit-changed", Factory, diff --git a/jack_mixer/indicator.py b/jack_mixer/indicator.py new file mode 100644 index 0000000..c6b7a58 --- /dev/null +++ b/jack_mixer/indicator.py @@ -0,0 +1,82 @@ +import gi +try: + gi.require_version('Gtk', '3.0') +except Exception as e: + print(e) + print('Repository version required not present') + exit(1) + +try: + from gi.repository import AppIndicator3 as appindicator +except ImportError: + appindicator = None + +import os +import logging as log +from gi.repository import Gtk +from os import environ, path + +prefix = environ.get('MESON_INSTALL_PREFIX', '/usr') +datadir = path.join(prefix, 'share') +icondir = path.join(datadir, 'icons', 'hicolor', 'scalable', 'apps') + +class Indicator: + def __init__(self, jack_mixer): + self.app = jack_mixer + if appindicator is None: + log.warning('AppIndicator3 not found, indicator will not be available') + self.available = False + return + self.available = True + icon = os.path.join(icondir, 'jack_mixer.svg') + self.indicator = appindicator.Indicator.new("Jack Mixer", + icon, + appindicator.IndicatorCategory.APPLICATION_STATUS) + self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE) + self.indicator.set_menu(self.create_menu()) + + def create_menu(self): + self.menu = Gtk.Menu() + self.menu.set_title('Jack Mixer') + + self.hidewindow = Gtk.MenuItem(label = 'Hide / Show Jack Mixer') + self.hidewindow.connect('activate', self.hideshow) + self.menu.append(self.hidewindow) + + self.separator = Gtk.SeparatorMenuItem() + self.menu.append(self.separator) + + self.exittray = Gtk.MenuItem(label = 'Quit') + self.exittray.connect('activate', self.quit) + self.menu.append(self.exittray) + + self.menu.show_all() + return self.menu + + def hideshow(self, source): + self.app.window.set_visible(not self.app.window.get_visible()) + + def quit(self, source, on_delete=False): + if not self.app.nsm_client and self.app.gui_factory.get_confirm_quit(): + dlg = Gtk.MessageDialog( + parent=self.app.window, + message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.NONE, + ) + dlg.set_markup(_("Quit application?")) + dlg.format_secondary_markup( + _( + "All jack_mixer ports will be closed and connections lost," + "\nstopping all sound going through jack_mixer.\n\n" + "Are you sure?" + ) + ) + dlg.add_buttons( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_QUIT, Gtk.ResponseType.OK + ) + response = dlg.run() + dlg.destroy() + if response != Gtk.ResponseType.OK: + return on_delete + + Gtk.main_quit() diff --git a/jack_mixer/meson.build b/jack_mixer/meson.build index edb5f60..9baa530 100644 --- a/jack_mixer/meson.build +++ b/jack_mixer/meson.build @@ -53,6 +53,7 @@ python_sources = files([ 'serialization_xml.py', 'slider.py', 'styling.py', + 'indicator.py', ]) diff --git a/jack_mixer/preferences.py b/jack_mixer/preferences.py index 920fbf6..3a575d9 100644 --- a/jack_mixer/preferences.py +++ b/jack_mixer/preferences.py @@ -80,6 +80,15 @@ def create_ui(self): self.language_box.pack_start(Gtk.Label(_("Language:")), False, True, 5) self.language_box.pack_start(self.language_combo, True, True, 0) + if self.app.indicator.available: + self.tray_minimized_checkbutton = Gtk.CheckButton(_("Minimize to tray")) + self.tray_minimized_checkbutton.set_tooltip_text( + _("Minimize the application to the system tray when the window is closed") + ) + self.tray_minimized_checkbutton.set_active(self.app.gui_factory.get_tray_minimized()) + self.tray_minimized_checkbutton.connect("toggled", self.on_tray_minimized_toggled) + interface_vbox.pack_start(self.tray_minimized_checkbutton, True, True, 3) + self.confirm_quit_checkbutton = Gtk.CheckButton(_("Confirm quit")) self.confirm_quit_checkbutton.set_tooltip_text( _("Always ask for confirmation before quitting the application") @@ -336,6 +345,9 @@ def on_vumeter_color_change(self, *args): self.custom_color_box.set_sensitive(self.vumeter_color_checkbutton.get_active()) + def on_tray_minimized_toggled(self, *args): + self.app.gui_factory.set_tray_minimized(self.tray_minimized_checkbutton.get_active()) + def on_confirm_quit_toggled(self, *args): self.app.gui_factory.set_confirm_quit(self.confirm_quit_checkbutton.get_active()) diff --git a/jackmixer.service b/jackmixer.service new file mode 100644 index 0000000..8a4a528 --- /dev/null +++ b/jackmixer.service @@ -0,0 +1,13 @@ +[Unit] +Description=Jack Mixer Service +Requires=pipewire.socket +After=graphical.target pwg.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/jack_mixer --minimized --config /home/lemonxah/.config/jack_mixer/lemonxah.xml +ExecStop=/bin/kill -9 $MAINPID +Restart=always + +[Install] +WantedBy=default.target