Improvements to the extension system

- Add category metadata in every extension
- Show this category in the extension list gui
- Add "mandatory_in" and "disable-in" configuration for an extension
- Add Ubuntu unity check to enable/disable unity specific extensions
- Move "gpodder.win32" and "gpodder.osx" setting to the "gpodder.ui" namespace to be able to use it in the extensions category settings
- Only show metadata information in the right-click dialog of an extension
This commit is contained in:
Bernd Schlapsi 2012-11-18 19:26:02 +01:00
parent de0cae32aa
commit d5eae16b9f
26 changed files with 185 additions and 76 deletions

View File

@ -121,7 +121,7 @@ gpodder.ui.cli = True
# Platform detection (i.e. MeeGo 1.2 Harmattan, etc..)
gpodder.detect_platform()
have_ansi = sys.stdout.isatty() and not gpodder.win32
have_ansi = sys.stdout.isatty() and not gpodder.ui.win32
interactive_console = sys.stdin.isatty() and sys.stdout.isatty()
is_single_command = False

View File

@ -119,7 +119,7 @@ def main():
help=_('Subscribe to the given URL'))
# On Mac OS X, support the "psn" parameter for compatibility (bug 939)
if gpodder.osx:
if gpodder.ui.osx:
parser.add_option('-p', '--psn', dest='macpsn', metavar='PSN',
help=_('Mac OS X application process number'))
@ -129,6 +129,9 @@ def main():
gpodder.ui.qml = True
else:
gpodder.ui.gtk = True
gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in
('ubuntu', 'ubuntu-2d'))
from gpodder import log
log.setup(options.verbose)
@ -156,7 +159,7 @@ def main():
except dbus.exceptions.DBusException, dbus_exception:
logger.info('Cannot connect to remote object.', exc_info=True)
if not gpodder.win32 and os.environ.get('DISPLAY', '') == '':
if not gpodder.ui.win32 and os.environ.get('DISPLAY', '') == '':
logger.error('Cannot start gPodder: $DISPLAY is not set.')
sys.exit(1)

View File

@ -16,6 +16,7 @@ _ = gpodder.gettext
__title__ = _('Enqueue in media players')
__description__ = _('Add a context menu item for enqueueing episodes in installed media players')
__author__ = 'Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__category__ = 'interface'
__only_for__ = 'gtk'
AMAROK = (['amarok', '--play', '--append'], 'Enqueue in Amarok')
@ -40,7 +41,6 @@ class gPodderExtension:
vlc = subprocess.Popen(cmd + filenames,
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = vlc.communicate()
def _enqueue_episodes_amarok(self, episodes):
self._enqueue_episodes_cmd(episodes, AMAROK[0])

View File

@ -23,6 +23,7 @@ _ = gpodder.gettext
__title__ = _('Convert .flv files from YouTube to .mp4')
__description__ = _('Useful for playing downloaded videos on hardware players')
__authors__ = 'Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {
'context_menu': True, # Show the conversion option in the context menu

View File

@ -10,7 +10,9 @@ _ = gpodder.gettext
__title__ = _('Gtk Status Icon')
__description__ = _('Show a status icon for Gtk-based Desktops.')
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__disable_in__ = 'unity'
import gtk

View File

@ -19,6 +19,7 @@ _ = gpodder.gettext
__title__ = _('Convert M4A audio to MP3 or OGG')
__description__ = _('Transcode .m4a files to .mp3 or .ogg using ffmpeg')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>, Thomas Perl <thp@gpodder.org>'
__category__ = 'post-download'
DefaultConfig = {

View File

@ -9,6 +9,7 @@ _ = gpodder.gettext
__title__ = _('Minimize on start')
__description__ = _('Minimizes the gPodder window on startup.')
__category__ = 'interface'
__only_for__ = 'gtk'
class gPodderExtension:

View File

@ -21,6 +21,7 @@ _ = gpodder.gettext
__title__ = _('Normalize audio with re-encoding')
__description__ = _('Normalize the volume of audio files with normalize-audio')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {

View File

@ -21,7 +21,9 @@
__title__ = 'Gtk+ Desktop Notifications'
__description__ = 'Display notification bubbles for different events.'
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__mandatory_in__ = 'gtk'
import gpodder

View File

@ -16,6 +16,7 @@ _ = gpodder.gettext
__title__ = _('Rename episodes after download')
__description__ = _('Rename episodes to "<Episode Title>.<ext>" on download')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>, Thomas Perl <thp@gpodder.org>'
__category__ = 'post-download'
class gPodderExtension:

View File

@ -37,6 +37,7 @@ _ = gpodder.gettext
__title__ = _('Remove cover art from OGG files')
__description__ = _('removes coverart from all downloaded ogg files')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {

View File

@ -26,6 +26,7 @@ _ = gpodder.gettext
__title__ = _('Convert video files to MP4 for Rockbox')
__description__ = _('Converts all videos to a Rockbox-compatible format')
__authors__ = 'Guy Sheffer <guysoft@gmail.com>, Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {

View File

@ -38,6 +38,7 @@ _ = gpodder.gettext
__title__ = _('Tag downloaded files using Mutagen')
__description__ = _('Add episode and podcast titles to MP3/OGG tags')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {

View File

@ -15,8 +15,9 @@ _ = gpodder.gettext
__title__ = _('Subtitle Downloader for TED Talks')
__description__ = _('Downloads .srt subtitles for TED Talks Videos')
__only_for__ = 'gtk, cli, qml'
__authors__ = 'Danilo Shiga <daniloshiga@gmail.com>'
__category__ = 'post-download'
__only_for__ = 'gtk, cli, qml'
class gPodderExtension(object):

View File

@ -9,7 +9,11 @@ _ = gpodder.gettext
__title__ = _('Ubuntu App Indicator')
__description__ = _('Show a status indicator in the top bar.')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__mandatory_in__ = 'unity'
import appindicator
import gtk

View File

@ -9,7 +9,11 @@ _ = gpodder.gettext
__title__ = _('Ubuntu Unity Integration')
__description__ = _('Show download progress in the Unity Launcher icon.')
__only_for__ = 'gtk'
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'desktop-integration'
__only_for__ = 'unity'
__mandatory_in__ = 'unity'
# FIXME: Due to the fact that we do not yet use the GI-style bindings, we will
# have to run this module in its own interpreter and send commands to it using
@ -17,7 +21,9 @@ __only_for__ = 'gtk'
# this and still expose the same "interface' (LauncherEntry and its methods)
# to our callers.
import os
import subprocess
import sys
import logging
if __name__ != '__main__':
@ -32,6 +38,7 @@ if __name__ != '__main__':
def on_load(self):
logger.info('Starting Ubuntu Unity Integration.')
os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
self.process = subprocess.Popen(['python', __file__],
stdin=subprocess.PIPE)

View File

@ -14,6 +14,7 @@ _ = gpodder.gettext
__title__ = _('Search for new episodes on startup')
__description__ = _('Starts the search for new episodes on startup')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__category__ = 'interface'
__only_for__ = 'gtk'

View File

@ -189,7 +189,7 @@
<property name="reorderable">True</property>
<property name="enable_search">False</property>
<property name="search_column">0</property>
<signal name="row-activated" handler="on_extensions_row_activated" swapped="no"/>
<signal name="button-press-event" handler="on_treeview_button_press_event" swapped="no"/>
</object>
</child>
</object>

View File

@ -93,9 +93,9 @@ dbus_podcasts = 'org.gpodder.podcasts'
dbus_session_bus = None
# Set "win32" to True if we are on Windows
win32 = (platform.system() == 'Windows')
ui.win32 = (platform.system() == 'Windows')
# Set "osx" to True if we are on Mac OS X
osx = (platform.system() == 'Darwin')
ui.osx = (platform.system() == 'Darwin')
# i18n setup (will result in "gettext" to be available)
# Use _ = gpodder.gettext in modules to enable string translations
@ -112,7 +112,7 @@ except AttributeError:
gettext = t.gettext
ngettext = t.ngettext
if win32:
if ui.win32:
try:
# Workaround for bug 650
from gtk.glade import bindtextdomain

View File

@ -88,7 +88,7 @@ defaults = {
# Software updates from gpodder.org (primary audience: Windows users)
'software_update': {
'check_on_startup': gpodder.win32, # check for updates on start
'check_on_startup': gpodder.ui.win32, # check for updates on start
'last_check': 0, # unix timestamp of last update check
'interval': 5, # interval (in days) to check for updates
},

View File

@ -51,6 +51,14 @@ import logging
logger = logging.getLogger(__name__)
CATEGORY_DICT = {
'desktop-integration': _('Desktop Integration'),
'interface': _('Interface'),
'post-download': _('Post download'),
}
DEFAULT_CATEGORY = _('Other')
def call_extensions(func):
"""Decorator to create handler functions in ExtensionManager
@ -91,45 +99,80 @@ class ExtensionMetadata(object):
DEFAULTS = {
'description': _('No description for this extension.'),
}
SORTKEYS = {
'title': 1,
'description': 2,
'category': 3,
'author': 4,
'only_for': 5,
'mandatory_in': 6,
'disable_in': 7,
}
def __init__(self, container, metadata):
if 'title' not in metadata:
metadata['title'] = container.name
category = metadata.get('category', 'other')
metadata['category'] = CATEGORY_DICT.get(category, DEFAULT_CATEGORY)
self.__dict__.update(metadata)
def __getattr__(self, name):
try:
return self.DEFAULTS[name]
except KeyError:
raise AttributeError(name)
except KeyError, e:
raise AttributeError(name, e)
def get_sorted(self):
kf = lambda x: self.SORTKEYS.get(x[0], 99)
return sorted([(k, v) for k, v in self.__dict__.items()], key=kf)
@property
def for_current_ui(self):
"""Check if this extension makes sense for the current UI
def check_ui(self, target, default):
"""Checks metadata information like
__only_for__ = 'gtk'
__mandatory_in__ = 'gtk'
__disable_in__ = 'gtk'
The __only_for__ metadata field in an extension can be a string with
comma-separated values for UIs. This will be checked against boolean
variables in the "gpodder.ui" object.
The metadata fields in an extension can be a string with
comma-separated values for UIs. This will be checked against
boolean variables in the "gpodder.ui" object.
Example metadata field in an extension:
__only_for__ = 'gtk,qml'
__only_for__ = 'unity'
In this case, this function will return True if any of the following
expressions will evaluate to True:
In this case, this function will return the value of the default
if any of the following expressions will evaluate to True:
gpodder.ui.gtk
gpodder.ui.qml
gpodder.ui.unity
gpodder.ui.cli
gpodder.ui.osx
gpodder.ui.win32
New, unknown UIs are silently ignored and will evaluate to False.
"""
if not hasattr(self, 'only_for'):
return True
if not hasattr(self, target):
return default
uis = filter(None, [x.strip() for x in self.only_for.split(',')])
uis = filter(None, [x.strip() for x in getattr(self, target).split(',')])
return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis)
@property
def available_for_current_ui(self):
return self.check_ui('only_for', True)
@property
def mandatory_in_current_ui(self):
return self.check_ui('mandatory_in', False)
@property
def disable_in_current_ui(self):
return self.check_ui('disable_in', False)
class MissingDependency(Exception):
def __init__(self, message, dependency, cause=None):
Exception.__init__(self, message)
@ -217,7 +260,7 @@ class ExtensionContainer(object):
logger.info('Module already loaded.')
return
if not self.metadata.for_current_ui:
if not self.metadata.available_for_current_ui:
logger.info('Not loading "%s" (only_for = "%s")',
self.name, self.metadata.only_for)
return
@ -264,8 +307,12 @@ class ExtensionManager(object):
logger.debug('Found extension "%s" in %s', name, filename)
config = getattr(core.config.extensions, name)
container = ExtensionContainer(self, name, config, filename)
if name in enabled_extensions:
if (name in enabled_extensions or
container.metadata.mandatory_in_current_ui):
container.set_enabled(True)
if (name in enabled_extensions and
container.metadata.disable_in_current_ui):
container.set_enabled(False)
self.containers.append(container)
def shutdown(self):
@ -273,19 +320,23 @@ class ExtensionManager(object):
container.set_enabled(False)
def _config_value_changed(self, name, old_value, new_value):
if name == 'extensions.enabled':
for container in self.containers:
new_enabled = (container.name in new_value)
if new_enabled != container.enabled:
logger.info('Extension "%s" is now %s', container.name,
'enabled' if new_enabled else 'disabled')
container.set_enabled(new_enabled)
if new_enabled and not container.enabled:
logger.warn('Could not enable extension: %s',
container.error)
self.core.config.extensions.enabled = [x
for x in self.core.config.extensions.enabled
if x != container.name]
if name != 'extensions.enabled':
return
for container in self.containers:
new_enabled = (container.name in new_value)
if new_enabled == container.enabled:
continue
logger.info('Extension "%s" is now %s', container.name,
'enabled' if new_enabled else 'disabled')
container.set_enabled(new_enabled)
if new_enabled and not container.enabled:
logger.warn('Could not enable extension: %s',
container.error)
self.core.config.extensions.enabled = [x
for x in self.core.config.extensions.enabled
if x != container.name]
def _find_extensions(self):
extensions = {}
@ -309,7 +360,10 @@ class ExtensionManager(object):
def get_extensions(self):
"""Get a list of all loaded extensions and their enabled flag"""
return self.containers
return [c for c in self.containers
if c.metadata.available_for_current_ui and
not c.metadata.mandatory_in_current_ui and
not c.metadata.disable_in_current_ui]
# Define all known handler functions here, decorate them with the
# "call_extension" decorator to forward all calls to extension scripts that have

View File

@ -166,7 +166,7 @@ class VideoFormatList(gtk.ListStore):
self._config.youtube.preferred_fmt_id = value
class gPodderPreferences(BuilderWidget):
C_TOGGLE, C_LABEL, C_EXTENSION = range(3)
C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = range(4)
def new(self):
for cb in (self.combo_audio_player_app, self.combo_video_player_app):
@ -249,7 +249,6 @@ class gPodderPreferences(BuilderWidget):
self._config.connect_gtk_togglebutton('device_sync.skip_played_episodes', self.checkbutton_skip_played_episodes)
# Have to do this before calling set_active on checkbutton_enable
self._enable_mygpo = self._config.mygpo.enabled
@ -266,29 +265,48 @@ class gPodderPreferences(BuilderWidget):
self.set_flattr_preferences()
# Configure the extensions manager GUI
self.set_extension_preferences()
def set_extension_preferences(self):
toggle_cell = gtk.CellRendererToggle()
toggle_cell.connect('toggled', self.on_extensions_cell_toggled)
toggle_column = gtk.TreeViewColumn('', toggle_cell, active=self.C_TOGGLE)
toggle_column.set_clickable(True)
self.treeviewExtensions.append_column(toggle_column)
toggle_cell.connect('toggled', self.on_extensions_cell_toggled)
renderer = gtk.CellRendererText()
renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
column = gtk.TreeViewColumn(_('Name'), renderer, markup=self.C_LABEL)
column.set_clickable(False)
column.set_resizable(True)
column.set_expand(True)
self.treeviewExtensions.append_column(column)
name_cell = gtk.CellRendererText()
name_cell.set_property('ellipsize', pango.ELLIPSIZE_END)
extension_column = gtk.TreeViewColumn(_('Name'))
extension_column.pack_start(toggle_cell, False)
extension_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE)
extension_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE)
extension_column.pack_start(name_cell, True)
extension_column.add_attribute(name_cell, 'markup', self.C_LABEL)
extension_column.set_clickable(False)
extension_column.set_resizable(True)
extension_column.set_expand(True)
self.treeviewExtensions.append_column(extension_column)
self.extensions_model = gtk.ListStore(bool, str, object)
self.extensions_model = gtk.ListStore(bool, str, object, bool)
def key_func(pair):
category, container = pair
return (category, container.metadata.title)
for container in gpodder.user_extensions.get_extensions():
def convert(extensions):
for container in extensions:
yield (container.metadata.category, container)
old_category = None
for category, container in sorted(convert(gpodder.user_extensions.get_extensions()), key=key_func):
if old_category != category:
label = '<span weight="bold">%s</span>' % cgi.escape(category)
self.extensions_model.append((None, label, None, False))
old_category = category
label = '%s\n<small>%s</small>' % (
cgi.escape(container.metadata.title),
cgi.escape(container.metadata.description))
self.extensions_model.append([container.enabled, label, container])
self.extensions_model.append((container.enabled, label, container, True))
self.extensions_model.set_sort_column_id(self.C_LABEL, gtk.SORT_ASCENDING)
self.treeviewExtensions.set_model(self.extensions_model)
self.treeviewExtensions.columns_autosize()
@ -347,18 +365,27 @@ class gPodderPreferences(BuilderWidget):
self.show_message(container.error.message,
_('Extension cannot be activated'), important=True)
model.set_value(it, self.C_TOGGLE, False)
def on_treeview_button_press_event(self, treeview, event):
if event.button != 3:
return
def on_extensions_row_activated(self, treeview, path, view_column):
x = int(event.x)
y = int(event.y)
path, _, _, _ = treeview.get_path_at_pos(x, y)
model = treeview.get_model()
container = model.get_value(model.get_iter(path), self.C_EXTENSION)
self.show_extension_info(model, container)
# This is one ugly hack, but it displays the container's attributes
# and the attributes of the metadata object of the container..
def show_extension_info(self, model, container):
if not container or not model:
return
# This is one ugly hack, but it displays the attributes of
# the metadata object of the container..
info = '\n'.join('<b>%s:</b> %s' %
tuple(map(cgi.escape, map(str, (key, value))))
for key, value in sorted(container.__dict__.items() +
[('metadata.'+k, v)
for k, v in container.metadata.__dict__.items()]))
for key, value in container.metadata.get_sorted())
self.show_message(info, _('Extension module info'), important=True)

View File

@ -91,7 +91,7 @@ from gpodder import extensions
macapp = None
if gpodder.osx and getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
if gpodder.ui.osx and getattr(gtk.gdk, 'WINDOWING', 'x11') == 'quartz':
try:
from gtk_osxapplication import *
macapp = OSXApplication()
@ -3450,7 +3450,7 @@ def main(options=None):
if options.subscribe:
util.idle_add(gp.subscribe_to_url, options.subscribe)
if gpodder.osx:
if gpodder.ui.osx:
from gpodder.gtkui import macosx
# Handle "subscribe to podcast" events from firefox

View File

@ -182,7 +182,7 @@ class Exporter(object):
if available < 2*len(data)+FREE_DISK_SPACE_AFTER:
# On Windows, if we have zero bytes available, assume that we have
# not had the win32file module available + assume enough free space
if not gpodder.win32 or available > 0:
if not gpodder.ui.win32 or available > 0:
logger.error('Not enough free disk space to save channel list to %s', self.filename)
return False
fp = open(self.filename+'.tmp', 'w')

View File

@ -196,7 +196,7 @@ class Device(services.ObservableService):
def close(self):
self.notify('status', _('Writing data to disk'))
if self._config.device_sync.after_sync.sync_disks and not gpodder.win32:
if self._config.device_sync.after_sync.sync_disks and not gpodder.ui.win32:
os.system('sync')
else:
logger.warning('Not syncing disks. Unmount your device before unplugging.')

View File

@ -87,7 +87,7 @@ if encoding is None:
logger.info('Detected encoding: %s', encoding)
elif gpodder.ui.harmattan:
encoding = 'utf-8'
elif gpodder.win32:
elif gpodder.ui.win32:
# To quote http://docs.python.org/howto/unicode.html:
# ,,on Windows, Python uses the name "mbcs" to refer
# to whatever the currently configured encoding is``
@ -444,7 +444,7 @@ def get_free_disk_space(path):
if not os.path.exists(path):
return 0
if gpodder.win32:
if gpodder.ui.win32:
return get_free_disk_space_win32(path)
s = os.statvfs(path)
@ -1020,7 +1020,7 @@ def find_command(command):
for path in os.environ['PATH'].split(os.pathsep):
command_file = os.path.join(path, command)
if gpodder.win32 and not os.path.exists(command_file):
if gpodder.ui.win32 and not os.path.exists(command_file):
for extension in ('.bat', '.exe'):
cmd = command_file + extension
if os.path.isfile(cmd):
@ -1230,9 +1230,9 @@ def gui_open(filename):
on Win32, os.startfile() is used
"""
try:
if gpodder.win32:
if gpodder.ui.win32:
os.startfile(filename)
elif gpodder.osx:
elif gpodder.ui.osx:
subprocess.Popen(['open', filename])
else:
subprocess.Popen(['xdg-open', filename])
@ -1560,7 +1560,7 @@ def atomic_rename(old_name, new_name):
the new contents into a temporary file and then moving the
temporary file over the original file to replace it.
"""
if gpodder.win32:
if gpodder.ui.win32:
# Win32 does not support atomic rename with os.rename
shutil.move(old_name, new_name)
else:
@ -1663,10 +1663,10 @@ def connection_available():
if no network interfaces are up (i.e. no connectivity).
"""
try:
if gpodder.win32:
if gpodder.ui.win32:
# FIXME: Implement for Windows
return True
elif gpodder.osx:
elif gpodder.ui.osx:
return len(list(osx_get_active_interfaces())) > 0
return True
else: