Salome HOME
Merge branch 'V8_0_BR'
[modules/gui.git] / src / SalomeApp / salome_pluginsmanager.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2007-2015  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 PyQt4.QtGui 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   - studyId : the SALOME studyId that must be used to execute the plugin
81   - study : the SALOME study object that must be used to execute the plugin
82
83 """
84
85 import os,sys,traceback
86 from PyQt4 import QtGui
87 from PyQt4 import QtCore
88
89 import salome
90
91 SEP=":"
92 if sys.platform == "win32":
93   SEP = ";"
94
95 # Get SALOME PyQt interface
96 import SalomePyQt
97 sgPyQt = SalomePyQt.SalomePyQt()
98
99 # Get SALOME Swig interface
100 import libSALOME_Swig
101 sg = libSALOME_Swig.SALOMEGUI_Swig()
102
103 plugins={}
104 current_plugins_manager=None
105
106 def initialize(module,name,basemenuname,menuname):
107   if not plugins.has_key(name):
108     if module:
109       plugins[name]={}
110     else:
111       plugins[name]=[]
112   if module:
113     d=sgPyQt.getDesktop()
114     if plugins[name].has_key(d):return
115     plugins[name][d]=PluginsManager(module,name,basemenuname,menuname)
116   else:
117     plugins[name].append(PluginsManager(module,name,basemenuname,menuname))
118
119 class Context:
120     def __init__(self,sgpyqt):
121         self.sg=sgpyqt
122         self.studyId=salome.sg.getActiveStudyId()
123         self.study= salome.myStudyManager.GetStudyByID(self.studyId)
124
125 def find_menu(smenu):
126   lmenus=smenu.split("|")
127   main=lmenus.takeFirst().trimmed()
128   menu=sgPyQt.getPopupMenu(main)
129   return findMenu(lmenus,menu)
130
131 def findMenu(lmenu,menu):
132   if not lmenu:return menu
133   m=lmenu.takeFirst().trimmed()
134   for a in menu.actions():
135     if a.menu():
136       if a.text() == m:
137         return findMenu(lmenu,a.menu())
138
139 PLUGIN_PATH_PATTERN="share/salome/plugins"
140 MATCH_ENDING_PATTERN="_plugins.py"
141 from salome.kernel.syshelper import walktree
142 from salome.kernel.logger import Logger
143 #from salome.kernel.termcolor import GREEN
144 logger=Logger("PluginsManager") #,color=GREEN)
145 # VSR 21/11/2011 : do not show infos in the debug mode
146 #logger.showDebug()
147
148 class PluginsManager:
149     def __init__(self,module,name,basemenuname,menuname):
150         self.name=name
151         self.basemenuname=QtCore.QString.fromUtf8(basemenuname)
152         self.menuname=QtCore.QString.fromUtf8(menuname)
153         self.module=module
154         self.registry={}
155         self.handlers={}
156         self.entries=[]
157         self.lasttime=0
158         self.plugindirs=[]
159         self.plugins_files=[]
160
161         # MODULES plugins directory.
162         # The SALOME modules may provides natively some plugins. These
163         # MODULES plugins are supposed to be located in the
164         # installation folder of the module, in the subdirectory
165         # "share/salome/plugins". We first look for these directories.
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:
171               logger.debug("Looking for plugins in the directory %s ..."%dirpath)
172               walktree(dirpath,self.analyseFile)
173
174         # USER plugins directory
175         user_dir = os.path.expanduser("~/.config/salome/Plugins")
176         self.plugindirs.append(user_dir)
177         logger.info("The user directory %s has been added to plugin paths"%user_dir)
178         # obsolete: USER plugins directory
179         # (for compatibility reasons only; new plugins should be stored in ~/.config/salome/Plugins)
180         user_obsolete_dir = os.path.expanduser("~/.salome/Plugins")
181         self.plugindirs.append(user_obsolete_dir)
182         logger.info("The user directory %s has been added to plugin paths (deprecated)"%user_obsolete_dir)
183
184         # APPLI plugins directory
185         appli=os.getenv("APPLI")
186         if appli:
187           appli_dir=os.path.join(os.path.expanduser("~"),appli,"Plugins")
188           self.plugindirs.append(appli_dir)
189           logger.info("The APPLI directory %s has been added to plugin paths"%appli_dir)
190
191         #SALOME_PLUGINS_PATH environment variable (list of directories separated by ":")
192         pluginspath=os.getenv("SALOME_PLUGINS_PATH")
193         if pluginspath:
194           for directory in pluginspath.split(SEP):
195             self.plugindirs.append(directory)
196             logger.info("The directory %s has been added to plugin paths"%directory)
197
198         self.basemenu = find_menu(self.basemenuname)
199
200         if self.module:
201           self.menu=QtGui.QMenu(self.menuname)
202           mid=sgPyQt.createMenu(self.menu.menuAction(),self.basemenuname)
203         else:
204           self.menu=QtGui.QMenu(self.menuname,self.basemenu)
205           self.basemenu.addMenu(self.menu)
206
207         self.menu.menuAction().setVisible(False)
208
209         self.basemenu.connect(self.basemenu, QtCore.SIGNAL("aboutToShow()"), self.importPlugins)
210
211     def analyseFile(self,filename):
212       """
213       This function checks if the specified file is a plugins python
214       module and add the directory name of this file to the list of
215       plugin paths. This function is aimed to be used as the callback
216       function of the walktree algorithm.
217       """
218       if str(filename).endswith(MATCH_ENDING_PATTERN):
219         dirpath=os.path.dirname(filename)
220         if dirpath not in self.plugindirs:
221           self.plugindirs.append(dirpath)
222           logger.debug("The directory %s has been added to plugin paths"%dirpath)
223         
224     def AddFunction(self,name,description,script):
225         """ Add a plugin function
226         """
227         self.registry[name]=script,description
228         self.entries.append(name)
229
230         def handler(obj=self,script=script):
231           try:
232             script(Context(sgPyQt))
233           except:
234             s=traceback.format_exc()
235             QtGui.QMessageBox.warning(None,"Exception occured",s)
236
237         self.handlers[name]=handler
238
239     def importPlugins(self):
240         """Execute the salome_plugins file that contains plugins definition """
241         studyId=sg.getActiveStudyId()
242         if studyId == 0:
243           self.menu.clear()
244           self.menu.menuAction().setVisible(False)
245           return
246         elif self.lasttime ==0 or salome.myStudy == None:
247           salome.salome_init(embedded=1)
248
249         lasttime=0
250
251         plugins_files=[]
252         plugins_file_name=self.name+MATCH_ENDING_PATTERN
253         for directory in self.plugindirs:
254           plugins_file = os.path.join(directory,plugins_file_name)
255           if os.path.isfile(plugins_file):
256             plugins_files.append((directory,plugins_file))
257             lasttime=max(lasttime,os.path.getmtime(plugins_file))
258
259         plugins_files.sort()
260
261         if not plugins_files:
262           self.registry.clear()
263           self.handlers.clear()
264           self.entries=[]
265           self.lasttime=0
266           self.menu.clear()
267           self.menu.menuAction().setVisible(False)
268           return
269
270         if self.plugins_files != plugins_files or lasttime > self.lasttime:
271           global current_plugins_manager
272           current_plugins_manager=self
273           self.registry.clear()
274           self.handlers.clear()
275           self.entries=[]
276           self.lasttime=lasttime
277           for directory,plugins_file in plugins_files:
278             logger.debug("look for python path: %s"%directory)
279             if directory not in sys.path:
280               sys.path.insert(0,directory)
281               logger.debug("The directory %s has been added to PYTHONPATH"%directory)
282             try:
283               execfile(plugins_file,globals(),{})
284             except:
285               logger.fatal("Error while loading plugins from file %s"%plugins_file)
286               traceback.print_exc()
287
288           self.updateMenu()
289
290     def updateMenu(self):
291         """Update the Plugins menu"""
292         self.menu.clear()
293         for entry in self.entries:
294           names=entry.split("/")
295           if len(names) < 1:continue
296           parentMenu=self.menu
297
298           if len(names) > 1:
299             #create or get submenus
300             submenus={}
301             for action in parentMenu.actions():
302               menu=action.menu()
303               if menu:
304                 submenus[str(menu.title())]=menu
305             while len(names) > 1:
306               name=names.pop(0)
307               if submenus.has_key(name):
308                 amenu=submenus[name]
309               else:
310                 amenu=QtGui.QMenu(name,parentMenu)
311                 parentMenu.addMenu(amenu)
312                 submenus[name]=amenu
313               parentMenu=amenu
314
315           name=names.pop(0)
316           act=parentMenu.addAction(name,self.handlers[entry])
317           act.setStatusTip(self.registry[entry][1])
318
319         self.menu.menuAction().setVisible(True)
320
321 def AddFunction(name,description,script):
322    """ Add a plugin function
323        Called by a user to register a function (script)
324    """
325    return current_plugins_manager.AddFunction(name,description,script)
326
327 def entries():
328   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
329   return current_plugins_manager.entries