From a94277cb139fd6ff78c0be397a29478a68011f60 Mon Sep 17 00:00:00 2001 From: getzze Date: Sun, 27 Nov 2016 20:30:03 +0000 Subject: [PATCH 1/2] Add detect fullscreen option using the ewmh module --- src/redshift-gtk/statusicon.py | 116 ++++++++++++++++++++++++++++-- src/redshift-gtk/watch_events.py | 120 +++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 src/redshift-gtk/watch_events.py diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py index 9d9835df..7fbef907 100644 --- a/src/redshift-gtk/statusicon.py +++ b/src/redshift-gtk/statusicon.py @@ -42,6 +42,7 @@ from . import defs from . import utils +from . import watch_events _ = gettext.gettext @@ -54,6 +55,7 @@ class RedshiftController(GObject.GObject): 'temperature-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 'period-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), 'location-changed': (GObject.SIGNAL_RUN_FIRST, None, (float, float)), + 'fullscreen-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 'error-occured': (GObject.SIGNAL_RUN_FIRST, None, (str,)) } @@ -70,6 +72,14 @@ def __init__(self, args): self._temperature = 0 self._period = 'Unknown' self._location = (0.0, 0.0) + self._fullscreen = False + self._manually_inhibited = False + self._thread = None + + # Toggle fullscreen detection + if '-f' in args: + args.remove('-f') + self._fullscreen = True # Start redshift with arguments args.insert(0, os.path.join(defs.BINDIR, 'redshift')) @@ -83,6 +93,9 @@ def __init__(self, args): flags=GLib.SPAWN_DO_NOT_REAP_CHILD, standard_output=True, standard_error=True) + # Start thread to detect fullscreen + self.set_run_threads(self._fullscreen) + # Wrap remaining contructor in try..except to avoid that the child # process is not closed properly. try: @@ -138,6 +151,24 @@ def location(self): '''Current location''' return self._location + @property + def fullscreen(self): + '''Current state of fullscreen detection''' + return self._fullscreen + + @fullscreen.setter + def fullscreen(self, state): + '''Set the fullscreen detection state''' + if self._fullscreen != state: + self._fullscreen == self.set_run_threads(state) + + def set_manually_inhibit(self, inhibit): + '''Set manual inhibition state''' + self._manually_inhibited = inhibit + if self.fullscreen: + self.set_run_threads(not inhibit) + self.set_inhibit(inhibit) + def set_inhibit(self, inhibit): '''Set inhibition state''' if inhibit != self._inhibited: @@ -230,12 +261,58 @@ def _child_data_cb(self, f, cond, data): return True + def set_run_threads(self, state): + '''Set the threads running if state=True or stop them.''' + if state and self._thread is None: + self.start_threads() + elif not state and self._thread is not None: + self.stop_threads() + return state + + def start_threads(self): + '''Start the threads + + Watch asynchronously for the active window getting fullscreen + ''' + self._thread = watch_events.WatchThread() + + # Connect the thread signals + self._thread.connect("completed", self._register_thread_cancelled) + self._thread.connect("inhibit-triggered", self.set_auto_inhibit) + + # Start thread + self._thread.start() + + def stop_threads(self, block=False): + """Stops all threads. If block is True then actually wait for + the thread to finish (may block the UI) + """ + if self._thread: + self._thread.cancel() + if block: + if self._thread.isAlive(): + self._thread.join() + self._thread = None + + def _register_thread_cancelled(self, thread, state): + '''Relaunch the thread if failed''' + pass + + def set_auto_inhibit(self, thread, state): + '''Set inhibit if the active window change fullscreen state''' + if state != self.inhibited: + if not self._manually_inhibited: + print(_("[{}] Change of fullscreen mode detected, redshift {}.").format(self.__class__.__name__, _('disabled') if state else _('enabled'))) + self.set_inhibit(state) + def terminate_child(self): """Send SIGINT to child process.""" + self.stop_threads() self._child_signal(signal.SIGINT) def kill_child(self): """Send SIGKILL to child process.""" + self.stop_threads() self._child_signal(signal.SIGKILL) @@ -279,6 +356,11 @@ def __init__(self, controller): suspend_menu_item.set_submenu(suspend_menu) self.status_menu.append(suspend_menu_item) + # Add fullscreen menu + self.fullscreen_item = Gtk.CheckMenuItem.new_with_label(_('Disable on fullscreen')) + self.fullscreen_item.connect('activate', self.fullscreen_item_cb) + self.status_menu.append(self.fullscreen_item) + # Add autostart option autostart_item = Gtk.CheckMenuItem.new_with_label(_('Autostart')) try: @@ -339,6 +421,7 @@ def __init__(self, controller): self._controller.connect('period-changed', self.period_change_cb) self._controller.connect('temperature-changed', self.temperature_change_cb) self._controller.connect('location-changed', self.location_change_cb) + self._controller.connect('fullscreen-changed', self.fullscreen_change_cb) self._controller.connect('error-occured', self.error_occured_cb) # Set info box text @@ -346,6 +429,7 @@ def __init__(self, controller): self.change_period(self._controller.period) self.change_temperature(self._controller.temperature) self.change_location(self._controller.location) + self.change_fullscreen(self._controller.fullscreen) if appindicator: self.status_menu.show_all() @@ -367,6 +451,12 @@ def remove_suspend_timer(self): GLib.source_remove(self.suspend_timer) self.suspend_timer = None + def manually_inhibit(self, inhibit): + '''Callback that handles manual inhibition''' + + # Inhibit + self._controller.set_manually_inhibit(inhibit) + def suspend_cb(self, item, minutes): '''Callback that handles activation of a suspend timer @@ -375,7 +465,7 @@ def suspend_cb(self, item, minutes): reactive redshift when the timer is up.''' # Inhibit - self._controller.set_inhibit(True) + self.manually_inhibit(True) # If "suspend" is clicked while redshift is disabled, we reenable # it after the last selected timespan is over. @@ -386,7 +476,7 @@ def suspend_cb(self, item, minutes): def reenable_cb(self): '''Callback to reenable redshift when a suspend timer expires''' - self._controller.set_inhibit(False) + self.manually_inhibit(False) def popup_menu_cb(self, widget, button, time, data=None): '''Callback when the popup menu on the status icon has to open''' @@ -397,7 +487,7 @@ def popup_menu_cb(self, widget, button, time, data=None): def toggle_cb(self, widget, data=None): '''Callback when a request to toggle redshift was made''' self.remove_suspend_timer() - self._controller.set_inhibit(not self._controller.inhibited) + self.manually_inhibit(not self._controller.inhibited) def toggle_item_cb(self, widget, data=None): '''Callback then a request to toggle redshift was made from a toggle item @@ -408,7 +498,17 @@ def toggle_item_cb(self, widget, data=None): active = not self._controller.inhibited if active != widget.get_active(): self.remove_suspend_timer() - self._controller.set_inhibit(not self._controller.inhibited) + self.manually_inhibit(not self._controller.inhibited) + + def fullscreen_item_cb(self, widget, data=None): + '''Callback then a request to disable redshift on fullscreen was made from a fullscreen item + + This ensures that the state of redshift is synchronised with + the fullscreen state of the widget (e.g. Gtk.CheckMenuItem).''' + + fullscreen = self._controller.fullscreen + if fullscreen != widget.get_active(): + self._controller.fullscreen = not fullscreen # Info dialog callbacks def show_info_cb(self, widget, data=None): @@ -454,6 +554,10 @@ def location_change_cb(self, controller, lat, lon): '''Callback when controlled changes location''' self.change_location((lat, lon)) + def fullscreen_change_cb(self, controller, state): + '''Callback when controlled changes fullscreen''' + self.change_fullscreen(state) + def error_occured_cb(self, controller, error): '''Callback when an error occurs in the controller''' error_dialog = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, @@ -485,6 +589,10 @@ def change_location(self, location): '''Change interface to new location''' self.location_label.set_markup('{}: {}, {}'.format(_('Location'), *location)) + def change_fullscreen(self, state): + '''Change interface to new fullscreen status''' + self.fullscreen_item.set_active(state) + def update_tooltip_text(self): '''Update text of tooltip status icon ''' if not appindicator: diff --git a/src/redshift-gtk/watch_events.py b/src/redshift-gtk/watch_events.py new file mode 100644 index 00000000..a0ad15ce --- /dev/null +++ b/src/redshift-gtk/watch_events.py @@ -0,0 +1,120 @@ +# detect-fullscreen.py -- Detect if a window is in fullscreen on the same monitor +# This file is part of Redshift. + +# Redshift is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Redshift is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Redshift. If not, see . + +# Copyright (c) 2013-2014 Jon Lund Steffensen + + +'''Detect if a window is in fullscreen + + +''' +import time +import threading +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import GObject + +from ewmh import EWMH + + +## Threading from https://gist.github.com/nzjrs/51686 +class _IdleObject(GObject.GObject): + """ + Override GObject.GObject to always emit signals in the main thread + by emmitting on an idle handler + """ + def __init__(self): + GObject.GObject.__init__(self) + + def emit(self, *args): + GObject.idle_add(GObject.GObject.emit,self,*args) + + +class WatchThread(threading.Thread, _IdleObject): + """ + Cancellable thread which uses GObject signals to return information + to the GUI. + """ + __gsignals__ = { + "completed": (GObject.SIGNAL_RUN_FIRST, None, (bool,)), + "inhibit-triggered": (GObject.SIGNAL_RUN_FIRST, None, (bool,)), + } + + def __init__(self, delta = 2, fullscreen=None): + threading.Thread.__init__(self) + _IdleObject.__init__(self) + self._ewmh = EWMH() + self.cancelled = False + self.delta = delta + self.fullscreen = fullscreen + self.setName("Detect fullscreen mode") + + def cancel(self): + """ + Threads in python are not cancellable, so we implement our own + cancellation logic + """ + self.cancelled = True + + def detect_fullscreen(self) -> bool: + """Return True if the active window is fullscreen + """ + fullscreen = False + + try: + # Determine if a fullscreen application is running + window = self._ewmh.getActiveWindow() + # ewmh.getWmState(window) returns None is scenarios where + # ewmh.getWmState(window, str=True) throws an exception + # (it's a bug in pyewmh): + if window and self._ewmh.getWmState(window): + list_states = self._ewmh.getWmState(window, True) + fullscreen = "_NET_WM_STATE_FULLSCREEN" in list_states + except Exception as e: + print("Error ignored:\n", e) + return None + return fullscreen + + def run(self): + """Watch if the fullscreen mode is detected + + Wait `delta` seconds between each check. It can be cancelled by + changing the `cancelled` attribute. The `inhibit-triggered` + signal is emitted when the state is changed, holding the value + True if the active window has become fullscreen, False if it has + quit the fullscreen mode. + """ + # Init the fullscreen state if it was not user-defined + if self.fullscreen is None: + self.fullscreen = self.detect_fullscreen() + + # Continuously detect changes in fullscreen mode + try: + while(not self.cancelled): + f = self.detect_fullscreen() + + if f is not None and f != self.fullscreen: + self.fullscreen = f + self.emit("inhibit-triggered", self.fullscreen) + time.sleep(self.delta) + except Exception as e: + raise + self.emit("completed", False) + else: + self.emit("completed", True) + + From e2ddf6634b760c83043e5abf413f6cad6464cacf Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 20 Oct 2017 01:01:35 +0100 Subject: [PATCH 2/2] Correct bugs with merging --- src/redshift-gtk/controller.py | 158 ++++++++++++++---- src/redshift-gtk/statusicon.py | 289 +-------------------------------- 2 files changed, 128 insertions(+), 319 deletions(-) diff --git a/src/redshift-gtk/controller.py b/src/redshift-gtk/controller.py index 24c58ae7..6f65784e 100644 --- a/src/redshift-gtk/controller.py +++ b/src/redshift-gtk/controller.py @@ -20,6 +20,7 @@ import re import fcntl import signal +import gettext import gi gi.require_version('GLib', '2.0') @@ -27,26 +28,33 @@ from gi.repository import GLib, GObject from . import defs +try: + from . import watch_events +except (ImportError, ValueError): + watch_events = None + +_ = gettext.gettext class RedshiftController(GObject.GObject): - """GObject wrapper around the Redshift child process.""" + '''A GObject wrapper around the child process''' __gsignals__ = { 'inhibit-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 'temperature-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), 'period-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), 'location-changed': (GObject.SIGNAL_RUN_FIRST, None, (float, float)), + 'fullscreen-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 'error-occured': (GObject.SIGNAL_RUN_FIRST, None, (str,)), 'stopped': (GObject.SIGNAL_RUN_FIRST, None, ()), - } + } def __init__(self, args): - """Initialize controller and start child process. + '''Initialize controller and start child process The parameter args is a list of command line arguments to pass on to - the child process. The "-v" argument is automatically added. - """ + the child process. The "-v" argument is automatically added.''' + GObject.GObject.__init__(self) # Initialize state variables @@ -54,6 +62,17 @@ def __init__(self, args): self._temperature = 0 self._period = 'Unknown' self._location = (0.0, 0.0) + self._fullscreen = False + self._manually_inhibited = False + self.detect_fullscreen = False + self._thread = None + + # Toggle fullscreen detection + if watch_events is not None: + self.detect_fullscreen = True + if '-f' in args: + args.remove('-f') + self._fullscreen = True # Start redshift with arguments args.insert(0, os.path.join(defs.BINDIR, 'redshift')) @@ -62,13 +81,14 @@ def __init__(self, args): # Start child process with C locale so we can parse the output env = os.environ.copy() - for key in ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES'): - env[key] = 'C' - self._process = GLib.spawn_async( - args, envp=['{}={}'.format(k, v) for k, v in env.items()], - flags=GLib.SPAWN_DO_NOT_REAP_CHILD, - standard_output=True, standard_error=True) - + env['LANG'] = env['LANGUAGE'] = env['LC_ALL'] = env['LC_MESSAGES'] = 'C' + self._process = GLib.spawn_async(args, envp=['{}={}'.format(k,v) for k, v in env.items()], + flags=GLib.SPAWN_DO_NOT_REAP_CHILD, + standard_output=True, standard_error=True) + + # Start thread to detect fullscreen + self.set_run_threads(self._fullscreen) + # Wrap remaining contructor in try..except to avoid that the child # process is not closed properly. try: @@ -83,19 +103,15 @@ class InputBuffer(object): self._errors = '' # Set non blocking - fcntl.fcntl( - self._process[2], fcntl.F_SETFL, - fcntl.fcntl(self._process[2], fcntl.F_GETFL) | os.O_NONBLOCK) + fcntl.fcntl(self._process[2], fcntl.F_SETFL, + fcntl.fcntl(self._process[2], fcntl.F_GETFL) | os.O_NONBLOCK) # Add watch on child process - GLib.child_watch_add( - GLib.PRIORITY_DEFAULT, self._process[0], self._child_cb) - GLib.io_add_watch( - self._process[2], GLib.PRIORITY_DEFAULT, GLib.IO_IN, - self._child_data_cb, (True, self._input_buffer)) - GLib.io_add_watch( - self._process[3], GLib.PRIORITY_DEFAULT, GLib.IO_IN, - self._child_data_cb, (False, self._error_buffer)) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self._process[0], self._child_cb) + GLib.io_add_watch(self._process[2], GLib.PRIORITY_DEFAULT, GLib.IO_IN, + self._child_data_cb, (True, self._input_buffer)) + GLib.io_add_watch(self._process[3], GLib.PRIORITY_DEFAULT, GLib.IO_IN, + self._child_data_cb, (False, self._error_buffer)) # Signal handler to relay USR1 signal to redshift process def relay_signal_handler(signal): @@ -110,26 +126,44 @@ def relay_signal_handler(signal): @property def inhibited(self): - """Current inhibition state.""" + '''Current inhibition state''' return self._inhibited @property def temperature(self): - """Current screen temperature.""" + '''Current screen temperature''' return self._temperature @property def period(self): - """Current period of day.""" + '''Current period of day''' return self._period @property def location(self): - """Current location.""" + '''Current location''' return self._location + @property + def fullscreen(self): + '''Current state of fullscreen detection''' + return self._fullscreen + + @fullscreen.setter + def fullscreen(self, state): + '''Set the fullscreen detection state''' + if self._fullscreen != state: + self._fullscreen == self.set_run_threads(state) + + def set_manually_inhibit(self, inhibit): + '''Set manual inhibition state''' + self._manually_inhibited = inhibit + if self.fullscreen: + self.set_run_threads(not inhibit) + self.set_inhibit(inhibit) + def set_inhibit(self, inhibit): - """Set inhibition state.""" + '''Set inhibition state''' if inhibit != self._inhibited: self._child_toggle_inhibit() @@ -138,11 +172,11 @@ def _child_signal(self, sg): os.kill(self._process[0], sg) def _child_toggle_inhibit(self): - """Sends a request to the child process to toggle state.""" + '''Sends a request to the child process to toggle state''' self._child_signal(signal.SIGUSR1) def _child_cb(self, pid, status, data=None): - """Called when the child process exists.""" + '''Called when the child process exists''' # Empty stdout and stderr for f in (self._process[2], self._process[3]): @@ -150,10 +184,11 @@ def _child_cb(self, pid, status, data=None): buf = os.read(f, 256).decode('utf-8') if buf == '': break - if f == self._process[3]: # stderr + if f == self._process[3]: # stderr self._errors += buf # Check exit status of child + report_errors = False try: GLib.spawn_check_exit_status(status) except GLib.GError: @@ -163,10 +198,10 @@ def _child_cb(self, pid, status, data=None): self.emit('stopped') def _child_key_change_cb(self, key, value): - """Called when the child process reports a change of internal state.""" + '''Called when the child process reports a change of internal state''' def parse_coord(s): - """Parse coordinate like `42.0 N` or `91.5 W`.""" + '''Parse coordinate like `42.0 N` or `91.5 W`''' v, d = s.split(' ') return float(v) * (1 if d in 'NE' else -1) @@ -192,7 +227,7 @@ def parse_coord(s): self.emit('location-changed', *new_location) def _child_stdout_line_cb(self, line): - """Called when the child process outputs a line to stdout.""" + '''Called when the child process outputs a line to stdout''' if line: m = re.match(r'([\w ]+): (.+)', line) if m: @@ -201,7 +236,8 @@ def _child_stdout_line_cb(self, line): self._child_key_change_cb(key, value) def _child_data_cb(self, f, cond, data): - """Called when the child process has new data on stdout/stderr.""" + '''Called when the child process has new data on stdout/stderr''' + stdout, ib = data ib.buf += os.read(f, 256).decode('utf-8') @@ -218,10 +254,62 @@ def _child_data_cb(self, f, cond, data): return True + def set_run_threads(self, state): + '''Set the threads running if state=True or stop them.''' + if state and self._thread is None: + self.start_threads() + elif not state and self._thread is not None: + self.stop_threads() + return state + + def start_threads(self): + '''Start the threads + + Watch asynchronously for the active window getting fullscreen + ''' + if watch_events is None: + self._thread = None + return + + # Initialize the thread + self._thread = watch_events.WatchThread() + + # Connect the thread signals + self._thread.connect("completed", self._register_thread_cancelled) + self._thread.connect("inhibit-triggered", self.set_auto_inhibit) + + # Start thread + self._thread.start() + + def stop_threads(self, block=False): + """Stops all threads. If block is True then actually wait for + the thread to finish (may block the UI) + """ + if self._thread: + self._thread.cancel() + if block: + if self._thread.isAlive(): + self._thread.join() + self._thread = None + + def _register_thread_cancelled(self, thread, state): + '''Relaunch the thread if failed''' + pass + + def set_auto_inhibit(self, thread, state): + '''Set inhibit if the active window change fullscreen state''' + if state != self.inhibited: + if not self._manually_inhibited: + print(_("[{}] Change of fullscreen mode detected, redshift {}.").format(self.__class__.__name__, _('disabled') if state else _('enabled'))) + self.set_inhibit(state) + def terminate_child(self): """Send SIGINT to child process.""" + self.stop_threads() self._child_signal(signal.SIGINT) def kill_child(self): """Send SIGKILL to child process.""" + self.stop_threads() self._child_signal(signal.SIGKILL) + diff --git a/src/redshift-gtk/statusicon.py b/src/redshift-gtk/statusicon.py index 76c11f26..bb2f03b8 100644 --- a/src/redshift-gtk/statusicon.py +++ b/src/redshift-gtk/statusicon.py @@ -38,299 +38,20 @@ except (ImportError, ValueError): appindicator = None -from .controller import RedshiftController +from . import controller from . import defs from . import utils -try: - from . import watch_events -except (ImportError, ValueError): - watch_events = None _ = gettext.gettext -class RedshiftController(GObject.GObject): - '''A GObject wrapper around the child process''' - - __gsignals__ = { - 'inhibit-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), - 'temperature-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)), - 'period-changed': (GObject.SIGNAL_RUN_FIRST, None, (str,)), - 'location-changed': (GObject.SIGNAL_RUN_FIRST, None, (float, float)), - 'fullscreen-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)), - 'error-occured': (GObject.SIGNAL_RUN_FIRST, None, (str,)) - } - - def __init__(self, args): - '''Initialize controller and start child process - - The parameter args is a list of command line arguments to pass on to - the child process. The "-v" argument is automatically added.''' - - GObject.GObject.__init__(self) - - # Initialize state variables - self._inhibited = False - self._temperature = 0 - self._period = 'Unknown' - self._location = (0.0, 0.0) - self._fullscreen = False - self._manually_inhibited = False - self._thread = None - - # Toggle fullscreen detection - if watch_events is not None: - if '-f' in args: - args.remove('-f') - self._fullscreen = True - - # Start redshift with arguments - args.insert(0, os.path.join(defs.BINDIR, 'redshift')) - if '-v' not in args: - args.insert(1, '-v') - - # Start child process with C locale so we can parse the output - env = os.environ.copy() - env['LANG'] = env['LANGUAGE'] = env['LC_ALL'] = env['LC_MESSAGES'] = 'C' - self._process = GLib.spawn_async(args, envp=['{}={}'.format(k,v) for k, v in env.items()], - flags=GLib.SPAWN_DO_NOT_REAP_CHILD, - standard_output=True, standard_error=True) - - # Start thread to detect fullscreen - self.set_run_threads(self._fullscreen) - - # Wrap remaining contructor in try..except to avoid that the child - # process is not closed properly. - try: - # Handle child input - # The buffer is encapsulated in a class so we - # can pass an instance to the child callback. - class InputBuffer(object): - buf = '' - - self._input_buffer = InputBuffer() - self._error_buffer = InputBuffer() - self._errors = '' - - # Set non blocking - fcntl.fcntl(self._process[2], fcntl.F_SETFL, - fcntl.fcntl(self._process[2], fcntl.F_GETFL) | os.O_NONBLOCK) - - # Add watch on child process - GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self._process[0], self._child_cb) - GLib.io_add_watch(self._process[2], GLib.PRIORITY_DEFAULT, GLib.IO_IN, - self._child_data_cb, (True, self._input_buffer)) - GLib.io_add_watch(self._process[3], GLib.PRIORITY_DEFAULT, GLib.IO_IN, - self._child_data_cb, (False, self._error_buffer)) - - # Signal handler to relay USR1 signal to redshift process - def relay_signal_handler(signal): - os.kill(self._process[0], signal) - return True - - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGUSR1, - relay_signal_handler, signal.SIGUSR1) - except: - self.termwait() - raise - - @property - def inhibited(self): - '''Current inhibition state''' - return self._inhibited - - @property - def temperature(self): - '''Current screen temperature''' - return self._temperature - - @property - def period(self): - '''Current period of day''' - return self._period - - @property - def location(self): - '''Current location''' - return self._location - - @property - def fullscreen(self): - '''Current state of fullscreen detection''' - return self._fullscreen - - @fullscreen.setter - def fullscreen(self, state): - '''Set the fullscreen detection state''' - if self._fullscreen != state: - self._fullscreen == self.set_run_threads(state) - - def set_manually_inhibit(self, inhibit): - '''Set manual inhibition state''' - self._manually_inhibited = inhibit - if self.fullscreen: - self.set_run_threads(not inhibit) - self.set_inhibit(inhibit) - - def set_inhibit(self, inhibit): - '''Set inhibition state''' - if inhibit != self._inhibited: - self._child_toggle_inhibit() - - def _child_signal(self, sg): - """Send signal to child process.""" - os.kill(self._process[0], sg) - - def _child_toggle_inhibit(self): - '''Sends a request to the child process to toggle state''' - self._child_signal(signal.SIGUSR1) - - def _child_cb(self, pid, status, data=None): - '''Called when the child process exists''' - - # Empty stdout and stderr - for f in (self._process[2], self._process[3]): - while True: - buf = os.read(f, 256).decode('utf-8') - if buf == '': - break - if f == self._process[3]: # stderr - self._errors += buf - - # Check exit status of child - report_errors = False - try: - GLib.spawn_check_exit_status(status) - except GLib.GError: - self.emit('error-occured', self._errors) - - GLib.spawn_close_pid(self._process[0]) - Gtk.main_quit() - - def _child_key_change_cb(self, key, value): - '''Called when the child process reports a change of internal state''' - - def parse_coord(s): - '''Parse coordinate like `42.0 N` or `91.5 W`''' - v, d = s.split(' ') - return float(v) * (1 if d in 'NE' else -1) - - if key == 'Status': - new_inhibited = value != 'Enabled' - if new_inhibited != self._inhibited: - self._inhibited = new_inhibited - self.emit('inhibit-changed', new_inhibited) - elif key == 'Color temperature': - new_temperature = int(value.rstrip('K'), 10) - if new_temperature != self._temperature: - self._temperature = new_temperature - self.emit('temperature-changed', new_temperature) - elif key == 'Period': - new_period = value - if new_period != self._period: - self._period = new_period - self.emit('period-changed', new_period) - elif key == 'Location': - new_location = tuple(parse_coord(x) for x in value.split(', ')) - if new_location != self._location: - self._location = new_location - self.emit('location-changed', *new_location) - - def _child_stdout_line_cb(self, line): - '''Called when the child process outputs a line to stdout''' - if line: - m = re.match(r'([\w ]+): (.+)', line) - if m: - key = m.group(1) - value = m.group(2) - self._child_key_change_cb(key, value) - - def _child_data_cb(self, f, cond, data): - '''Called when the child process has new data on stdout/stderr''' - - stdout, ib = data - ib.buf += os.read(f, 256).decode('utf-8') - - # Split input at line break - while True: - first, sep, last = ib.buf.partition('\n') - if sep == '': - break - ib.buf = last - if stdout: - self._child_stdout_line_cb(first) - else: - self._errors += first + '\n' - - return True - - def set_run_threads(self, state): - '''Set the threads running if state=True or stop them.''' - if state and self._thread is None: - self.start_threads() - elif not state and self._thread is not None: - self.stop_threads() - return state - - def start_threads(self): - '''Start the threads - - Watch asynchronously for the active window getting fullscreen - ''' - if watch_events is None: - self._thread = None - return - - # Initialize the thread - self._thread = watch_events.WatchThread() - - # Connect the thread signals - self._thread.connect("completed", self._register_thread_cancelled) - self._thread.connect("inhibit-triggered", self.set_auto_inhibit) - - # Start thread - self._thread.start() - - def stop_threads(self, block=False): - """Stops all threads. If block is True then actually wait for - the thread to finish (may block the UI) - """ - if self._thread: - self._thread.cancel() - if block: - if self._thread.isAlive(): - self._thread.join() - self._thread = None - - def _register_thread_cancelled(self, thread, state): - '''Relaunch the thread if failed''' - pass - - def set_auto_inhibit(self, thread, state): - '''Set inhibit if the active window change fullscreen state''' - if state != self.inhibited: - if not self._manually_inhibited: - print(_("[{}] Change of fullscreen mode detected, redshift {}.").format(self.__class__.__name__, _('disabled') if state else _('enabled'))) - self.set_inhibit(state) - - def terminate_child(self): - """Send SIGINT to child process.""" - self.stop_threads() - self._child_signal(signal.SIGINT) - - def kill_child(self): - """Send SIGKILL to child process.""" - self.stop_threads() - self._child_signal(signal.SIGKILL) - - class RedshiftStatusIcon(object): """The status icon tracking the RedshiftController.""" - def __init__(self, controller): + def __init__(self, controller_instance): """Creates a new instance of the status icon.""" - self._controller = controller + self._controller = controller_instance if appindicator: # Create indicator @@ -366,7 +87,7 @@ def __init__(self, controller): self.status_menu.append(suspend_menu_item) # Add fullscreen menu - if watch_events is not None: + if self._controller.detect_fullscreen: self.fullscreen_item = Gtk.CheckMenuItem.new_with_label(_('Disable on fullscreen')) self.fullscreen_item.connect('activate', self.fullscreen_item_cb) self.status_menu.append(self.fullscreen_item) @@ -664,7 +385,7 @@ def run(): sys.exit(-1) # Create redshift child process controller - c = RedshiftController(sys.argv[1:]) + c = controller.RedshiftController(sys.argv[1:]) def terminate_child(data=None): c.terminate_child()