# -*- coding: utf-8 -*-
#
# pympress
#
# Copyright 2009, 2010 Thomas Jost <thomas.jost@gmail.com>
# Copyright 2015 Cimbali <me@cimba.li>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
"""
:mod:`pympress.app` -- The Gtk.Application managing the lifetime and CLI
------------------------------------------------------------------------
"""
import logging
logger = logging.getLogger(__name__)
import os
import sys
import signal
import platform
import gi
import cairo
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, Gio
from pympress import util, config, document, ui, builder
[docs]
class Pympress(Gtk.Application):
""" Class representing the single pympress Gtk application.
"""
#: The :class:`~pympress.ui.UI` object that is the interface of pympress
gui = None
#: The :class:`~pympress.config.Config` object that holds pympress conferences
config = None
#: `list` of actions to be passed to the GUI that were queued before GUI was created
action_startup_queue = []
#: `bool` to automatically upgrade log level (DEBUG / INFO at init, then ERROR), False if user set log level
auto_log_level = True
options = {
# long_name: (short_name (int), flags (GLib.OptionFlags), arg (GLib.OptionArg)
'talk-time': (ord('t'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
'notes': (ord('N'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
'log': (0, GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
'version': (ord('v'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'pause': (ord('P'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'reset': (ord('r'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'next': (ord('n'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'prev': (ord('p'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'first': (ord('f'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'last': (ord('l'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'blank': (ord('b'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
'quit': (ord('q'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
}
option_descriptions = {
# long_name: (description, arg_description)
'talk-time': (_('The estimated (intended) talk time in minutes') + ' ' +
_('(and optionally seconds)'), 'mm[:ss]'),
'notes': (_('Set the position of notes on the pdf page') + ' ' +
_('(none, left, right, top, bottom, after, odd, or prefix).') + ' ' +
_('Overrides the detection from the file.'), '<position>'),
'log': (_('Set level of verbosity in log file:') + ' ' +
_('{}, {}, {}, {}, or {}').format('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), '<level>'),
'version': (_('Print version and exit'), None),
'pause': (_('Toggle pause of talk timer'), None),
'reset': (_('Reset talk timer'), None),
'next': (_('Next slide'), None),
'prev': (_('Previous slide'), None),
'first': (_('First slide'), None),
'last': (_('Last slide'), None),
'blank': (_('Blank/unblank content screen'), None),
'quit': (_('Close opened pympress instance'), None),
}
version_string = ' '.join([
'Pympress:', util.get_pympress_meta()['version'],
'; Python:', platform.python_version(),
'; OS:', platform.system(), platform.release(), platform.version(),
'; Gtk {}.{}.{}'.format(Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()),
'; GLib {}.{}.{}'.format(GLib.MAJOR_VERSION, GLib.MINOR_VERSION, GLib.MICRO_VERSION),
'; Poppler', document.Poppler.get_version(), document.Poppler.get_backend().value_nick,
'; Cairo', cairo.cairo_version_string(), ', pycairo', cairo.version,
])
def __init__(self):
GLib.set_application_name('pympress')
# GLib.set_prgname('pympress') # Let prgname be auto-determined from sys.argv[0]
Gtk.Application.__init__(self, application_id='io.github.pympress',
flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID)
self.register(None)
if not self.get_is_remote():
builder.Builder.setup_actions({
'log-level': dict(activate=self.set_log_level, state=logger.getEffectiveLevel(), parameter_type=int),
}, action_map=self)
# Connect proper exit function to interrupt
signal.signal(signal.SIGINT, self.quit)
for opt in self.options:
self.add_main_option(opt, *self.options[opt], *self.option_descriptions.get(opt, ['', None]))
[docs]
def quit(self, *args): # noqa: A003 -- we need to override app.quit()
""" Quit and ignore other arguments e.g. sent by signals.
"""
if self.gui is not None and self.gui.unsaved_changes():
return
Gtk.Application.quit(self)
return False
[docs]
def do_startup(self):
""" Common start-up tasks for primary and remote instances.
NB. super(self) causes segfaults, Gtk.Application needs to be used as base.
"""
self.config = config.Config()
# prefer X11 on posix systems because Wayland still has some shortcomings for us,
# specifically libVLC and the ability to disable screensavers
if util.IS_POSIX:
Gdk.set_allowed_backends('x11,*')
logger.info(self.version_string)
# If there is no display, we can’t get further than this.
if os.getenv('PYMPRESS_HEADLESS_TEST', ''):
sys.exit(0)
Gtk.Application.do_startup(self)
[docs]
def do_activate(self, timestamp=GLib.get_current_time()):
""" Activate: show UI windows.
Build them if they do not exist, otherwise bring to front.
"""
if self.gui is None:
if self.auto_log_level:
self.activate_action('log-level', logging.INFO)
self.action_startup_queue.append(('log-level', logging.ERROR))
# Build the UI and windows
self.gui = ui.UI(self, self.config)
while self.action_startup_queue:
self.activate_action(*self.action_startup_queue.pop(0))
Gtk.Application.do_activate(self)
self.gui.p_win.present_with_time(timestamp)
[docs]
def set_action_enabled(self, name, value):
""" Parse an action name and set its enabled state to True or False.
Args:
name (`str`): the name of the stateful action
value (`bool`): wheether the action should be enabled or disabled
"""
self.lookup_action(name).set_enabled(value)
[docs]
def set_action_state(self, name, value):
""" Parse an action name and set its state wrapped in a :class:`~GLib.Variant`.
Args:
name (`str`): the name of the stateful action
value (`str`, `int`, `bool` or `float`): the value to set.
"""
self.lookup_action(name).change_state(GLib.Variant(builder.Builder._glib_type_strings[type(value)], value))
[docs]
def get_action_state(self, name):
""" Parse an action name and return its unwrapped state from the :class:`~GLib.Variant`.
Args:
name (`str`): the name of the stateful action
Returns:
`str`, `int`, `bool` or `float`: the value contained in the action
"""
state = self.lookup_action(name).get_state()
return builder.Builder._glib_type_getters[state.get_type_string()](state)
[docs]
def activate_action(self, name, parameter=None):
""" Parse an action name and activate it, with parameter wrapped in a :class:`~GLib.Variant` if it is not None.
Args:
name (`str`): the name of the stateful action
parameter: an object or None to pass as a parameter to the action, wrapped in a GLib.Variant
"""
if not self.get_is_remote() and self.gui is None and name not in ['log-level']:
self.action_startup_queue.append((name, parameter))
return
if parameter is not None:
parameter = GLib.Variant(builder.Builder._glib_type_strings[type(parameter)], parameter)
Gio.ActionGroup.activate_action(self, name, parameter)
[docs]
def do_open(self, files, n_files, hint):
""" Handle opening files. In practice we only open once, the last one.
Args:
files (`list` of :class:`~Gio.File`): representing an array of files to open
n_files (`int`): the number of files passed.
hint (`str`): a hint, such as view, edit, etc. Should always be the empty string.
"""
if not n_files:
return
self.do_activate(timestamp=GLib.get_current_time())
self.gui.swap_document(files[-1].get_uri())
[docs]
def do_shutdown(self):
""" Perform various cleanups and save preferences.
"""
if self.gui is not None:
self.gui.cleanup()
self.config.save_config()
Gtk.Application.do_shutdown(self)
util.close_opened_resources()
[docs]
def set_log_level(self, action, param):
""" Action that sets the logging level (on the root logger of the active instance)
Args:
action (:class:`~Gio.Action`): The action activatd
param (:class:~`GLib.Variant`): The desired level as an int wrapped in a GLib.Variant
"""
logging.getLogger(None).setLevel(param.get_int64())
action.change_state(param)
[docs]
def do_handle_local_options(self, opts_variant_dict):
""" Parse command line options, returned as a VariantDict
Returns:
`tuple`: estimated talk time, log level, notes positions.
"""
# convert GVariantDict -> GVariant -> dict
opts = opts_variant_dict.end().unpack()
simple_actions = {
'pause': 'pause-timer',
'reset': 'reset-timer',
'next': 'next-page',
'prev': 'prev-page',
'blank': 'blank-screen',
'quit': 'quit',
'first': 'first-page',
'last': 'last-page',
}
for opt, arg in opts.items():
if opt == "version":
print(self.version_string)
return 0
elif opt == "log":
numeric_level = getattr(logging, arg.upper(), None)
if isinstance(numeric_level, int):
self.auto_log_level = False
self.activate_action('log-level', numeric_level)
else:
print(_("Invalid log level \"{}\", try one of {}").format(
arg, "DEBUG, INFO, WARNING, ERROR, CRITICAL"
))
elif opt == "notes":
arg = arg.lower()[:1]
if arg == 'n': self.activate_action('notes-pos', 'none')
if arg == 'l': self.activate_action('notes-pos', 'left')
if arg == 'r': self.activate_action('notes-pos', 'right')
if arg == 't': self.activate_action('notes-pos', 'top')
if arg == 'b': self.activate_action('notes-pos', 'bottom')
if arg == 'a': self.activate_action('notes-pos', 'after')
if arg == 'o': self.activate_action('notes-pos', 'odd')
if arg == 'p': self.activate_action('notes-pos', 'map') # prefix -> map
elif opt == "talk-time":
t = ["0" + n.strip() for n in arg.split(':')]
try:
m = int(t[0])
s = int(t[1])
except ValueError:
print(_("Invalid time (mm or mm:ss expected), got \"{}\"").format(arg))
return 2
except IndexError:
s = 0
self.activate_action('set-talk-time', m * 60 + s)
elif opt in simple_actions:
self.activate_action(simple_actions[opt])
return -1