add work for the new extension system (bugs #1491, #1500, #1555)

This commit is contained in:
Bernd Schlapsi 2012-02-04 21:40:47 +01:00 committed by Thomas Perl
parent a94819a233
commit 263c9092b5
5 changed files with 1237 additions and 0 deletions

View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 2.12 -->
<object class="GtkDialog" id="gPodderExtensionManager">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Extension Manager</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">700</property>
<property name="default_height">400</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="vbox_extension">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkVBox" id="vbox_for_extension_selection">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="labelInstructions">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label">Choose which plugins should be loaded at startup:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow2">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="treeviewExtensions">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="reorderable">True</property>
<property name="enable_search">False</property>
<property name="search_column">0</property>
<signal name="row-activated" handler="on_row_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection1">
<signal name="changed" handler="on_selection_changed" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkStatusbar" id="extension_statusbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHButtonBox" id="hbox_plugin_buttons">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="btnExtensionPrefs">
<property name="label">gtk-properties</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnExtensionPrefs_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="separator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkButtonBox" id="hbox_main_buttons">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButton" id="btnOK">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">btnOK</action-widget>
</action-widgets>
</object>
</interface>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<object class="GtkDialog" id="gPodderExtensionPreference">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="default_width">400</property>
<property name="default_height">400</property>
<property name="type_hint">dialog</property>
<property name="has_resize_grip">False</property>
<child internal-child="vbox">
<object class="GtkBox" id="vbox_extensionpref">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="buttons_extensionpref">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="btnRevert">
<property name="label">gtk-revert-to-saved</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_text" translatable="yes">Revert to default settings</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnRevert_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnClose">
<property name="label">gtk-close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnClose_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll_extensionpref">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">never</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport" id="viewport_extensionpref">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">btnRevert</action-widget>
<action-widget response="0">btnClose</action-widget>
</action-widgets>
</object>
</interface>

478
src/gpodder/extensions.py Normal file
View File

@ -0,0 +1,478 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2009 Thomas Perl and 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/>.
"""
Loads and executes user extentions.
Extentions are python scripts in the "Extensions" folder of $GPODDER_HOME. Each script
must define a class named "gPodderExtensions", otherwise it will be ignored.
The extentions class defines several callbacks that will be called by the
gPodder application at certain points. See the methods defined below
for a list on what these callbacks are and the parameters they take.
For an example extension see examples/extensions.py
"""
import glob
import imp
import inspect
import json
import os
import functools
import sys
from datetime import datetime
import gpodder
from gpodder import util
from gpodder.jsonconfig import JsonConfig
import logging
logger = logging.getLogger(__name__)
# The class name that has to appear in a extension module
EXTENSION_CLASS = 'gPodderExtension'
# The variable name that stores the extensions parameters
EXTENSION_PARAMS = 'PARAMS'
# The variable name that stores the extensions parameters
EXTENSION_CONFIG = 'DEFAULT_CONFIG'
EXTENSION_FOLDER = 'gpodder_extensions'
FILE_EXTENSION_NAME = 'extension.py'
FILE_META_NAME = 'metadata.json'
def call_extensions(func):
"""Decorator to create handler functions in ExtensionManager
Calls the specified function in all user extensions that define it.
"""
method_name = func.__name__
@functools.wraps(func)
def handler(self, *args, **kwargs):
result = None
for extension_consumer, state in self.modules:
if state:
try:
callback = getattr(extension_consumer.module, method_name, None)
if callback is not None:
# If the results are lists, concatenate them to show all
# possible items that are generated by all extension together
cb_res = callback(*args, **kwargs)
if isinstance(result, list) and isinstance(cb_res, list):
result.extend(cb_res)
elif cb_res is not None:
result = cb_res
except Exception, e:
logger.error('Error in %s, function %s: %s', extension_consumer.extension_file,
method_name, e, exc_info=True)
func(self, *args, **kwargs)
return result
return handler
class ExtensionParent(object):
"""Super class for every extension"""
def __init__(self, **kwargs):
self.metadata = kwargs.get('metadata', None)
self.config = kwargs.get('config', None)
self.context_menu_callback = None
self.notify = gpodder.notify
self.read_metadata()
# this code is needed when running the extensions unittests
if isinstance(self.config, dict) and self.metadata:
self.config = JsonConfig(data=json.dumps(self.config))
self.config = getattr(self.config.extensions, self.metadata['id'])
def read_metadata(self):
self.id = None
self.name = None
self.desc = None
self.authors = []
if self.metadata is not None:
if self.metadata.has_key('id'):
self.id = self.metadata['id']
if self.metadata.has_key('name'):
self.name = self.metadata['name']
if self.metadata.has_key('desc'):
self.desc = self.metadata['desc']
if self.metadata.has_key('authors'):
self.desc = self.metadata['authors']
def check_command(self, cmd):
"""Check if a command line command/program exists"""
import shlex
import subprocess
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
if isinstance(cmd, unicode):
cmd = cmd.encode('ascii', 'ignore')
program = shlex.split(cmd)[0]
try:
subprocess.Popen(shlex.split('%s --version' % program),
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except Exception as (errno, errstr):
raise ImportError('%s: %s' % (errstr, program))
def notify_action(self, action, episode):
"""method to simple use the notification system"""
now = datetime.now()
name = 'gPodder-Extension'
if self.name is not None:
name = '%s: %s' % (name, self.name)
self.notify.message(name, "%s(%s): '%s/%s'" % (action,
now.strftime('%x %X'), episode.channel.title, episode.title))
def _show_context_menu(self, episodes):
"""return if a context menu entry should be displayed"""
return False
def on_episodes_context_menu(self, episodes):
"""add context menu entry for a specific extension"""
if self.name is None:
return None
if not self._show_context_menu(episodes) or self.context_menu_callback is None:
return None
return [(self.name, self.context_menu_callback)]
def update_episode_file(self, episode, filename):
"""method for simple update an episode with filename information"""
if not os.path.exists(filename):
return
basename, extension = os.path.splitext(filename)
episode.download_filename = os.path.basename(filename)
episode.file_size = os.path.getsize(filename)
episode.mime_type = util.mimetype_from_extension(extension)
episode.save()
episode.db.commit()
class ExtensionConsumer(object):
"""A class which manage one extension
it loads the meta information from metadata.json,
it loads the extension class from extension.py
and it loads default configuration
"""
def __init__(self, config=None, path=None, module=None):
self.module = module
self._gpo_config = config
self.config = None
self.extension_file = None
self.metadata = None
self.params = None
if path is not None and module is None:
self.extension_file = os.path.join(path, FILE_EXTENSION_NAME)
metadata_file = os.path.join(path, FILE_META_NAME)
self.metadata = self._load_metadata(metadata_file)
elif path is None and module is not None:
pass
else:
logger.error("ExtensionConsumer couldn't initialize successfully")
def _load_module(self, filepath):
path, filename = os.path.split(filepath)
package_name = os.path.split(path)[1]
module_name, module_extension = os.path.splitext(filename)
pkg_file, pkg_path, pkg_desc = imp.find_module(package_name, [ os.path.split(path)[0] ])
pkg = imp.load_module(package_name, None, pkg_path, pkg_desc)
module_file, module_path, module_desc = imp.find_module(module_name, pkg.__path__)
return imp.load_module('%s.%s' % (package_name, module_name),
file(filepath, 'r'), module_path, module_desc)
def _load_metadata(self, metadata_file):
with open(metadata_file, 'r') as f:
metadata = json.load(f)
return metadata
def _load_user_prefs(self, module_file):
if not self.metadata['id'] in self._gpo_config.extensions.keys():
config = getattr(module_file, EXTENSION_CONFIG, None)
if config is not None:
self._gpo_config.register_defaults(config)
return getattr(self._gpo_config.extensions, self.metadata['id'])
def load_extension(self):
"""Load a Python module by filename
Returns an instance of the EXTENSION_CLASS class defined
in the module, or None if the module does not contain
such a class.
"""
try:
module_file = self._load_module(self.extension_file)
self.config = self._load_user_prefs(module_file)
self.params = getattr(module_file, EXTENSION_PARAMS, None)
extension_class = getattr(module_file, EXTENSION_CLASS, None)
self.module = extension_class(
metadata=self.metadata,
config=self.config
)
logger.info('Module loaded: %s', self.extension_file)
return None
except Exception, e:
logger.error('Cannot load %s: %s', self.extension_file, e, exc_info=True)
return e
def revert_settings(self):
"""revert stored extension settings to the default values"""
if self.metadata['id'] in self._gpo_config.extensions.keys():
del self._gpo_config.extensions[self.metadata['id']]
module_file = self._load_module(self.extension_file)
config = getattr(module_file, EXTENSION_CONFIG, None)
if config is not None:
self._gpo_config.register_defaults(config)
self._gpo_config.save()
class ExtensionManager(object):
"""Manager class for the extensions
This class loads all available extensions from the filesystem
it also holds all "hook"-methods which can be used in an
extension
"""
DISABLED, ENABLED = range(2)
EXTENSIONCONSUMER, STATE = range(2)
def __init__(self, config):
extension_root_path = self._get_extension_root_path()
if extension_root_path is None:
return
self.modules = []
enabled_extensions = []
self._config = config
enabled_extensions = self._config.extensions.enabled
pathname = os.path.join(extension_root_path, '*/')
for extension_path in glob.glob(pathname):
extension_consumer = ExtensionConsumer(
config=self._config, path=extension_path)
state = self.DISABLED
extension_id = extension_consumer.metadata['id']
if extension_id in enabled_extensions:
error = extension_consumer.load_extension()
if error is None:
state = self.ENABLED
else:
state = self.DISABLED
enabled_extensions.remove(extension_id)
self.modules.append((extension_consumer, state, ))
def register_extensions(self, obj):
"""Register an object that implements some extensions."""
self.modules.append((ExtensionConsumer(module=obj), self.ENABLED))
def unregister_extensions(self, obj):
"""Unregister a previously registered object."""
extension_module = (ExtensionConsumer(module=obj), self.ENABLED)
if extension_module in self.modules:
self.modules.remove(extension_module)
else:
logger.warn('Unregistered extension which was not registered.')
def _get_extension_root_path(self):
"""returns the file-system path where the extensions are stored"""
extension_root_path = os.environ.get('GPODDER_EXTENSIONS', None)
if extension_root_path is None:
gpo_folder = os.path.dirname(gpodder.__file__)
extension_root_path = os.path.join(gpo_folder, EXTENSION_FOLDER)
if extension_root_path is not None:
sys.path.append(extension_root_path)
logger.info('Reading extension script from %s', extension_root_path)
return extension_root_path
def get_extensions(self):
"""returns a list of all loaded extensions with the enabled/disable state"""
enabled_extensions = self._config.extensions.enabled
for index, (extension_consumer, enabled) in enumerate(self.modules):
if extension_consumer.metadata:
if extension_consumer.metadata['id'] in enabled_extensions:
enabled = self.ENABLED
else:
enabled = self.DISABLED
self.modules[index] = (extension_consumer, enabled, )
return self.modules
# Define all known handler functions here, decorate them with the
# "call_extension" decorator to forward all calls to extension scripts that have
# the same function defined in them. If the handler functions here contain
# any code, it will be called after all the extensions have been called.
@call_extensions
def on_ui_initialized(self, model, update_podcast_callback,
download_episode_callback):
"""Called when the user interface is initialized.
@param model: A gpodder.model.Model instance
@param update_podcast_callback: Function to update a podcast feed
@param download_episode_callback: Function to download an episode
"""
pass
@call_extensions
def on_podcast_subscribe(self, podcast):
"""Called when the user subscribes to a new podcast feed.
@param podcast: A gpodder.model.PodcastChannel instance
"""
pass
@call_extensions
def on_podcast_updated(self, podcast):
"""Called when a podcast feed was updated
This extension will be called even if there were no new episodes.
@param podcast: A gpodder.model.PodcastChannel instance
"""
pass
@call_extensions
def on_podcast_update_failed(self, podcast, exception):
"""Called when a podcast update failed.
@param podcast: A gpodder.model.PodcastChannel instance
@param exception: The reason.
"""
pass
@call_extensions
def on_podcast_save(self, podcast):
"""Called when a podcast is saved to the database
This extensions will be called when the user edits the metadata of
the podcast or when the feed was updated.
@param podcast: A gpodder.model.PodcastChannel instance
"""
pass
@call_extensions
def on_podcast_delete(self, podcast):
"""Called when a podcast is deleted from the database
@param podcast: A gpodder.model.PodcastChannel instance
"""
pass
@call_extensions
def on_episode_save(self, episode):
"""Called when an episode is saved to the database
This extension will be called when a new episode is added to the
database or when the state of an existing episode is changed.
@param episode: A gpodder.model.PodcastEpisode instance
"""
pass
@call_extensions
def on_episode_downloaded(self, episode):
"""Called when an episode has been downloaded
You can retrieve the filename via episode.local_filename(False)
@param episode: A gpodder.model.PodcastEpisode instance
"""
pass
@call_extensions
def on_all_episodes_downloaded(self):
"""Called when all episodes has been downloaded
"""
pass
@call_extensions
def on_episodes_context_menu(self, episodes):
"""Called when the episode list context menu is opened
You can add additional context menu entries here. You have to
return a list of tuples, where the first item is a label and
the second item is a callable that will get the episode as its
first and only parameter.
Example return value:
[('Mark as new', lambda episodes: ...)]
@param episodes: A list of gpodder.model.PodcastEpisode instances
"""
pass
@call_extensions
def on_episode_delete(self, episode, filename):
"""Called just before the episode's disk file is about to be
deleted."""
pass
@call_extensions
def on_episode_removed_from_podcast(self, episode):
"""Called just before the episode is about to be removed from
the podcast channel, e.g., when the episode has not been
downloaded and it disappears from the feed.
@param podcast: A gpodder.model.PodcastChannel instance
"""
pass

View File

@ -0,0 +1,415 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2011 Thomas Perl and 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 gobject
import gtk
import pango
import os.path
import gpodder
_ = gpodder.gettext
N_ = gpodder.ngettext
import logging
logger = logging.getLogger(__name__)
from gpodder.gtkui.interface.common import BuilderWidget
class ExtensionTextItem(gtk.VBox):
def __init__(self, key, settings, value):
gtk.VBox.__init__(self, spacing=6)
self.set_border_width(6)
self.key = key
self.__label = gtk.Label(settings['desc'])
self.__label.set_alignment(0, 0)
self.__entry = gtk.Entry(max=0)
self.__entry.set_visibility(True)
self.__entry.set_editable(True)
self.__entry.set_text(value)
self.pack_start(self.__label, False, False, 0)
self.pack_start(self.__entry, False, False, 0)
def get_key(self):
return self.key
def get_value(self):
return self.__entry.get_text()
def set_value(self, settings, value):
self.__entry.set_text(value)
class ExtensionCheckBox(gtk.CheckButton):
def __init__(self, key, settings, value):
gtk.CheckButton.__init__(self)
self.set_border_width(6)
self.key = key
self.set_label(settings['desc'])
self.set_active(value)
def get_key(self):
return self.key
def get_value(self):
return self.get_active()
def set_value(self, settings, value):
self.set_active(value)
class ExtensionSpinButton(gtk.VBox):
def __init__(self, key, settings, value):
gtk.VBox.__init__(self, spacing=6)
self.set_border_width(6)
self.key = key
self.__label = gtk.Label(settings['desc'])
self.__label.set_alignment(0, 0)
self.__spin = gtk.SpinButton(
adjustment=gtk.Adjustment(value, 0, 1000, 1),
climb_rate=1, digits=2
)
self.pack_start(self.__label, False, False, 0)
self.pack_start(self.__spin, False, False, 0)
def get_key(self):
return self.key
def get_value(self):
return self.__spin.get_value()
def set_value(self, settings, value):
self.__spin.set_value(value)
class ExtensionMultiChoice(gtk.VBox):
def __init__(self, key, settings, value):
gtk.VBox.__init__(self, spacing=6)
self.set_border_width(6)
self.key = key
self.__label = gtk.Label(settings['desc'])
self.__label.set_alignment(0, 0)
self.__treeView = gtk.TreeView(self._get_model(settings, value))
toggle_cell = gtk.CellRendererToggle()
toggle_cell.connect('toggled', self.value_changed, self.__treeView)
toggle_column = gtk.TreeViewColumn('', toggle_cell, active=0)
toggle_column.set_clickable(True)
self.__treeView.append_column(toggle_column)
renderer = gtk.CellRendererText()
renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
column = gtk.TreeViewColumn(_('Podcast'), renderer, markup=1)
column.set_clickable(False)
column.set_resizable(True)
column.set_expand(True)
self.__treeView.append_column(column)
self.__treeView.columns_autosize()
self.pack_start(self.__label, False, False, 0)
self.pack_start(self.__treeView, False, False, 0)
def _get_model(self, settings, value):
model = gtk.ListStore(
gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_INT
)
multichoice_list = zip(value, settings['list'])
for index, (state, text) in enumerate(multichoice_list):
model.append(row=(state, text, index))
return model
def value_changed(self, cell, path, treeview):
model = treeview.get_model()
iter = model.get_iter(path)
value = model.get_value(iter, 0)
model.set_value(iter, 0, not value)
def get_key(self):
return self.key
def get_value(self):
model = self.__treeView.get_model()
result = []
iter = model.get_iter_first()
while iter is not None:
value = model.get_value(iter, 0)
result.append(value)
iter = model.iter_next(iter)
return result
def set_value(self, settings, value):
model = self._get_model(settings, value)
self.__treeView.set_model(model)
class ExtensionRadioGroup(gtk.VBox):
def __init__(self, key, settings, value):
gtk.VBox.__init__(self, spacing=6)
self.set_border_width(6)
self.key = key
self.__label = gtk.Label(settings['desc'])
self.__label.set_alignment(0, 0)
self.__radioButtons = []
choices = zip(value, settings['list'])
group = None
for state, label in choices:
rb = gtk.RadioButton(group, label, False)
rb.set_active(state)
group = rb
self.__radioButtons.append(rb)
self.pack_start(self.__label, False, False, 0)
for rb in self.__radioButtons:
self.pack_start(rb, False, False, 0)
def get_key(self):
return self.key
def get_value(self):
values = []
for rb in self.__radioButtons:
values.append(rb.get_active())
return values
def set_value(self, settings, value):
choices = zip(value, self.__radioButtons)
for state, rb in choices:
rb.set_active(state)
#TODO: Replace this implementation with a ComboBoxEntry or expandable TreeView
class ExtensionComboBoxEntry(ExtensionTextItem):
def __init__(self, key, settings, value):
value = ';'.join(value)
ExtensionTextItem.__init__(self, key, settings, value)
def get_value(self):
value = super(ExtensionComboBoxEntry, self).get_value()
return value.split(';')
def set_value(self, settings, value):
value = ';'.join(value)
super(ExtensionComboBoxEntry, self).set_value(settings, value)
class gPodderExtensionPreference(BuilderWidget):
widgets = { 'textitem': ExtensionTextItem,
'checkbox': ExtensionCheckBox,
'spinbutton': ExtensionSpinButton,
'multichoice-list': ExtensionMultiChoice,
'combobox': ExtensionComboBoxEntry,
'radiogroup': ExtensionRadioGroup,
}
def new(self):
"""Extension Preference Dialog
Optional keyword arguments that modify the behaviour of this dialog:
- _extenstion_consumer: ExtensionConsumer class for which the preferences should be displayed
{'cmd': {
'type': 'str',
'desc': 'Defines the command line bittorrent program'}
}
"""
self.params = self._extension_consumer.params
self.config = self._extension_consumer.config
self.vbox = gtk.VBox()
self.viewport_extensionpref.add(self.vbox)
for key, settings in self.params.items():
value = getattr(self._extension_consumer.config, key)
widget = self.widgets[settings['type']](key, settings, value)
self.vbox.pack_start(widget, False, False, 0)
self.gPodderExtensionPreference.show_all()
def on_btnRevert_clicked(self, widget):
self._extension_consumer.revert_settings()
self.config = self._extension_consumer.config
for w in self.vbox.get_children():
key = w.get_key()
value = getattr(self._extension_consumer.config, key)
w.set_value(self.params[key], value)
def on_btnClose_clicked(self, widget):
for w in self.vbox.get_children():
key = w.get_key()
value = w.get_value()
setattr(self.config, key, value)
self.main_window.destroy()
class gPodderExtensionManager(BuilderWidget):
C_INDEX, C_TOOLTIP, C_TOGGLE, C_NAME, C_ID, C_EXTENSIONCONSUMER = range(6)
def new(self):
toggle_cell = gtk.CellRendererToggle()
toggle_cell.connect('toggled', self.toggle_cell_handler)
toggle_column = gtk.TreeViewColumn('', toggle_cell, active=self.C_TOGGLE)
toggle_column.set_clickable(True)
self.treeviewExtensions.append_column(toggle_column)
renderer = gtk.CellRendererText()
renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
column = gtk.TreeViewColumn(_('Name'), renderer, markup=self.C_NAME)
column.set_clickable(False)
column.set_resizable(True)
column.set_expand(True)
self.treeviewExtensions.append_column(column)
renderer = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Extension-ID'), renderer, markup=self.C_ID)
column.set_clickable(False)
column.set_resizable(True)
column.set_expand(False)
self.treeviewExtensions.append_column(column)
self.treeviewExtensions.set_property('has-tooltip', True)
self.treeviewExtensions.connect('query-tooltip', self.treeview_show_tooltip)
column_types = [ gobject.TYPE_INT,
gobject.TYPE_STRING,
gobject.TYPE_BOOLEAN,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_PYOBJECT ]
self.model = gtk.ListStore(*column_types)
for index, (extension_consumer, state) in enumerate( gpodder.user_extensions.get_extensions() ):
if extension_consumer.extension_file is not None:
tooltip = extension_consumer.metadata['desc']
name = extension_consumer.metadata['name']
row = [ index, tooltip, state, name,
extension_consumer.metadata['id'], extension_consumer
]
self.model.append(row)
self.model.set_sort_column_id(self.C_NAME, gtk.SORT_ASCENDING)
self.treeviewExtensions.set_model(self.model)
self.treeviewExtensions.columns_autosize()
self.context_id = self.extension_statusbar.get_context_id('Extension messages')
def _set_enabled_extension_in_config(self, model, path):
iter = model.get_iter(path)
value = model.get_value(iter, self.C_TOGGLE)
name = model.get_value(iter, self.C_NAME)
extension_id = model.get_value(iter, self.C_ID)
extension_consumer = model.get_value(iter, self.C_EXTENSIONCONSUMER)
new_value = not value
if new_value and extension_id not in self._config.extensions.enabled:
error = extension_consumer.load_extension()
if error is None:
self._config.extensions.enabled.append(extension_id)
self.extension_statusbar.remove_all(self.context_id)
else:
self.extension_statusbar.push(self.context_id, "Error %s: %s" % (name, error))
return
if not new_value and extension_id in self._config.extensions.enabled:
self._config.extensions.enabled.remove(extension_id)
self._config.schedule_save()
self._set_preferences_button(not value)
model.set_value(iter, self.C_TOGGLE, not value)
def _get_selected_extension_consumer(self):
selection = self.treeviewExtensions.get_selection()
model, iter = selection.get_selected()
if not iter:
return None
return model.get_value(iter, self.C_EXTENSIONCONSUMER)
def _set_preferences_button(self, value):
extension = self._get_selected_extension_consumer()
if extension and extension.params is not None and value:
self.btnExtensionPrefs.set_sensitive(True)
else:
self.btnExtensionPrefs.set_sensitive(False)
def on_button_close_clicked(self, widget):
self.main_window.destroy()
def on_btnOK_clicked(self, widget):
self.main_window.destroy()
def on_btnExtensionPrefs_clicked(self, widget):
gPodderExtensionPreference(self.main_window,
_extension_consumer = self._get_selected_extension_consumer()
)
def toggle_cell_handler(self, cell, path):
model = self.treeviewExtensions.get_model()
self._set_enabled_extension_in_config(model, path)
def on_row_activated(self, treeview, path, view_column):
model = treeview.get_model()
self._set_enabled_extension_in_config(model, path)
def on_selection_changed(self, treeselection):
model, iter = treeselection.get_selected()
if not iter:
value = False
else:
value = model.get_value(iter, self.C_TOGGLE)
self._set_preferences_button(value)
def treeview_show_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
# TODO: Copied some of the code from src/gpodder/gtkui/desktop/episodeselector.py (gPodderEpisodeSelector.treeview_episodes_query_tooltip)
# maybe we should don't duplicate the code and implement this as a function globaly?!
# With get_bin_window, we get the window that contains the rows without
# the header. The Y coordinate of this window will be the height of the
# treeview header. This is the amount we have to subtract from the
# event's Y coordinate to get the coordinate to pass to get_path_at_pos
(x_bin, y_bin) = treeview.get_bin_window().get_position()
y -= x_bin
y -= y_bin
(path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
if column != treeview.get_columns()[1]:
return False
model = treeview.get_model()
iter = model.get_iter(path)
description = model.get_value(iter, self.C_TOOLTIP)
if description:
tooltip.set_text(description)
return True
else:
return False

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2011 Thomas Perl and 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/>.
#
# gpodder.notify - Initialize the platforms notification system
# Bernd Schlapsi <brot@gmx.info>; 2011-11-20
import gpodder
import platform
class NotifyInterface(object):
def __init__(self, config):
self.config = config
def message(self, title, message):
pass
class NotifyPyNotify(object):
def __init__(self, config):
import pynotify
pynotify.init('gPodder')
self.pynotify = pynotify
self.config = config
def message(self, title, message):
if not self.pynotify.is_initted():
return
if self.config is None or not self.config.enable_notifications:
return
notification = self.pynotify.Notification(title, message,
gpodder.icon_file)
try:
notification.show()
except:
# See http://gpodder.org/bug/966
pass
def init_notify(config):
system = platform.system()
if system == 'Linux':
try:
notify = NotifyPyNotify(config)
except ImportError:
# TODO: Notification class for harmattan?
notify = NotifyInterface(config)
elif system == 'Windows':
# TODO: Notification class for Windows (growl for windows?)
notify = NotifyInterface(config)
elif system == 'Darwin':
# TODO: Notification class for Mac (growl?)
notify = NotifyInterface(config)
return notify