cd28f97e1e
svn: r5277
682 lines
23 KiB
Python
682 lines
23 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2000-2005 Donald N. Allingham
|
|
#
|
|
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# $Id$
|
|
|
|
"""
|
|
The core of the GRAMPS plugin system. This module provides tasks to load
|
|
plugins from specfied directories, build menus for the different categories,
|
|
and provide dialog to select and execute plugins.
|
|
|
|
Plugins are divided into several categories. This are: reports, tools,
|
|
importers, exporters, and document generators.
|
|
"""
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# GTK libraries
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import gobject
|
|
import gtk
|
|
import gtk.glade
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Standard Python modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import traceback
|
|
import os
|
|
import sys
|
|
import re
|
|
from gettext import gettext as _
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# GRAMPS modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import const
|
|
import Utils
|
|
import GrampsKeys
|
|
import Errors
|
|
import Report
|
|
import Tool
|
|
import PluginMgr
|
|
import GrampsDisplay
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Constants
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
REPORTS = 0
|
|
TOOLS = 1
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# PluginDialog interface class
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
|
|
class PluginDialog:
|
|
"""Displays the dialog box that allows the user to select the
|
|
report that is desired."""
|
|
|
|
def __init__(self,parent,db,active,item_list,categories,msg,label=None,
|
|
button_label=None,tool_tip=None,content=REPORTS):
|
|
"""Display the dialog box, and build up the list of available
|
|
reports. This is used to build the selection tree on the left
|
|
hand side of the dailog box."""
|
|
|
|
self.parent = parent
|
|
if self.parent.child_windows.has_key(msg):
|
|
self.parent.child_windows[msg].present(None)
|
|
return
|
|
self.db = db
|
|
self.active = active
|
|
self.update = None
|
|
self.imap = {}
|
|
self.msg = msg
|
|
self.content = content
|
|
|
|
self.dialog = gtk.glade.XML(const.pluginsFile,"report","gramps")
|
|
self.dialog.signal_autoconnect({
|
|
"on_report_apply_clicked" : self.on_apply_clicked,
|
|
"destroy_passed_object" : self.close,
|
|
"on_delete_event" : self.on_delete_event,
|
|
})
|
|
|
|
self.tree = self.dialog.get_widget("tree")
|
|
self.top = self.dialog.get_widget("report")
|
|
self.title = self.dialog.get_widget("title")
|
|
|
|
Utils.set_titles(self.top, self.title, msg )
|
|
|
|
self.store = gtk.TreeStore(gobject.TYPE_STRING)
|
|
self.selection = self.tree.get_selection()
|
|
self.selection.connect('changed', self.on_node_selected)
|
|
col = gtk.TreeViewColumn('',gtk.CellRendererText(),text=0)
|
|
self.tree.append_column(col)
|
|
self.tree.set_model(self.store)
|
|
|
|
self.description = self.dialog.get_widget("description")
|
|
if label:
|
|
self.description.set_text(label)
|
|
self.status = self.dialog.get_widget("report_status")
|
|
|
|
Utils.set_title_label(self.dialog,msg)
|
|
|
|
self.author_name = self.dialog.get_widget("author_name")
|
|
self.author_email = self.dialog.get_widget("author_email")
|
|
self.statbox = self.dialog.get_widget("statbox")
|
|
|
|
self.apply_button = self.dialog.get_widget("apply")
|
|
if button_label:
|
|
self.apply_button.set_label(button_label)
|
|
else:
|
|
self.apply_button.set_label(_("_Apply"))
|
|
self.apply_button.set_use_underline(True)
|
|
if tool_tip:
|
|
try:
|
|
tt = gtk.tooltips_data_get(self.apply_button)
|
|
if tt:
|
|
tt[0].set_tip(self.apply_button,tool_tip)
|
|
except AttributeError:
|
|
pass
|
|
|
|
self.item = None
|
|
self.build_plugin_tree(item_list,categories)
|
|
self.add_itself_to_menu()
|
|
self.top.show()
|
|
|
|
def on_delete_event(self,obj,b):
|
|
self.remove_itself_from_menu()
|
|
|
|
def close(self,ok=0):
|
|
self.remove_itself_from_menu()
|
|
self.top.destroy()
|
|
|
|
def add_itself_to_menu(self):
|
|
self.parent.child_windows[self.msg] = self
|
|
self.win_menu_item = gtk.MenuItem(self.msg)
|
|
self.win_menu_item.connect("activate",self.present)
|
|
self.win_menu_item.show()
|
|
self.parent.winsmenu.append(self.win_menu_item)
|
|
|
|
def remove_itself_from_menu(self):
|
|
del self.parent.child_windows[self.msg]
|
|
self.win_menu_item.destroy()
|
|
|
|
def present(self,obj):
|
|
self.top.present()
|
|
|
|
def on_apply_clicked(self,obj):
|
|
"""Execute the selected report"""
|
|
|
|
(item_class,options_class,title,category,name) = self.item
|
|
if self.content == REPORTS:
|
|
Report.report(self.db,self.active,
|
|
item_class,options_class,title,name,category)
|
|
else:
|
|
Tool.gui_tool(self.db,self.active,
|
|
item_class,options_class,title,name,category,
|
|
self.update,self.parent)
|
|
|
|
def on_node_selected(self,obj):
|
|
"""Updates the informational display on the right hand side of
|
|
the dialog box with the description of the selected report"""
|
|
|
|
store,node = self.selection.get_selected()
|
|
if node:
|
|
path = store.get_path(node)
|
|
if not node or not self.imap.has_key(path):
|
|
self.statbox.hide()
|
|
return
|
|
self.statbox.show()
|
|
data = self.imap[path]
|
|
|
|
(report_class,options_class,title,category,name,
|
|
doc,status,author,email) = data
|
|
self.description.set_text(doc)
|
|
self.status.set_text(status)
|
|
self.title.set_text('<span weight="bold" size="larger">%s</span>' % title)
|
|
self.title.set_use_markup(1)
|
|
self.author_name.set_text(author)
|
|
self.author_email.set_text(email)
|
|
self.item = (report_class,options_class,title,category,name)
|
|
|
|
def build_plugin_tree(self,item_list,categories):
|
|
"""Populates a GtkTree with each menu item assocated with a entry
|
|
in the lists. The list must consist of a tuples with the following
|
|
format:
|
|
|
|
(item_class,options_class,title,category,name,
|
|
doc,status,author,email)
|
|
|
|
Items in the same category are grouped under the same submenu.
|
|
The categories must be dicts from integer to string.
|
|
"""
|
|
|
|
ilist = []
|
|
|
|
# build the tree items and group together based on the category name
|
|
item_hash = {}
|
|
for plugin in item_list:
|
|
category = categories[plugin[3]]
|
|
if item_hash.has_key(category):
|
|
item_hash[category].append(plugin)
|
|
else:
|
|
item_hash[category] = [plugin]
|
|
|
|
# add a submenu for each category, and populate it with the
|
|
# GtkTreeItems that are associated with it.
|
|
key_list = item_hash.keys()
|
|
key_list.sort()
|
|
key_list.reverse()
|
|
|
|
prev = None
|
|
for key in key_list:
|
|
data = item_hash[key]
|
|
node = self.store.insert_after(None,prev)
|
|
self.store.set(node,0,key)
|
|
next = None
|
|
data.sort(lambda x,y: cmp(x[2],y[2]))
|
|
for item in data:
|
|
next = self.store.insert_after(node,next)
|
|
ilist.append((next,item))
|
|
self.store.set(next,0,item[2])
|
|
for next,tab in ilist:
|
|
path = self.store.get_path(next)
|
|
self.imap[path] = tab
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# ReportPlugins interface class
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class ReportPlugins(PluginDialog):
|
|
"""Displays the dialog box that allows the user to select the
|
|
report that is desired."""
|
|
|
|
def __init__(self,parent,db,active):
|
|
"""Display the dialog box, and build up the list of available
|
|
reports. This is used to build the selection tree on the left
|
|
hand side of the dailog box."""
|
|
PluginDialog.__init__(
|
|
self,parent,
|
|
db,
|
|
active,
|
|
PluginMgr.report_list,
|
|
Report.standalone_categories,
|
|
_("Report Selection"),
|
|
_("Select a report from those available on the left."),
|
|
_("_Generate"), _("Generate selected report"),
|
|
REPORTS)
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# ToolPlugins interface class
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class ToolPlugins(PluginDialog):
|
|
"""Displays the dialog box that allows the user to select the tool
|
|
that is desired."""
|
|
|
|
def __init__(self,parent,db,active,update):
|
|
"""Display the dialog box, and build up the list of available
|
|
reports. This is used to build the selection tree on the left
|
|
hand side of the dailog box."""
|
|
|
|
PluginDialog.__init__(
|
|
self,
|
|
parent,
|
|
db,
|
|
active,
|
|
PluginMgr.tool_list,
|
|
Tool.tool_categories,
|
|
_("Tool Selection"),
|
|
_("Select a tool from those available on the left."),
|
|
_("_Run"),
|
|
_("Run selected tool"),
|
|
TOOLS)
|
|
self.update = update
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# PluginStatus
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
status_up = None
|
|
parent_window = None
|
|
|
|
class PluginStatus:
|
|
"""Displays a dialog showing the status of loaded plugins"""
|
|
|
|
def __init__(self,parent_class=None):
|
|
global status_up, parent_window
|
|
if status_up:
|
|
status_up.close(None)
|
|
if parent_class:
|
|
parent_window = parent_class.topWindow
|
|
status_up = self
|
|
|
|
import cStringIO
|
|
|
|
self.glade = gtk.glade.XML(const.pluginsFile,"plugstat","gramps")
|
|
self.top = self.glade.get_widget("plugstat")
|
|
self.top.set_title("%s - GRAMPS" % _('Plugin status'))
|
|
window = self.glade.get_widget("text")
|
|
self.pop_button = self.glade.get_widget("pop_button")
|
|
self.pop_button.set_active(GrampsKeys.get_pop_plugin_status())
|
|
self.pop_button.connect('toggled',
|
|
lambda obj: GrampsKeys.save_pop_plugin_status(self.pop_button.get_active()))
|
|
GrampsKeys.client.notify_add("/apps/gramps/behavior/pop-plugin-status",
|
|
self.pop_button_update)
|
|
self.glade.signal_autoconnect({
|
|
'on_close_clicked' : self.close,
|
|
'on_help_clicked' : self.help,
|
|
'on_plugstat_delete_event' : self.on_delete,
|
|
})
|
|
|
|
if parent_window:
|
|
self.top.set_transient_for(parent_window)
|
|
|
|
info = cStringIO.StringIO()
|
|
|
|
if len(PluginMgr.expect_list) + len(PluginMgr.failmsg_list) == 0:
|
|
window.get_buffer().set_text(_('All modules were successfully loaded.'))
|
|
else:
|
|
info.write(_("The following modules could not be loaded:"))
|
|
info.write("\n\n")
|
|
|
|
for (filename,msg) in PluginMgr.expect_list:
|
|
info.write("%s: %s\n\n" % (filename,msg))
|
|
|
|
for (filename,msgs) in PluginMgr.failmsg_list:
|
|
error = str(msgs[0])
|
|
if error[0:11] == "exceptions.":
|
|
error = error[11:]
|
|
info.write("%s: %s\n" % (filename,error) )
|
|
traceback.print_exception(msgs[0],msgs[1],msgs[2],None,info)
|
|
info.write('\n')
|
|
info.seek(0)
|
|
window.get_buffer().set_text(info.read())
|
|
|
|
def on_delete(self,obj1,obj2):
|
|
status_up = None
|
|
|
|
def close(self,obj):
|
|
self.top.destroy()
|
|
status_up = None
|
|
|
|
def help(self,obj):
|
|
"""Display the GRAMPS manual"""
|
|
GrampsDisplay.help('gramps-getting-started')
|
|
|
|
def pop_button_update(self, client,cnxn_id,entry,data):
|
|
self.pop_button.set_active(GrampsKeys.get_pop_plugin_status())
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Building pulldown menus
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
def build_tools_menu(top_menu,callback):
|
|
build_plugin_menu(PluginMgr.tool_list,
|
|
Tool.tool_categories,
|
|
Tool.gui_tool,
|
|
top_menu,callback)
|
|
|
|
def build_report_menu(top_menu,callback):
|
|
build_plugin_menu(PluginMgr.report_list,
|
|
Report.standalone_categories,
|
|
Report.report,
|
|
top_menu,callback)
|
|
|
|
def build_plugin_menu(item_list,categories,func,top_menu,callback):
|
|
menu = gtk.Menu()
|
|
menu.show()
|
|
|
|
hash_data = {}
|
|
for item in item_list:
|
|
category = categories[item[3]]
|
|
if hash_data.has_key(category):
|
|
hash_data[category].append(
|
|
(item[0],item[1],item[2],item[4],item[3]))
|
|
else:
|
|
hash_data[category] = [
|
|
(item[0],item[1],item[2],item[4],item[3])]
|
|
|
|
catlist = hash_data.keys()
|
|
catlist.sort()
|
|
for key in catlist:
|
|
entry = gtk.MenuItem(key)
|
|
entry.show()
|
|
menu.append(entry)
|
|
submenu = gtk.Menu()
|
|
submenu.show()
|
|
entry.set_submenu(submenu)
|
|
lst = hash_data[key]
|
|
lst.sort(by_menu_name)
|
|
for name in lst:
|
|
subentry = gtk.MenuItem("%s..." % name[2])
|
|
subentry.show()
|
|
subentry.connect("activate",callback,func,
|
|
name[0],name[1],name[2],name[3],name[4])
|
|
submenu.append(subentry)
|
|
top_menu.set_submenu(menu)
|
|
|
|
|
|
def by_menu_name(a,b):
|
|
return cmp(a[2],b[2])
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# build_tools_menu
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
#def build_tools_menu(top_menu,callback):
|
|
# build_menu(top_menu,PluginMgr.tool_list,callback)
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# get_text_doc_menu
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class GrampsTextFormatComboBox(gtk.ComboBox):
|
|
|
|
def set(self,tables,callback,obj=None,active=None):
|
|
self.store = gtk.ListStore(str)
|
|
self.set_model(self.store)
|
|
cell = gtk.CellRendererText()
|
|
self.pack_start(cell,True)
|
|
self.add_attribute(cell,'text',0)
|
|
|
|
out_pref = GrampsKeys.get_output_preference()
|
|
index = 0
|
|
PluginMgr.textdoc_list.sort()
|
|
active_index = 0
|
|
for item in PluginMgr.textdoc_list:
|
|
if tables and item[2] == 0:
|
|
continue
|
|
name = item[0]
|
|
self.store.append(row=[name])
|
|
#if callback:
|
|
# menuitem.connect("activate",callback)
|
|
if item[7] == active:
|
|
active_index = index
|
|
elif not active and name == out_pref:
|
|
active_index = index
|
|
index = index + 1
|
|
self.set_active(active_index)
|
|
|
|
def get_label(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][0]
|
|
|
|
def get_reference(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][1]
|
|
|
|
def get_paper(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][3]
|
|
|
|
def get_styles(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][4]
|
|
|
|
def get_ext(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][5]
|
|
|
|
def get_printable(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][6]
|
|
|
|
def get_clname(self):
|
|
return PluginMgr.textdoc_list[self.get_active()][7]
|
|
|
|
class GrampsDrawFormatComboBox(gtk.ComboBox):
|
|
|
|
def set(self,tables,callback,obj=None,active=None):
|
|
self.store = gtk.ListStore(str)
|
|
self.set_model(self.store)
|
|
cell = gtk.CellRendererText()
|
|
self.pack_start(cell,True)
|
|
self.add_attribute(cell,'text',0)
|
|
|
|
out_pref = GrampsKeys.get_output_preference()
|
|
index = 0
|
|
PluginMgr.drawdoc_list.sort()
|
|
active_index = 0
|
|
for item in PluginMgr.drawdoc_list:
|
|
if tables and item[2] == 0:
|
|
continue
|
|
name = item[0]
|
|
self.store.append(row=[name])
|
|
#if callback:
|
|
# menuitem.connect("activate",callback)
|
|
if item[6] == active:
|
|
active_index = index
|
|
elif not active and name == out_pref:
|
|
active_index = index
|
|
index = index + 1
|
|
self.set_active(active_index)
|
|
|
|
def get_reference(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][1]
|
|
|
|
def get_label(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][0]
|
|
|
|
def get_paper(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][2]
|
|
|
|
def get_styles(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][3]
|
|
|
|
def get_ext(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][4]
|
|
|
|
def get_printable(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][5]
|
|
|
|
def get_clname(self):
|
|
return PluginMgr.drawdoc_list[self.get_active()][6]
|
|
|
|
class GrampsBookFormatComboBox(gtk.ComboBox):
|
|
|
|
def set(self,tables,callback,obj=None,active=None):
|
|
self.store = gtk.ListStore(str)
|
|
self.set_model(self.store)
|
|
cell = gtk.CellRendererText()
|
|
self.pack_start(cell,True)
|
|
self.add_attribute(cell,'text',0)
|
|
|
|
out_pref = GrampsKeys.get_output_preference()
|
|
index = 0
|
|
PluginMgr.drawdoc_list.sort()
|
|
active_index = 0
|
|
self.data = []
|
|
for item in PluginMgr.bookdoc_list:
|
|
if tables and item[2] == 0:
|
|
continue
|
|
self.data.append(item)
|
|
name = item[0]
|
|
self.store.append(row=[name])
|
|
if item[7] == active:
|
|
active_index = index
|
|
elif not active and name == out_pref:
|
|
active_index = index
|
|
index += 1
|
|
self.set_active(active_index)
|
|
|
|
def get_reference(self):
|
|
return self.data[self.get_active()][1]
|
|
|
|
def get_label(self):
|
|
return self.data[self.get_active()][0]
|
|
|
|
def get_paper(self):
|
|
return self.data[self.get_active()][3]
|
|
|
|
def get_ext(self):
|
|
return self.data[self.get_active()][5]
|
|
|
|
def get_printable(self):
|
|
return self.data[self.get_active()][6]
|
|
|
|
def get_clname(self):
|
|
return self.data[self.get_active()][7]
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Reload plugins
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class Reload(Tool.Tool):
|
|
def __init__(self,db,person,options_class,name,callback=None,parent=None):
|
|
Tool.Tool.__init__(self,db,person,options_class,name)
|
|
|
|
"""
|
|
Treated as a callback, causes all plugins to get reloaded.
|
|
This is useful when writing and debugging a plugin.
|
|
"""
|
|
|
|
pymod = re.compile(r"^(.*)\.py$")
|
|
|
|
oldfailmsg = PluginMgr.failmsg_list[:]
|
|
PluginMgr.failmsg_list = []
|
|
|
|
# attempt to reload all plugins that have succeeded in the past
|
|
for plugin in PluginMgr._success_list:
|
|
filename = os.path.basename(plugin.__file__)
|
|
filename = filename.replace('pyc','py')
|
|
filename = filename.replace('pyo','py')
|
|
try:
|
|
reload(plugin)
|
|
except:
|
|
PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
|
|
|
# attempt to load the plugins that have failed in the past
|
|
|
|
for (filename,message) in oldfailmsg:
|
|
name = os.path.split(filename)
|
|
match = pymod.match(name[1])
|
|
if not match:
|
|
continue
|
|
PluginMgr.attempt_list.append(filename)
|
|
plugin = match.groups()[0]
|
|
try:
|
|
# For some strange reason second importing of a failed plugin
|
|
# results in success. Then reload reveals the actual error.
|
|
# Looks like a bug in Python.
|
|
a = __import__(plugin)
|
|
reload(a)
|
|
PluginMgr._success_list.append(a)
|
|
except:
|
|
PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
|
|
|
# attempt to load any new files found
|
|
for directory in PluginMgr.loaddir_list:
|
|
for filename in os.listdir(directory):
|
|
name = os.path.split(filename)
|
|
match = pymod.match(name[1])
|
|
if not match:
|
|
continue
|
|
if filename in PluginMgr.attempt_list:
|
|
continue
|
|
PluginMgr.attempt_list.append(filename)
|
|
plugin = match.groups()[0]
|
|
try:
|
|
a = __import__(plugin)
|
|
if a not in PluginMgr._success_list:
|
|
PluginMgr._success_list.append(a)
|
|
except:
|
|
PluginMgr.failmsg_list.append((filename,sys.exc_info()))
|
|
|
|
if GrampsKeys.get_pop_plugin_status() and len(PluginMgr.failmsg_list):
|
|
PluginStatus()
|
|
else:
|
|
global status_up
|
|
if status_up:
|
|
status_up.close(None)
|
|
status_up = None
|
|
|
|
class ReloadOptions(Tool.ToolOptions):
|
|
"""
|
|
Defines options and provides handling interface.
|
|
"""
|
|
|
|
def __init__(self,name,person_id=None):
|
|
Tool.ToolOptions.__init__(self,name,person_id)
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Register the plugin reloading tool
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
PluginMgr.register_tool(
|
|
name = 'reload',
|
|
category = Tool.TOOL_DEBUG,
|
|
tool_class = Reload,
|
|
options_class = ReloadOptions,
|
|
modes = Tool.MODE_GUI,
|
|
translated_name = _("Reload plugins"),
|
|
description=_("Attempt to reload plugins. "
|
|
"Note: This tool itself is not reloaded!"),
|
|
)
|