Salome HOME
Merge remote branch 'origin/V7_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 not plugins.has_key(name):
107     if module:
108       plugins[name]={}
109     else:
110       plugins[name]=[]
111   if module:
112     d=sgPyQt.getDesktop()
113     if plugins[name].has_key(d):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=unicode(basemenuname, "utf-8")
153         self.menuname=unicode(menuname, "utf-8")
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=QMenu(self.menuname)
203           mid=sgPyQt.createMenu(self.menu.menuAction(),self.basemenuname)
204         else:
205           self.menu=QMenu(self.menuname,self.basemenu)
206           self.basemenu.addMenu(self.menu)
207
208         self.menu.menuAction().setVisible(False)
209
210         self.basemenu.aboutToShow.connect(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             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 or salome.myStudy == None:
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             logger.debug("look for python path: %s"%directory)
280             if directory not in sys.path:
281               sys.path.insert(0,directory)
282               logger.debug("The directory %s has been added to PYTHONPATH"%directory)
283             try:
284               execfile(plugins_file,globals(),{})
285             except:
286               logger.fatal("Error while loading plugins from file %s"%plugins_file)
287               traceback.print_exc()
288
289           self.updateMenu()
290
291     def updateMenu(self):
292         """Update the Plugins menu"""
293         self.menu.clear()
294         for entry in self.entries:
295           names=entry.split("/")
296           if len(names) < 1:continue
297           parentMenu=self.menu
298
299           if len(names) > 1:
300             #create or get submenus
301             submenus={}
302             for action in parentMenu.actions():
303               menu=action.menu()
304               if menu:
305                 submenus[str(menu.title())]=menu
306             while len(names) > 1:
307               name=names.pop(0)
308               if submenus.has_key(name):
309                 amenu=submenus[name]
310               else:
311                 amenu=QMenu(name,parentMenu)
312                 parentMenu.addMenu(amenu)
313                 submenus[name]=amenu
314               parentMenu=amenu
315
316           name=names.pop(0)
317           act=parentMenu.addAction(name,self.handlers[entry])
318           act.setStatusTip(self.registry[entry][1])
319
320         self.menu.menuAction().setVisible(True)
321
322 def AddFunction(name,description,script):
323    """ Add a plugin function
324        Called by a user to register a function (script)
325    """
326    return current_plugins_manager.AddFunction(name,description,script)
327
328 def entries():
329   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
330   return current_plugins_manager.entries