Salome HOME
Merge Python 3 porting.
[modules/gui.git] / src / SalomeApp / salome_pluginsmanager.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2007-2016  CEA/DEN, EDF R&D, OPEN CASCADE
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License, or (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19 #
20
21 """
22 This module is imported from C++ SalomeApp_Application and initialized
23 (call to initialize function with 4 parameters)
24 module :       0 if it is plugins manager at the application level, 1 if it is at the module level
25 name :         the name of the plugins manager. This name is used to build the name of the plugins files
26 basemenuname : the name of the menu into we want to add the menu of the plugins ("Tools" for example)
27 menuname :     the name of plugins menu
28
29 A plugins manager is created when calling initialize.
30
31 The plugins manager creates a submenu <menuname> in the <basemenuname>
32 menu.
33
34 The plugins manager searches in $HOME/.config/salome/Plugins,
35 $HOME/$APPLI/Plugins, $SALOME_PLUGINS_PATH directories files named
36 <name>_plugins.py and executes them.
37
38 These files should contain python code that register functions into
39 the plugins manager.
40
41 Example of a plugins manager with name salome. It searches files with
42 name salome_plugins.py (example follows)::
43
44   import salome_pluginsmanager
45
46   def about(context):
47     from qtsalome import QMessageBox
48     QMessageBox.about(None, "About SALOME pluginmanager", "SALOME plugins manager in SALOME virtual application ")
49
50   salome_pluginsmanager.AddFunction('About plugins','About SALOME pluginmanager',about)
51
52 All entries in menu are added in the same order as the calls to
53 AddFunction.  It is possible to customize this presentation by getting
54 the entries list (salome_pluginsmanager.entries()) and modifying it in
55 place. For example, you can do that :
56 salome_pluginsmanager.entries().sort() to order them alphabetically or
57 salome_pluginsmanager.entries().remove("a") to remove the entry named "a".
58
59 It is possible to put entries in submenus. You only need to give a
60 name with / to the entry. for example::
61
62   salome_pluginsmanager.AddFunction('a/b/About','About SALOME pluginmanager',about)
63
64 will add 2 submenus a and b before creating the entry.
65
66 In short to add a plugin:
67
68   1. import the python module salome_pluginsmanager (in your
69   salome_plugins.py or <module>_plugins.py)
70
71   2. write a function with one argument context (it's an object with 3
72   attributes)
73
74   3. register the function with a call to AddFunction (entry in menu plugins,
75   tooltip, function)
76
77 context attributes:
78
79   - sg : the SALOME Swig interface
80   - study : the SALOME study object that must be used to execute the plugin
81
82 """
83
84 import os,sys,traceback
85 from qtsalome import *
86
87 import salome
88
89 SEP=":"
90 if sys.platform == "win32":
91   SEP = ";"
92
93 # Get SALOME PyQt interface
94 import SalomePyQt
95 sgPyQt = SalomePyQt.SalomePyQt()
96
97 # Get SALOME Swig interface
98 import libSALOME_Swig
99 sg = libSALOME_Swig.SALOMEGUI_Swig()
100
101 plugins={}
102 current_plugins_manager=None
103
104 def initialize(module,name,basemenuname,menuname):
105   if name not in plugins:
106     if module:
107       plugins[name]={}
108     else:
109       plugins[name]=[]
110   if module:
111     d=sgPyQt.getDesktop()
112     if d in plugins[name]:return
113     plugins[name][d]=PluginsManager(module,name,basemenuname,menuname)
114   else:
115     plugins[name].append(PluginsManager(module,name,basemenuname,menuname))
116
117 class Context:
118     def __init__(self,sgpyqt):
119         self.sg=sgpyqt
120         self.study=salome.myStudy
121
122 def find_menu(smenu):
123   lmenus=smenu.split("|")
124   # Take first element from the list
125   main=lmenus.pop(0).strip()
126   menu=sgPyQt.getPopupMenu(main)
127   return findMenu(lmenus,menu)
128
129 def findMenu(lmenu,menu):
130   if not lmenu:return menu
131   # Take first element from the list
132   m=lmenu.pop(0).strip()
133   for a in menu.actions():
134     if a.menu():
135       if a.text() == m:
136         return findMenu(lmenu,a.menu())
137
138 PLUGIN_PATH_PATTERN="share/salome/plugins"
139 MATCH_ENDING_PATTERN="_plugins.py"
140 from salome.kernel.syshelper import walktree
141 from salome.kernel.logger import Logger
142 #from salome.kernel.termcolor import GREEN
143 logger=Logger("PluginsManager") #,color=GREEN)
144 # VSR 21/11/2011 : do not show infos in the debug mode
145 #logger.showDebug()
146
147 class PluginsManager:
148     def __init__(self,module,name,basemenuname,menuname):
149         self.name=name
150         self.basemenuname=basemenuname
151         self.menuname=menuname
152         self.module=module
153         self.registry={}
154         self.handlers={}
155         self.entries=[]
156         self.lasttime=0
157         self.plugindirs=[]
158         self.plugins_files=[]
159
160         # MODULES plugins directory.
161         # The SALOME modules may provides natively some plugins. These
162         # MODULES plugins are supposed to be located in the
163         # installation folder of the module, in the subdirectory
164         # "share/salome/plugins". We first look for these directories.
165         searched = []
166         for key in os.environ.keys():
167           if key.endswith("_ROOT_DIR"):
168             rootpath=os.environ[key]
169             dirpath=os.path.join(rootpath,PLUGIN_PATH_PATTERN)
170             if os.path.isdir(dirpath) and dirpath not in self.plugindirs + searched:
171               logger.debug("Looking for plugins in the directory %s ..."%dirpath)
172               walktree(dirpath,self.analyseFile)
173               if dirpath not in self.plugindirs and dirpath not in searched:
174                 searched.append(dirpath)
175
176         # USER plugins directory
177         user_dir = os.path.expanduser("~/.config/salome/Plugins")
178         self.plugindirs.append(user_dir)
179         logger.info("The user directory %s has been added to plugin paths"%user_dir)
180         # obsolete: USER plugins directory
181         # (for compatibility reasons only; new plugins should be stored in ~/.config/salome/Plugins)
182         user_obsolete_dir = os.path.expanduser("~/.salome/Plugins")
183         self.plugindirs.append(user_obsolete_dir)
184         logger.info("The user directory %s has been added to plugin paths (deprecated)"%user_obsolete_dir)
185
186         # APPLI plugins directory
187         appli=os.getenv("APPLI")
188         if appli:
189           appli_dir=os.path.join(os.path.expanduser("~"),appli,"Plugins")
190           self.plugindirs.append(appli_dir)
191           logger.info("The APPLI directory %s has been added to plugin paths"%appli_dir)
192
193         #SALOME_PLUGINS_PATH environment variable (list of directories separated by ":")
194         pluginspath=os.getenv("SALOME_PLUGINS_PATH")
195         if pluginspath:
196           for directory in pluginspath.split(SEP):
197             self.plugindirs.append(directory)
198             logger.info("The directory %s has been added to plugin paths"%directory)
199
200         self.basemenu = find_menu(self.basemenuname)
201
202         if self.module:
203           self.menu=QMenu(self.menuname)
204           mid=sgPyQt.createMenu(self.menu.menuAction(),self.basemenuname)
205         else:
206           self.menu=QMenu(self.menuname,self.basemenu)
207           self.basemenu.addMenu(self.menu)
208
209         self.menu.menuAction().setVisible(False)
210
211         self.basemenu.aboutToShow.connect(self.importPlugins)
212
213     def analyseFile(self,filename):
214       """
215       This function checks if the specified file is a plugins python
216       module and add the directory name of this file to the list of
217       plugin paths. This function is aimed to be used as the callback
218       function of the walktree algorithm.
219       """
220       if str(filename).endswith(MATCH_ENDING_PATTERN):
221         dirpath=os.path.dirname(filename)
222         if dirpath not in self.plugindirs:
223           self.plugindirs.append(dirpath)
224           logger.debug("The directory %s has been added to plugin paths"%dirpath)
225         
226     def AddFunction(self,name,description,script):
227         """ Add a plugin function
228         """
229         self.registry[name]=script,description
230         self.entries.append(name)
231
232         def handler(obj=self,script=script):
233           try:
234             script(Context(sgPyQt))
235           except:
236             s=traceback.format_exc()
237             QMessageBox.warning(None,"Exception occured",s)
238
239         self.handlers[name]=handler
240
241     def importPlugins(self):
242         """Execute the salome_plugins file that contains plugins definition """
243         if self.lasttime ==0 or salome.myStudy == None:
244           salome.salome_init(embedded=1)
245
246         lasttime=0
247
248         plugins_files=[]
249         plugins_file_name=self.name+MATCH_ENDING_PATTERN
250         for directory in self.plugindirs:
251           plugins_file = os.path.join(directory,plugins_file_name)
252           if os.path.isfile(plugins_file):
253             plugins_files.append((directory,plugins_file))
254             lasttime=max(lasttime,os.path.getmtime(plugins_file))
255
256         plugins_files.sort()
257
258         if not plugins_files:
259           self.registry.clear()
260           self.handlers.clear()
261           self.entries=[]
262           self.lasttime=0
263           self.menu.clear()
264           self.menu.menuAction().setVisible(False)
265           return
266
267         if self.plugins_files != plugins_files or lasttime > self.lasttime:
268           global current_plugins_manager
269           current_plugins_manager=self
270           self.registry.clear()
271           self.handlers.clear()
272           self.entries=[]
273           self.lasttime=lasttime
274           for directory,plugins_file in plugins_files:
275             logger.debug("look for python path: %s"%directory)
276             if directory not in sys.path:
277               sys.path.insert(0,directory)
278               logger.debug("The directory %s has been added to PYTHONPATH"%directory)
279             try:
280               exec(compile(open(plugins_file).read(), plugins_file, 'exec'),globals(),{})
281             except:
282               logger.fatal("Error while loading plugins from file %s"%plugins_file)
283               traceback.print_exc()
284
285           self.updateMenu()
286
287     def updateMenu(self):
288         """Update the Plugins menu"""
289         self.menu.clear()
290         for entry in self.entries:
291           names=entry.split("/")
292           if len(names) < 1:continue
293           parentMenu=self.menu
294
295           if len(names) > 1:
296             #create or get submenus
297             submenus={}
298             for action in parentMenu.actions():
299               menu=action.menu()
300               if menu:
301                 submenus[str(menu.title())]=menu
302             while len(names) > 1:
303               name=names.pop(0)
304               if name in submenus:
305                 amenu=submenus[name]
306               else:
307                 amenu=QMenu(name,parentMenu)
308                 parentMenu.addMenu(amenu)
309                 submenus[name]=amenu
310               parentMenu=amenu
311
312           name=names.pop(0)
313           act=parentMenu.addAction(name,self.handlers[entry])
314           act.setStatusTip(self.registry[entry][1])
315
316         self.menu.menuAction().setVisible(True)
317
318 def AddFunction(name,description,script):
319    """ Add a plugin function
320        Called by a user to register a function (script)
321    """
322    return current_plugins_manager.AddFunction(name,description,script)
323
324 def entries():
325   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
326   return current_plugins_manager.entries