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