package Padre::PluginManager;
=pod
=head1 NAME
Padre::PluginManager - Padre plug-in manager
=head1 DESCRIPTION
The C<PluginManager> class contains logic for locating and loading Padre
plug-ins, as well as providing part of the interface to plug-in writers.
=head1 METHODS
=cut
# API NOTES:
# - This class uses english-style verb_noun method naming
use 5.008;
use strict;
use warnings;
use Carp ();
use File::Path ();
use File::Spec ();
use File::Basename ();
use Scalar::Util ();
use Params::Util ();
use Class::Inspector ();
use Padre::Constant ();
use Padre::Current ();
use Padre::Util ();
use Padre::PluginHandle ();
use Padre::DB ();
use Padre::Wx ();
use Padre::Wx::Menu::Tools ();
use Padre::Locale::T;
our $VERSION = '0.96';
#####################################################################
# Contants and definitions
# Constants limited to this file
use constant PADRE_HOOK_RETURN_IGNORE => 1;
use constant PADRE_HOOK_RETURN_ERROR => 2;
# List if valid Padre hooks:
our %PADRE_HOOKS = (
before_delete => PADRE_HOOK_RETURN_ERROR,
after_delete => PADRE_HOOK_RETURN_IGNORE,
before_save => PADRE_HOOK_RETURN_ERROR,
after_save => PADRE_HOOK_RETURN_IGNORE,
);
#####################################################################
# Constructor and Accessors
=pod
=head2 C<new>
The constructor returns a new C<Padre::PluginManager> object, but
you should normally access it via the main Padre object:
my $manager = Padre->ide->plugin_manager;
First argument should be a Padre object.
=cut
sub new {
my $class = shift;
my $parent = Params::Util::_INSTANCE( shift, 'Padre' )
or Carp::croak("Creation of a Padre::PluginManager without a Padre not possible");
my $self = bless {
parent => $parent,
plugins => {},
plugin_dir => Padre::Constant::PLUGIN_DIR,
plugin_order => [],
@_,
}, $class;
# Initialize empty My Plugin if needed
$self->reset_my_plugin(0);
return $self;
}
=pod
=head2 C<parent>
Stores a reference back to the parent IDE object.
=head2 C<plugin_dir>
Returns the user plug-in directory (below the Padre configuration directory).
This directory was added to the C<@INC> module search path.
=head2 C<plugins>
Returns a hash (reference) of plug-in names associated with a
L<Padre::PluginHandle>.
This hash is only populated after C<load_plugins()> was called.
=cut
use Class::XSAccessor {
getters => {
parent => 'parent',
plugin_dir => 'plugin_dir',
plugins => 'plugins',
},
};
=head2 current
Gets a L<Padre::Current> context for the plugin manager.
=cut
sub current {
Padre::Current->new( ide => $_[0]->{parent} );
}
=head2 C<main>
A convenience method to get to the main window.
=cut
sub main {
$_[0]->parent->wx->main;
}
# Get the preferred plugin order.
# The order calculation cost is higher than we might like,
# so cache the result.
sub plugin_order {
my $self = shift;
unless ( $self->{plugin_order} ) {
# Schwartzian transform that sorts the plugins by their
# full names, but always puts "My Plug-in" first.
$self->{plugin_order} = [
map { $_->[0] } sort {
( $b->[0] eq 'Padre::Plugin::My' ) <=> ( $a->[0] eq 'Padre::Plugin::My' )
or $a->[1] cmp $b->[1]
} map {
[ $_->class, $_->plugin_name ]
} values %{ $self->{plugins} }
];
}
return @{ $self->{plugin_order} };
}
sub handles {
map { $_[0]->{plugins}->{$_} } $_[0]->plugin_order;
}
#####################################################################
# Bulk Plug-in Operations
#
# $pluginmgr->relocale;
#
# update Padre's locale object to handle new plug-in l10n.
#
sub relocale {
my $self = shift;
my $locale = $self->main->{locale};
foreach my $handle ( $self->handles ) {
# Only process enabled plug-ins
next unless $handle->enabled;
# Add the plug-in locale dir to search path
if ( $handle->plugin_can('plugin_directory_locale') ) {
my $dir = $handle->plugin->plugin_directory_locale;
if ( defined $dir and -d $dir ) {
$locale->AddCatalogLookupPathPrefix($dir);
}
}
# Add the plug-in catalog to the locale
my $code = Padre::Locale::rfc4646();
my $prefix = $handle->locale_prefix;
$locale->AddCatalog("$prefix-$code");
}
return 1;
}
#
# $pluginmgr->reset_my_plugin( $overwrite );
#
# reset the my plug-in if needed. if $overwrite is set, remove it first.
#
sub reset_my_plugin {
my $self = shift;
my $overwrite = shift;
# Do not overwrite it unless stated so.
my $dst = File::Spec->catfile(
Padre::Constant::PLUGIN_LIB,
'My.pm'
);
if ( -e $dst and not $overwrite ) {
return;
}
# Find the My Plug-in
my $src = File::Spec->catfile(
File::Basename::dirname( $INC{'Padre/Config.pm'} ),
'Plugin', 'My.pm',
);
unless ( -e $src ) {
Carp::croak("Could not find the original My plug-in");
}
# Copy the My Plug-in
unlink $dst;
require File::Copy;
unless ( File::Copy::copy( $src, $dst ) ) {
Carp::croak("Could not copy the My plug-in ($src) to $dst: $!");
}
chmod( 0644, $dst );
}
# Disable (but don't unload) all plug-ins when Padre exits.
# Save the plug-in enable/disable states for the next start-up.
sub shutdown {
my $self = shift;
my $lock = $self->main->lock('DB');
foreach my $handle ( $self->handles ) {
if ( $handle->enabled ) {
$handle->update( enabled => 1 );
$self->plugin_disable($handle);
} elsif ( $handle->disabled ) {
$handle->update( enabled => 0 );
}
}
# Remove the circular reference between the main application and
# the plugin manager to complete the destruction.
# This breaks encapsulation a bit, but will do for now.
delete $self->{parent}->{plugin_manager};
delete $self->{parent};
return 1;
}
=pod
=head2 C<load_plugins>
Scans for new plug-ins in the user plug-in directory, in C<@INC>,
and in F<.par> files in the user plug-in directory.
Loads any given module only once, i.e. does not refresh if the
plug-in has changed while Padre was running.
=cut
sub load_plugins {
my $self = shift;
my $lock = $self->main->lock( 'DB', 'refresh_menu_plugins' );
# Put the plug-in directory first in the load order
my $plugin_dir = $self->plugin_dir;
unless ( grep { $_ eq $plugin_dir } @INC ) {
unshift @INC, $plugin_dir;
}
# Attempt to load all plug-ins in the Padre::Plugin::* namespace
my %seen = ();
foreach my $inc (@INC) {
my $dir = File::Spec->catdir( $inc, 'Padre', 'Plugin' );
next unless -d $dir;
local *DIR;
opendir( DIR, $dir ) or die("opendir($dir): $!");
my @files = readdir(DIR) or die("readdir($dir): $!");
closedir(DIR) or die("closedir($dir): $!");
foreach (@files) {
next unless s/\.pm$//;
my $module = "Padre::Plugin::$_";
next if $seen{$module}++;
# Specifically ignore the redundant "Perl 5 Plug-in"
next if $module eq 'Padre::Plugin::Perl5';
$self->_load_plugin($module);
}
}
# Attempt to load all plug-ins in the Acme::Padre::* namespace
# TO DO: Put this code behind some kind of future security option,
# once we have one.
foreach my $inc (@INC) {
my $dir = File::Spec->catdir( $inc, 'Acme', 'Padre' );
next unless -d $dir;
local *DIR;
opendir( DIR, $dir ) or die("opendir($dir): $!");
my @files = readdir(DIR) or die("readdir($dir): $!");
closedir(DIR) or die("closedir($dir): $!");
foreach (@files) {
next unless s/\.pm$//;
my $module = "Acme::Padre::$_";
next if $seen{$module}++;
$self->_load_plugin($module);
}
}
return;
}
=pod
=head2 C<reload_plugins>
For all registered plug-ins, unload them if they were loaded
and then reload them.
=cut
sub reload_plugins {
my $self = shift;
my $lock = $self->main->lock( 'UPDATE', 'DB', 'refresh_menu_plugins' );
# Do not use the reload_plugin method since that
# refreshes the menu every time.
foreach my $module ( $self->plugin_order ) {
$self->_unload_plugin($module);
$self->_load_plugin($module);
$self->enable_editors($module);
}
return 1;
}
=pod
=head2 C<alert_new>
The C<alert_new> method is called by the main window post-initialisation and
checks for new plug-ins. If any are found, it presents a message to
the user.
=cut
sub alert_new {
my $self = shift;
my $plugins = $self->plugins;
my @loaded = sort
map { $_->plugin_name }
grep { $_->loaded } values %$plugins;
if ( @loaded and not $ENV{HARNESS_ACTIVE} ) {
my $msg = Wx::gettext(<<"END_MSG") . join( "\n", @loaded );
We found several new plug-ins.
In order to configure and enable them go to
Plug-ins -> Plug-in Manager
List of new plug-ins:
END_MSG
$self->main->message(
$msg,
Wx::gettext('New plug-ins detected')
);
}
return 1;
}
=pod
=head2 C<failed>
Returns the list of all plugins that the editor attempted to load but
failed. Note that after a failed attempt, the plug-in is usually disabled
in the configuration and not loaded again when the editor is restarted.
=cut
sub failed {
return map { $_->class } grep { $_->error or $_->incompatible } $_[0]->handles;
}
######################################################################
# Loading and Unloading a Plug-in
=pod
=head2 C<load_plugin>
Given a plug-in name such as C<Foo> (the part after C<Padre::Plugin>),
load the corresponding module, enable the plug-in and update the Plug-ins
menu, etc.
=cut
sub load_plugin {
my $self = shift;
my $lock = $self->main->lock( 'DB', 'refresh_menu_plugins' );
$self->_load_plugin(@_);
}
# This method implements the actual mechanics of loading a plug-in,
# without regard to the context it is being called from.
# So this method doesn't do stuff like refresh the plug-in menu.
#
# NOTE: This method looks fairly long, but it's doing
# a very specific and controlled series of steps. Splitting this up
# would just make the process harder to understand, so please don't.
sub _load_plugin {
my $self = shift;
my $module = shift;
my $main = $self->main;
my $plugins = $self->plugins;
# Shortcut and skip if loaded
return if $plugins->{$module};
# Create the plug-in object (and flush the old sort order)
my $handle = $plugins->{$module} = Padre::PluginHandle->new(
class => $module,
);
delete $self->{plugin_order};
# Attempt to load the plug-in
SCOPE: {
# Suppress warnings while loading plugins
local $SIG{__WARN__} = sub () { };
eval "use $module ();";
}
if ($@) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Crashed while loading: %s"),
$module, $@,
)
);
$handle->status('error');
return;
}
# Is the module versioned?
unless ( defined $module->VERSION ) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Plugin is empty or unversioned"),
$module,
)
);
$handle->status('error');
return;
}
# Plug-in must be a Padre::Plugin subclass
unless ( $module->isa('Padre::Plugin') ) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Not a Padre::Plugin subclass"),
$module,
)
);
$handle->status('error');
return;
}
# Is the plugin compatible with this Padre
my $compatible = $self->compatible($module);
if ($compatible) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Not compatible with Padre %s - %s"),
$module,
$Padre::PluginManager::VERSION,
$compatible,
)
);
$handle->status('incompatible');
return;
}
# Attempt to instantiate the plug-in
my $plugin = eval { $module->new( $self->{parent} ); };
if ($@) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Crashed while instantiating: %s"),
$module, $@,
)
);
$handle->status('error');
return;
}
unless ( Params::Util::_INSTANCE( $plugin, 'Padre::Plugin' ) ) {
$handle->errstr(
sprintf(
Wx::gettext("%s - Failed to instantiate plug-in"),
$module,
)
);
$handle->status('error');
return;
}
# Plug-in is now loaded
$handle->{plugin} = $plugin;
$handle->status('loaded');
# Return unless we will enable the plugin
unless ( $handle->db->enabled ) {
$handle->status('disabled');
return;
}
# Add a new directory for locale to search translation catalogs.
if ( $handle->plugin_can('plugin_directory_locale') ) {
my $dir = $plugin->plugin_directory_locale;
if ( defined $dir and -d $dir ) {
my $locale = $main->{locale};
$locale->AddCatalogLookupPathPrefix($dir);
}
}
# FINALLY we can enable the plug-in
$self->plugin_enable($handle);
return 1;
}
sub compatible {
my $self = shift;
my $plugin = shift;
# What interfaces does the plugin need
unless ( $plugin->can('padre_interfaces') ) {
return "$plugin does not declare Padre interface requirements";
}
my @needs = $plugin->padre_interfaces;
while (@needs) {
my $module = shift @needs;
my $need = shift @needs;
# We take two different approaches to the capture of the
# version and compatibility values depending on whether
# the module has been loaded or not.
my $version;
my $compat;
if ( Class::Inspector->loaded($module) ) {
no strict 'refs';
$version = ${"${module}::VERSION"} || 0;
$compat = ${"${module}::COMPATIBLE"} || 0;
} else {
# Find the unloaded file
my $file = Class::Inspector->resolved_filename($module);
unless ( defined $file and length $file ) {
return "$module is not installed or undetectable";
}
# Scan the unloaded file ala EU:MakeMaker
$version = Padre::Util::parse_variable( $file, 'VERSION' );
$compat = Padre::Util::parse_variable( $file, 'COMPATIBLE' );
}
# Does the dependency meet the criteria?
$version = 0 if $version eq 'undef';
$compat = 0 if $compat eq 'undef';
unless ( $need <= $version ) {
return "$module is needed at newer version $need";
}
unless ( $need >= $compat ) {
return "$module is not back-compatible with $need";
}
}
return '';
}
=pod
=head2 C<unload_plugin>
Given a plug-in name such as C<Foo> (the part after C<Padre::Plugin>),
B<disable> the plug-in, B<unload> the corresponding module, and update the Plug-ins
menu, etc.
=cut
sub unload_plugin {
my $self = shift;
my $lock = $self->main->lock('refresh_menu_plugins');
$self->_unload_plugin(@_);
}
# the guts of unload_plugin which don't refresh the menu
sub _unload_plugin {
my $self = shift;
my $handle = $self->handle(shift);
my $lock = $self->main->lock('DB');
# Save state and disable if needed
if ( $handle->enabled ) {
$handle->update( enabled => 1 );
$handle->disable;
} else {
$handle->update( enabled => 0 );
}
# Destruct the plug-in
if ( defined $handle->plugin ) {
$handle->{plugin} = undef;
}
# Unload the plug-in class itself
$handle->unload;
# Finally, remove the handle (and flush the sort order)
delete $self->{plugins}->{ $handle->class };
delete $self->{plugin_order};
return 1;
}
sub plugin_enable {
my $self = shift;
my $handle = $self->handle(shift) or return;
$handle->enable;
}
sub plugin_disable {
my $self = shift;
my $handle = $self->handle(shift) or return;
$handle->disable;
}
sub user_enable {
my $self = shift;
my $handle = $self->handle(shift) or return;
$handle->update( enabled => 1 );
$self->plugin_enable($handle);
}
sub user_disable {
my $self = shift;
my $handle = $self->handle(shift) or return;
$handle->update( enabled => 0 );
$self->plugin_disable($handle);
}
=pod
=head2 C<reload_plugin>
Reload a single plug-in whose name (without C<Padre::Plugin::>)
is passed in as first argument.
=cut
sub reload_plugin {
my $self = shift;
my $handle = self->handle(shift) or return;
my $lock = $self->main->lock( 'UPDATE', 'DB', 'refresh_menu_plugins' );
$self->_unload_plugin($handle);
$self->_load_plugin($handle) or return;
$self->enable_editors($handle) or return;
return 1;
}
# Fire a event on all active plugins
sub plugin_event {
my $self = shift;
my $event = shift;
foreach my $handle ( $self->handles ) {
next unless $handle->enabled;
next unless $handle->plugin_can($event);
eval { $handle->plugin->$event(@_); };
if ($@) {
$self->_error(
$handle,
sprintf(
Wx::gettext('Plugin error on event %s: %s'),
$event,
$@,
)
);
next;
}
}
return 1;
}
# Run a plugin hook
sub hook {
my $self = shift;
my $hookname = shift;
my @args = @_;
my $result = 1; # Default to success
if ( ref( $self->{hooks}->{$hookname} ) eq 'ARRAY' ) {
for my $hook ( @{ $self->{hooks}->{$hookname} } ) {
my @retval = eval { &{ $hook->[1] }( $hook->[0], @args ); };
if ($@) {
warn 'Plugin ' . $hook->[0] . ', hook ' . $hookname . ', code ' . $hook->[1] . ' crashed with ' . $@;
next;
}
# Return value handling depends on hook type
if ( $PADRE_HOOKS{$hookname} == PADRE_HOOK_RETURN_ERROR ) {
next unless defined( $retval[0] ); # Returned undef = no error
$self->main->error(
$retval[0] || sprintf(
Wx::gettext('Plugin %s, hook %s returned an emtpy error message'), $hook->[0], $hookname
)
);
$result = 0;
}
}
}
return $result;
}
# Show an error message
sub _error {
my $self = shift;
my $plugin = shift || Wx::gettext('(core)');
my $text = shift || Wx::gettext('Unknown error');
# Report detailed plugin error to console
my @callerinfo = caller(0);
my @callerinfo1 = caller(1);
print STDERR 'Plugin ', $plugin, ' error at ', $callerinfo[1] . ' line ' . $callerinfo[2],
' in ' . $callerinfo[0] . '::' . $callerinfo1[3], ': ' . $text . "\n";
# Report to user
$self->main->error( sprintf( Wx::gettext('Plugin %s'), $plugin ) . ': ' . $text );
}
# Enable all the plug-ins for a single editor
sub editor_enable {
my $self = shift;
my $editor = shift;
return $self->plugin_event( 'editor_enable', $editor, $editor->{Document} );
}
sub editor_disable {
my $self = shift;
my $editor = shift;
return $self->plugin_event( 'editor_disable', $editor, $editor->{Document} );
}
sub enable_editors_for_all {
my $self = shift;
foreach my $handle ( $self->handles ) {
$self->enable_editors($handle);
}
return 1;
}
sub enable_editors {
my $self = shift;
my $handle = $self->handle(shift) or return;
return unless $handle->enabled;
return unless $handle->plugin_can('editor_enable');
foreach my $editor ( $self->main->editors ) {
local $@;
eval { $handle->plugin->editor_enable( $editor, $editor->{Document} ); };
}
return 1;
}
######################################################################
# Menu Integration
# Generate the menu for a plug-in
sub get_menu {
my $self = shift;
my $main = shift;
my $handle = $self->handle(shift) or return ();
return () unless $handle->enabled;
return () unless $handle->plugin_can('menu_plugins');
my @menu = eval { $handle->plugin->menu_plugins($main); };
if ($@) {
$handle->{status} = 'error';
$handle->errstr(
_T('Error when calling menu for plug-in %s: %s'),
$handle->class,
$@,
);
# TO DO: make sure these error messages show up somewhere or it will drive
# crazy anyone trying to write a plug-in
return ();
}
# Plugin provides a single menu item
if ( @menu == 1
and Params::Util::_INSTANCE( $menu[0], 'Wx::MenuItem' ) )
{
return @menu;
}
# Plugin provides a full submenu
if ( @menu == 2
and defined Params::Util::_STRING( $menu[0] )
and Params::Util::_INSTANCE( $menu[1], 'Wx::Menu' ) )
{
return ( -1, @menu );
}
# Unrecognised or unsupported menu type
return ();
}
=pod
=head2 C<reload_current_plugin>
When developing a plug-in one usually edits the
files belonging to the plug-in (The C<Padre::Plugin::Wonder> itself
or C<Padre::Documents::Wonder> located in the same project as the plug-in
itself.
This call and the appropriate menu option should be able to load
(or reload) that plug-in.
=cut
# TODO: Move this into the developer plugin, nobody needs this unless they
# are actively working on a Padre plugin.
sub reload_current_plugin {
my $self = shift;
my $current = $self->current;
my $main = $current->main;
my $filename = $current->filename;
my $project = $current->project;
# Do we have what we need?
unless ($filename) {
return $main->error( Wx::gettext('No filename') );
}
unless ($project) {
return $main->error( Wx::gettext('Could not locate project directory.') );
}
# TO DO shall we relax the assumption of a lib subdir?
my $root = $project->root;
$root = File::Spec->catdir( $root, 'lib' );
local @INC = ( $root, grep { $_ ne $root } @INC );
my ($plugin_filename) = glob File::Spec->catdir( $root, 'Padre', 'Plugin', '*.pm' );
# Load plug-in
my $plugin = 'Padre::Plugin::' . File::Basename::basename($plugin_filename);
$plugin =~ s/\.pm$//;
my $plugins = $self->plugins;
if ( $plugins->{$plugin} ) {
$self->reload_plugin($plugin);
} else {
$self->load_plugin($plugin);
if ( $self->plugins->{$plugin}->{status} eq 'error' ) {
$main->error(
sprintf(
Wx::gettext("Failed to load the plug-in '%s'\n%s"),
$plugin, $self->plugins->{$plugin}->errstr
)
);
return;
}
}
return;
}
=pod
=head2 C<on_context_menu>
Called by C<Padre::Wx::Editor> when a context menu is about to
be displayed. The method calls the context menu hooks in all plug-ins
that have one for plug-in specific manipulation of the context menu.
=cut
sub on_context_menu {
my $self = shift;
foreach my $handle ( $self->handles ) {
next unless $handle->can_context;
foreach my $handle ( $self->handles ) {
$handle->plugin->event_on_context_menu(@_);
}
}
return ();
}
# TO DO: document this.
# TO DO: make it also reload the file?
sub test_a_plugin {
my $self = shift;
my $main = $self->main;
my $config = $self->parent->config;
my $plugins = $self->plugins;
my $last_filename = $main->current->filename;
my $default_dir = '';
if ($last_filename) {
$default_dir = File::Basename::dirname($last_filename);
}
my $dialog = Wx::FileDialog->new(
$main, Wx::gettext('Open file'), $default_dir, '', '*.*', Wx::FD_OPEN,
);
unless (Padre::Constant::WIN32) {
$dialog->SetWildcard("*");
}
if ( $dialog->ShowModal == Wx::ID_CANCEL ) {
return;
}
my $filename = $dialog->GetFilename;
$default_dir = $dialog->GetDirectory;
# Save into plug-in for next time
my $file = File::Spec->catfile( $default_dir, $filename );
# Last catfile's parameter is to ensure trailing slash
my $plugin_folder_name = qr/Padre[\\\/]Plugin[\\\/]/;
( $default_dir, $filename ) = split( $plugin_folder_name, $file, 2 );
unless ($filename) {
Wx::MessageBox(
sprintf(
Wx::gettext("Plug-in must have '%s' as base directory"),
$plugin_folder_name
),
'Error loading plug-in',
Wx::OK, $main
);
return;
}
$filename =~ s/\.pm$//; # Remove last .pm
$filename =~ s/[\\\/]/\:\:/;
unless ( $INC[0] eq $default_dir ) {
unshift @INC, $default_dir;
}
# Unload any previously existant plug-in with the same name
if ( $plugins->{$filename} ) {
$self->unload_plugin($filename);
delete $plugins->{$filename};
}
# Load the selected plug-in
$self->load_plugin($filename);
if ( $self->plugins->{$filename}->{status} eq 'error' ) {
$main->error(
sprintf(
Wx::gettext("Failed to load the plug-in '%s'\n%s"), $filename, $self->plugins->{$filename}->errstr
)
);
return;
}
return;
}
######################################################################
# Support Functions
sub handle {
my $self = shift;
my $it = shift;
if ( Params::Util::_INSTANCE( $it, 'Padre::PluginHandle' ) ) {
my $current = $self->{plugins}->{ $it->class };
unless ( defined $current ) {
Carp::croak("Unknown plug-in '$it' provided to PluginManager");
}
unless ( Scalar::Util::refaddr($it) == Scalar::Util::refaddr($current) ) {
Carp::croak("Duplicate plug-in '$it' provided to PluginManager");
}
return $it;
}
# Convert from class to name if needed
if ( defined Params::Util::_CLASS($it) ) {
unless ( defined $self->{plugins}->{$it} ) {
Carp::croak("Plug-in '$it' does not exist in PluginManager");
}
return $self->{plugins}->{$it};
}
Carp::croak("Missing or invalid plug-in provided to Padre::PluginManager");
}
1;
=pod
=head1 SEE ALSO
L<Padre>, L<Padre::Config>
=head1 COPYRIGHT
Copyright 2008-2012 The Padre development team as listed in Padre.pm.
=head1 LICENSE
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl 5 itself.
=cut
# Copyright 2008-2012 The Padre development team as listed in Padre.pm.
# LICENSE
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl 5 itself.