Salome HOME
0023618: EDF 14415 - Plugins toolbar
[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   - 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         if self.lasttime ==0 or salome.myStudy == None:
248           salome.salome_init(embedded=True)
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               exec(compile(open(plugins_file,'rb').read(), plugins_file, 'exec'),globals(),{})
285             except:
286               logger.critical("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         sgPyQt.clearTool(self.menuname)
295         for entry in self.entries:
296           names=entry.split("/")
297           if len(names) < 1:continue
298           parentMenu=self.menu
299
300           if len(names) > 1:
301             #create or get submenus
302             submenus={}
303             for action in parentMenu.actions():
304               menu=action.menu()
305               if menu:
306                 submenus[str(menu.title())]=menu
307             while len(names) > 1:
308               name=names.pop(0)
309               if name in submenus:
310                 amenu=submenus[name]
311               else:
312                 amenu=QMenu(name,parentMenu)
313                 parentMenu.addMenu(amenu)
314                 submenus[name]=amenu
315               parentMenu=amenu
316
317           name=names.pop(0)
318           act=parentMenu.addAction(name,self.handlers[entry])
319           act.setStatusTip(self.registry[entry][1])
320           icon = self.registry[entry][2] if len(self.registry[entry])>2 else None
321           if icon is not None and not icon.isNull() and icon.availableSizes():
322             act.setIcon(icon)
323             sgPyQt.createTool(act, self.toolbar)
324
325         self.menu.menuAction().setVisible(True)
326
327 def AddFunction(name,description,script,icon=None):
328    """ Add a plugin function
329        Called by a user to register a function (script)
330    """
331    return current_plugins_manager.AddFunction(name,description,script,icon)
332
333 def entries():
334   """ Return the list of entries in menu: can be sorted or modified in place to customize menu content """
335   return current_plugins_manager.entries