SafeEyes/safeeyes/model.py

353 lines
11 KiB
Python

#!/usr/bin/env python
# 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/>.
"""
This module contains the entity classes used by Safe Eyes and its plugins.
"""
from distutils.version import LooseVersion
from enum import Enum
import logging
from safeeyes import utility
class Break:
"""
An entity class which represents a break.
"""
def __init__(self, break_type, name, time, duration, image, plugins):
self.type = break_type
self.name = name
self.duration = duration
self.image = image
self.plugins = plugins
self.time = time
self.next = None
def __str__(self):
return 'Break: {{name: "{}", type: {}, duration: {}}}\n'.format(self.name, self.type, self.duration)
def __repr__(self):
return str(self)
def is_long_break(self):
"""
Check whether this break is a long break.
"""
return self.type == BreakType.LONG_BREAK
def is_short_break(self):
"""
Check whether this break is a short break.
"""
return self.type == BreakType.SHORT_BREAK
def plugin_enabled(self, plugin_id, is_plugin_enabled):
"""
Check whether this break supports the given plugin.
"""
if self.plugins:
return plugin_id in self.plugins
else:
return is_plugin_enabled
class BreakType(Enum):
"""
Type of Safe Eyes breaks.
"""
SHORT_BREAK = 1
LONG_BREAK = 2
class BreakQueue:
def __init__(self, config, context):
self.context = context
self.__current_break = None
self.__first_break = None
self.__short_break_time = config.get('short_break_interval')
self.__long_break_time = config.get('long_break_interval')
self.__short_pointer = self.__build_queue(BreakType.SHORT_BREAK,
config.get('short_breaks'),
self.__short_break_time,
config.get('short_break_duration'))
self.__long_pointer = self.__build_queue(BreakType.LONG_BREAK,
config.get('long_breaks'),
self.__long_break_time,
config.get('long_break_duration'))
# Restore the last break from session
if not self.is_empty():
last_break = context['session'].get('break')
if last_break is not None:
current_break = self.get_break()
if last_break != current_break.name:
pointer = self.next()
while(pointer != current_break and pointer.name != last_break):
pointer = self.next()
def get_break(self):
if self.__current_break is None:
self.__current_break = self.next()
return self.__current_break
def is_long_break(self):
return self.__current_break is not None and self.__current_break.type == BreakType.LONG_BREAK
def next(self):
if self.is_empty():
return None
break_obj = None
if self.__short_pointer is None:
# No short breaks
break_obj = self.__long_pointer
self.context['break_type'] = 'long'
# Update the pointer to next
self.__long_pointer = self.__long_pointer.next
elif self.__long_pointer is None:
# No long breaks
break_obj = self.__short_pointer
self.context['break_type'] = 'short'
# Update the pointer to next
self.__short_pointer = self.__short_pointer.next
elif self.__long_pointer.time <= self.__short_pointer.time:
# Time for a long break
break_obj = self.__long_pointer
self.context['break_type'] = 'long'
# Update the pointer to next
self.__long_pointer = self.__long_pointer.next
else:
# Time for a short break
break_obj = self.__short_pointer
self.context['break_type'] = 'short'
# Reduce the break time from the next long break
self.__long_pointer.time -= self.__short_pointer.time
# Update the pointer to next
self.__short_pointer = self.__short_pointer.next
if self.__first_break is None:
self.__first_break = break_obj
self.context['new_cycle'] = self.__first_break == break_obj
if self.__current_break is not None:
# Reset the time of long breaks
if self.__current_break.type == BreakType.LONG_BREAK:
self.__current_break.time = self.__long_break_time
self.__current_break = break_obj
self.context['session']['break'] = self.__current_break.name
return break_obj
def is_empty(self):
return self.__short_pointer is None and self.__long_pointer is None
def __build_queue(self, break_type, break_configs, break_time, break_duration):
"""
Build a circular queue of breaks.
"""
head = None
tail = None
for break_config in break_configs:
name = _(break_config['name'])
duration = break_config.get('duration', break_duration)
image = break_config.get('image')
plugins = break_config.get('plugins', None)
interval = break_config.get('interval', break_time)
# Validate time value
if not isinstance(duration, int) or duration <= 0:
logging.error('Invalid break duration in: ' +
str(break_config))
continue
break_obj = Break(break_type, name, interval,
duration, image, plugins)
if head is None:
head = break_obj
tail = break_obj
else:
tail.next = break_obj
tail = break_obj
# Connect the tail to the head
if tail is not None:
tail.next = head
return head
class State(Enum):
"""
Possible states of Safe Eyes.
"""
START = 0,
WAITING = 1,
PRE_BREAK = 2,
BREAK = 3,
STOPPED = 4,
QUIT = 5
class EventHook:
"""
Hook to attach and detach listeners to system events.
"""
def __init__(self):
self.__handlers = []
def __iadd__(self, handler):
self.__handlers.append(handler)
return self
def __isub__(self, handler):
self.__handlers.remove(handler)
return self
def fire(self, *args, **keywargs):
"""
Fire all listeners attached with.
"""
for handler in self.__handlers:
if not handler(*args, **keywargs):
return False
return True
class Config:
"""
The configuration of Safe Eyes.
"""
def __init__(self, init=True):
# Read the config files
self.__user_config = utility.load_json(utility.CONFIG_FILE_PATH)
self.__system_config = utility.load_json(
utility.SYSTEM_CONFIG_FILE_PATH)
self.__force_upgrade = ['long_breaks', 'short_breaks']
if init:
if self.__user_config is None:
utility.initialize_safeeyes()
self.__user_config = self.__system_config
self.save()
else:
system_config_version = self.__system_config['meta']['config_version']
meta_obj = self.__user_config.get('meta', None)
if meta_obj is None:
# Corrupted user config
self.__user_config = self.__system_config
else:
user_config_version = str(
meta_obj.get('config_version', '0.0.0'))
if LooseVersion(user_config_version) != LooseVersion(system_config_version):
# Update the user config
self.__merge_dictionary(
self.__user_config, self.__system_config)
self.__user_config = self.__system_config
# Update the style sheet
utility.replace_style_sheet()
utility.merge_plugins(self.__user_config)
self.save()
def __merge_dictionary(self, old_dict, new_dict):
"""
Merge the dictionaries.
"""
for key in new_dict:
if key == "meta" or key in self.__force_upgrade:
continue
if key in old_dict:
new_value = new_dict[key]
old_value = old_dict[key]
if type(new_value) is type(old_value):
# Both properties have same type
if isinstance(new_value, dict):
self.__merge_dictionary(old_value, new_value)
else:
new_dict[key] = old_value
def clone(self):
config = Config(init=False)
return config
def save(self):
"""
Save the configuration to file.
"""
utility.write_json(utility.CONFIG_FILE_PATH, self.__user_config)
def get(self, key, default_value=None):
"""
Get the value.
"""
value = self.__user_config.get(key, default_value)
if value is None:
value = self.__system_config.get(key, None)
return value
def set(self, key, value):
"""
Set the value.
"""
self.__user_config[key] = value
def __eq__(self, config):
return self.__user_config == config.__user_config
def __ne__(self, config):
return self.__user_config != config.__user_config
class TrayAction:
"""
Data object wrapping name, icon and action.
"""
def __init__(self, name, icon, action, system_icon):
self.name = name
self.__icon = icon
self.action = action
self.system_icon = system_icon
self.__toolbar_buttons = []
def get_icon(self):
if self.system_icon:
return self.__icon
else:
image = utility.load_and_scale_image(self.__icon, 16, 16)
image.show()
return image
def add_toolbar_button(self, button):
self.__toolbar_buttons.append(button)
def reset(self):
for button in self.__toolbar_buttons:
button.hide()
self.__toolbar_buttons.clear()
@classmethod
def build(cls, name, icon_path, icon_id, action):
image = utility.load_and_scale_image(icon_path, 12, 12)
if image is None:
return TrayAction(name, icon_id, action, True)
else:
return TrayAction(name, icon_path, action, False)