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