From 3cd6ba4e825aa44f9f2caecac108bf2ed0448b67 Mon Sep 17 00:00:00 2001 From: Gobinath Date: Sun, 23 Oct 2016 17:26:54 +0530 Subject: [PATCH] Disable keyboard on break and bug fixes --- safeeyes/debian/changelog | 4 +- safeeyes/debian/control | 2 +- safeeyes/safeeyes/BreakScreen.py | 40 ++++- safeeyes/safeeyes/Notification.py | 2 +- safeeyes/safeeyes/SafeEyesCore.py | 148 ++++++++++-------- safeeyes/safeeyes/SettingsDialog.py | 6 +- safeeyes/safeeyes/TrayIcon.py | 2 +- safeeyes/safeeyes/config/safeeyes.json | 1 + .../safeeyes/config/style/safeeyes_style.css | 1 + safeeyes/safeeyes/glade/break_screen.glade | 2 +- safeeyes/safeeyes/glade/settings_dialog.glade | 2 +- safeeyes/safeeyes/safeeyes | 17 +- 12 files changed, 146 insertions(+), 81 deletions(-) diff --git a/safeeyes/debian/changelog b/safeeyes/debian/changelog index 962dc34..139f49e 100644 --- a/safeeyes/debian/changelog +++ b/safeeyes/debian/changelog @@ -1,5 +1,7 @@ -safeeyes (1.0.4-1) xenial; urgency=medium +safeeyes (1.0.5-1) xenial; urgency=medium + * Bug fixes for Ubuntu 14.04 and keyboard lock during break + * Reducing minimal Python requirement * Fixing appindicator version mismatch diff --git a/safeeyes/debian/control b/safeeyes/debian/control index b41a82e..58e0ee8 100644 --- a/safeeyes/debian/control +++ b/safeeyes/debian/control @@ -8,7 +8,7 @@ Homepage: https://github.com/slgobinath/SafeEyes/ Package: safeeyes Architecture: any -Depends: gir1.2-appindicator3-0.1, python (>= 2.7.0), python-apscheduler (>= 2.1.2) +Depends: gir1.2-appindicator3-0.1, python (>= 2.7.0), python-apscheduler (>= 2.1.2), python-xlib 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. . diff --git a/safeeyes/safeeyes/BreakScreen.py b/safeeyes/safeeyes/BreakScreen.py index 787fcef..2358aac 100644 --- a/safeeyes/safeeyes/BreakScreen.py +++ b/safeeyes/safeeyes/BreakScreen.py @@ -18,16 +18,21 @@ import gi import signal +from Xlib import Xatom, Xutil +from Xlib.display import Display, X +import sys, threading gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Gtk, Gdk, GLib, GdkX11 class BreakScreen: """Full screen break window""" + def __init__(self, on_skip, glade_file, style_sheet_path): self.on_skip = on_skip self.style_sheet = style_sheet_path self.is_pretified = False + self.key_lock_condition = threading.Condition() builder = Gtk.Builder() builder.add_from_file(glade_file) @@ -41,11 +46,12 @@ class BreakScreen: self.window.stick() self.window.set_keep_above(True) screen = self.window.get_screen() - self.window.resize(screen.get_width(), screen.get_height()) + # self.window.resize(screen.get_width(), screen.get_height()) """ Initialize the internal properties from configuration """ + def initialize(self, config): self.skip_button_text = config['skip_button_text'] self.strict_break = config['strict_break'] @@ -53,6 +59,7 @@ class BreakScreen: self.btn_skip.set_visible(not self.strict_break) def on_window_delete(self, *args): + self.lock_keyboard = False self.close() def on_skip_clicked(self, button): @@ -65,10 +72,35 @@ class BreakScreen: 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): + 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 + display.ungrab_keyboard(X.CurrentTime) + display.flush() + + 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): self.lbl_message.set_text(message) self.window.show_all() self.window.present() + self.window.fullscreen() # Set the style only for the first time if not self.is_pretified: @@ -81,7 +113,11 @@ class BreakScreen: # If the style is changed, the visibility must be redefined self.btn_skip.set_visible(not self.strict_break) + # Lock the keyboard + thread = threading.Thread(target=self.block_keyboard) + thread.start() def close(self): + self.release_keyboard() GLib.idle_add(lambda: self.window.hide()) diff --git a/safeeyes/safeeyes/Notification.py b/safeeyes/safeeyes/Notification.py index dd0f54c..6be6e1b 100644 --- a/safeeyes/safeeyes/Notification.py +++ b/safeeyes/safeeyes/Notification.py @@ -20,7 +20,7 @@ import gi gi.require_version('Gtk', '3.0') gi.require_version('AppIndicator3', '0.1') gi.require_version('Notify', '0.7') -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Gtk, Gdk, GLib, GdkX11 from gi.repository import AppIndicator3 as appindicator from gi.repository import Notify APPINDICATOR_ID = 'safeeyes' diff --git a/safeeyes/safeeyes/SafeEyesCore.py b/safeeyes/safeeyes/SafeEyesCore.py index 4406fb4..dec8cc0 100644 --- a/safeeyes/safeeyes/SafeEyesCore.py +++ b/safeeyes/safeeyes/SafeEyesCore.py @@ -18,24 +18,26 @@ import gi gi.require_version('Gdk', '3.0') -from gi.repository import Gdk, Gio, GLib +from gi.repository import Gdk, Gio, GLib, GdkX11 from apscheduler.scheduler import Scheduler import time, threading, sys, subprocess, logging logging.basicConfig() class SafeEyesCore: - break_count = 0 - long_break_message_index = 0 - short_break_message_index = 0 - def __init__(self, show_alert, start_break, end_break, on_countdown): + def __init__(self, show_notification, start_break, end_break, on_countdown): + # Initialize the variables + self.break_count = 0 + self.long_break_message_index = 0 + self.short_break_message_index = 0 self.skipped = False self.scheduler = None - self.show_alert = show_alert + self.show_notification = show_notification self.start_break = start_break self.end_break = end_break self.on_countdown = on_countdown + self.notification_condition = threading.Condition() """ Initialize the internal properties from configuration @@ -57,32 +59,39 @@ class SafeEyesCore: return # Pause the scheduler until the break - if self.scheduler and self.scheduled_job: - self.scheduler.unschedule_job(self.scheduled_job) - self.scheduled_job = None + if self.scheduler and self.scheduled_job_id: + self.scheduler.unschedule_job(self.scheduled_job_id) + self.scheduled_job_id = None 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 + """ def process_job(self): if self.is_full_screen_app_found(): # If full screen app found, do not show break screen. # Resume the scheduler if self.scheduler: - self.scheduled_job = self.scheduler.add_interval_job(self.scheduler_job, minutes=self.break_interval) + self.schedule_job() return self.break_count = ((self.break_count + 1) % self.no_of_short_breaks_per_long_break) - thread = threading.Thread(target=self.show_notification) + thread = threading.Thread(target=self.notify_and_start_break) thread.start() - def show_notification(self): + """ + Show notification and start the break after given number of seconds + """ + def notify_and_start_break(self): # Show a notification - self.show_alert() + self.show_notification() # Wait for the pre break warning period - time.sleep(self.pre_break_warning_time) + self.notification_condition.acquire() + self.notification_condition.wait(self.pre_break_warning_time) + self.notification_condition.release() # User can disable SafeEyes during notification if self.active: @@ -94,38 +103,33 @@ class SafeEyesCore: self.short_break_message_index = (self.short_break_message_index + 1) % len(self.short_break_messages) message = self.short_break_messages[self.short_break_message_index] + # Show the break screen self.start_break(message) + # Start the countdown - thread = threading.Thread(target=self.countdown) - thread.start() + seconds = 0 + if self.is_long_break(): + seconds = self.long_break_duration + else: + seconds = self.short_break_duration - """ - Countdown the seconds of break interval, call the on_countdown and finally call the end_break method - """ - def countdown(self): - seconds = 0 - if self.is_long_break(): - seconds = self.long_break_duration - else: - seconds = self.short_break_duration + while seconds and self.active and not self.skipped: + mins, secs = divmod(seconds, 60) + timeformat = '{:02d}:{:02d}'.format(mins, secs) + self.on_countdown(timeformat) + time.sleep(1) # Sleep for 1 second + seconds -= 1 - while seconds and self.active and not self.skipped: - mins, secs = divmod(seconds, 60) - timeformat = '{:02d}:{:02d}'.format(mins, secs) - self.on_countdown(timeformat) - time.sleep(1) # Sleep for 1 second - seconds -= 1 + # Loop terminated because of timeout (not skipped) -> Close the break alert + if not self.skipped: + self.end_break() - # Loop terminated because of timeout (not skipped) -> Close the break alert - if not self.skipped: - self.end_break() + # Resume the scheduler + if self.active: + if self.scheduler: + self.schedule_job() - # Resume the scheduler - if self.active: - if self.scheduler: - self.scheduled_job = self.scheduler.add_interval_job(self.scheduler_job, minutes=self.break_interval) - - self.skipped = False + self.skipped = False """ Check if the current break is long break or short current @@ -134,47 +138,69 @@ class SafeEyesCore: return self.break_count == self.no_of_short_breaks_per_long_break - 1 # User skipped the break using Skip button - def reset(self): + def skip_break(self): self.skipped = True """ - Resume the timer + Reschedule the job """ - def resume(self): - if not self.active: - self.active = True - if self.scheduler: - self.scheduled_job = self.scheduler.add_interval_job(self.scheduler_job, minutes=self.break_interval) - - """ - Pause the timer - """ - def pause(self): + def toggle_active_state(self): if self.active: self.active = False - if self.scheduler and self.scheduled_job: - self.scheduler.unschedule_job(self.scheduled_job) - self.scheduled_job = None + if self.scheduler and self.scheduled_job_id: + self.scheduler.unschedule_job(self.scheduled_job_id) + self.scheduled_job_id = None + # If waiting after notification, notify the thread to wake up and die + self.notification_condition.acquire() + self.notification_condition.notify() + self.notification_condition.release() + else: + self.active = True + if self.scheduler: + self.schedule_job() + + """ + Unschedule the job and shutdown the scheduler + """ def stop(self): if self.scheduler: self.active = False - if self.scheduled_job: - self.scheduler.unschedule_job(self.scheduled_job) - self.scheduled_job = None + if self.scheduled_job_id: + self.scheduler.unschedule_job(self.scheduled_job_id) + self.scheduled_job_id = None self.scheduler.shutdown(wait=False) self.scheduler = None + + # 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 the timer + Schedule the job and start the scheduler """ def start(self): self.active = True if not self.scheduler: self.scheduler = Scheduler() - self.scheduled_job = self.scheduler.add_interval_job(self.scheduler_job, minutes=self.break_interval) + self.schedule_job() self.scheduler.start() + """ + Restart the scheduler after changing settings + """ + def restart(self): + if self.active: + self.stop() + self.start() + + """ + Schedule the job + """ + def schedule_job(self): + self.scheduled_job_id = self.scheduler.add_interval_job(self.scheduler_job, seconds=self.break_interval) + """ Check for full-screen applications """ diff --git a/safeeyes/safeeyes/SettingsDialog.py b/safeeyes/safeeyes/SettingsDialog.py index cce8b66..eae71f2 100644 --- a/safeeyes/safeeyes/SettingsDialog.py +++ b/safeeyes/safeeyes/SettingsDialog.py @@ -18,7 +18,7 @@ import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk +from gi.repository import Gtk, Gdk, GdkX11 class SettingsDialog: """docstring for SettingsDialog""" @@ -43,7 +43,7 @@ class SettingsDialog: self.spin_interval_between_two_breaks.set_value(config['break_interval']) 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_state(config['strict_break']) + self.switch_strict_break.set_active(config['strict_break']) def show(self): @@ -58,7 +58,7 @@ class SettingsDialog: self.config['break_interval'] = self.spin_interval_between_two_breaks.get_value_as_int() 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_state() + self.config['strict_break'] = self.switch_strict_break.get_active() self.on_save_settings(self.config) # Call the provided save method self.window.destroy() # Close the settings window diff --git a/safeeyes/safeeyes/TrayIcon.py b/safeeyes/safeeyes/TrayIcon.py index e2f1908..a830dfb 100644 --- a/safeeyes/safeeyes/TrayIcon.py +++ b/safeeyes/safeeyes/TrayIcon.py @@ -19,7 +19,7 @@ import gi gi.require_version('Gtk', '3.0') gi.require_version('AppIndicator3', '0.1') -from gi.repository import Gtk, Gdk, GLib +from gi.repository import Gtk, Gdk, GLib, GdkX11 from gi.repository import AppIndicator3 as appindicator # Global variables diff --git a/safeeyes/safeeyes/config/safeeyes.json b/safeeyes/safeeyes/config/safeeyes.json index e33dec9..7f956d8 100644 --- a/safeeyes/safeeyes/config/safeeyes.json +++ b/safeeyes/safeeyes/config/safeeyes.json @@ -11,6 +11,7 @@ "short_break_messages": [ "Tightly close your eyes", "Roll your eyes", + "Rotate your eyes", "Blink your eyes", "Have some water" ], diff --git a/safeeyes/safeeyes/config/style/safeeyes_style.css b/safeeyes/safeeyes/config/style/safeeyes_style.css index 297c8d8..5d1e23c 100644 --- a/safeeyes/safeeyes/config/style/safeeyes_style.css +++ b/safeeyes/safeeyes/config/style/safeeyes_style.css @@ -33,6 +33,7 @@ border-color: white; background: transparent; border-width: 2px; + border-image: none; } .btn_skip:hover { diff --git a/safeeyes/safeeyes/glade/break_screen.glade b/safeeyes/safeeyes/glade/break_screen.glade index ee60a07..60d0c29 100644 --- a/safeeyes/safeeyes/glade/break_screen.glade +++ b/safeeyes/safeeyes/glade/break_screen.glade @@ -25,7 +25,7 @@ False - popup + center True safeeyes diff --git a/safeeyes/safeeyes/glade/settings_dialog.glade b/safeeyes/safeeyes/glade/settings_dialog.glade index bfdd461..c8e3ad1 100644 --- a/safeeyes/safeeyes/glade/settings_dialog.glade +++ b/safeeyes/safeeyes/glade/settings_dialog.glade @@ -22,7 +22,7 @@ --> - + 1 60 diff --git a/safeeyes/safeeyes/safeeyes b/safeeyes/safeeyes/safeeyes index 7df72b4..a8e2b8a 100755 --- a/safeeyes/safeeyes/safeeyes +++ b/safeeyes/safeeyes/safeeyes @@ -53,16 +53,16 @@ def show_alert(message): # Hide and show tray icon if strict break is on # This feature is required because, by disabling Safe Eyes, # user can skip the break. - if config['strict_break']: - tray_icon.hide_icon() + # if config['strict_break']: + # tray_icon.hide_icon() break_screen.show_message(message) def close_alert(): # Hide and show tray icon if strict break is on # This feature is required because, by disabling Safe Eyes, # user can skip the break. - if config['strict_break']: - tray_icon.show_icon() + # if config['strict_break']: + # tray_icon.show_icon() break_screen.close() def on_countdown(count): @@ -79,7 +79,7 @@ def on_skipped(): # user can skip the break. if config['strict_break']: tray_icon.show_icon() - core.reset() + core.skip_break() def save_settings(config): # Write the configuration to file @@ -87,16 +87,15 @@ def save_settings(config): json.dump(config, config_file, indent=4, sort_keys=True) # Restart the core and intialize the components - core.stop() core.initialize(config) break_screen.initialize(config) - core.start() + core.restart() def enable_safeeyes(): - core.resume() + core.toggle_active_state() def disable_safeeyes(): - core.pause() + core.toggle_active_state() def prepare_local_config_dir(): config_dir_path = os.path.join(os.path.expanduser('~'), '.config/safeeyes/style')