Compare commits

...

4 Commits

Author SHA1 Message Date
deltragon 911e32aae1
Merge 3b792db159 into 00c33908a3 2024-02-23 23:40:55 +01:00
aerowolf 00c33908a3
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (116 of 116 strings)

Translation: Safe Eyes/Translations
Translate-URL: https://hosted.weblate.org/projects/safe-eyes/translations/zh_Hans/
2024-02-21 11:01:57 +01:00
deltragon 3b792db159
trayicon: migrate from libappindicator to statusnotifier 2023-12-31 00:02:06 +01:00
deltragon 8b5007c1c1
switch from dbus-python to gio 2023-12-31 00:01:57 +01:00
7 changed files with 546 additions and 267 deletions

View File

@ -67,7 +67,7 @@ sudo apt-get install safeeyes
### Fedora
```bash
sudo dnf install libappindicator-gtk3 python3-psutil cairo-devel python3-devel gobject-introspection-devel cairo-gobject-devel
sudo dnf install python3-psutil cairo-devel python3-devel gobject-introspection-devel cairo-gobject-devel
sudo pip3 install safeeyes
sudo gtk-update-icon-cache /usr/share/icons/hicolor
```
@ -95,9 +95,7 @@ flatpak install flathub io.github.slgobinath.SafeEyes
Ensure to meet the following dependencies:
- gir1.2-appindicator3-0.1 or gir1.2-ayatanaappindicator3-0.1
- gir1.2-notify-0.7
- libappindicator-gtk3
- python3-psutil
- xprintidle (optional)
- wlrctl (wayland optional)
@ -140,7 +138,7 @@ Some Linux systems like Cent OS do not have matching dependencies available in t
pip3 install virtualenv --user
virtualenv --no-site-packages venv
source venv/bin/activate
pip3 install dbus-python safeeyes
pip3 install safeeyes
```
3. Start Safe Eyes from terminal

2
debian/control vendored
View File

@ -9,7 +9,7 @@ Homepage: https://github.com/slgobinath/SafeEyes/
Package: safeeyes
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}, gir1.2-ayatanaappindicator3-0.1, python3 (>= 3.5.0), python3-xlib, python3-dbus, gir1.2-notify-0.7, python3-babel, x11-utils, xprintidle, alsa-utils, python3-psutil, python3-croniter
Depends: ${misc:Depends}, ${python3:Depends}, python3 (>= 3.5.0), python3-xlib, gir1.2-notify-0.7, python3-babel, x11-utils, xprintidle, alsa-utils, python3-psutil, python3-croniter
Description: Safe Eyes
Safe Eyes is a simple tool to remind you to take periodic breaks for your eyes. This is essential for anyone spending more time on the computer to avoid eye strain and other physical problems.
.

View File

@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2021-10-10 05:05+0000\n"
"Last-Translator: Frank.wu <me@wuzhiping.top>\n"
"PO-Revision-Date: 2024-02-21 10:01+0000\n"
"Last-Translator: aerowolf <aerowolf@tom.com>\n"
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
"safe-eyes/translations/zh_Hans/>\n"
"Language: zh_CN\n"
@ -15,23 +15,23 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.9-dev\n"
"X-Generator: Weblate 5.5-dev\n"
# Short break
msgid "Tightly close your eyes"
msgstr "闭上您的眼睛休息一会"
msgstr "闭上眼睛休息一下"
# Short break
msgid "Roll your eyes a few times to each side"
msgstr "向两边各翻几下眼睛"
msgstr "左右转动眼球"
# Short break
msgid "Rotate your eyes in clockwise direction"
msgstr "按顺时针方向旋转你的眼球"
msgstr "顺时针方向转动眼球"
# Short break
msgid "Rotate your eyes in counterclockwise direction"
msgstr "按逆时针方向旋转你的眼球"
msgstr "逆时针方向转动眼球"
# Short break
msgid "Blink your eyes"
@ -39,11 +39,11 @@ msgstr "眨眨眼"
# Short break
msgid "Focus on a point in the far distance"
msgstr "望远:将视线聚焦到远处物体上"
msgstr "远眺:看看尽可能远的远处"
# Short break
msgid "Have some water"
msgstr "喝点水"
msgstr "喝点水"
# Long break
msgid "Walk for a while"
@ -51,7 +51,7 @@ msgstr "站起来走走"
# Long break
msgid "Lean back at your seat and relax"
msgstr "靠在椅背上休息一"
msgstr "靠在椅背上休息一"
# Commandline arg description
msgid "show the about dialog"
@ -59,15 +59,15 @@ msgstr "显示“关于”对话框"
# Commandline arg description
msgid "disable the currently running safeeyes instance"
msgstr "禁用当前正在运行的Safe Eyes"
msgstr "暂停当前正在运行的Safe Eyes"
# Commandline arg description
msgid "enable the currently running safeeyes instance"
msgstr "启用当前正在运行的Safe Eyes"
msgstr "恢复当前正在运行的Safe Eyes"
# Commandline arg description
msgid "quit the running safeeyes instance and exit"
msgstr "退出正在运行的Safe Eeyes"
msgstr "退出Safe Eeyes"
# Commandline arg description
msgid "show the settings dialog"
@ -79,17 +79,17 @@ msgstr "以调试模式运行Safe Eyes"
# Commandline arg description
msgid "print the status of running safeeyes instance and exit"
msgstr "打印Safe Eyes运行状态后退出"
msgstr "显示Safe Eyes运行状态后退出"
# Status message
msgid "Safe Eyes is not running"
msgstr "Safe Eyes 尚未运行"
msgstr "Safe Eyes 没有运行"
# RPC not enabled message
msgid ""
"Safe Eyes is running without an RPC server. Turn it on to use command-line "
"arguments."
msgstr "Safe Eyes 正在没有RPC服务器的情况下运行。打开以使用命令行参数。"
msgstr "Safe Eyes 正在没有RPC服务器的情况下运行。打开RPC服务器可以使用命令行参数。"
# About dialog
msgid "Close"
@ -106,7 +106,7 @@ msgstr ""
# About dialog
msgid "License"
msgstr "许可"
msgstr "许可"
# Break screen
msgid "Skip"
@ -142,7 +142,7 @@ msgstr "以随机顺序显示休息"
# Settings dialog
msgid "Strict break (No way to skip breaks)"
msgstr "坚持休息(无法跳过)"
msgstr "坚持休息(不允许跳过)"
# Settings dialog
msgid "Allow postponing breaks"
@ -150,7 +150,7 @@ msgstr "允许推迟休息"
# Settings dialog
msgid "Persist the internal state"
msgstr "持内部状态"
msgstr "持内部状态"
# Settings dialog
msgid "Use RPC server to receive runtime commands"
@ -414,11 +414,11 @@ msgstr "在通知区域显示托盘图标"
#: plugins/trayicon
msgid "Show next break time in tray icon"
msgstr "在托盘图标显示下次休息时间"
msgstr "在托盘图标显示下次休息时间"
#: plugins/trayicon
msgid "Allow disabling Safe Eyes"
msgstr "允许禁用Safe Eyes"
msgstr "允许暂停Safe Eyes"
#: plugins/trayicon
msgid "About"
@ -426,19 +426,19 @@ msgstr "关于"
#: plugins/trayicon
msgid "Disable Safe Eyes"
msgstr "禁用 Safe Eyes"
msgstr "暂停 Safe Eyes"
#: plugins/trayicon
msgid "Disabled until %s"
msgstr "禁用软件直到 %s"
msgstr "暂停,直到 %s"
#: plugins/trayicon
msgid "Disabled until restart"
msgstr "禁用软件直到重启"
msgstr "暂停,直到重启"
#: plugins/trayicon
msgid "Enable Safe Eyes"
msgstr "启用 Safe Eyes"
msgstr "恢复 Safe Eyes"
#: plugins/trayicon
msgid "For %d Hour"
@ -457,7 +457,7 @@ msgstr[0] "%d 秒"
#: plugins/trayicon
msgid "Next break at %s"
msgstr "下次休息在 %s"
msgstr "下次休息在 %s"
#: plugins/trayicon
msgid "No Breaks Available"

View File

@ -5,7 +5,7 @@
"version": "0.0.1"
},
"dependencies": {
"python_modules": ["dbus"],
"python_modules": [],
"shell_commands": [],
"operating_systems": [],
"desktop_environments": [],

View File

@ -22,12 +22,11 @@ Media Control plugin lets users to pause currently playing media player from the
import logging
import os
import dbus
import re
import gi
from safeeyes.model import TrayAction
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gtk, Gio
tray_icon_path = None
@ -37,13 +36,31 @@ def __active_players():
List of all media players which are playing now.
"""
players = []
bus = dbus.SessionBus()
for service in bus.list_names():
dbus_proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SESSION,
flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
info=None,
name='org.freedesktop.DBus',
object_path='/org/freedesktop/DBus',
interface_name='org.freedesktop.DBus',
cancellable=None,
)
for service in dbus_proxy.ListNames():
if re.match('org.mpris.MediaPlayer2.', service):
player = bus.get_object(service, "/org/mpris/MediaPlayer2")
interface = dbus.Interface(player, 'org.freedesktop.DBus.Properties')
status = str(interface.Get('org.mpris.MediaPlayer2.Player', 'PlaybackStatus')).lower()
player = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SESSION,
flags=Gio.DBusProxyFlags.NONE,
info=None,
name=service,
object_path='/org/mpris/MediaPlayer2',
interface_name='org.mpris.MediaPlayer2.Player',
cancellable=None,
)
status = player.get_cached_property('PlaybackStatus').unpack().lower()
if status == "playing":
players.append(player)
return players
@ -54,8 +71,7 @@ def __pause_players(players):
Pause all playing media players using dbus.
"""
for player in players:
interface = dbus.Interface(player, dbus_interface='org.mpris.MediaPlayer2.Player')
interface.Pause()
player.Pause()
def init(ctx, safeeyes_config, plugin_config):

View File

@ -20,14 +20,7 @@ import datetime
from safeeyes.model import BreakType
import gi
gi.require_version('Gtk', '3.0')
try:
gi.require_version('AppIndicator3', '0.1')
from gi.repository import AppIndicator3 as appindicator
except:
#fall back to Ayatana
gi.require_version('AyatanaAppIndicator3', '0.1')
from gi.repository import AyatanaAppIndicator3 as appindicator
from gi.repository import Gtk
from gi.repository import Gio, GLib
import logging
from safeeyes import utility
import threading
@ -37,11 +30,291 @@ import time
Safe Eyes tray icon plugin
"""
APPINDICATOR_ID = 'safeeyes_2'
context = None
tray_icon = None
safeeyes_config = None
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>
<method name="Event">
<arg type="i" direction="in"/>
<arg type="s" direction="in"/>
<arg type="v" direction="in"/>
<arg type="u" direction="in"/>
</method>
<method name="AboutToShow">
<arg type="i" direction="in"/>
<arg type="b" direction="out"/>
</method>
<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 = []
idToCallback = {}
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
self.idToCallback = self.getItemCallbacks(items, {})
self.revision += 1
self.LayoutUpdated(self.revision, 0)
@staticmethod
def getItemCallbacks(items, idToCallback):
for item in items:
if item.get('hidden', False) == True:
continue
if 'callback' in item:
idToCallback[item['id']] = item['callback']
if 'children' in item:
idToCallback = DBusMenuService.getItemCallbacks(item['children'], idToCallback)
return idToCallback
@staticmethod
def itemToDbus(item, recursion_depth):
result = {}
if item.get('hidden', False) == True:
return None
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])
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]
return GLib.Variant("(ia{sv}av)", (item['id'], result, children))
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
def Event(self, idx, event_id, data, timestamp):
if event_id != "clicked":
return
if idx in self.idToCallback:
self.idToCallback[idx]()
def AboutToShow(self, item_id):
return (False,)
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'
)
class TrayIcon:
"""
@ -66,185 +339,185 @@ class TrayIcon:
self.lock = threading.Lock()
self.allow_disabling = plugin_config['allow_disabling']
self.animate = False
self.menu_locked = False
# Construct the tray icon
self.indicator = appindicator.Indicator.new(
APPINDICATOR_ID, "io.github.slgobinath.SafeEyes-enabled", appindicator.IndicatorCategory.APPLICATION_STATUS)
self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
session_bus = Gio.bus_get_sync(Gio.BusType.SESSION)
# Construct the context menu
self.menu = Gtk.Menu()
self.sni_service = StatusNotifierItemService(
session_bus,
context,
menu_items = self.get_items()
)
self.sni_service.register()
# Next break info menu item
self.item_info = Gtk.ImageMenuItem()
img_timer = Gtk.Image()
img_timer.set_from_icon_name("io.github.slgobinath.SafeEyes-timer", 16)
self.item_info.set_image(img_timer)
self.item_separator = Gtk.SeparatorMenuItem()
self.item_enable = Gtk.MenuItem()
self.item_enable.connect('activate', self.on_enable_clicked)
self.item_disable = Gtk.MenuItem()
self.sub_menu_disable = Gtk.Menu()
self.sub_menu_disable_items = []
# Read disable options and build the sub menu
for disable_option in plugin_config['disable_options']:
time_in_minutes = 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']
else:
# Invalid unit
logging.error('Invalid unit in disable option: ' + str(disable_option))
continue
# Create submenu
sub_menu_item = Gtk.MenuItem()
sub_menu_item.connect('activate', self.on_disable_clicked, time_in_minutes)
self.sub_menu_disable_items.append([sub_menu_item, label, disable_option['time']])
self.sub_menu_disable.append(sub_menu_item)
# Disable until restart submenu
self.sub_menu_item_until_restart = Gtk.MenuItem()
self.sub_menu_item_until_restart.connect('activate', self.on_disable_clicked, -1)
self.sub_menu_disable.append(self.sub_menu_item_until_restart)
# Add the sub menu to the enable/disable menu
self.item_disable.set_submenu(self.sub_menu_disable)
# Manual break menu item
self.item_manual_break = Gtk.MenuItem()
self.sub_menu_manual_next_break = Gtk.MenuItem()
self.sub_menu_manual_next_break.connect('activate', self.on_manual_break_clicked, None)
self.sub_menu_manual_next_short_break = Gtk.MenuItem()
self.sub_menu_manual_next_short_break.connect('activate', self.on_manual_break_clicked, BreakType.SHORT_BREAK)
self.sub_menu_manual_next_long_break = Gtk.MenuItem()
self.sub_menu_manual_next_long_break.connect('activate', self.on_manual_break_clicked, BreakType.LONG_BREAK)
self.sub_menu_manual_break = Gtk.Menu()
self.sub_menu_manual_break.append(self.sub_menu_manual_next_break)
self.sub_menu_manual_break.append(self.sub_menu_manual_next_short_break)
self.sub_menu_manual_break.append(self.sub_menu_manual_next_long_break)
self.item_manual_break.set_submenu(self.sub_menu_manual_break)
# Settings menu item
self.item_settings = Gtk.MenuItem()
self.item_settings.connect('activate', self.show_settings)
# About menu item
self.item_about = Gtk.MenuItem()
self.item_about.connect('activate', self.show_about)
# Quit menu item
self.item_quit = Gtk.MenuItem()
self.item_quit.connect('activate', self.quit_safe_eyes)
self.set_labels()
# At startup, no need for activate menu
self.item_enable.set_sensitive(False)
# Append all menu items and show the menu
self.menu.append(self.item_info)
self.menu.append(self.item_separator)
self.menu.append(self.item_enable)
self.menu.append(self.item_disable)
self.menu.append(self.item_manual_break)
self.menu.append(self.item_settings)
self.menu.append(self.item_about)
self.menu.append(self.item_quit)
self.menu.show_all()
self.item_enable.set_visible(self.allow_disabling)
self.item_disable.set_visible(self.allow_disabling)
self.item_quit.set_visible(self.allow_disabling)
self.item_quit.set_visible(self.allow_disabling)
self.indicator.set_menu(self.menu)
self.update_tooltip()
def initialize(self, plugin_config):
"""
Initialize the tray icon by setting the config.
"""
self.plugin_config = plugin_config
self.set_labels()
self.allow_disabling = plugin_config['allow_disabling']
self.item_enable.set_visible(self.allow_disabling)
self.item_disable.set_visible(self.allow_disabling)
self.item_quit.set_visible(self.allow_disabling)
self.item_quit.set_visible(self.allow_disabling)
def set_labels(self):
"""
Update the text of menu items based on the selected language.
"""
for entry in self.sub_menu_disable_items:
# print(self.context['locale'].ngettext('For %d Hour', 'For %d Hours', 1) % 1)
entry[0].set_label(self.context['locale'].ngettext(entry[1][0], entry[1][1], entry[2]) % entry[2])
self.sub_menu_item_until_restart.set_label(_('Until restart'))
self.item_enable.set_label(_('Enable Safe Eyes'))
self.item_disable.set_label(_('Disable Safe Eyes'))
self.update_menu()
self.update_tooltip()
def get_items(self):
breaks_found = self.has_breaks()
info_message = _('No breaks available')
if breaks_found:
if self.active:
if self.date_time:
self.__set_next_break_info()
self.indicator.set_icon("io.github.slgobinath.SafeEyes-enabled")
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)
else:
if self.wakeup_time:
self.item_info.set_label(_('Disabled until %s') % utility.format_time(self.wakeup_time))
info_message = _('Disabled until %s') % utility.format_time(self.wakeup_time)
else:
self.item_info.set_label(_('Disabled until restart'))
self.indicator.set_label('', '')
self.indicator.set_icon("io.github.slgobinath.SafeEyes-disabled")
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']
else:
# 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
else:
self.item_info.set_label(_('No breaks available'))
self.indicator.set_label('', '')
self.indicator.set_icon("io.github.slgobinath.SafeEyes-disabled")
self.item_info.set_sensitive(breaks_found and self.active)
self.item_enable.set_sensitive(breaks_found and not self.active)
self.item_disable.set_sensitive(breaks_found and self.active)
self.item_manual_break.set_sensitive(breaks_found and self.active)
description = ''
self.item_manual_break.set_label(_('Take a break now'))
self.sub_menu_manual_next_break.set_label(_('Any break'))
self.sub_menu_manual_next_short_break.set_label(_('Short break'))
self.sub_menu_manual_next_long_break.set_label(_('Long break'))
self.item_settings.set_label(_('Settings'))
self.item_about.set_label(_('About'))
self.item_quit.set_label(_('Quit'))
self.sni_service.set_tooltip(_('Safe Eyes'), description)
def show_icon(self):
"""
Show the tray icon.
"""
self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
def hide_icon(self):
"""
Hide the tray icon.
"""
self.indicator.set_status(appindicator.IndicatorStatus.PASSIVE)
def quit_safe_eyes(self, *args):
def quit_safe_eyes(self):
"""
Handle Quit menu action.
This action terminates the application.
@ -257,14 +530,14 @@ class TrayIcon:
self.idle_condition.release()
self.quit()
def show_settings(self, *args):
def show_settings(self):
"""
Handle Settings menu action.
This action shows the Settings dialog.
"""
self.on_show_settings()
def show_about(self, *args):
def show_about(self):
"""
Handle About menu action.
This action shows the About dialog.
@ -277,50 +550,37 @@ class TrayIcon:
"""
logging.info("Update next break information")
self.date_time = dateTime
self.__set_next_break_info()
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
def __set_next_break_info(self):
"""
A private method to be called within this class to update the next break information using self.dateTime.
"""
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:
message = _('Next long break at %s') % (long_time)
return (long_time, long_time, True)
else:
message = _('Next breaks at %s/%s') % (formatted_time, long_time)
else:
message = _('Next break at %s') % (formatted_time)
return (formatted_time, long_time, False)
# Update the menu item label
utility.execute_main_thread(self.item_info.set_label, message)
return (formatted_time, None, False)
# Update the tray icon label
if self.plugin_config.get('show_time_in_tray', False):
show_long = long_time and self.plugin_config.get('show_long_time_in_tray', False)
self.indicator.set_label(long_time if show_long else formatted_time, '')
else:
self.indicator.set_label('', '')
def on_manual_break_clicked(self, *args):
def on_manual_break_clicked(self, break_type):
"""
Trigger a break manually.
"""
if len(args) > 1:
break_type = args[1]
self.take_break(break_type)
else:
self.take_break()
self.take_break(break_type)
def on_enable_clicked(self, *args):
def on_enable_clicked(self):
"""
Handle 'Enable Safe Eyes' menu action.
This action enables the application if it is currently disabled.
"""
# active = self.item_enable.get_active()
if not self.active:
with self.lock:
self.enable_ui()
@ -330,41 +590,40 @@ class TrayIcon:
self.idle_condition.notify_all()
self.idle_condition.release()
def on_disable_clicked(self, *args):
def on_disable_clicked(self, time_to_wait):
"""
Handle the menu actions of all the sub menus of 'Disable Safe Eyes'.
This action disables the application if it is currently active.
"""
# active = self.item_enable.get_active()
if self.active and len(args) > 1:
if self.active:
self.disable_ui()
time_to_wait = args[1]
if time_to_wait <= 0:
info = _('Disabled until restart')
self.disable_safeeyes(info)
self.wakeup_time = None
self.item_info.set_label(info)
else:
self.wakeup_time = datetime.datetime.now() + datetime.timedelta(minutes=time_to_wait)
info = _('Disabled until %s') % utility.format_time(self.wakeup_time)
self.disable_safeeyes(info)
self.item_info.set_label(info)
utility.start_thread(self.__schedule_resume, time_minutes=time_to_wait)
self.update_menu()
def lock_menu(self):
"""
This method is called by the core to prevent user from disabling Safe Eyes after the notification.
"""
if self.active:
self.menu.set_sensitive(False)
self.menu_locked = True
self.update_menu()
def unlock_menu(self):
"""
This method is called by the core to activate the menu after the the break.
"""
if self.active:
self.menu.set_sensitive(True)
self.menu_locked = False
self.update_menu()
def disable_ui(self):
"""
@ -373,13 +632,9 @@ class TrayIcon:
if self.active:
logging.info('Disable Safe Eyes')
self.active = False
self.indicator.set_icon("io.github.slgobinath.SafeEyes-disabled")
self.item_info.set_label(_('Disabled until restart'))
self.indicator.set_label('', '')
self.item_info.set_sensitive(False)
self.item_enable.set_sensitive(True)
self.item_disable.set_sensitive(False)
self.item_manual_break.set_sensitive(False)
self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled")
self.update_menu()
def enable_ui(self):
"""
@ -388,11 +643,9 @@ class TrayIcon:
if not self.active:
logging.info('Enable Safe Eyes')
self.active = True
self.indicator.set_icon("io.github.slgobinath.SafeEyes-enabled")
self.item_info.set_sensitive(True)
self.item_enable.set_sensitive(False)
self.item_disable.set_sensitive(True)
self.item_manual_break.set_sensitive(True)
self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled")
self.update_menu()
def __schedule_resume(self, time_minutes):
"""
@ -404,14 +657,14 @@ class TrayIcon:
with self.lock:
if not self.active:
utility.execute_main_thread(self.item_enable.activate)
utility.execute_main_thread(self.on_enable_clicked)
def start_animation(self):
if not self.active or not self.animate:
return
utility.execute_main_thread(lambda: self.indicator.set_icon("io.github.slgobinath.SafeEyes-disabled"))
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled"))
time.sleep(0.5)
utility.execute_main_thread(lambda: self.indicator.set_icon("io.github.slgobinath.SafeEyes-enabled"))
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled"))
if self.animate and self.active:
time.sleep(0.5)
if self.animate and self.active:
@ -420,9 +673,9 @@ class TrayIcon:
def stop_animation(self):
self.animate = False
if self.active:
utility.execute_main_thread(lambda: self.indicator.set_icon("io.github.slgobinath.SafeEyes-enabled"))
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-enabled"))
else:
utility.execute_main_thread(lambda: self.indicator.set_icon("io.github.slgobinath.SafeEyes-disabled"))
utility.execute_main_thread(lambda: self.sni_service.set_icon("io.github.slgobinath.SafeEyes-disabled"))
def init(ctx, safeeyes_cfg, plugin_config):
"""

View File

@ -25,9 +25,7 @@ import logging
import os
from threading import Timer
import dbus
import gi
from dbus.mainloop.glib import DBusGMainLoop
from safeeyes import utility
from safeeyes.ui.about_dialog import AboutDialog
from safeeyes.ui.break_screen import BreakScreen
@ -38,7 +36,7 @@ from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gtk, Gio
SAFE_EYES_VERSION = "2.1.6"
@ -166,14 +164,28 @@ class SafeEyes:
self.plugins_manager.start()
self.safe_eyes_core.start()
def handle_suspend_signal(self, proxy, sender, signal, parameters):
if signal != "PrepareForSleep":
return
(sleeping, ) = parameters
self.handle_suspend_callback(sleeping)
def handle_system_suspend(self):
"""
Setup system suspend listener.
"""
DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
bus.add_signal_receiver(self.handle_suspend_callback, 'PrepareForSleep',
'org.freedesktop.login1.Manager', 'org.freedesktop.login1')
self.suspend_proxy = Gio.DBusProxy.new_for_bus_sync(
bus_type=Gio.BusType.SYSTEM,
flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
info=None,
name='org.freedesktop.login1',
object_path='/org/freedesktop/login1',
interface_name='org.freedesktop.login1.Manager',
cancellable=None,
)
self.suspend_proxy.connect('g-signal', self.handle_suspend_signal)
def on_skipped(self):
"""