Salome HOME
Merge remote-tracking branch 'origin/V8_3_BR' into ngr/python3_dev
[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   - 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 qtsalome import *
87
88 import salome
89
90 SEP=":"
91 if sys.platform == "win32":
92   SEP = ";"
93
94 # Get SALOME PyQt interface
95 import SalomePyQt
96 sgPyQt = SalomePyQt.SalomePyQt()
97
98 # Get SALOME Swig interface
99 import libSALOME_Swig
100 sg = libSALOME_Swig.SALOMEGUI_Swig()
101
102 plugins={}
103 current_plugins_manager=None
104
105 def initialize(module,name,basemenuname,menuname):
106   if name not in plugins:
107     if module:
108       plugins[name]={}
109     else:
110       plugins[name]=[]
111   if module:
112     d=sgPyQt.getDesktop()
113     if d in plugins[name]:return
114     plugins[name][d]=PluginsManager(module,name,basemenuname,menuname)
115   else:
116     plugins[name].append(PluginsManager(module,name,basemenuname,menuname))
117
118 class Context:
119     def __init__(self,sgpyqt):
120         self.sg=sgpyqt
121         self.studyId=salome.sg.getActiveStudyId()
122         self.study= salome.myStudyManager.GetStudyByID(self.studyId)
123
124 def find_menu(smenu):
125   lmenus=smenu.split("|")
126   # Take first element from the list
127   main=lmenus.pop(0).strip()
128   menu=sgPyQt.getPopupMenu(main)
129   return findMenu(lmenus,menu)
130
131 def findMenu(lmenu,menu):
132   if not lmenu:return menu
133   # Take first element from the list
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         searched = []
168         for key in os.environ.keys():
169           if key.endswith("_ROOT_DIR"):
170             rootpath=os.environ[key]
171             dirpath=os.path.join(rootpath,PLUGIN_PATH_PATTERN)
172             if os.path.isdir(dirpath) and dirpath not in self.plugindirs + searched:
173               logger.debug("Looking for plugins in the directory %s ..."%dirpath)
174               walktree(dirpath,self.analyseFile)
175               if dirpath not in self.plugindirs and dirpath not in searched:
176                 searched.append(dirpath)
177
178         # USER plugins directory
179         user_dir = os.path.expanduser("~/.config/salome/Plugins")
180         self.plugindirs.append(user_dir)
181         logger.info("The user directory %s has been added to plugin paths"%user_dir)
182         # obsolete: USER plugins directory
183         # (for compatibility reasons only; new plugins should be stored in ~/.config/salome/Plugins)
184         user_obsolete_dir = os.path.expanduser("~/.salome/Plugins")
185         self.plugindirs.append(user_obsolete_dir)
186         logger.info("The user directory %s has been added to plugin paths (deprecated)"%user_obsolete_dir)
187
188         # APPLI plugins directory
189         appli=os.getenv("APPLI")
190         if appli:
191           appli_dir=os.path.join(os.path.expanduser("~"),appli,"Plugins")
192           self.plugindirs.append(appli_dir)
193           logger.info("The APPLI directory %s has been added to plugin paths"%appli_dir)
194
195         #SALOME_PLUGINS_PATH environment variable (list of directories separated by ":")
196         pluginspath=os.getenv("SALOME_PLUGINS_PATH")
197         if pluginspath:
198           for directory in pluginspath.split(SEP):
199             self.plugindirs.append(directory)
200             logger.info("The directory %s has been added to plugin paths"%directory)
201
202         self.basemenu = find_menu(self.basemenuname)
203
204         if self.module:
205           self.menu=QMenu(self.menuname)
206           mid=sgPyQt.createMenu(self.menu.menuAction(),self.basemenuname)
207         else:
208           self.menu=QMenu(self.menuname,self.basemenu)
209           self.basemenu.addMenu(self.menu)
210
211         self.menu.menuAction().setVisible(False)
212
213         self.basemenu.aboutToShow.connect(self.importPlugins)
214
215     def analyseFile(self,filename):
216       """
217       This function checks if the specified file is a plugins python
218       module and add the directory name of this file to the list of
219       plugin paths. This function is aimed to be used as the callback
220       function of the walktree algorithm.
221       """
222       if str(filename).endswith(MATCH_ENDING_PATTERN):
223         dirpath=os.path.dirname(filename)
224         if dirpath not in self.plugindirs:
225           self.plugindirs.append(dirpath)
226           logger.debug("The directory %s has been added to plugin paths"%dirpath)
227         
228     def AddFunction(self,name,description,script):
229         """ Add a plugin function
230         """
231         self.registry[name]=script,description
232         self.entries.append(name)
233
234         def handler(obj=self,script=script):
235           try:
236             script(Context(sgPyQt))
237           except:
238             s=traceback.format_exc()
239             QMessageBox.warning(None,"Exception occured",s)
240
241         self.handlers[name]=handler
242
243     def importPlugins(self):
244         """Execute the salome_plugins file that contains plugins definition """
245         studyId=sg.getActiveStudyId()
246         if studyId == 0:
247           self.menu.clear()
248           self.menu.menuAction().setVisible(False)
249           return
250         elif self.lasttime ==0 or salome.myStudy == None:
251           salome.salome_init(embedded=1)
252
253         lasttime=0
254
255         plugins_files=[]
256         plugins_file_name=self.name+MATCH_ENDING_PATTERN
257         for directory in self.plugindirs:
258           plugins_file = os.path.join(directory,plugins_file_name)
259           if os.path.isfile(plugins_file):
260             plugins_files.append((directory,plugins_file))
261             lasttime=max(lasttime,os.path.getmtime(plugins_file))
262
263         plugins_files.sort()
264
265         if not plugins_files:
266           self.registry.clear()
267           self.handlers.clear()
268           self.entries=[]
269           self.lasttime=0
270           self.menu.clear()
271           self.menu.menuAction().setVisible(False)
272           return
273
274         if self.plugins_files != plugins_files or lasttime > self.lasttime:
275           global current_plugins_manager
276           current_plugins_manager=self
277           self.registry.clear()
278           self.handlers.clear()
279           self.entries=[]
280           self.lasttime=lasttime
281           for directory,plugins_file in plugins_files:
282             logger.debug("look for python path: %s"%directory)
283             if directory not in sys.path:
284               sys.path.insert(0,directory)
285               logger.debug("The directory %s has been added to PYTHONPATH"%directory)
286             try:
287               exec(compile(open(plugins_file).read(), plugins_file, 'exec'),globals(),{})
288             except:
289               logger.fatal("Error while loading plugins from file %s"%plugins_file)
290               traceback.print_exc()
291
292           self.updateMenu()
293
294     def updateMenu(self):
295         """Update the Plugins menu"""
296         self.menu.clear()
297         for entry in self.entries:
298           names=entry.split("/")
299           if len(names) < 1:continue
300           parentMenu=self.menu
301
302           if len(names) > 1:
303             #create or get submenus
304             submenus={}
305             for action in parentMenu.actions():
306               menu=action.menu()
307               if menu:
308                 submenus[str(menu.title())]=menu
309             while len(names) > 1:
310               name=names.pop(0)
311               if name in submenus:
312                 amenu=submenus[name]
313               else:
314                 amenu=QMenu(name,parentMenu)
315                 parentMenu.addMenu(amenu)
316                 submenus[name]=amenu
317               parentMenu=amenu
318
319           name=names.pop(0)
320           act=parentMenu.addAction(name,self.handlers[entry])
321           act.setStatusTip(self.registry[entry][1])
322
323         self.menu.menuAction().setVisible(True)
324
325 def AddFunction(name,description,script):
326    """ Add a plugin function
327        Called by a user to register a function (script)
328    """
329    return current_plugins_manager.AddFunction(name,description,script)
330
331 def entries():
332   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
333   return current_plugins_manager.entries