Files
PlantAI-dev/tools/pymol-open-source/modules/pymol/plugins/managergui.py
T

634 lines
22 KiB
Python
Raw Normal View History

2026-08-03 18:42:47 +08:00
'''
PyMOL Plugins Engine, Graphical Plugin Manager
(c) 2011-2012 Thomas Holder, PyMOL OS Fellow
License: BSD-2-Clause
'''
import sys
import Pmw
import tkinter as Tkinter
import tkinter.simpledialog as tkSimpleDialog
from .legacysupport import tkMessageBox, tkFileDialog
from . import pref_get
default_pad = {'padx': 5, 'pady': 5}
default_top = {'fill': 'x', 'anchor': 'n', 'side': 'top'}
default_top.update(default_pad)
def confirm_network_access():
'''
Popup dialog with network access notification (only once per session)
'''
self = confirm_network_access
if self.ok < 0:
tkMessageBox.showinfo('Info',
'Network download has been disabled, sorry!')
return False
if self.ok > 0:
return True
self.ok = tkMessageBox.askyesno('Confirm', 'PyMOL will now download'
' executable code from the internet! Proceed?')
return self.ok
# valid values: 1=never ask, 0=ask once per session, -1=network access disabled
confirm_network_access.ok = pref_get('network_access_ok', 0)
def manager_dialog():
'''
Create the Plugin Manager dialog (with Pmw)
'''
from .legacysupport import get_tk_root
dialog = PluginManager(get_tk_root())
def plugin_info_dialog(parent, info):
'''
Shows a popup with plugin info (name, metadata, doc-string, ...)
'''
dialog = Pmw.MegaToplevel(parent,
title = 'Info about plugin ' + info.name)
grid = dialog.interior()
highlighted_labels = ['Citation', 'Citation-Required']
bg_important = '#ff6666'
bg_notice = '#ffff99'
def add_line(label, text):
bg = None
if label in highlighted_labels:
bg = bg_important
row = grid.grid_size()[1]
Tkinter.Label(grid, text=label + ':', bg=bg).grid(row=row, column=0, sticky='nw', padx=5, pady=2)
e = Tkinter.Entry(grid)
e.insert(0, str(text))
e.config(state='readonly')
e.grid(row=row, column=1, sticky='nwe', padx=5, pady=2)
if info.get_citation_required():
Tkinter.Label(grid, text='This Plugin requires a citation. See below for details.',
bg=bg_important, padx=10, pady=10).grid(columnspan=2, sticky='nesw')
add_line('Name', info.name)
if not info.is_temporary:
add_line('Python Module Name', info.mod_name)
add_line('Filename', info.filename)
grid.columnconfigure(1, weight=1)
metadata = info.get_metadata()
for label, value in metadata.items():
add_line(label, value)
if not info.is_temporary:
if not info.loaded:
Tkinter.Label(grid, text='more information might be available after plugin is loaded',
bg=bg_notice, padx=10, pady=10).grid(columnspan=2, sticky='nesw')
else:
add_line('commands', ', '.join(info.commands))
docstring = info.get_docstring()
if docstring is not None:
st = Pmw.ScrolledText(grid, text_wrap='none', text_padx = 4, text_pady = 4)
st.appendtext(docstring.strip())
st.configure(text_state='disabled')
st.grid(columnspan=2, sticky='nesw', padx=5, pady=2)
grid.rowconfigure(grid.grid_size()[1] - 1, weight=1)
elif info.loaded or info.is_temporary:
Tkinter.Label(grid, text='no documentation available',
bg=bg_notice, padx=10, pady=10).grid(columnspan=2, sticky='nesw')
class PluginManager(Pmw.MegaToplevel):
'''
Graphical Plugin Manager Dialog
'''
def __init__(self, parent):
self.super = self.__class__.__bases__[0]
self.super.__init__(self, parent, title='Plugin Manager')
self.minsize(600, 400)
master = self.interior()
# save button (only show if not in "instantsave" mode)
from . import pref_get, pref_save
def c_save():
pref_save()
b_save.configure(background=b_save_background)
b_save = Tkinter.Button(master, text='Save Settings', command=c_save)
b_save_background = b_save.cget('background')
PluginManager.b_save = b_save
if not pref_get('instantsave', True):
b_save.pack(side='bottom', **default_pad)
# main tabs
notebook = Pmw.NoteBook(master)
notebook.pack(fill='both', expand=1, padx=5, pady=5)
self.page_installed(notebook)
self.page_install_new(notebook)
self.page_settings(notebook)
self.page_about(notebook)
def page_installed(self, notebook):
page = notebook.add('Installed Plugins')
# frames
f_filter = Tkinter.Frame(page)
f_installed = InstalledPluginsWidget(page)
def filter_command():
f_installed.filter(e_filter.getvalue())
# filter items
Tkinter.Checkbutton(f_filter, text='startup', variable=f_installed.v_fstartup, command=filter_command).pack(side='right')
Tkinter.Checkbutton(f_filter, text='loaded', variable=f_installed.v_floaded, command=filter_command).pack(side='right')
e_filter = Pmw.EntryField(f_filter, labelpos='w', label_text='Filter:', modifiedcommand=filter_command)
e_filter.pack(side='left', expand=1, fill='x')
# enable/disable all items
f_all = Tkinter.Frame(page)
Tkinter.Button(f_all, text='startup all', command=f_installed.startup_all).pack(side='left')
Tkinter.Button(f_all, text='startup none', command=f_installed.startup_none).pack(side='left')
# pack
f_filter.pack(**default_top)
f_all.pack(side='bottom', anchor='w', **default_pad)
f_installed.pack(side='top', fill='both', expand=1, **default_pad)
self.f_installed = f_installed
def page_install_new(self, notebook):
from pymol import Scratch_Storage, CmdException
page = notebook.add('Install New Plugin')
# local file
def c_local():
from .legacysupport import installPlugin, get_pmgapp
installPlugin(get_pmgapp())
self.f_installed.reload()
w = Pmw.Group(page, tag_text='Install from local file')
w.pack(**default_top)
b_local = Tkinter.Button(w.interior(), text='Choose file...', command=c_local)
b_local.pack(side='left', **default_pad)
if confirm_network_access.ok < 0:
Tkinter.Label(page,
text='Network access has been disabled').pack(expand=1)
return
# pymolwiki
def fetchplugin():
if not confirm_network_access():
return
from .installation import installPluginFromFile
from .repository import fetchscript
url = e_wiki.get()
if not len(url):
return
import tempfile, shutil
tmpdir = tempfile.mkdtemp()
try:
filename = fetchscript(url, tmpdir, False)
except CmdException as e:
tkMessageBox.showerror('Error', 'Fetching Plugin failed.\n' + str(e))
return
if filename:
installPluginFromFile(filename, self.interior())
shutil.rmtree(tmpdir)
self.f_installed.reload()
w = Pmw.Group(page, tag_text='Install from PyMOLWiki or any URL')
w.pack(**default_top)
w = w.interior()
l_repo = Tkinter.Label(w,
text='Paste a link to a script or plugin, or a PyMOLWiki url which then will be downloaded and '
'scanned for scripts that extend the PyMOL API', justify='left', anchor='w', wraplength=500)
l_repo.pack(**default_top)
e_wiki = Pmw.EntryField(w, labelpos='w', label_text='URL:', command=fetchplugin)
e_wiki.pack(side='left', fill='x', expand=1, **default_pad)
b_wiki = Tkinter.Button(w, text='Fetch',
command=fetchplugin)
b_wiki.pack(side='right', **default_pad)
# repository
w = Pmw.Group(page, tag_text='Install from Repository')
w.pack(fill='both', **default_pad)
pw = Pmw.PanedWidget(w.interior(), orient='horizontal')
pane_left = pw.add('left', min=.2)
pane_right = pw.add('right', min=.2, max=.5)
repo_tmp = Scratch_Storage()
def selecmd_left():
'''
Get plugins listing for selected repository.
'''
if not confirm_network_access():
return
from .repository import guess
sels = slb_left.getcurselection()
if len(sels) == 0:
slb_right.setlist(['- empty -'])
return
try:
url = sels[0]
repo_tmp.r = guess(url)
slb_right.setlist(repo_tmp.r.list())
except:
slb_right.setlist(['- listing failed -'])
def infocmd_right():
'''
Download file, parse for metadata, show info-popup and delete file
'''
from . import PluginInfo
from .installation import get_name_and_ext, extract_zipfile, zip_extensions
sels = slb_right.getcurselection()
if len(sels) == 0:
return
import tempfile, shutil, os
tmpdir = tempfile.mkdtemp()
tmpdirs = [tmpdir]
try:
name = sels[0]
filename = repo_tmp.r.copy(name, tmpdir)
name, ext = get_name_and_ext(filename)
if ext in zip_extensions:
tmpdir, dirnames = extract_zipfile(filename, ext)
tmpdirs.append(tmpdir)
name = dirnames[-1]
filename = os.path.join(os.path.join(tmpdir, *dirnames), '__init__.py')
info = PluginInfo(name, filename)
plugin_info_dialog(self.interior(), info)
except:
tkMessageBox.showinfo('Error', 'Could not get plugin info')
finally:
for tmpdir in tmpdirs:
shutil.rmtree(tmpdir)
def selecmd_right():
'''
Download plugin from repository and install it.
'''
from .installation import installPluginFromFile, get_plugdir
sels = slb_right.getcurselection()
if len(sels) == 0:
return
plugdir = get_plugdir(self.interior())
if not plugdir:
return
import tempfile, shutil, os
tmpdir = tempfile.mkdtemp()
try:
for name in sels:
filename = repo_tmp.r.copy(name, tmpdir)
installPluginFromFile(filename, self.interior(), plugdir)
except:
err = str(sys.exc_info()[1])
tkMessageBox.showinfo('Error', 'Could not install plugin ' + name + '\n\n' + err)
finally:
shutil.rmtree(tmpdir)
self.f_installed.reload()
slb_left = Pmw.ScrolledListBox(pane_left, items=(
'http://pldserver1.biochem.queensu.ca/~rlc/work/pymol/',
'https://github.com/Pymol-Scripts/Pymol-script-repo',
# for testing
'http://www.thomas-holder.de/projects/pymol/repository/',
),
listbox_height=30, labelpos='nw', label_text='Repositories',
selectioncommand=selecmd_left)
slb_right = Pmw.ScrolledListBox(pane_right, items=(),
listbox_selectmode=Tkinter.EXTENDED,
listbox_height=30, labelpos='nw', label_text='Items',)
repo_bb_left = Pmw.ButtonBox(pane_left)
repo_bb_right = Pmw.ButtonBox(pane_right)
def dummy_command():
showinfo = tkMessageBox.showinfo
showinfo('Dummy', 'Not implemented', parent=self.interior())
def slb_left_setlist(items):
slb_left.setlist(items)
self.b_save.configure(background='red')
def slb_left_add():
url = tkSimpleDialog.askstring('Repository URL', 'Please enter Repository URL', parent=self.interior())
if not url:
return
items = list(slb_left.get())
items.append(url)
slb_left_setlist(items)
slb_left.setvalue([url]) # provide list to avoid unicode problem
def slb_left_remove():
v = slb_left.getvalue()
items = list(slb_left.get())
for item in v:
items.remove(item)
slb_left_setlist(items)
repo_bb_left.add('Add ...', command=slb_left_add)
repo_bb_left.add('Remove', command=slb_left_remove)
repo_bb_right.add('Info', command=infocmd_right)
repo_bb_right.add('Install', command=selecmd_right)
repo_bb_left.pack(side='bottom', fill='x',)
repo_bb_right.pack(side='bottom', fill='x',)
slb_left.pack(fill='both', **default_pad)
slb_right.pack(fill='both', **default_pad)
pw.pack(fill='both')
def page_settings(self, notebook):
page = notebook.add('Settings')
from . import get_startup_path, set_startup_path
# plugin search path
w = Pmw.Group(page, tag_text='Plugin override search path')
w.pack(**default_top)
def slb_path_setlist(items):
set_startup_path(items)
slb_path.setlist(items)
self.b_save.configure(background='red')
def slb_path_add():
import os
from .installation import get_default_user_plugin_path as userpath
d = tkFileDialog.askdirectory(initialdir=userpath(), parent=self.interior())
if not len(d):
return
if not os.path.exists(d):
os.makedirs(d)
items = list(slb_path.get())
items.append(d)
slb_path_setlist(items)
slb_path.setvalue([d]) # provide list to avoid unicode problem
def slb_path_remove():
v = slb_path.getvalue()
items = list(slb_path.get())
for item in v:
items.remove(item)
slb_path_setlist(items)
def slb_path_move(j=1):
v = slb_path.getvalue()
if len(v) == 0:
return
items = list(slb_path.get())
i = items.index(v[0])
if not (0 <= i+j < len(items)):
return
items.pop(i)
items.insert(i+j, v[0])
slb_path_setlist(items)
slb_path.setvalue([v[0]]) # provide list to avoid unicode problem
slb_path = Pmw.ScrolledListBox(w.interior(),
items=get_startup_path(True), listbox_height=4)
if slb_path.get(0):
slb_path.setvalue([slb_path.get(0)]) # provide list to avoid unicode problem
slb_path.pack(**default_top)
bb_path = Pmw.ButtonBox(w.interior())
bb_path.add('Add new directory...', command=slb_path_add)
bb_path.add('Remove', command=slb_path_remove)
bb_path.add('Move up', command=lambda: slb_path_move(-1))
bb_path.add('Move down', command=slb_path_move)
bb_path.pack(side='top', fill='x')
l_path = Tkinter.Label(w.interior(),
text='PyMOL restart required in order to find plugins in modified plugin search path')
l_path.pack(**default_top)
w = Pmw.Group(page, tag_text='Preferences (Read-Only)')
w.pack(**default_top)
w = w.interior()
from . import preferences
for row, key in enumerate(preferences):
value = str(preferences[key])
if len(value) > 100:
value = value[:97] + '...'
Tkinter.Label(w, text=key).grid(row=row, column=0, sticky='nw', padx=3, pady=2)
e = Tkinter.Entry(w)
e.insert(0, value)
e.config(state='readonly')
e.grid(row=row, column=1, sticky='nwe', padx=5, pady=2)
w.columnconfigure(1, weight=1)
def page_about(self, notebook):
page = notebook.add('About')
st = Pmw.ScrolledText(page, text_wrap='word')
st.appendtext('Plugins are external modules which extend PyMOL\'s capabilities.\n\n'
'Plugins can provide new commands and/or add menu items to the "Plugin" menu.\n\n'
'For technical details, visit\n'
'http://pymolwiki.org/index.php/PluginArchitecture\n'
'http://pymolwiki.org/index.php/Script_Tutorial\n')
st.configure(text_state='disabled')
st.pack(fill='both', expand=1, **default_pad)
class InstalledPluginsWidget(Pmw.ScrolledFrame):
'''
Scrolled widget that shows all installed plugins.
'''
def __init__(self, parent):
self.super = self.__class__.__bases__[0]
self.super.__init__(self, parent,
vscrollmode='static',
horizflex='expand')
# filter variables
self.v_floaded = Tkinter.BooleanVar(parent, False)
self.v_fstartup = Tkinter.BooleanVar(parent, False)
self.reload()
def reload(self):
from . import plugins
for child in self.children():
child.pack_forget()
child.destroy()
for info in sorted(plugins.values(), key=lambda i: i.name.lower()):
PluginWidget(self, info)
# pack children
self.filter()
self.bind_mousewheel()
def children(self):
return self.interior().winfo_children()
def filter(self, text=''):
'''
Pack all children that match filter criteria.
'''
for child in self.children():
child.pack_forget()
for child in self.children():
if text.lower() not in child.info.name.lower():
continue
if self.v_fstartup.get() and not child.info.autoload:
continue
if self.v_floaded.get() and not child.info.loaded:
continue
child.pack()
def bind_mousewheel(self):
'''
Enable scrolling with mouse wheel.
'''
delta = 0.1
def scrollUp(event=None):
self.yview('scroll', -delta, 'pages')
def scrollDown(event=None):
self.yview('scroll', delta, 'pages')
def bind_rec(other):
if hasattr(other, 'interior'):
other = other.interior()
other.bind('<Button-4>', scrollUp)
other.bind('<Button-5>', scrollDown)
for child in other.winfo_children():
bind_rec(child)
bind_rec(self)
def startup_all(self):
for child in self.children():
child.w_startup.select()
if not child.info.autoload:
child.c_startup(False)
def startup_none(self):
for child in self.children():
child.w_startup.deselect()
if child.info.autoload:
child.c_startup(False)
class PluginWidget(Tkinter.Frame):
'''
Item of InstalledPluginsWidget that represents a installed plugin.
'''
def __init__(self, parent, info):
self.info = info
if hasattr(parent, 'interior'):
parent = parent.interior()
self.super = self.__class__.__bases__[0]
self.super.__init__(self, parent,
borderwidth=1,
padx=5, pady=5,
relief=Tkinter.SUNKEN)
topmaster = self
master = Tkinter.Frame(topmaster)
master.pack(side=Tkinter.TOP, fill='x')
w_title = Tkinter.Label(master, text=info.name)
w_title.pack(side=Tkinter.LEFT)
self.w_version = Tkinter.Label(master, text='', foreground='gray50')
self.w_version.pack(side=Tkinter.LEFT)
self.w_loadtime = Tkinter.Label(master, text='', foreground='gray50')
self.w_loadtime.pack(side=Tkinter.RIGHT)
master = Tkinter.Frame(topmaster)
master.pack(side=Tkinter.TOP, fill='x')
self.w_info = Tkinter.Button(master, text='Info', command=self.plugin_info)
self.w_info.pack(side=Tkinter.LEFT)
self.w_enable = Tkinter.Button(master, text='Load', command=self.plugin_load)
self.w_enable.pack(side=Tkinter.LEFT)
self.v_startup = Tkinter.BooleanVar(master, self.info.autoload)
self.w_startup = Tkinter.Checkbutton(master, text='Load on startup',
variable=self.v_startup, command=self.c_startup)
self.w_startup.pack(side=Tkinter.LEFT)
self.f_settings = Tkinter.Frame(master)
self.f_settings.pack(side=Tkinter.LEFT)
w_uninstall = Tkinter.Button(master, text='Uninstall', command=self.plugin_remove)
w_uninstall.pack(side=Tkinter.RIGHT)
self.status_update()
def c_startup(self, askload=True):
'''
Callback for "autoload" checkbox.
'''
self.info.autoload = self.v_startup.get()
if askload and self.info.autoload and not self.info.loaded:
if tkMessageBox.askyesno('Confirm', 'Load plugin now?', parent=self):
self.plugin_load()
PluginManager.b_save.configure(background='red')
def pack(self):
self.super.pack(self, fill='x', anchor=Tkinter.N)
def plugin_info(self):
plugin_info_dialog(self, self.info)
def plugin_load(self):
self.info.load()
self.status_update()
def plugin_remove(self):
if self.info.uninstall(self):
self.pack_forget()
self.destroy()
def status_update(self):
'''
Update contents of this item that may have changed after loading plugin.
'''
version = self.info.get_version()
if version:
text = 'version ' + self.info.get_version()
self.w_version.config(text=text)
if self.info.loaded:
text = 'Took %.3f seconds to load' % (self.info.loadtime)
self.w_loadtime.config(text=text)
self.w_enable.config(state=Tkinter.DISABLED)
# add 'Settings' button
if hasattr(self.info.module, 'settings_dialog') \
and not len(self.f_settings.winfo_children()):
Tkinter.Button(self.f_settings, text='Settings',
command=self.info.module.settings_dialog).pack()
# vi:expandtab:smarttab:sw=4:nowrap