gpodder/src/gpodder/gtkui/app.py

312 lines
12 KiB
Python

# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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 html
import logging
import os
import sys
import xml.etree.ElementTree as ET
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gpodder
from gpodder import core, util
from gpodder.model import check_root_folder_path
from .config import UIConfig
from .desktop.preferences import gPodderPreferences
from .main import gPodder
from .model import Model
import gi # isort:skip
gi.require_version('Gtk', '3.0') # isort:skip
from gi.repository import GdkPixbuf, Gio, GObject, Gtk # isort:skip
logger = logging.getLogger(__name__)
_ = gpodder.gettext
N_ = gpodder.ngettext
def parse_app_menu_for_accels(filename):
"""
grab (accelerator, action) bindings from menus.ui.
See #815 Ctrl-Q doesn't quit for justification.
Unfortunately it's not available from the Gio.MenuModel we get from the Gtk.Builder,
so we get it ourself.
"""
res = []
menu_tree = ET.parse(filename)
assert menu_tree.getroot().tag == 'interface'
for menu in menu_tree.getroot():
assert menu.tag == 'menu'
if menu.attrib.get('id') == 'app-menu':
for itm in menu.iter('item'):
action = None
accel = None
for att in itm.findall('attribute'):
if att.get('name') == 'action':
action = att.text.strip()
elif att.get('name') == 'accel':
accel = att.text.strip()
if action and accel:
res.append((accel, action))
return res
class gPodderApplication(Gtk.Application):
def __init__(self, options):
Gtk.Application.__init__(self, application_id='org.gpodder.gpodder',
flags=Gio.ApplicationFlags.FLAGS_NONE)
self.window = None
self.options = options
self.connect('window-removed', self.on_window_removed)
def create_actions(self):
action = Gio.SimpleAction.new('about', None)
action.connect('activate', self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new('quit', None)
action.connect('activate', self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new('help', None)
action.connect('activate', self.on_help_activate)
self.add_action(action)
action = Gio.SimpleAction.new('logs', None)
action.connect('activate', self.on_logs_activate)
self.add_action(action)
action = Gio.SimpleAction.new('preferences', None)
action.connect('activate', self.on_itemPreferences_activate)
self.add_action(action)
action = Gio.SimpleAction.new('gotoMygpo', None)
action.connect('activate', self.on_goto_mygpo)
self.add_action(action)
action = Gio.SimpleAction.new('checkForUpdates', None)
action.connect('activate', self.on_check_for_updates_activate)
self.add_action(action)
action = Gio.SimpleAction.new('menu', None)
action.connect('activate', self.on_menu)
self.add_action(action)
def do_startup(self):
Gtk.Application.do_startup(self)
self.create_actions()
builder = Gtk.Builder()
builder.set_translation_domain(gpodder.textdomain)
self.builder = builder
menu_filename = None
for ui_folder in gpodder.ui_folders:
if gpodder.ui.adaptive:
filename = os.path.join(ui_folder, '..', 'adaptive', 'menus.ui')
else:
filename = os.path.join(ui_folder, 'menus.ui')
if os.path.exists(filename):
builder.add_from_file(filename)
menu_filename = filename
break
menubar = builder.get_object('menubar')
if menubar is None:
logger.error('Cannot find menus.ui in %r, exiting' % gpodder.ui_folders)
sys.exit(1)
self.menu_extras = builder.get_object('menuExtras')
self.menu_view_columns = builder.get_object('menuViewColumns')
if not gpodder.ui.adaptive:
self.set_menubar(menubar)
# If $XDG_CURRENT_DESKTOP is set then it contains a colon-separated list of strings.
# https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
# See https://askubuntu.com/a/227669 for a list of values in different environments
xdg_current_desktops = os.environ.get('XDG_CURRENT_DESKTOP', '').split(':')
# See https://developer.gnome.org/gtk3/stable/gtk-running.html
# GTK_CSD=0 is used to disable client side decorations
csd_disabled = os.environ.get('GTK_CSD') == '0'
self.want_headerbar = (gpodder.ui.adaptive or
(('GNOME' in xdg_current_desktops) and
not gpodder.ui.osx and not csd_disabled))
self.app_menu = builder.get_object('app-menu')
if self.want_headerbar:
# Use GtkHeaderBar for client-side decorations on recent GNOME 3 versions
self.header_bar_menu_button = Gtk.Button.new_from_icon_name('open-menu-symbolic', Gtk.IconSize.SMALL_TOOLBAR)
self.header_bar_menu_button.set_action_name('app.menu')
self.header_bar_refresh_button = Gtk.Button.new_from_icon_name('view-refresh-symbolic', Gtk.IconSize.SMALL_TOOLBAR)
# self.header_bar_refresh_button.set_action_name('win.updateChannel')
self.header_bar_refresh_button.set_action_name('win.update')
self.menu_popover = Gtk.Popover.new_from_model(self.header_bar_menu_button, self.app_menu)
self.menu_popover.set_position(Gtk.PositionType.BOTTOM)
for (accel, action) in parse_app_menu_for_accels(menu_filename):
self.add_accelerator(accel, action, None)
else:
self.set_app_menu(self.app_menu)
Gtk.Window.set_default_icon_name('gpodder')
try:
dbus_main_loop = DBusGMainLoop(set_as_default=True)
gpodder.dbus_session_bus = dbus.SessionBus(dbus_main_loop)
self.bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
except dbus.exceptions.DBusException as dbe:
logger.warning('Cannot get "on the bus".', exc_info=True)
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, _('Cannot start gPodder'))
dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
dlg.set_title('gPodder')
dlg.run()
dlg.destroy()
sys.exit(0)
util.idle_add(self.check_root_folder_path_gui)
def do_activate(self):
# We only allow a single window and raise any existing ones
if not self.window:
# Windows are associated with the application
# when the last one is closed the application shuts down
self.window = gPodder(self, self.bus_name,
core.Core(UIConfig, model_class=Model), self.options,
ui_folder=os.path.join(gpodder.ui_folders[0], '..', 'adaptive'))
self.add_window(self.window.main_window)
if gpodder.ui.osx:
from . import macosx
# Handle "subscribe to podcast" events from firefox
macosx.register_handlers(self.window)
self.window.gPodder.present()
def on_menu(self, action, param):
self.menu_popover.popup()
def on_about(self, action, param):
dlg = Gtk.Dialog(_('About gPodder'), self.window.gPodder,
Gtk.DialogFlags.MODAL)
dlg.add_button(_('_Close'), Gtk.ResponseType.OK).show()
dlg.set_resizable(True)
bg = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, margin=16)
pb = GdkPixbuf.Pixbuf.new_from_file_at_size(gpodder.icon_file, 160, 160)
bg.pack_start(Gtk.Image.new_from_pixbuf(pb), False, False, 0)
label = Gtk.Label(justify=Gtk.Justification.CENTER)
label.set_selectable(True)
label.set_markup('\n'.join(x.strip() for x in """
<b>gPodder {version} ({date})</b>
{copyright}
{license}
<a href="{url}">{tr_website}</a> · <a href="{bugs_url}">{tr_bugtracker}</a>
""".format(version=gpodder.__version__,
date=gpodder.__date__,
copyright=gpodder.__copyright__,
license=gpodder.__license__,
bugs_url='https://github.com/gpodder/gpodder/issues',
url=html.escape(gpodder.__url__),
tr_website=_('Website'),
tr_bugtracker=_('Bug Tracker')).strip().split('\n')))
label.connect('activate-link', lambda label, url: util.open_website(url))
bg.pack_start(label, False, False, 0)
bg.pack_start(Gtk.Label(), False, False, 0)
dlg.vbox.pack_start(bg, False, False, 0)
dlg.connect('response', lambda dlg, response: dlg.destroy())
dlg.vbox.show_all()
dlg.run()
def on_quit(self, *args):
self.window.on_gPodder_delete_event()
def on_window_removed(self, *args):
self.quit()
def on_help_activate(self, action, param):
util.open_website('https://gpodder.github.io/docs/')
def on_logs_activate(self, action, param):
util.gui_open(os.path.join(gpodder.home, 'Logs'), gui=self.window)
def on_itemPreferences_activate(self, action, param=None):
gPodderPreferences(self.window.gPodder,
_config=self.window.config,
user_apps_reader=self.window.user_apps_reader,
parent_window=self.window.main_window,
mygpo_client=self.window.mygpo_client,
on_send_full_subscriptions=self.window.on_send_full_subscriptions,
on_itemExportChannels_activate=self.window.on_itemExportChannels_activate,
on_extension_enabled=self.on_extension_enabled,
on_extension_disabled=self.on_extension_disabled)
def on_goto_mygpo(self, action, param):
self.window.mygpo_client.open_website()
def on_check_for_updates_activate(self, action, param):
if os.path.exists(gpodder.no_update_check_file):
self.window.check_for_distro_updates()
else:
self.window.check_for_updates(silent=False)
def on_extension_enabled(self, extension):
self.window.on_extension_enabled(extension)
def on_extension_disabled(self, extension):
self.window.on_extension_disabled(extension)
@staticmethod
def check_root_folder_path_gui():
msg = check_root_folder_path()
if msg:
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING,
Gtk.ButtonsType.CLOSE, msg)
dlg.set_title(_('Path to gPodder home is too long'))
dlg.run()
dlg.destroy()
def main(options=None):
GObject.set_application_name('gPodder')
gp = gPodderApplication(options)
gp.run()
sys.exit(0)