2017-10-07 15:10:31 +02:00
|
|
|
# Safe Eyes is a utility to remind you to take break frequently
|
|
|
|
# to protect your eyes from eye strain.
|
|
|
|
|
|
|
|
# Copyright (C) 2017 Gobinath
|
|
|
|
|
|
|
|
# This program 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.
|
|
|
|
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
import datetime
|
2021-05-11 01:03:12 +02:00
|
|
|
from safeeyes.model import BreakType
|
2017-10-07 15:10:31 +02:00
|
|
|
import gi
|
|
|
|
gi.require_version('Gtk', '3.0')
|
2023-12-31 00:02:06 +01:00
|
|
|
from gi.repository import Gio, GLib
|
2017-10-07 15:10:31 +02:00
|
|
|
import logging
|
2020-03-18 13:33:11 +01:00
|
|
|
from safeeyes import utility
|
2017-10-07 15:10:31 +02:00
|
|
|
import threading
|
2019-03-14 01:28:30 +01:00
|
|
|
import time
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
Safe Eyes tray icon plugin
|
|
|
|
"""
|
|
|
|
|
|
|
|
context = None
|
|
|
|
tray_icon = None
|
|
|
|
safeeyes_config = None
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
SNI_NODE_INFO = Gio.DBusNodeInfo.new_for_xml("""
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
<node>
|
|
|
|
<interface name="org.kde.StatusNotifierItem">
|
|
|
|
<property name="Category" type="s" access="read"/>
|
|
|
|
<property name="Id" type="s" access="read"/>
|
|
|
|
<property name="Title" type="s" access="read"/>
|
|
|
|
<property name="ToolTip" type="(sa(iiay)ss)" access="read"/>
|
|
|
|
<property name="Menu" type="o" access="read"/>
|
|
|
|
<property name="ItemIsMenu" type="b" access="read"/>
|
|
|
|
<property name="IconName" type="s" access="read"/>
|
|
|
|
<property name="IconThemePath" type="s" access="read"/>
|
|
|
|
<property name="Status" type="s" access="read"/>
|
|
|
|
<signal name="NewIcon"/>
|
|
|
|
<signal name="NewTooltip"/>
|
|
|
|
</interface>
|
|
|
|
</node>""").interfaces[0]
|
|
|
|
|
|
|
|
MENU_NODE_INFO = Gio.DBusNodeInfo.new_for_xml("""
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
<node>
|
|
|
|
<interface name="com.canonical.dbusmenu">
|
|
|
|
<method name="GetLayout">
|
|
|
|
<arg type="i" direction="in"/>
|
|
|
|
<arg type="i" direction="in"/>
|
|
|
|
<arg type="as" direction="in"/>
|
|
|
|
<arg type="u" direction="out"/>
|
|
|
|
<arg type="(ia{sv}av)" direction="out"/>
|
|
|
|
</method>
|
2024-07-08 12:34:05 +02:00
|
|
|
<method name="GetGroupProperties">
|
|
|
|
<arg type="ai" name="ids" direction="in"/>
|
|
|
|
<arg type="as" name="propertyNames" direction="in" />
|
|
|
|
<arg type="a(ia{sv})" name="properties" direction="out" />
|
|
|
|
</method>
|
|
|
|
<method name="GetProperty">
|
|
|
|
<arg type="i" name="id" direction="in"/>
|
|
|
|
<arg type="s" name="name" direction="in"/>
|
|
|
|
<arg type="v" name="value" direction="out"/>
|
|
|
|
</method>
|
2023-12-31 00:02:06 +01:00
|
|
|
<method name="Event">
|
|
|
|
<arg type="i" direction="in"/>
|
|
|
|
<arg type="s" direction="in"/>
|
|
|
|
<arg type="v" direction="in"/>
|
|
|
|
<arg type="u" direction="in"/>
|
|
|
|
</method>
|
2024-07-08 12:34:05 +02:00
|
|
|
<method name="EventGroup">
|
|
|
|
<arg type="a(isvu)" name="events" direction="in" />
|
|
|
|
<arg type="ai" name="idErrors" direction="out" />
|
|
|
|
</method>
|
2023-12-31 00:02:06 +01:00
|
|
|
<method name="AboutToShow">
|
|
|
|
<arg type="i" direction="in"/>
|
|
|
|
<arg type="b" direction="out"/>
|
|
|
|
</method>
|
2024-07-08 12:34:05 +02:00
|
|
|
<method name="AboutToShowGroup">
|
|
|
|
<arg type="ai" name="ids" direction="in" />
|
|
|
|
<arg type="ai" name="updatesNeeded" direction="out" />
|
|
|
|
<arg type="ai" name="idErrors" direction="out" />
|
|
|
|
</method>
|
2023-12-31 00:02:06 +01:00
|
|
|
<signal name="LayoutUpdated">
|
|
|
|
<arg type="u"/>
|
|
|
|
<arg type="i"/>
|
|
|
|
</signal>
|
|
|
|
</interface>
|
|
|
|
</node>""").interfaces[0]
|
|
|
|
|
|
|
|
class DBusService:
|
|
|
|
def __init__(self, interface_info, object_path, bus):
|
|
|
|
self.interface_info = interface_info
|
|
|
|
self.bus = bus
|
|
|
|
self.object_path = object_path
|
|
|
|
self.registration_id = None
|
|
|
|
|
|
|
|
def register(self):
|
|
|
|
self.registration_id = self.bus.register_object(
|
|
|
|
object_path=self.object_path,
|
|
|
|
interface_info=self.interface_info,
|
|
|
|
method_call_closure=self.on_method_call,
|
|
|
|
get_property_closure=self.on_get_property
|
|
|
|
)
|
|
|
|
|
|
|
|
if not self.registration_id:
|
|
|
|
raise GLib.Error(f"Failed to register object with path {self.object_path}")
|
|
|
|
|
|
|
|
self.interface_info.cache_build()
|
|
|
|
|
|
|
|
def unregister(self):
|
|
|
|
self.interface_info.cache_release()
|
|
|
|
|
|
|
|
if self.registration_id is not None:
|
|
|
|
self.bus.unregister_object(self.registration_id)
|
|
|
|
self.registration_id = None
|
|
|
|
|
|
|
|
def on_method_call(self, _connection, _sender, _path, _interface_name, method_name, parameters, invocation):
|
|
|
|
method_info = self.interface_info.lookup_method(method_name)
|
|
|
|
method = getattr(self, method_name)
|
|
|
|
result = method(*parameters.unpack())
|
|
|
|
out_arg_types = "".join([arg.signature for arg in method_info.out_args])
|
|
|
|
return_value = None
|
|
|
|
|
|
|
|
if method_info.out_args:
|
|
|
|
return_value = GLib.Variant(f"({out_arg_types})", result)
|
|
|
|
|
|
|
|
invocation.return_value(return_value)
|
|
|
|
|
|
|
|
def on_get_property(self, _connection, _sender, _path, _interface_name, property_name):
|
|
|
|
property_info = self.interface_info.lookup_property(property_name)
|
|
|
|
return GLib.Variant(property_info.signature, getattr(self, property_name))
|
|
|
|
|
|
|
|
def emit_signal(self, signal_name, args = None):
|
|
|
|
signal_info = self.interface_info.lookup_signal(signal_name)
|
|
|
|
if len(signal_info.args) == 0:
|
|
|
|
parameters = None
|
|
|
|
else:
|
|
|
|
arg_types = "".join([arg.signature for arg in signal_info.args])
|
|
|
|
parameters = GLib.Variant(f"({arg_types})", args)
|
|
|
|
self.bus.emit_signal(
|
|
|
|
destination_bus_name=None,
|
|
|
|
object_path=self.object_path,
|
|
|
|
interface_name=self.interface_info.name,
|
|
|
|
signal_name=signal_name,
|
|
|
|
parameters=parameters
|
|
|
|
)
|
|
|
|
|
|
|
|
class DBusMenuService(DBusService):
|
|
|
|
DBUS_SERVICE_PATH = '/io/github/slgobinath/SafeEyes/Menu'
|
|
|
|
|
|
|
|
revision = 0
|
|
|
|
|
|
|
|
items = []
|
2024-07-08 12:34:05 +02:00
|
|
|
idToItems = {}
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
def __init__(self, session_bus, context, items):
|
|
|
|
super().__init__(
|
|
|
|
interface_info=MENU_NODE_INFO,
|
|
|
|
object_path=self.DBUS_SERVICE_PATH,
|
|
|
|
bus=session_bus
|
|
|
|
)
|
|
|
|
|
|
|
|
self.set_items(items)
|
|
|
|
|
|
|
|
def set_items(self, items):
|
|
|
|
self.items = items
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
self.idToItems = self.getItemsFlat(items, {})
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
self.revision += 1
|
|
|
|
|
|
|
|
self.LayoutUpdated(self.revision, 0)
|
|
|
|
|
|
|
|
@staticmethod
|
2024-07-08 12:34:05 +02:00
|
|
|
def getItemsFlat(items, idToItems):
|
2023-12-31 00:02:06 +01:00
|
|
|
for item in items:
|
|
|
|
if item.get('hidden', False) == True:
|
|
|
|
continue
|
2024-07-08 12:34:05 +02:00
|
|
|
|
|
|
|
idToItems[item['id']] = item
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
if 'children' in item:
|
2024-07-08 12:34:05 +02:00
|
|
|
idToItems = DBusMenuService.getItemsFlat(item['children'], idToItems)
|
2023-12-31 00:02:06 +01:00
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
return idToItems
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2024-07-08 12:34:05 +02:00
|
|
|
def singleItemToDbus(item):
|
|
|
|
props = DBusMenuService.itemPropsToDbus(item)
|
2023-12-31 00:02:06 +01:00
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
return (item['id'], props)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def itemPropsToDbus(item):
|
|
|
|
result = {}
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
string_props = ['label', 'icon-name', 'type', 'children-display']
|
|
|
|
for key in string_props:
|
|
|
|
if key in item:
|
|
|
|
result[key] = GLib.Variant('s', item[key])
|
|
|
|
|
|
|
|
bool_props = ['enabled']
|
|
|
|
for key in bool_props:
|
|
|
|
if key in item:
|
|
|
|
result[key] = GLib.Variant('b', item[key])
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def itemToDbus(item, recursion_depth):
|
|
|
|
if item.get('hidden', False) == True:
|
|
|
|
return None
|
|
|
|
|
|
|
|
props = DBusMenuService.itemPropsToDbus(item)
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
children = []
|
|
|
|
if recursion_depth > 1 or recursion_depth == -1:
|
|
|
|
if "children" in item:
|
|
|
|
children = [DBusMenuService.itemToDbus(item, recursion_depth - 1) for item in item['children']]
|
|
|
|
children = [i for i in children if i is not None]
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
return GLib.Variant("(ia{sv}av)", (item['id'], props, children))
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
def findItemsWithParent(self, parent_id, items):
|
|
|
|
for item in items:
|
|
|
|
if item.get('hidden', False) == True:
|
|
|
|
continue
|
|
|
|
if 'children' in item:
|
|
|
|
if item['id'] == parent_id:
|
|
|
|
return item['children']
|
|
|
|
else:
|
|
|
|
ret = self.findItemsWithParent(parent_id, item['children'])
|
|
|
|
if ret is not None:
|
|
|
|
return ret
|
|
|
|
return None
|
|
|
|
|
|
|
|
def GetLayout(self, parent_id, recursion_depth, property_names):
|
|
|
|
children = []
|
|
|
|
|
|
|
|
if parent_id == 0:
|
|
|
|
children = self.items
|
|
|
|
else:
|
|
|
|
children = self.findItemsWithParent(parent_id, self.items)
|
|
|
|
if children is None:
|
|
|
|
children = []
|
|
|
|
|
|
|
|
children = [self.itemToDbus(item, recursion_depth) for item in children]
|
|
|
|
children = [i for i in children if i is not None]
|
|
|
|
|
|
|
|
ret = (
|
|
|
|
self.revision,
|
|
|
|
(
|
|
|
|
0,
|
|
|
|
{ 'children-display': GLib.Variant('s', 'submenu') },
|
|
|
|
children
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
def GetGroupProperties(self, ids, property_names):
|
|
|
|
ret = []
|
|
|
|
|
|
|
|
for idx in ids:
|
|
|
|
if idx in self.idToItems:
|
|
|
|
props = DBusMenuService.singleItemToDbus(self.idToItems[idx])
|
|
|
|
if props is not None:
|
|
|
|
ret.append(props)
|
|
|
|
|
|
|
|
return (ret,)
|
|
|
|
|
|
|
|
def GetProperty(self, idx, name):
|
|
|
|
ret = None
|
|
|
|
|
|
|
|
if idx in self.idToItems:
|
|
|
|
props = DBusMenuService.singleItemToDbus(self.idToItems[idx])
|
|
|
|
if props is not None and name in props:
|
|
|
|
ret = props[name]
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def Event(self, idx, event_id, data, timestamp):
|
|
|
|
if event_id != "clicked":
|
|
|
|
return
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
if idx in self.idToItems:
|
|
|
|
item = self.idToItems[idx]
|
|
|
|
if 'callback' in item:
|
|
|
|
item['callback']()
|
|
|
|
|
|
|
|
def EventGroup(self, events):
|
|
|
|
not_found = []
|
|
|
|
|
|
|
|
for (idx, event_id, data, timestamp) in events:
|
|
|
|
if idx not in self.idToItems:
|
|
|
|
not_found.append(idx)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if event_id != "clicked":
|
|
|
|
continue
|
|
|
|
|
|
|
|
item = self.idToItems[idx]
|
|
|
|
if 'callback' in item:
|
|
|
|
item['callback']()
|
|
|
|
|
|
|
|
return not_found
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
def AboutToShow(self, item_id):
|
|
|
|
return (False,)
|
|
|
|
|
2024-07-08 12:34:05 +02:00
|
|
|
def AboutToShowGroup(self, ids):
|
|
|
|
not_found = []
|
|
|
|
|
|
|
|
for idx in ids:
|
|
|
|
if idx not in self.idToItems:
|
|
|
|
not_found.append(idx)
|
|
|
|
continue
|
|
|
|
|
|
|
|
return ([], not_found)
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def LayoutUpdated(self, revision, parent):
|
|
|
|
self.emit_signal(
|
|
|
|
'LayoutUpdated',
|
|
|
|
(revision, parent)
|
|
|
|
)
|
|
|
|
|
|
|
|
class StatusNotifierItemService(DBusService):
|
|
|
|
DBUS_SERVICE_PATH = '/io/github/slgobinath/SafeEyes'
|
|
|
|
|
|
|
|
Category = 'ApplicationStatus'
|
|
|
|
Id = 'io.github.slgobinath.SafeEyes'
|
|
|
|
Title = _('Safe Eyes')
|
|
|
|
Status = 'Active'
|
|
|
|
IconName = 'io.github.slgobinath.SafeEyes-enabled'
|
|
|
|
IconThemePath = ''
|
|
|
|
ToolTip = ('', [], _('Safe Eyes'), '')
|
|
|
|
ItemIsMenu = True
|
|
|
|
Menu = None
|
|
|
|
|
|
|
|
def __init__(self, session_bus, context, menu_items):
|
|
|
|
super().__init__(
|
|
|
|
interface_info=SNI_NODE_INFO,
|
|
|
|
object_path=self.DBUS_SERVICE_PATH,
|
|
|
|
bus=session_bus
|
|
|
|
)
|
|
|
|
|
|
|
|
self.bus = session_bus
|
|
|
|
|
|
|
|
self._menu = DBusMenuService(session_bus, context, menu_items)
|
|
|
|
self.Menu = self._menu.DBUS_SERVICE_PATH
|
|
|
|
|
|
|
|
def register(self):
|
|
|
|
self._menu.register()
|
|
|
|
super().register()
|
|
|
|
|
|
|
|
watcher = Gio.DBusProxy.new_sync(
|
|
|
|
connection=self.bus,
|
|
|
|
flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
|
|
|
|
info=None,
|
|
|
|
name='org.kde.StatusNotifierWatcher',
|
|
|
|
object_path='/StatusNotifierWatcher',
|
|
|
|
interface_name='org.kde.StatusNotifierWatcher',
|
|
|
|
cancellable=None,
|
|
|
|
)
|
|
|
|
|
|
|
|
watcher.RegisterStatusNotifierItem('(s)', self.DBUS_SERVICE_PATH)
|
|
|
|
|
|
|
|
def unregister(self):
|
|
|
|
super().unregister()
|
|
|
|
self._menu.unregister()
|
|
|
|
|
|
|
|
def set_items(self, items):
|
|
|
|
self._menu.set_items(items)
|
|
|
|
|
|
|
|
def set_icon(self, icon):
|
|
|
|
self.IconName = icon
|
|
|
|
|
|
|
|
self.emit_signal(
|
|
|
|
'NewIcon'
|
|
|
|
)
|
|
|
|
|
|
|
|
def set_tooltip(self, title, description):
|
|
|
|
self.ToolTip = ('', [], title, description)
|
|
|
|
|
|
|
|
self.emit_signal(
|
|
|
|
'NewTooltip'
|
|
|
|
)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2020-03-18 13:33:11 +01:00
|
|
|
class TrayIcon:
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Create and show the tray icon along with the tray menu.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, context, plugin_config):
|
|
|
|
self.context = context
|
|
|
|
self.on_show_settings = context['api']['show_settings']
|
|
|
|
self.on_show_about = context['api']['show_about']
|
|
|
|
self.quit = context['api']['quit']
|
2023-11-30 15:57:37 +01:00
|
|
|
self.enable_safeeyes = context['api']['enable_safeeyes']
|
|
|
|
self.disable_safeeyes = context['api']['disable_safeeyes']
|
2017-10-07 15:10:31 +02:00
|
|
|
self.take_break = context['api']['take_break']
|
|
|
|
self.has_breaks = context['api']['has_breaks']
|
2023-02-02 20:19:41 +01:00
|
|
|
self.get_break_time = context['api']['get_break_time']
|
2017-10-07 15:10:31 +02:00
|
|
|
self.plugin_config = plugin_config
|
|
|
|
self.date_time = None
|
|
|
|
self.active = True
|
|
|
|
self.wakeup_time = None
|
|
|
|
self.idle_condition = threading.Condition()
|
|
|
|
self.lock = threading.Lock()
|
2019-02-24 01:14:26 +01:00
|
|
|
self.allow_disabling = plugin_config['allow_disabling']
|
2019-03-14 01:28:30 +01:00
|
|
|
self.animate = False
|
2023-12-31 00:02:06 +01:00
|
|
|
self.menu_locked = False
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
self.sni_service = StatusNotifierItemService(
|
|
|
|
session_bus,
|
|
|
|
context,
|
|
|
|
menu_items = self.get_items()
|
|
|
|
)
|
|
|
|
self.sni_service.register()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
self.update_tooltip()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def initialize(self, plugin_config):
|
|
|
|
"""
|
|
|
|
Initialize the tray icon by setting the config.
|
|
|
|
"""
|
|
|
|
self.plugin_config = plugin_config
|
2019-02-24 01:14:26 +01:00
|
|
|
self.allow_disabling = plugin_config['allow_disabling']
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
self.update_menu()
|
|
|
|
self.update_tooltip()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def get_items(self):
|
2017-10-07 15:10:31 +02:00
|
|
|
breaks_found = self.has_breaks()
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
info_message = _('No breaks available')
|
|
|
|
|
2017-10-07 15:10:31 +02:00
|
|
|
if breaks_found:
|
|
|
|
if self.active:
|
2023-12-31 00:02:06 +01:00
|
|
|
next_break = self.get_next_break_time()
|
|
|
|
|
|
|
|
if next_break is not None:
|
|
|
|
(next_time, next_long_time, next_is_long) = next_break
|
|
|
|
|
|
|
|
if next_long_time:
|
|
|
|
if next_is_long:
|
|
|
|
info_message = _('Next long break at %s') % (next_long_time)
|
|
|
|
else:
|
|
|
|
info_message = _('Next breaks at %s/%s') % (next_time, next_long_time)
|
|
|
|
else:
|
|
|
|
info_message = _('Next break at %s') % (next_time)
|
2017-10-07 15:10:31 +02:00
|
|
|
else:
|
|
|
|
if self.wakeup_time:
|
2023-12-31 00:02:06 +01:00
|
|
|
info_message = _('Disabled until %s') % utility.format_time(self.wakeup_time)
|
|
|
|
else:
|
|
|
|
info_message = _('Disabled until restart')
|
|
|
|
|
|
|
|
disable_items = []
|
|
|
|
|
|
|
|
if self.allow_disabling:
|
|
|
|
disable_option_dynamic_id = 13
|
|
|
|
|
|
|
|
for disable_option in self.plugin_config['disable_options']:
|
|
|
|
time_in_minutes = time_in_x = disable_option['time']
|
|
|
|
label = []
|
|
|
|
# Validate time value
|
|
|
|
if not isinstance(time_in_minutes, int) or time_in_minutes <= 0:
|
|
|
|
logging.error('Invalid time in disable option: ' + str(time_in_minutes))
|
|
|
|
continue
|
|
|
|
time_unit = disable_option['unit'].lower()
|
|
|
|
if time_unit == 'seconds' or time_unit == 'second':
|
|
|
|
time_in_minutes = int(time_in_minutes / 60)
|
|
|
|
label = ['For %d Second', 'For %d Seconds']
|
|
|
|
elif time_unit == 'minutes' or time_unit == 'minute':
|
|
|
|
time_in_minutes = int(time_in_minutes * 1)
|
|
|
|
label = ['For %d Minute', 'For %d Minutes']
|
|
|
|
elif time_unit == 'hours' or time_unit == 'hour':
|
|
|
|
time_in_minutes = int(time_in_minutes * 60)
|
|
|
|
label = ['For %d Hour', 'For %d Hours']
|
2017-10-07 15:10:31 +02:00
|
|
|
else:
|
2023-12-31 00:02:06 +01:00
|
|
|
# Invalid unit
|
|
|
|
logging.error('Invalid unit in disable option: ' + str(disable_option))
|
|
|
|
continue
|
|
|
|
|
|
|
|
label = self.context['locale'].ngettext(label[0], label[1], time_in_x) % time_in_x
|
|
|
|
|
|
|
|
disable_items.append({
|
|
|
|
'id': disable_option_dynamic_id,
|
|
|
|
'label': label,
|
|
|
|
'callback': lambda: self.on_disable_clicked(time_in_minutes),
|
|
|
|
})
|
|
|
|
|
|
|
|
disable_option_dynamic_id += 1
|
|
|
|
|
|
|
|
disable_items.append({
|
|
|
|
'id': 12,
|
|
|
|
'label': _('Until restart'),
|
|
|
|
'callback': lambda: self.on_disable_clicked(-1),
|
|
|
|
})
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
'id': 1,
|
|
|
|
'label': info_message,
|
|
|
|
'icon-name': "io.github.slgobinath.SafeEyes-timer",
|
|
|
|
'enabled': breaks_found and self.active,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 2,
|
|
|
|
'type': "separator",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 3,
|
|
|
|
'label': _("Enable Safe Eyes"),
|
|
|
|
'enabled': breaks_found and not self.active,
|
|
|
|
'callback': self.on_enable_clicked,
|
|
|
|
'hidden': not self.allow_disabling,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 4,
|
|
|
|
'label': _("Disable Safe Eyes"),
|
|
|
|
'enabled': breaks_found and self.active and not self.menu_locked,
|
|
|
|
'children-display': 'submenu',
|
|
|
|
'children': disable_items,
|
|
|
|
'hidden': not self.allow_disabling,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 5,
|
|
|
|
'label': _('Take a break now'),
|
|
|
|
'enabled': breaks_found and self.active and not self.menu_locked,
|
|
|
|
'children-display': 'submenu',
|
|
|
|
'children': [
|
|
|
|
{
|
|
|
|
'id': 9,
|
|
|
|
'label': _('Any Break'),
|
|
|
|
'callback': lambda: self.on_manual_break_clicked(None),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 10,
|
|
|
|
'label': _('Short Break'),
|
|
|
|
'callback': lambda: self.on_manual_break_clicked(BreakType.SHORT_BREAK),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 11,
|
|
|
|
'label': _('Long Break'),
|
|
|
|
'callback': lambda: self.on_manual_break_clicked(BreakType.LONG_BREAK),
|
|
|
|
},
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 6,
|
|
|
|
'label': _('Settings'),
|
|
|
|
'enabled': not self.menu_locked,
|
|
|
|
'callback': self.show_settings,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 7,
|
|
|
|
'label': _('About'),
|
|
|
|
'callback': self.show_about,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 8,
|
|
|
|
'label': _('Quit'),
|
|
|
|
'enabled': not self.menu_locked,
|
|
|
|
'callback': self.quit_safe_eyes,
|
|
|
|
'hidden': not self.allow_disabling,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
def update_menu(self):
|
|
|
|
self.sni_service.set_items(self.get_items())
|
|
|
|
|
|
|
|
def update_tooltip(self):
|
|
|
|
next_break = self.get_next_break_time()
|
|
|
|
|
|
|
|
if next_break is not None and self.plugin_config.get('show_time_in_tray', False):
|
|
|
|
(next_time, next_long_time, _next_is_long) = next_break
|
|
|
|
|
|
|
|
if next_long_time and self.plugin_config.get('show_long_time_in_tray', False):
|
|
|
|
description = next_long_time
|
|
|
|
else:
|
|
|
|
description = next_time
|
2017-10-07 15:10:31 +02:00
|
|
|
else:
|
2023-12-31 00:02:06 +01:00
|
|
|
description = ''
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
self.sni_service.set_tooltip(_('Safe Eyes'), description)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def quit_safe_eyes(self):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Handle Quit menu action.
|
|
|
|
This action terminates the application.
|
|
|
|
"""
|
|
|
|
with self.lock:
|
|
|
|
self.active = True
|
|
|
|
# Notify all schedulers
|
|
|
|
self.idle_condition.acquire()
|
|
|
|
self.idle_condition.notify_all()
|
|
|
|
self.idle_condition.release()
|
2019-02-23 15:57:53 +01:00
|
|
|
self.quit()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def show_settings(self):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Handle Settings menu action.
|
|
|
|
This action shows the Settings dialog.
|
|
|
|
"""
|
|
|
|
self.on_show_settings()
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def show_about(self):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Handle About menu action.
|
|
|
|
This action shows the About dialog.
|
|
|
|
"""
|
|
|
|
self.on_show_about()
|
|
|
|
|
|
|
|
def next_break_time(self, dateTime):
|
|
|
|
"""
|
|
|
|
Update the next break time to be displayed in the menu and optionally in the tray icon.
|
|
|
|
"""
|
|
|
|
logging.info("Update next break information")
|
|
|
|
self.date_time = dateTime
|
2023-12-31 00:02:06 +01:00
|
|
|
self.update_menu()
|
|
|
|
self.update_tooltip()
|
|
|
|
|
|
|
|
def get_next_break_time(self):
|
|
|
|
if not (self.has_breaks() and self.active and self.date_time):
|
|
|
|
return None
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-02-02 20:19:41 +01:00
|
|
|
formatted_time = utility.format_time(self.get_break_time())
|
|
|
|
long_time = self.get_break_time(BreakType.LONG_BREAK)
|
|
|
|
|
|
|
|
if long_time:
|
|
|
|
long_time = utility.format_time(long_time)
|
|
|
|
if long_time == formatted_time:
|
2023-12-31 00:02:06 +01:00
|
|
|
return (long_time, long_time, True)
|
2023-02-02 20:19:41 +01:00
|
|
|
else:
|
2023-12-31 00:02:06 +01:00
|
|
|
return (formatted_time, long_time, False)
|
2023-02-02 20:19:41 +01:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
return (formatted_time, None, False)
|
2023-02-02 20:19:41 +01:00
|
|
|
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def on_manual_break_clicked(self, break_type):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Trigger a break manually.
|
|
|
|
"""
|
2023-12-31 00:02:06 +01:00
|
|
|
self.take_break(break_type)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def on_enable_clicked(self):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Handle 'Enable Safe Eyes' menu action.
|
|
|
|
This action enables the application if it is currently disabled.
|
|
|
|
"""
|
|
|
|
if not self.active:
|
|
|
|
with self.lock:
|
|
|
|
self.enable_ui()
|
2023-11-30 15:57:37 +01:00
|
|
|
self.enable_safeeyes()
|
2017-10-07 15:10:31 +02:00
|
|
|
# Notify all schedulers
|
|
|
|
self.idle_condition.acquire()
|
|
|
|
self.idle_condition.notify_all()
|
|
|
|
self.idle_condition.release()
|
|
|
|
|
2023-12-31 00:02:06 +01:00
|
|
|
def on_disable_clicked(self, time_to_wait):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Handle the menu actions of all the sub menus of 'Disable Safe Eyes'.
|
|
|
|
This action disables the application if it is currently active.
|
|
|
|
"""
|
2023-12-31 00:02:06 +01:00
|
|
|
if self.active:
|
2017-10-07 15:10:31 +02:00
|
|
|
self.disable_ui()
|
|
|
|
|
|
|
|
if time_to_wait <= 0:
|
2018-01-29 02:16:02 +01:00
|
|
|
info = _('Disabled until restart')
|
2023-11-30 15:57:37 +01:00
|
|
|
self.disable_safeeyes(info)
|
2017-10-07 15:10:31 +02:00
|
|
|
self.wakeup_time = None
|
|
|
|
else:
|
|
|
|
self.wakeup_time = datetime.datetime.now() + datetime.timedelta(minutes=time_to_wait)
|
2020-03-18 13:33:11 +01:00
|
|
|
info = _('Disabled until %s') % utility.format_time(self.wakeup_time)
|
2023-11-30 15:57:37 +01:00
|
|
|
self.disable_safeeyes(info)
|
2020-03-18 13:33:11 +01:00
|
|
|
utility.start_thread(self.__schedule_resume, time_minutes=time_to_wait)
|
2023-12-31 00:02:06 +01:00
|
|
|
self.update_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def lock_menu(self):
|
|
|
|
"""
|
|
|
|
This method is called by the core to prevent user from disabling Safe Eyes after the notification.
|
|
|
|
"""
|
|
|
|
if self.active:
|
2023-12-31 00:02:06 +01:00
|
|
|
self.menu_locked = True
|
|
|
|
self.update_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def unlock_menu(self):
|
|
|
|
"""
|
|
|
|
This method is called by the core to activate the menu after the the break.
|
|
|
|
"""
|
|
|
|
if self.active:
|
2023-12-31 00:02:06 +01:00
|
|
|
self.menu_locked = False
|
|
|
|
self.update_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def disable_ui(self):
|
|
|
|
"""
|
|
|
|
Change the UI to disabled state.
|
|
|
|
"""
|
|
|
|
if self.active:
|
|
|
|
logging.info('Disable Safe Eyes')
|
|
|
|
self.active = False
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled")
|
|
|
|
self.update_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def enable_ui(self):
|
|
|
|
"""
|
|
|
|
Change the UI to enabled state.
|
|
|
|
"""
|
|
|
|
if not self.active:
|
|
|
|
logging.info('Enable Safe Eyes')
|
|
|
|
self.active = True
|
2023-12-31 00:02:06 +01:00
|
|
|
|
|
|
|
self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled")
|
|
|
|
self.update_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def __schedule_resume(self, time_minutes):
|
|
|
|
"""
|
|
|
|
Schedule a local timer to enable Safe Eyes after the given timeout.
|
|
|
|
"""
|
|
|
|
self.idle_condition.acquire()
|
|
|
|
self.idle_condition.wait(time_minutes * 60) # Convert to seconds
|
|
|
|
self.idle_condition.release()
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
if not self.active:
|
2023-12-31 00:02:06 +01:00
|
|
|
utility.execute_main_thread(self.on_enable_clicked)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2019-03-14 01:28:30 +01:00
|
|
|
def start_animation(self):
|
|
|
|
if not self.active or not self.animate:
|
|
|
|
return
|
2023-12-31 00:02:06 +01:00
|
|
|
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled"))
|
2019-03-14 01:28:30 +01:00
|
|
|
time.sleep(0.5)
|
2023-12-31 00:02:06 +01:00
|
|
|
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled"))
|
2019-03-14 01:28:30 +01:00
|
|
|
if self.animate and self.active:
|
|
|
|
time.sleep(0.5)
|
|
|
|
if self.animate and self.active:
|
2020-03-18 13:33:11 +01:00
|
|
|
utility.start_thread(self.start_animation)
|
2019-03-14 01:28:30 +01:00
|
|
|
|
|
|
|
def stop_animation(self):
|
|
|
|
self.animate = False
|
|
|
|
if self.active:
|
2023-12-31 00:02:06 +01:00
|
|
|
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled"))
|
2019-03-14 01:28:30 +01:00
|
|
|
else:
|
2023-12-31 00:02:06 +01:00
|
|
|
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled"))
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
def init(ctx, safeeyes_cfg, plugin_config):
|
|
|
|
"""
|
|
|
|
Initialize the tray icon.
|
|
|
|
"""
|
|
|
|
global context
|
|
|
|
global tray_icon
|
|
|
|
global safeeyes_config
|
|
|
|
logging.debug('Initialize Tray Icon plugin')
|
|
|
|
context = ctx
|
|
|
|
safeeyes_config = safeeyes_cfg
|
|
|
|
if not tray_icon:
|
|
|
|
tray_icon = TrayIcon(context, plugin_config)
|
|
|
|
else:
|
|
|
|
tray_icon.initialize(plugin_config)
|
|
|
|
|
|
|
|
|
2017-10-17 20:50:57 +02:00
|
|
|
def update_next_break(break_obj, next_break_time):
|
2017-10-07 15:10:31 +02:00
|
|
|
"""
|
|
|
|
Update the next break time.
|
|
|
|
"""
|
2017-10-17 20:50:57 +02:00
|
|
|
tray_icon.next_break_time(next_break_time)
|
2017-10-07 15:10:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
def on_pre_break(break_obj):
|
|
|
|
"""
|
|
|
|
Disable the menu if strict_break is enabled
|
|
|
|
"""
|
|
|
|
if safeeyes_config.get('strict_break'):
|
|
|
|
tray_icon.lock_menu()
|
2019-03-14 01:28:30 +01:00
|
|
|
tray_icon.animate = True
|
|
|
|
tray_icon.start_animation()
|
|
|
|
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2019-03-14 01:28:30 +01:00
|
|
|
def on_start_break(break_obj):
|
|
|
|
tray_icon.stop_animation()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2020-04-12 14:39:24 +02:00
|
|
|
|
|
|
|
def on_stop_break():
|
|
|
|
tray_icon.unlock_menu()
|
2017-10-07 15:10:31 +02:00
|
|
|
|
2017-11-11 16:53:24 +01:00
|
|
|
|
2017-10-07 15:10:31 +02:00
|
|
|
def on_start():
|
|
|
|
"""
|
|
|
|
Enable the tray icon.
|
|
|
|
"""
|
|
|
|
tray_icon.enable_ui()
|
|
|
|
|
2017-11-11 16:53:24 +01:00
|
|
|
|
2017-10-07 15:10:31 +02:00
|
|
|
def on_stop():
|
|
|
|
"""
|
|
|
|
Disable the tray icon.
|
|
|
|
"""
|
2017-11-11 16:53:24 +01:00
|
|
|
tray_icon.disable_ui()
|