/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- * * Copyright (C) 2014 Canonical Ltd * Copyright (C) 2017 Mike Gabriel * Copyright (C) 2023 Robert Tari * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program 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 this program. If not, see . * */ public delegate void IdleMonitorWatchFunc (IdleMonitor monitor, uint id); public class IdleMonitor { private unowned X.Display display; private HashTable watches; private GenericSet alarms; private int sync_event_base; private X.ID counter; private X.ID user_active_alarm; private int serial = 0; public IdleMonitor () { watches = new HashTable (null, null); alarms = new GenericSet (null, null); init_xsync (); } ~IdleMonitor () { foreach (var watch in watches.get_values ()) remove_watch (watch.id); if (user_active_alarm != X.None) X.Sync.DestroyAlarm (display, user_active_alarm); /* Note this is a bit weird, since we need to pass null as the window by Vala treats this as a method */ Gdk.Window w = null; w.remove_filter (xevent_filter); } public uint add_idle_watch (uint64 interval_msec, IdleMonitorWatchFunc callback) { var watch = make_watch (xsync_alarm_set (X.Sync.TestType.PositiveTransition, interval_msec, true), callback); alarms.add ((uint32) watch.xalarm); return watch.id; } public uint add_user_active_watch (IdleMonitorWatchFunc callback) { set_alarm_enabled (display, user_active_alarm, true); var watch = make_watch (user_active_alarm, callback); return watch.id; } public void remove_watch (uint id) { var watch = watches.lookup (id); watches.remove (id); if (watch.xalarm != user_active_alarm) X.Sync.DestroyAlarm (display, watch.xalarm); } private void init_xsync () { var d = Gdk.Display.get_default (); if (!(d is Gdk.X11.Display)) { warning ("Only support idle monitor under X"); return; } if (d is Gdk.X11.Display) { Gdk.X11.Display pX11Display = (Gdk.X11.Display) d; display = pX11Display.get_xdisplay (); } int sync_error_base; var res = X.Sync.QueryExtension (display, out sync_event_base, out sync_error_base); if (res == 0) { warning ("IdleMonitor: Sync extension not present"); return; } int major, minor; res = X.Sync.Initialize (display, out major, out minor); if (res == 0) { warning ("IdleMonitor: Unable to initialize Sync extension"); return; } counter = find_idletime_counter (); /* IDLETIME counter not found? */ if (counter == X.None) return; user_active_alarm = xsync_alarm_set (X.Sync.TestType.NegativeTransition, 1, false); /* Note this is a bit weird, since we need to pass null as the window by Vala treats this as a method */ Gdk.Window w = null; w.add_filter (xevent_filter); } private Gdk.FilterReturn xevent_filter (Gdk.XEvent xevent, Gdk.Event event) { var ev = (X.Event*) xevent; if (ev.xany.type != sync_event_base + X.Sync.AlarmNotify) return Gdk.FilterReturn.CONTINUE; var alarm_event = (X.Sync.AlarmNotifyEvent*) xevent; handle_alarm_notify_event (alarm_event); return Gdk.FilterReturn.CONTINUE; } private IdleMonitorWatch make_watch (X.ID xalarm, IdleMonitorWatchFunc callback) { var watch = new IdleMonitorWatch (); watch.id = get_next_watch_serial (); watch.callback = callback; watch.xalarm = xalarm; watches.insert (watch.id, watch); return watch; } private X.ID xsync_alarm_set (X.Sync.TestType test_type, uint64 interval, bool want_events) { var attr = X.Sync.AlarmAttributes (); X.Sync.Value delta; X.Sync.IntToValue (out delta, 0); attr.trigger.counter = counter; attr.trigger.value_type = X.Sync.ValueType.Absolute; attr.delta = delta; attr.events = want_events; X.Sync.IntsToValue (out attr.trigger.wait_value, (uint) interval, (int) (interval >> 32)); attr.trigger.test_type = test_type; return X.Sync.CreateAlarm (display, X.Sync.CA.Counter | X.Sync.CA.ValueType | X.Sync.CA.TestType | X.Sync.CA.Value | X.Sync.CA.Delta | X.Sync.CA.Events, attr); } private void ensure_alarm_rescheduled (X.Display dpy, X.ID alarm) { /* Some versions of Xorg have an issue where alarms aren't * always rescheduled. Calling X.Sync.ChangeAlarm, even * without any attributes, will reschedule the alarm. */ var attr = X.Sync.AlarmAttributes (); X.Sync.ChangeAlarm (dpy, alarm, 0, attr); } private void set_alarm_enabled (X.Display dpy, X.ID alarm, bool enabled) { var attr = X.Sync.AlarmAttributes (); attr.events = enabled; X.Sync.ChangeAlarm (dpy, alarm, X.Sync.CA.Events, attr); } private void handle_alarm_notify_event (X.Sync.AlarmNotifyEvent* alarm_event) { if (alarm_event.state != X.Sync.AlarmState.Active) return; var alarm = alarm_event.alarm; var has_alarm = false; if (alarm == user_active_alarm) { set_alarm_enabled (display, alarm, false); has_alarm = true; } else if (alarms.contains ((uint32) alarm)) { ensure_alarm_rescheduled (display, alarm); has_alarm = true; } if (has_alarm) { foreach (var watch in watches.get_values ()) fire_watch (watch, alarm); } } private void fire_watch (IdleMonitorWatch watch, X.ID alarm) { if (watch.xalarm != alarm) return; if (watch.callback != null) watch.callback (this, watch.id); if (watch.xalarm == user_active_alarm) remove_watch (watch.id); } private X.ID find_idletime_counter () { X.ID counter = X.None; int ncounters; var counters = X.Sync.ListSystemCounters (display, out ncounters); for (var i = 0; i < ncounters; i++) { if (counters[i].name != null && strcmp (counters[i].name, "IDLETIME") == 0) { counter = counters[i].counter; break; } } X.Sync.FreeSystemCounterList (counters); return counter; } private uint32 get_next_watch_serial () { AtomicInt.inc (ref serial); return serial; } } public class IdleMonitorWatch { public uint id; public unowned IdleMonitorWatchFunc callback; public X.ID xalarm; }