Salome HOME
e4ffb88f40c0a0367f66c6db6f092f62f7414534
[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 not plugins.has_key(name):
106     if module:
107       plugins[name]={}
108     else:
109       plugins[name]=[]
110   if module:
111     d=sgPyQt.getDesktop()
112     if plugins[name].has_key(d):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=unicode(basemenuname, "utf-8")
151         self.menuname=unicode(menuname, "utf-8")
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         for key in os.environ.keys():
166           if key.endswith("_ROOT_DIR"):
167             rootpath=os.environ[key]
168             dirpath=os.path.join(rootpath,PLUGIN_PATH_PATTERN)
169             if os.path.isdir(dirpath) and dirpath not in self.plugindirs:
170               logger.debug("Looking for plugins in the directory %s ..."%dirpath)
171               walktree(dirpath,self.analyseFile)
172
173         # USER plugins directory
174         user_dir = os.path.expanduser("~/.config/salome/Plugins")
175         self.plugindirs.append(user_dir)
176         logger.info("The user directory %s has been added to plugin paths"%user_dir)
177         # obsolete: USER plugins directory
178         # (for compatibility reasons only; new plugins should be stored in ~/.config/salome/Plugins)
179         user_obsolete_dir = os.path.expanduser("~/.salome/Plugins")
180         self.plugindirs.append(user_obsolete_dir)
181         logger.info("The user directory %s has been added to plugin paths (deprecated)"%user_obsolete_dir)
182
183         # APPLI plugins directory
184         appli=os.getenv("APPLI")
185         if appli:
186           appli_dir=os.path.join(os.path.expanduser("~"),appli,"Plugins")
187           self.plugindirs.append(appli_dir)
188           logger.info("The APPLI directory %s has been added to plugin paths"%appli_dir)
189
190         #SALOME_PLUGINS_PATH environment variable (list of directories separated by ":")
191         pluginspath=os.getenv("SALOME_PLUGINS_PATH")
192         if pluginspath:
193           for directory in pluginspath.split(SEP):
194             self.plugindirs.append(directory)
195             logger.info("The directory %s has been added to plugin paths"%directory)
196
197         self.basemenu = find_menu(self.basemenuname)
198
199         if self.module:
200           self.menu=QMenu(self.menuname)
201           mid=sgPyQt.createMenu(self.menu.menuAction(),self.basemenuname)
202         else:
203           self.menu=QMenu(self.menuname,self.basemenu)
204           self.basemenu.addMenu(self.menu)
205
206         self.menu.menuAction().setVisible(False)
207
208         self.basemenu.aboutToShow.connect(self.importPlugins)
209
210     def analyseFile(self,filename):
211       """
212       This function checks if the specified file is a plugins python
213       module and add the directory name of this file to the list of
214       plugin paths. This function is aimed to be used as the callback
215       function of the walktree algorithm.
216       """
217       if str(filename).endswith(MATCH_ENDING_PATTERN):
218         dirpath=os.path.dirname(filename)
219         if dirpath not in self.plugindirs:
220           self.plugindirs.append(dirpath)
221           logger.debug("The directory %s has been added to plugin paths"%dirpath)
222         
223     def AddFunction(self,name,description,script):
224         """ Add a plugin function
225         """
226         self.registry[name]=script,description
227         self.entries.append(name)
228
229         def handler(obj=self,script=script):
230           try:
231             script(Context(sgPyQt))
232           except:
233             s=traceback.format_exc()
234             QMessageBox.warning(None,"Exception occured",s)
235
236         self.handlers[name]=handler
237
238     def importPlugins(self):
239         """Execute the salome_plugins file that contains plugins definition """
240         if self.lasttime ==0 or salome.myStudy == None:
241           salome.salome_init(embedded=1)
242
243         lasttime=0
244
245         plugins_files=[]
246         plugins_file_name=self.name+MATCH_ENDING_PATTERN
247         for directory in self.plugindirs:
248           plugins_file = os.path.join(directory,plugins_file_name)
249           if os.path.isfile(plugins_file):
250             plugins_files.append((directory,plugins_file))
251             lasttime=max(lasttime,os.path.getmtime(plugins_file))
252
253         plugins_files.sort()
254
255         if not plugins_files:
256           self.registry.clear()
257           self.handlers.clear()
258           self.entries=[]
259           self.lasttime=0
260           self.menu.clear()
261           self.menu.menuAction().setVisible(False)
262           return
263
264         if self.plugins_files != plugins_files or lasttime > self.lasttime:
265           global current_plugins_manager
266           current_plugins_manager=self
267           self.registry.clear()
268           self.handlers.clear()
269           self.entries=[]
270           self.lasttime=lasttime
271           for directory,plugins_file in plugins_files:
272             logger.debug("look for python path: %s"%directory)
273             if directory not in sys.path:
274               sys.path.insert(0,directory)
275               logger.debug("The directory %s has been added to PYTHONPATH"%directory)
276             try:
277               execfile(plugins_file,globals(),{})
278             except:
279               logger.fatal("Error while loading plugins from file %s"%plugins_file)
280               traceback.print_exc()
281
282           self.updateMenu()
283
284     def updateMenu(self):
285         """Update the Plugins menu"""
286         self.menu.clear()
287         for entry in self.entries:
288           names=entry.split("/")
289           if len(names) < 1:continue
290           parentMenu=self.menu
291
292           if len(names) > 1:
293             #create or get submenus
294             submenus={}
295             for action in parentMenu.actions():
296               menu=action.menu()
297               if menu:
298                 submenus[str(menu.title())]=menu
299             while len(names) > 1:
300               name=names.pop(0)
301               if submenus.has_key(name):
302                 amenu=submenus[name]
303               else:
304                 amenu=QMenu(name,parentMenu)
305                 parentMenu.addMenu(amenu)
306                 submenus[name]=amenu
307               parentMenu=amenu
308
309           name=names.pop(0)
310           act=parentMenu.addAction(name,self.handlers[entry])
311           act.setStatusTip(self.registry[entry][1])
312
313         self.menu.menuAction().setVisible(True)
314
315 def AddFunction(name,description,script):
316    """ Add a plugin function
317        Called by a user to register a function (script)
318    """
319    return current_plugins_manager.AddFunction(name,description,script)
320
321 def entries():
322   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
323   return current_plugins_manager.entries