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 3325403e..bb2f03b8 100644
--- a/src/redshift-gtk/statusicon.py
+++ b/src/redshift-gtk/statusicon.py
@@ -38,7 +38,7 @@
except (ImportError, ValueError):
appindicator = None
-from .controller import RedshiftController
+from . import controller
from . import defs
from . import utils
@@ -48,10 +48,10 @@
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
@@ -86,6 +86,14 @@ def __init__(self, controller):
suspend_menu_item.set_submenu(suspend_menu)
self.status_menu.append(suspend_menu_item)
+ # Add fullscreen menu
+ 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)
+ else:
+ self.fullscreen_item = None
+
# Add autostart option
if utils.supports_autostart():
autostart_item = Gtk.CheckMenuItem.new_with_label(_('Autostart'))
@@ -154,6 +162,7 @@ def __init__(self, controller):
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)
self._controller.connect('stopped', self.controller_stopped_cb)
@@ -162,6 +171,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()
@@ -183,6 +193,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.
@@ -191,7 +207,7 @@ def suspend_cb(self, item, minutes):
and 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.
@@ -203,7 +219,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."""
@@ -214,7 +230,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 when a request to toggle redshift was made.
@@ -225,7 +241,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):
@@ -275,6 +301,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(
@@ -317,6 +347,11 @@ def change_location(self, location):
self.location_label.set_markup(
'{}: {}, {}'.format(_('Location'), *location))
+ def change_fullscreen(self, state):
+ '''Change interface to new fullscreen status'''
+ if self.fullscreen_item is not None:
+ self.fullscreen_item.set_active(state)
+
def update_tooltip_text(self):
"""Update text of tooltip status icon."""
if not appindicator:
@@ -350,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()
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)
+
+