Source code for pympress.app

# -*- 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 passsed 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() # prefere 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