Code refactored and audible alert is added

This commit is contained in:
Gobinath 2017-01-31 19:20:17 -05:00
parent f81a5fe264
commit d1f9042ac7
11 changed files with 257 additions and 115 deletions

View File

@ -20,19 +20,42 @@ import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkX11
"""
AboutDialog reads the about_dialog.glade and build the user interface using that file.
It shows the application name with version, a small description, license and the GitHub url.
"""
class AboutDialog:
"""
Read the about_dialog.glade and build the user interface.
"""
def __init__(self, glade_file, version):
builder = Gtk.Builder()
builder.add_from_file(glade_file)
builder.connect_signals(self)
self.window = builder.get_object("window_about")
# Set the version at the runtime
builder.get_object("lbl_app_name").set_label("Safe Eyes " + version)
"""
Show the About dialog.
"""
def show(self):
self.window.show_all()
"""
Window close event handler.
"""
def on_window_delete(self, *args):
self.window.destroy()
"""
Close button click event handler.
"""
def on_close_clicked(self, button):
self.window.destroy()

View File

@ -23,13 +23,18 @@ import threading
import logging
from Xlib import Xatom, Xutil
from Xlib.display import Display, X
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkX11
class BreakScreen:
"""Full screen break window"""
"""
The fullscreen window which prevents users from using the computer.
"""
class BreakScreen:
"""
Read the break_screen.glade and build the user interface.
"""
def __init__(self, on_skip, glade_file, style_sheet_path):
self.on_skip = on_skip
self.is_pretified = False
@ -52,52 +57,58 @@ class BreakScreen:
self.skip_button_text = language['ui_controls']['skip']
self.strict_break = config['strict_break']
"""
Window close event handler.
"""
def on_window_delete(self, *args):
logging.info("Closing the break screen")
self.lock_keyboard = False
self.close()
"""
Skip button press event handler.
"""
def on_skip_clicked(self, button):
logging.info("User skipped the break")
self.on_skip()
self.close()
"""
Show/update the count down on all screens.
"""
def show_count_down(self, count):
GLib.idle_add(lambda: self.__show_count_down(count))
GLib.idle_add(lambda: self.__update_count_down(count))
def __show_count_down(self, count):
for label in self.count_labels:
label.set_text(count)
"""
Show the break screen with the given message on all displays.
"""
def show_message(self, message):
GLib.idle_add(lambda: self.__show_message(message))
"""
Lock the keyboard to prevent the user from using keyboard shortcuts
"""
def block_keyboard(self):
logging.info("Lock the keyboard")
self.lock_keyboard = True
display = Display()
root = display.screen().root
# Grap the keyboard
root.grab_keyboard(owner_events = False, pointer_mode = X.GrabModeAsync, keyboard_mode = X.GrabModeAsync, time = X.CurrentTime)
# Consume keyboard events
self.key_lock_condition.acquire()
while self.lock_keyboard:
self.key_lock_condition.wait()
self.key_lock_condition.release()
# Ungrap the keyboard
logging.info("Unlock the keyboard")
display.ungrab_keyboard(X.CurrentTime)
display.flush()
GLib.idle_add(lambda: self.__show_break_screen(message))
"""
Show an empty break screen on each non-active screens.
Hide the break screen from active window and destroy all other windows
"""
def show_break_screen(self, message):
def close(self):
logging.info("Close the break screen(s)")
self.__release_keyboard()
# Destroy other windows if exists
GLib.idle_add(lambda: self.__destroy_all_screens())
"""
Show an empty break screen on all screens.
"""
def __show_break_screen(self, message):
# Lock the keyboard
thread = threading.Thread(target=self.__lock_keyboard)
thread.start()
logging.info("Show break screens in all displays")
screen = Gtk.Window().get_screen()
no_of_monitors = screen.get_n_monitors()
@ -131,33 +142,52 @@ class BreakScreen:
window.set_keep_above(True)
window.present()
window.fullscreen()
def release_keyboard(self):
"""
Update the countdown on all break screens.
"""
def __update_count_down(self, count):
for label in self.count_labels:
label.set_text(count)
"""
Lock the keyboard to prevent the user from using keyboard shortcuts
"""
def __lock_keyboard(self):
logging.info("Lock the keyboard")
self.lock_keyboard = True
display = Display()
root = display.screen().root
# Grap the keyboard
root.grab_keyboard(owner_events = False, pointer_mode = X.GrabModeAsync, keyboard_mode = X.GrabModeAsync, time = X.CurrentTime)
# Consume keyboard events
self.key_lock_condition.acquire()
while self.lock_keyboard:
self.key_lock_condition.wait()
self.key_lock_condition.release()
# Ungrap the keyboard
logging.info("Unlock the keyboard")
display.ungrab_keyboard(X.CurrentTime)
display.flush()
"""
Release the locked keyboard.
"""
def __release_keyboard(self):
self.key_lock_condition.acquire()
self.lock_keyboard = False
self.key_lock_condition.notify()
self.key_lock_condition.release()
def __show_message(self, message):
# Lock the keyboard
thread = threading.Thread(target=self.block_keyboard)
thread.start()
self.show_break_screen(message)
"""
Hide the break screen from active window and destroy all other windows
Close all the break screens.
"""
def close(self):
logging.info("Close the break screen(s)")
self.release_keyboard()
# Destroy other windows if exists
GLib.idle_add(lambda: self.__close())
def __close(self):
def __destroy_all_screens(self):
for win in self.windows:
win.destroy()
del self.windows[:]

View File

@ -24,27 +24,48 @@ gi.require_version('Notify', '0.7')
from gi.repository import Gtk, Gdk, GLib, GdkX11
from gi.repository import AppIndicator3 as appindicator
from gi.repository import Notify
APPINDICATOR_ID = 'safeeyes'
"""
This class is responsible for the notification to the user before the break.
"""
class Notification:
"""
Initialize the notification.
"""
def __init__(self, language):
logging.info("Initialize the notification")
Notify.init(APPINDICATOR_ID)
self.language = language
"""
Show the notification"
"""
def show(self, warning_time):
logging.info("Show pre-break notification")
self.notification = Notify.Notification.new("Safe Eyes", "\n" + self.language['messages']['ready_for_a_break'].format(warning_time), icon="safeeyes_enabled")
self.notification.show()
"""
Close the notification if it is not closed by the system already.
"""
def close(self):
logging.info("Close pre-break notification")
try:
self.notification.close()
except:
logging.warning("Notification is already closed")
# Some Linux systems automatically close the notification.
pass
"""
Uninitialize the notification. Call this method when closing the application.
"""
def quite(self):
logging.info("Uninitialize Safe Eyes notification")
GLib.idle_add(lambda: Notify.uninit())

View File

@ -21,8 +21,15 @@ gi.require_version('Gdk', '3.0')
from gi.repository import Gdk, Gio, GLib, GdkX11
import time, datetime, threading, sys, subprocess, logging
"""
Core of Safe Eyes which runs the scheduler and notifies the breaks.
"""
class SafeEyesCore:
"""
Initialize the internal variables of the core.
"""
def __init__(self, show_notification, start_break, end_break, on_countdown, update_next_break_info):
# Initialize the variables
self.break_count = 0
@ -38,6 +45,7 @@ class SafeEyesCore:
self.notification_condition = threading.Condition()
self.break_condition = threading.Condition()
"""
Initialize the internal properties from configuration
"""
@ -51,10 +59,50 @@ class SafeEyesCore:
self.short_break_duration = config['short_break_duration']
self.break_interval = config['break_interval']
"""
Start Safe Eyes is it is not running already.
"""
def start(self):
if not self.active:
logging.info("Scheduling next break")
self.active = True
self.__schedule_next_break()
"""
Stop Safe Eyes if it is running.
"""
def stop(self):
if self.active:
logging.info("Stop the core")
# Reset the state properties in case of restart
# self.break_count = 0
# self.long_break_message_index = -1
# self.short_break_message_index = -1
self.notification_condition.acquire()
self.active = False
self.notification_condition.notify()
self.notification_condition.release()
# If waiting after notification, notify the thread to wake up and die
self.notification_condition.acquire()
self.notification_condition.notify()
self.notification_condition.release()
"""
User skipped the break using Skip button
"""
def skip_break(self):
self.skipped = True
"""
Scheduler task to execute during every interval
"""
def scheduler_job(self):
def __scheduler_job(self):
if not self.active:
return
@ -75,30 +123,29 @@ class SafeEyesCore:
logging.info("Ready to show the break")
GLib.idle_add(lambda: self.process_job())
GLib.idle_add(lambda: self.__process_job())
"""
Used to process the job in default thread because is_full_screen_app_found must be run by default thread
Used to process the job in default thread because __is_full_screen_app_found must be run by default thread
"""
def process_job(self):
if self.is_full_screen_app_found():
def __process_job(self):
if self.__is_full_screen_app_found():
# If full screen app found, do not show break screen
logging.info("Found a full-screen application. Skip the break")
if self.active:
# Schedule the break again
thread = threading.Thread(target=self.scheduler_job)
thread.start()
self.__schedule_next_break()
return
self.break_count = ((self.break_count + 1) % self.no_of_short_breaks_per_long_break)
thread = threading.Thread(target=self.notify_and_start_break)
thread = threading.Thread(target=self.__notify_and_start_break)
thread.start()
"""
Show notification and start the break after given number of seconds
"""
def notify_and_start_break(self):
def __notify_and_start_break(self):
# Show a notification
self.show_notification()
@ -111,7 +158,7 @@ class SafeEyesCore:
# User can disable SafeEyes during notification
if self.active:
message = ""
if self.is_long_break():
if self.__is_long_break():
logging.info("Count is {}; get a long beak message".format(self.break_count))
self.long_break_message_index = (self.long_break_message_index + 1) % len(self.long_break_exercises)
message = self.long_break_exercises[self.long_break_message_index]
@ -125,7 +172,7 @@ class SafeEyesCore:
# Start the countdown
seconds = 0
if self.is_long_break():
if self.__is_long_break():
seconds = self.long_break_duration
else:
seconds = self.short_break_duration
@ -145,58 +192,30 @@ class SafeEyesCore:
# Resume
if self.active:
# Schedule the break again
thread = threading.Thread(target=self.scheduler_job)
thread.start()
self.__schedule_next_break()
self.skipped = False
"""
Check if the current break is long break or short current
"""
def is_long_break(self):
def __is_long_break(self):
return self.break_count == self.no_of_short_breaks_per_long_break - 1
"""
User skipped the break using Skip button
"""
def skip_break(self):
self.skipped = True
"""
Stop Safe Eyes
Start a new thread to schedule the next break.
"""
def stop(self):
if self.active:
logging.info("Stop the core")
# Reset the state properties in case of restart
# self.break_count = 0
# self.long_break_message_index = -1
# self.short_break_message_index = -1
def __schedule_next_break(self):
thread = threading.Thread(target=self.__scheduler_job)
thread.start()
self.notification_condition.acquire()
self.active = False
self.notification_condition.notify()
self.notification_condition.release()
# If waiting after notification, notify the thread to wake up and die
self.notification_condition.acquire()
self.notification_condition.notify()
self.notification_condition.release()
"""
Start Safe Eyes
"""
def start(self):
if not self.active:
logging.info("Scheduling next break")
self.active = True
thread = threading.Thread(target=self.scheduler_job)
thread.start()
"""
Check for full-screen applications
"""
def is_full_screen_app_found(self):
def __is_full_screen_app_found(self):
logging.info("Searching for full-screen application")
screen = Gdk.Screen.get_default()
active_xid = str(screen.get_active_window().get_xid())
@ -210,4 +229,3 @@ class SafeEyesCore:
else:
if stdout:
return 'FULLSCREEN' in stdout

View File

@ -40,6 +40,7 @@ class SettingsDialog:
self.spin_short_between_long = builder.get_object('spin_short_between_long')
self.spin_time_to_prepare = builder.get_object('spin_time_to_prepare')
self.switch_strict_break = builder.get_object('switch_strict_break')
self.switch_audible_alert = builder.get_object('switch_audible_alert')
self.cmb_language = builder.get_object('cmb_language')
builder.get_object('lbl_short_break').set_label(language['ui_controls']['short_break_duration'])
@ -48,6 +49,7 @@ class SettingsDialog:
builder.get_object('lbl_short_per_long').set_label(language['ui_controls']['no_of_short_breaks_between_two_long_breaks'])
builder.get_object('lbl_time_to_prepare').set_label(language['ui_controls']['time_to_prepare_for_break'])
builder.get_object('lbl_strict_break').set_label(language['ui_controls']['strict_break'])
builder.get_object('lbl_audible_alert').set_label(language['ui_controls']['audible_alert'])
builder.get_object('lbl_language').set_label(language['ui_controls']['language'])
builder.get_object('btn_cancel').set_label(language['ui_controls']['cancel'])
builder.get_object('btn_save').set_label(language['ui_controls']['save'])
@ -58,6 +60,7 @@ class SettingsDialog:
self.spin_short_between_long.set_value(config['no_of_short_breaks_per_long_break'])
self.spin_time_to_prepare.set_value(config['pre_break_warning_time'])
self.switch_strict_break.set_active(config['strict_break'])
self.switch_audible_alert.set_active(config['audible_alert'])
# Initialize the language combobox
language_list_store = Gtk.ListStore(GObject.TYPE_STRING)
@ -90,6 +93,7 @@ class SettingsDialog:
self.config['no_of_short_breaks_per_long_break'] = self.spin_short_between_long.get_value_as_int()
self.config['pre_break_warning_time'] = self.spin_time_to_prepare.get_value_as_int()
self.config['strict_break'] = self.switch_strict_break.get_active()
self.config['audible_alert'] = self.switch_audible_alert.get_active()
self.config['language'] = self.languages[self.cmb_language.get_active()]
self.on_save_settings(self.config) # Call the provided save method

View File

@ -0,0 +1,28 @@
# 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 subprocess
"""
Play the alert.mp3
"""
def play_notification():
try:
subprocess.Popen(['mpg123', '-q', 'resource/alert.mp3'])
except:
pass

View File

@ -33,6 +33,7 @@
"no_of_short_breaks_between_two_long_breaks": "Number of short breaks between two long breaks",
"time_to_prepare_for_break": "Time to prepare for break (in seconds)",
"strict_break": "Strict break (Hide skip button)",
"audible_alert": "Audible alert at the end of break",
"language": "Language",
"enable": "Enable Safe Eyes",
"settings": "Settings",

View File

@ -8,5 +8,6 @@
"pre_break_warning_time": 10,
"short_break_duration": 15,
"strict_break": false,
"audible_alert": true,
"language": "en"
}

View File

@ -161,6 +161,32 @@
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="lbl_audible_alert">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Audible alert at the end of break</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="lbl_language">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Language</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="spin_short_break_duration">
<property name="visible">True</property>
@ -265,15 +291,12 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="lbl_language">
<object class="GtkSwitch" id="switch_audible_alert">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Language</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
@ -284,7 +307,7 @@
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
<property name="top_attach">7</property>
</packing>
</child>
</object>

Binary file not shown.

View File

@ -18,24 +18,15 @@
# 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 os
import gi
import json
import shutil
import errno
import dbus
import logging
import operator
import os, gi, json, shutil, errno, dbus, logging, operator, Utility
from threading import Timer
from dbus.mainloop.glib import DBusGMainLoop
from BreakScreen import BreakScreen
from TrayIcon import TrayIcon
from SettingsDialog import SettingsDialog
from AboutDialog import AboutDialog
from SafeEyesCore import SafeEyesCore
from Notification import Notification
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
@ -91,6 +82,8 @@ def show_alert(message):
def close_alert():
logging.info("Close the break screen")
break_screen.close()
if config['audible_alert']:
Utility.play_notification()
"""
Receive the count from core and pass it to the break screen.