Salome HOME
[CEA] Automatically expand tree in Object Browser
[modules/gui.git] / src / LightApp / LightApp_Application.cxx
index e687135f6a656301e7679061818e350dba879162..d72e414f5a293eca0416426cc5a2a28f8b0bf946 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2007-2021  CEA/DEN, EDF R&D, OPEN CASCADE
+// Copyright (C) 2007-2024  CEA, EDF, OPEN CASCADE
 //
 // Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
 // CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
@@ -23,7 +23,6 @@
 // File:      LightApp_Application.cxx
 // Created:   6/20/2005 18:39:45 PM
 // Author:    Natalia Donis
-
 #ifdef WIN32
 // E.A. : On windows with python 2.6, there is a conflict
 // E.A. : between pymath.h and Standard_math.h which define
@@ -50,6 +49,7 @@
 #include "LightApp_PreferencesDlg.h"
 #include "LightApp_ModuleDlg.h"
 #include "LightApp_AboutDlg.h"
+#include "LightApp_ExtInfoDlg.h"
 #include "LightApp_ModuleAction.h"
 // temporary commented
 #include "LightApp_EventFilter.h"
@@ -98,6 +98,7 @@
 #include <QtxMap.h>
 
 #include <LogWindow.h>
+#include <SalomeApprc_utils.h>
 
 #ifndef DISABLE_GLVIEWER
   #include <GLViewer_Viewer.h>
   #include <PyViewer_ViewWindow.h>
 #endif
 
+#ifndef DISABLE_PV3DVIEWER
+#ifndef DISABLE_SALOMEOBJECT
+  #include <SPV3D_ViewModel.h>
+  #include <SPV3D_ViewManager.h>
+  #include "LightApp_PV3DSelector.h"
+#else
+  #include <PV3DViewer_ViewModel.h>
+  #include <PV3DViewer_ViewManager.h>
+#endif
+  #include <PV3DViewer_ViewManager.h>
+  #include <PV3DViewer_ViewModel.h>
+  #include "PV3DViewer_ViewWindow.h"
+#endif
+
 
 #define VISIBILITY_COLUMN_WIDTH 25
 
@@ -250,6 +265,11 @@ static const char* imageEmptyIcon[] = {
 //since the 'toolbar marker' is not unique, find index of first occurrence of the
 //'toolbar marker' in the array and check that next string is name of the toolbar
 
+namespace
+{
+  const char* salomeAppDir = "SALOME_APPLICATION_DIR";
+}
+
 void LightAppCleanUpAppResources()
 {
   if ( LightApp_Application::_prefs_ ) {
@@ -323,6 +343,13 @@ namespace
     }
     return QString();
   }
+
+  const bool HAS_SALOME_ON_DEMAND =
+#if defined(WITH_SALOME_ON_DEMAND)
+    true;
+#else
+    false;
+#endif
 }
 
 /*!Create new instance of LightApp_Application.*/
@@ -542,17 +569,21 @@ bool LightApp_Application::activateModule( const QString& modName )
   if ( prevMod )
     actName = prevMod->moduleName();
 
-  if ( actName == modName )
+  QString name = modName;
+  if ( !name.isEmpty() && !moduleTitle( modName ).isEmpty() )
+    name = moduleTitle( modName );
+
+  if ( actName == name )
     return true;
 
-  putInfo( tr( "ACTIVATING_MODULE" ).arg( modName ) );
+  putInfo( tr( "ACTIVATING_MODULE" ).arg( name ) );
 
   saveDockWindowsState();
 
   if ( infoPanel() )
     infoPanel()->clear();
 
-  bool status = CAM_Application::activateModule( modName );
+  bool status = CAM_Application::activateModule( name );
 
   updateModuleActions();
 
@@ -568,6 +599,25 @@ bool LightApp_Application::activateModule( const QString& modName )
     if ( objectBrowser()->root() != activeStudy()->root() )
       objectBrowser()->setRoot( activeStudy()->root() );
     updateObjectBrowser( true );
+
+    // expand SHAPERSTUDY data after leaving Shaper module
+    // bos #40645 [CEA] Automatically expand tree in Object Browser
+    if (actName == "Shaper") {
+      SUIT_AbstractModel* aModel = dynamic_cast<SUIT_AbstractModel*>(objectBrowser()->model());
+      LightApp_Study* aStudy = dynamic_cast<LightApp_Study*>( activeStudy() );
+      if (aModel && aStudy) {
+        DataObjectList aComps;
+        aStudy->root()->children(aComps);
+        for(auto aCompSUIT : aComps){
+          LightApp_DataObject* aComp = dynamic_cast<LightApp_DataObject*>( aCompSUIT );
+          if ( aComp && aComp->componentDataType() == "SHAPERSTUDY") {
+            QModelIndex anIndex = aModel->index(aComp);
+            objectBrowser()->treeView()->expand(anIndex);
+            break;
+          }
+        }
+      }
+    }
   }
 
   if ( activeModule() ) activeModule()->updateModuleVisibilityState();
@@ -621,8 +671,6 @@ void LightApp_Application::createActions()
 
   int helpMenu = createMenu( tr( "MEN_DESK_HELP" ), -1, -1, 1000 );
 
-  int id = LightApp_Application::UserID + FIRST_HELP_ID;
-
   QString url;
 
   // a) Link to web site
@@ -684,87 +732,17 @@ void LightApp_Application::createActions()
 
   // e) Help for modules
 
-  // - First create top-level menus to preserve correct order
-  QString userGuide = "User's Guide";
-  QString devGuide = "Developer's Guide";
-  createMenu( userGuide, helpMenu, -1, 5 );
-  createMenu( devGuide, helpMenu, -1, 5 );
-
   QStringList aModuleList;
   modules( aModuleList, false );
   aModuleList.prepend( "GUI" );
   aModuleList.prepend( "KERNEL" );
 
-  QString aModule;
-  foreach( aModule, aModuleList ) {
-    if ( aModule.isEmpty() )                                         // module title (user name)
-      continue;
-    IMap <QString, QString> helpData;                                // list of help files for the module
-    QString helpSubMenu;                                             // help submenu name (empty if not needed)
-    QString modName = moduleName( aModule );                         // module name
-    if ( modName.isEmpty() ) modName = aModule;                      // for KERNEL and GUI
-    QString rootDir = QString( "%1_ROOT_DIR" ).arg( modName );       // module root dir env variable
-    QString modDir  = Qtx::getenv( rootDir.toUtf8().constData() );   // module root dir path
-    QString docSection;
-    if (resMgr->hasValue( modName, "documentation" ) )
-      docSection = resMgr->stringValue(modName, "documentation");
-    else if ( resMgr->hasSection( modName + "_documentation" ) )
-      docSection = modName + "_documentation";
-    if ( !docSection.isEmpty() ) {
-      helpSubMenu = resMgr->stringValue( docSection, "sub_menu", "" );
-      if ( helpSubMenu.contains( "%1" ) )
-        helpSubMenu = helpSubMenu.arg( aModule );
-      foreach( QString paramName, resMgr->parameters( docSection ) ) {
-        QString key = paramName.contains( "%1" ) ? paramName.arg( aModule ) : paramName;
-        QString helpItem = getHelpItem( docSection, paramName );
-        if ( !helpItem.isEmpty() )
-          helpData.insert( key, helpItem );
-      }
-    }
-
-    if ( helpData.isEmpty() && !modDir.isEmpty() ) {
-      QStringList idxLst = QStringList() << modDir << "share" << "doc" << "salome" << "gui" << modName << "index.html";
-      QString indexFile = idxLst.join( QDir::separator() );          // index file
-      if ( QFile::exists( indexFile ) )
-        helpData.insert( tr( "%1 module Users's Guide" ).arg( aModule ), indexFile );
-    }
-
-    IMapConstIterator<QString, QString > fileIt;
-    for ( fileIt = helpData.begin(); fileIt != helpData.end(); fileIt++ ) {
-      QString helpItemPath = fileIt.key();
-      // remove all '//' occurances
-      while ( helpItemPath.contains( "//" ) )
-        helpItemPath.replace( "//", "" );
-      // obtain submenus hierarchy if given
-      QStringList smenus = helpItemPath.split( "/" );
-      helpItemPath = smenus.takeLast();
-      // workaround for User's Guide and Developer's Guide to avoid having single item in module's submenu.
-      if ( helpItemPath == userGuide || helpItemPath == devGuide ) {
-       QString menuPath = smenus.join( "/" );
-       QStringList allKeys = helpData.keys();
-       QStringList total = allKeys.filter( QRegExp( QString( "^%1" ).arg( menuPath ) ) );
-       if ( total.count() == 1 && smenus.count() > 0 )
-         helpItemPath = smenus.takeLast();
-      }
-      QPixmap helpIcon = fileIt.value().startsWith( "http", Qt::CaseInsensitive ) ?
-        resMgr->loadPixmap( "STD", tr( "ICON_WWW" ), false ) : resMgr->loadPixmap( "STD", tr( "ICON_HELP" ), false );
-      QAction* a = createAction( id, helpItemPath, helpIcon, helpItemPath, helpItemPath,
-                                 0, desk, false, this, SLOT( onHelpContentsModule() ) );
-      a->setData( fileIt.value() );
-      if ( !helpSubMenu.isEmpty() ) {
-        smenus.prepend( helpSubMenu );
-      }
-      // create sub-menus hierarchy
-      int menuId = helpMenu;
-      foreach ( QString subMenu, smenus )
-        menuId = createMenu( subMenu, menuId, -1, 5 );
-      createMenu( a, menuId, -1, ( menuId != helpMenu && (helpItemPath == userGuide || helpItemPath == devGuide) ) ? 0 : 5 );
-      id++;
-    }
-  }
+  foreach( QString aModule, aModuleList )
+    createHelpItems( aModule );
 
-  // - Additional help items
+  // f) Additional help items
 
+  int id = LightApp_Application::UserID + FIRST_HELP_ID + 1000;
   createMenu( separator(), helpMenu, -1, 10 );
 
   QStringList addHelpItems = resMgr->parameters( "add_help" );
@@ -786,57 +764,28 @@ void LightApp_Application::createActions()
   connect( mru, SIGNAL( activated( const QString& ) ), this, SLOT( onMRUActivated( const QString& ) ) );
   registerAction( MRUId, mru );
 
-  // default icon for neutral point ('SALOME' module)
-  QPixmap defIcon = resMgr->loadPixmap( "LightApp", tr( "APP_DEFAULT_ICO" ), false );
-  if ( defIcon.isNull() )
-    defIcon = QPixmap( imageEmptyIcon );
-
-  //! default icon for any module
-  QPixmap modIcon = resMgr->loadPixmap( "LightApp", tr( "APP_MODULE_ICO" ), false );
-  if ( modIcon.isNull() )
-    modIcon = QPixmap( imageEmptyIcon );
-
+  // List of modules
+  LightApp_ModuleAction* moduleAction = new LightApp_ModuleAction( resMgr, desk );
+  registerAction( ModulesListId, moduleAction );
+  // a. here we add regular modules (specified to GUI via --modules cmd line option, or default list from configuration)
+  // b. custom modules are added in customize() method
   QStringList modList;
   modules( modList, false );
-
-  if ( modList.count() > 1 )
-  {
-    LightApp_ModuleAction* moduleAction =
-      new LightApp_ModuleAction( tr( "APP_NAME" ), defIcon, desk );
-
-    QMap<QString, QString> iconMap;
-    moduleIconNames( iconMap );
-
-    const int iconSize = 20;
-
-    QStringList::Iterator it;
-    for ( it = modList.begin(); it != modList.end(); ++it )
-    {
-      QString modName = moduleName( *it );
-
-      QString iconName;
-      if ( iconMap.contains( *it ) )
-        iconName = iconMap[*it];
-
-      QPixmap icon = resMgr->loadPixmap( modName, iconName, false );
-      if ( icon.isNull() )
-      {
-        icon = modIcon;
-        INFOS( std::endl <<
-               "****************************************************************" << std::endl <<
-               "     Warning: icon for " << qPrintable(*it) << " is not found!" << std::endl <<
-               "     Using the default icon." << std::endl <<
-               "****************************************************************" << std::endl);
-      }
-      icon = Qtx::scaleIcon( icon, iconSize );
-
-      moduleAction->insertModule( *it, icon );
-    }
-
-    connect( moduleAction, SIGNAL( moduleActivated( const QString& ) ),
-             this, SLOT( onModuleActivation( const QString& ) ) );
-    registerAction( ModulesListId, moduleAction );
-  }
+  foreach ( QString aModule, modList )
+    moduleAction->insertModule( aModule, moduleIcon( aModule, 20 ) ); // scale icon to 20x20 pix
+
+  connect( this, SIGNAL( moduleActivated( QString ) ),
+           moduleAction, SLOT( setActiveModule( QString ) ) );
+  connect( moduleAction, SIGNAL( moduleActivated( const QString& ) ),
+           this, SLOT( onModuleActivation( const QString& ) ) );
+  connect( moduleAction, SIGNAL( adding() ),
+           this, SLOT( onExtAdding() ) );
+  connect( moduleAction, SIGNAL( removing( QString ) ),
+           this, SLOT( onExtRemoving( QString ) ) );
+  connect( moduleAction, SIGNAL(showExtInfo()),
+           this, SLOT(onShowExtInfo()));
+
+  addExtensionsActions(moduleAction);
 
   // New window
   int windowMenu = createMenu( tr( "MEN_DESK_WINDOW" ), -1, MenuWindowId, 100 );
@@ -878,6 +827,9 @@ void LightApp_Application::createActions()
 #ifndef DISABLE_PYVIEWER
   createActionForViewer( NewPyViewerId, newWinMenu, QString::number( 7 ), Qt::ALT+Qt::Key_Y );
 #endif
+#ifndef DISABLE_PV3DVIEWER
+  createActionForViewer( NewPV3DViewId, newWinMenu, QString::number( 8 ), Qt::ALT+Qt::Key_3 );
+#endif
 
   createAction( RenameId, tr( "TOT_RENAME" ), QIcon(), tr( "MEN_DESK_RENAME" ), tr( "PRP_RENAME" ),
                 Qt::ALT+Qt::SHIFT+Qt::Key_R, desk, false, this, SLOT( onRenameWindow() ) );
@@ -906,26 +858,105 @@ void LightApp_Application::createActions()
   createMenu( StyleId, viewMenu, 20, -1 );
 #endif // USE_SALOME_STYLE
   createMenu( FullScreenId, viewMenu, 20, -1 );
+  createMenu( separator(), viewMenu, -1, 20, -1 );
+  createMenu( ModulesListId, viewMenu );
 
   int modTBar = createTool( tr( "INF_TOOLBAR_MODULES" ),    // title (language-dependant)
                             QString( "SalomeModules" ) );   // name (language-independant)
   createTool( ModulesListId, modTBar );
 }
 
+/*!Create actions for installed extensions:*/
+void LightApp_Application::addExtensionsActions(LightApp_ModuleAction* moduleAction)
+{
+  if (!moduleAction)
+  {
+    MESSAGE("Couldn't get a moduleAction! Return.");
+    return;
+  }
+
+  // It should be set on the app start if we use an --on_demand 1 command line option
+  auto extRootDir = getenv(salomeAppDir);
+  if (!extRootDir)
+  {
+    // It's ok if we don't use --on_demand
+    return;
+  }
+  SCRUTE(extRootDir);
+
+   // Import Python module that manages SALOME extensions.
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionQuery = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_query");
+  PyObjWrapper installedExtensions = PyObject_CallMethod(
+      extensionQuery, (char*)"ext_by_name", (char*)"s", extRootDir);
+  if (!installedExtensions)
+  {
+    return;
+  }
+
+  // Iterate installed extensions
+  for (Py_ssize_t pos = 0; pos < PyList_Size(installedExtensions); ++pos)
+  {
+    // Get the current ext name
+    auto extNameItem = PyList_GetItem(installedExtensions, pos);
+    QString extName(PyUnicode_AsUTF8(extNameItem));
+    SCRUTE(extName.toStdString());
+
+    moduleAction->insertExtension(extName);
+  }
+
+  // Udate actions only once after all of them were already inserted
+  moduleAction->updateExtActions();
+}
+
+/*!
+  Customize actions.
+*/
+void LightApp_Application::customize()
+{
+  // List of modules
+  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
+  // a. regular modules were added in createActions() method
+  // b. here we add custom modules (manually added by the user)
+  if ( HAS_SALOME_ON_DEMAND && QString::compare(getenv("SALOME_ON_DEMAND"),"HIDE", Qt::CaseInsensitive) != 0)
+  {
+    // Update rc file
+    updateSalomeApprc();
+
+    QStringList modList = resourceMgr()->stringValue( "launch", "user_modules" ).split( ";", QString::SkipEmptyParts );
+    foreach ( QString aModule, modList )
+      addUserModule(  aModule, resourceMgr()->stringValue( "user_modules", aModule ), true );
+  }
+  else
+  {
+    moduleAction->setModeEnabled( LightApp_ModuleAction::AddRemove, false );
+  }
+}
+
+/*!
+  Update rc file with SALOME_APPLICATION_DIR or with SALOME_MODULES.
+*/
+void LightApp_Application::updateSalomeApprc()
+{
+    SUIT_ResourceMgr* resMgr = resourceMgr();
+    auto extRootDir = getenv(salomeAppDir);
+
+    QString salomemodules(getenv("SALOME_MODULES"));
+    if(salomemodules.isEmpty())
+        UpdateCompInfo_with_salomeappdir(  QDir(extRootDir), resMgr );
+    else
+        UpdateCompInfo_with_salomemodules(salomemodules, QDir(extRootDir), resMgr);
+}
+
 /*!On module activation action.*/
-void LightApp_Application::onModuleActivation( const QString& modName )
+void LightApp_Application::onModuleActivation( const QString& modTitle )
 {
   // Force user to create/open a study before module activation
-  QMap<QString, QString> iconMap;
-  moduleIconNames( iconMap );
-  QPixmap icon = resourceMgr()->loadPixmap( moduleName( modName ), iconMap[ modName ], false );
-  if ( icon.isNull() )
-    icon = resourceMgr()->loadPixmap( "LightApp", tr( "APP_MODULE_BIG_ICO" ), false ); // default icon for any module
-
+  QPixmap icon = moduleIcon( modTitle );
   bool cancelled = false;
 
-  while ( !modName.isEmpty() && !activeStudy() && !cancelled ){
-    LightApp_ModuleDlg aDlg( desktop(), modName, icon );
+  while ( !modTitle.isEmpty() && !activeStudy() && !cancelled ){
+    LightApp_ModuleDlg aDlg( desktop(), modTitle, icon );
     QMap<int, QString> opmap = activateModuleActions();
     for ( QMap<int, QString>::ConstIterator it = opmap.begin(); it != opmap.end(); ++it )
       aDlg.addButton( it.value(), it.key() );
@@ -938,17 +969,329 @@ void LightApp_Application::onModuleActivation( const QString& modName )
     else {
       // cancelled
       putInfo( tr("INF_CANCELLED") );
-
-      LightApp_ModuleAction* moduleAction =
-        qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
-      if ( moduleAction )
-        moduleAction->setActiveModule( QString() );
+      emit moduleActivated( QString() );
       cancelled = true;
     }
   }
 
   if ( !cancelled )
-    activateModule( modName );
+    activateModule( modTitle );
+}
+
+/*!On extension adding action.*/
+void LightApp_Application::onExtAdding()
+{
+  // Show dialog to browse a salome extension file
+  QStringList filters = (QStringList() << tr("Salome extension files") + " (*.salomex)" << tr("All files") + " (*)");
+  QStringList paths = getOpenFileNames(QString(), filters.join(";;"), QString(), desktop());
+  if (paths.isEmpty())
+  {
+    MESSAGE("Adding an extension was cancelled.");
+    return;
+  }
+
+  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>(action(ModulesListId));
+  if (!moduleAction)
+  {
+    MESSAGE("Couldn't get a moduleAction! Return.");
+    return;
+  }
+
+  // It should be set on the app start
+  auto extRootDir = getenv(salomeAppDir);
+  if (!extRootDir)
+  {
+    SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_SALOME_APPLICATION_DIR"));
+    return;
+  }
+  SCRUTE(extRootDir);
+
+  // We'll load all the extensions modules from this path
+  auto SalomeExtDir = QDir::cleanPath(QString(extRootDir) + QDir::separator() + "__SALOME_EXT__");
+  SCRUTE(SalomeExtDir.toStdString());
+
+  // Import Python module that manages SALOME extensions.
+  // It seems to be faster to lock and unlock once than on each iteration,
+  // but I didn't compare the performance for each case.
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionUnpacker = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_unpacker");
+  PyObjWrapper runSalomeOnDemand = PyImport_ImportModule((char*)"runSalomeOnDemand");
+
+  // Loop via selected extensions files
+  foreach(QString path, paths)
+  {
+    std::string extPath = path.toStdString();
+    SCRUTE(extPath);
+
+    PyObjWrapper unpackedModules = PyObject_CallMethod(
+      extensionUnpacker, (char*)"install_salomex", (char*)"s", extPath.c_str());
+    if (!unpackedModules || unpackedModules == Py_None)
+    {
+      SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_FAILED_UNPACK_EXTENSION").arg(path) );
+      continue;
+    }
+
+    PyObjWrapper pKeys = PyDict_Keys(unpackedModules);
+    // Iterate all the components (modules) for this extension
+    for (Py_ssize_t pos = 0; pos < PyDict_Size(unpackedModules); ++pos)
+    {
+      auto moduleNameItem = PyList_GetItem(pKeys, pos);
+      auto interactiveItem = PyDict_GetItem(unpackedModules, moduleNameItem);
+
+      QString moduleName(PyUnicode_AsUTF8(moduleNameItem));
+      SCRUTE(moduleName.toStdString());
+      addUserModule(moduleName, SalomeExtDir, PyObject_IsTrue(interactiveItem));
+    }
+
+    // Add an extension to GUI
+    QFileInfo extFileInfo(path);
+    QString extName = extFileInfo.baseName();
+    if (moduleAction)
+    {
+      moduleAction->insertExtension(extName);
+    }
+
+    // Update environment of salome
+    PyObjWrapper update_env = PyObject_CallMethod(
+      runSalomeOnDemand, (char*)"set_selext_env", (char*)"ss", extRootDir, extName.toStdString().c_str());
+    if (!update_env)
+    {
+      SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_FAILED_UPDATE_ENV").arg(extName + "_env.py") );
+      continue;
+    }
+  }
+
+  // Udate actions only once after all of them were already inserted
+  moduleAction->updateExtActions();
+}
+
+/*Add user module.*/
+bool LightApp_Application::addUserModule( const QString& name, const QString& root, bool interactive )
+{
+  if ( name == "KERNEL" || name == "GUI" )
+    return false; // skip KERNEL and GUI modules
+
+  if ( name.isEmpty() || root.isEmpty() )
+    return false;
+
+  if ( !moduleTitle( name ).isEmpty() ) // module alread in current session
+  {
+    if ( interactive )
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_DUPLICATED" ).arg( name ) );
+    return false;
+  }
+  if ( !QFileInfo( root ).exists() ) // root directory does not exist
+  {
+    if ( interactive )
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_ROOT_DOES_NOT_EXIST" ).arg( root ) );
+    return false;
+  }
+  // resources directory
+  QString resDir = Qtx::joinPath( QStringList() << root << "share" << "salome" << "resources" << name.toLower() );
+  if ( !QFileInfo( resDir ).exists() ) // resources directory does not exist
+  {
+    if ( interactive )
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_BAD_RESDIR" ).arg( resDir ) );
+    return false;
+  }
+
+  SUIT_ResourceMgr* resMgr = resourceMgr();
+
+  // read XML configuration file
+  resMgr->setConstant(QString("%1_ROOT_DIR").arg(name), root);
+  if (!resMgr->addResource(resDir)) // cannot read configuration
+  {
+    if ( interactive )
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_CANNOT_READ_CFG" ).arg( resDir ) );
+    return false;
+  }
+  // fill in information about module
+  if ( !appendModuleInfo( name ) ) // cannot append module information to internal table
+  {
+    if ( interactive )
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_BAD_CFG_FILE" ).arg( name ) );
+    return false;
+  }
+
+  // load translations
+  resMgr->loadLanguage(name);
+
+  // Do all the GUI related stuff only if the module supports that.
+  // We already did check for GUI inside CAM_Application::appendModuleInfo, but
+  // need to do that again.
+  // TODO: Maybe it's better to return ModuleInfo from appendModuleInfo() and check status.
+  const QString title = resMgr->stringValue(name, "name", QString()).trimmed();
+  if (resMgr->booleanValue(name, "gui", false) || !title.isEmpty() && interactive)
+  {
+    // Append module to the menu / toolbar
+    LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>(action(ModulesListId));
+    if (moduleAction)
+    {
+      // Scale icon to 20x20 pix
+      moduleAction->insertModule(moduleTitle(name), moduleIcon(moduleTitle(name), 20), true);
+    }
+  }
+
+  // add empty page to Preferences dialog
+  LightApp_Preferences* prefs = preferences();
+  if ( prefs && !prefs->hasModule( moduleTitle( name ) ) )
+  {
+    int prefId = prefs->addPreference( moduleTitle( name ) );
+    prefs->setItemIcon( prefId, moduleIcon( moduleTitle( name ), 20 ) ); // scale icon to 20x20 pix
+    LightApp_Module* m = qobject_cast<LightApp_Module*>( module( moduleTitle( name ) ) );
+    if ( m )
+    {
+      m->createPreferences();
+      emptyPreferences( moduleTitle( name ) );
+    }
+  }
+  // add Help items
+  createHelpItems( moduleTitle( name ) );
+  // extend module catalog
+  QString catalogue = QDir( resDir ).filePath( QString( "%1Catalog.xml" ).arg( name ) );
+  addCatalogue( name, catalogue );
+  // update windows (in particular, Info panel)
+  updateWindows();
+  // save module in the resource manager
+  if ( interactive )
+  {
+    QStringList customModules = resMgr->stringValue("launch", "user_modules").split(";", QString::SkipEmptyParts);
+    customModules << name;
+    customModules.removeDuplicates();
+    resMgr->setValue( "launch", "user_modules", customModules.join( ";" ) );
+    resMgr->setValue( "user_modules", name, root );
+  }
+  return true;
+}
+
+/*!Remove user module from UI.*/
+void LightApp_Application::removeUserModule(const QString& moduleInnerName, LightApp_ModuleAction* moduleAction)
+{
+  MESSAGE("Remove a module from UI...");
+  SCRUTE(moduleInnerName.toStdString());
+
+  // There is a some confusion point, because now we have a module's 'inner' name
+  // from the extension's salomexd file.
+  // But, in the next GUI methods we need to use a module title (user name).
+  // For example, PYHELLO (inner name) and PyHello (user name to display in GUI).
+  // Then, from the inner module's name we need to get a user one.
+  QString moduleUserName = moduleTitle(moduleInnerName);
+  SCRUTE(moduleUserName.toStdString());
+
+  // Set current state in modules combo box
+  // Don't confuse again, because activeModule()->moduleName() returns a module title, not an inner one!
+  if (activeModule() && activeModule()->moduleName() == moduleUserName)
+    activateModule("");
+
+  // Remove from "Modules" menu and toolbar
+  if (moduleAction)
+  {
+    moduleAction->removeModule(moduleUserName);
+  }
+
+  // Remove Help menu items
+  removeHelpItems(moduleUserName);
+
+  // Remove Preferences
+  LightApp_Preferences* prefs = preferences();
+  if (prefs)
+    prefs->removeModule(moduleUserName);
+
+  // Remove settings
+  // Here we use an inner module name!
+  QStringList customModules = resourceMgr()->stringValue("launch", "user_modules").split(";", QString::SkipEmptyParts);
+  customModules.removeAll(moduleInnerName);
+  resourceMgr()->setValue("launch", "user_modules", customModules.join(";"));
+  removeModuleInfo(moduleInnerName);
+}
+
+/*!On module removing action.*/
+void LightApp_Application::onExtRemoving(const QString& title)
+{
+  MESSAGE("Remove an extension...");
+  std::string extName = title.toStdString();
+  SCRUTE(extName);
+
+  // Ask user if he's ready to completely remove an extension and all its modules.
+  int answer = SUIT_MessageBox::question(
+    desktop(),
+    tr("TLT_REMOVE_EXTENSION"),
+    tr("QUE_REMOVE_EXTENSION").arg(title),
+    SUIT_MessageBox::Ok | SUIT_MessageBox::Cancel,
+    SUIT_MessageBox::Ok
+  );
+
+  if (answer == SUIT_MessageBox::Cancel)
+  {
+    MESSAGE("Removing of an extension was cancelled");
+    return; // cancelled
+  }
+  
+  if (activeStudy() && activeStudy()->isModified() && !onSaveDoc())
+  {
+    // doc is not saved, or saving cancelled
+    SUIT_MessageBox::warning(
+      desktop(),
+      tr("WRN_WARNING"), tr("WRN_CANCEL_REMOVE_EXTENSION_UNSAVE").arg(title)
+    );
+
+    return;
+  }
+
+  // It should be set on the app start
+  auto extRootDir = getenv(salomeAppDir);
+  if (!extRootDir)
+  {
+    SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_SALOME_APPLICATION_DIR"));
+    return;
+  }
+  SCRUTE(extRootDir);
+
+  // Import Python module that manages SALOME extensions.
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionRemover = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_remover");
+  PyObjWrapper removedModules = PyObject_CallMethod(
+      extensionRemover, (char*)"remove_salomex", (char*)"ss", extRootDir, extName.c_str());
+  if (!removedModules || removedModules == Py_None)
+  {
+    SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_FAILED_REMOVE_EXTENSION").arg(title));
+    return;
+  }
+
+  // We need it to remove ext and modules from UI
+  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>(action(ModulesListId));
+  if (!moduleAction)
+  {
+    MESSAGE("Cannot get a pointer to LightApp_ModuleAction! Removing from menue and toolbars will skipped.");
+  }
+
+  // Module's content was already removed on python remove_salomex call,
+  // then all we do next - just remove UI items.
+  for (Py_ssize_t pos = 0; pos < PyList_Size(removedModules); ++pos)
+  {
+    // Get the current module's name
+    auto moduleNameItem = PyList_GetItem(removedModules, pos);
+    const QString moduleInnerName(PyUnicode_AsUTF8(moduleNameItem));
+
+    removeUserModule(moduleInnerName, moduleAction);
+  }
+
+  // Remove an ext from UI
+  if (moduleAction)
+  {
+    moduleAction->removeExtension(title);
+  }
+
+  // Update windows (in particular, Info panel)
+  updateWindows();
+}
+
+/*!On show extension info action.*/
+void LightApp_Application::onShowExtInfo()
+{
+  // Show dialog with information about loaded salome extensions
+  LightApp_ExtInfoDlg dlg(desktop());
+  dlg.exec();
 }
 
 /*!Default module activation.*/
@@ -1011,6 +1354,11 @@ void LightApp_Application::onNewWindow()
   case NewPyViewerId:
     type = PyViewer_Viewer::Type();
     break;
+#endif
+#ifndef DISABLE_PV3DVIEWER
+  case NewPV3DViewId:
+    type = PV3DViewer_ViewModel::Type();
+    break;
 #endif
   }
 
@@ -1038,16 +1386,16 @@ void LightApp_Application::onNewDoc()
 void LightApp_Application::onOpenDoc()
 {
   SUIT_Study* study = activeStudy();
-  
+
   if ( !checkExistingDoc( false ) )
     return;
-  
+
   QString aName = getFileName( true, QString(), getFileFilter( true ), QString(), 0 );
   if ( aName.isNull() ) //Cancel
     return;
-  
+
   onOpenDoc( aName );
-  
+
   if ( !study ) // new study will be create in THIS application
   {
     updateWindows();
@@ -1185,6 +1533,12 @@ void LightApp_Application::updateCommandsStatus()
   if( a )
     a->setEnabled( activeStudy() );
 #endif
+
+#ifndef DISABLE_PV3DVIEWER
+  a = action( NewPV3DViewId );
+  if( a )
+    a->setEnabled( activeStudy() );
+#endif
 }
 
 /*!
@@ -1216,7 +1570,7 @@ protected:
     {
       // normalize path
       if ( myUrl.startsWith( "file://", Qt::CaseInsensitive ) )
-       myUrl = myUrl.remove( 0, QString( "file://" ).count() );
+        myUrl = myUrl.remove( 0, QString( "file://" ).count() );
       // For the external browser we always specify 'file://' protocol,
       // because some web browsers (e.g. Mozilla Firefox) can't open local file without protocol.
       myUrl = myUrl.prepend( "file://" );
@@ -1234,7 +1588,7 @@ protected:
 #else
       QString cmdLine = QString( "%1 %2 \"%3\"" ).arg( myBrowser, myParameters, myUrl );
       // remove LD_LIBRARY_PATH from the environement before starting launcher to avoid bad interactions.
-      // (especially in the case of universal binaries) 
+      // (especially in the case of universal binaries)
       env.remove("LD_LIBRARY_PATH");
 #endif
       QProcess* proc = new QProcess();
@@ -1331,7 +1685,7 @@ void LightApp_Application::onHelpContextModule( const QString& component,
       QString rootDir = Qtx::getenv( (component + "_ROOT_DIR").toLatin1().constData() );
       if ( !rootDir.isEmpty() )
       {
-       path = (QStringList() << rootDir << "share" << "doc" << "salome" << "gui" << component << url).join( QDir::separator() );
+        path = (QStringList() << rootDir << "share" << "doc" << "salome" << "gui" << component << url).join( QDir::separator() );
       }
     }
   }
@@ -1750,6 +2104,32 @@ SUIT_ViewManager* LightApp_Application::createViewManager( const QString& vmType
   }
 #endif
 
+#ifndef DISABLE_PV3DVIEWER
+# ifndef DISABLE_SALOMEOBJECT
+  if ( vmType == SPV3D_ViewModel::Type() )
+# else
+  if ( vmType == PV3DViewer_ViewModel::Type() )
+# endif
+  {
+    viewMgr = new SPV3D_ViewManager( activeStudy(), desktop() );
+    SPV3D_ViewModel* vm = dynamic_cast<SPV3D_ViewModel*>( viewMgr->getViewModel() );
+    if ( vm )
+    {
+      // vm->setBackground(...); //NYI
+      // vm->...
+
+      new LightApp_PV3DSelector( vm, mySelMgr );
+    }
+#else
+    viewMgr = new PV3DViewer_ViewManager( activeStudy(), desktop() );
+    PV3DViewer_ViewModel* vm = dynamic_cast<PV3DViewer_ViewModel*>( viewMgr->getViewModel() );
+    if ( vm )
+    {
+      // vm->setBackground(...); //NYI
+    }
+#endif
+  }
+
   if ( !viewMgr )
     return 0;
 
@@ -2256,8 +2636,6 @@ LightApp_Preferences* LightApp_Application::preferences( const bool crt ) const
   if ( !crt )
     return myPrefs;
 
-  SUIT_ResourceMgr* resMgr = resourceMgr();
-
   QList<SUIT_Application*> appList = SUIT_Session::session()->applications();
   for ( QList<SUIT_Application*>::iterator appIt = appList.begin(); appIt != appList.end(); ++appIt )
   {
@@ -2269,10 +2647,6 @@ LightApp_Preferences* LightApp_Application::preferences( const bool crt ) const
     QStringList names;
     app->modules( names, false );
 
-    // icons of modules
-    QMap<QString, QString> icons;
-    app->moduleIconNames( icons );
-
     // step 1: iterate through list of all available modules
     // and add empty preferences page
     for ( QStringList::const_iterator it = names.begin(); it != names.end(); ++it )
@@ -2280,9 +2654,7 @@ LightApp_Preferences* LightApp_Application::preferences( const bool crt ) const
       if ( !_prefs_->hasModule( *it ) ) // prevent possible duplications
       {
         int modId = _prefs_->addPreference( *it ); // add empty page
-        if ( icons.contains( *it ) )               // set icon
-          _prefs_->setItemIcon( modId, Qtx::scaleIcon( resMgr->loadPixmap( moduleName( *it ),
-                                                                           icons[*it], false ), 20 ) );
+        _prefs_->setItemIcon( modId, moduleIcon( *it, 20 ) ); // scale icon to 20x20 pix
       }
     }
 
@@ -2330,6 +2702,12 @@ void LightApp_Application::moduleAdded( CAM_Module* mod )
   }
 }
 
+void LightApp_Application::moduleDeactivated( CAM_Module* /*mod*/ )
+{
+  if ( infoPanel() )
+    infoPanel()->clear();
+}
+
 void LightApp_Application::emptyPreferences( const QString& modName )
 {
   QtxPreferenceItem* item = myPrefs->findItem( modName, true );
@@ -2764,6 +3142,41 @@ void LightApp_Application::createPreferences( LightApp_Preferences* pref )
   pref->setItemProperty( "step", 0.1, light_dz );
   // ... "Light source" group <<end>>
 
+  // ... "View cube" group <<start>>
+  int occViewCubeGroup = pref->addPreference( tr( "PREF_GROUP_VIEWCUBE" ), occGroup );
+  pref->setItemProperty( "columns", 2, occViewCubeGroup );
+  // .... -> show view cube on viewer start
+  pref->addPreference( tr( "PREF_VIEWCUBE_SHOW" ), occViewCubeGroup,
+               LightApp_Preferences::Bool, "OCCViewer", "viewcube_show" );
+  // .... -> view cube duration of animation (sec)
+  int viewcube_dur = pref->addPreference( tr( "PREF_VIEWCUBE_DURATION" ), occViewCubeGroup,
+               LightApp_Preferences::DblSpin, "OCCViewer", "viewcube_duration" );
+  pref->setItemProperty( "min", 0.1, viewcube_dur );
+  pref->setItemProperty( "max", 10.0, viewcube_dur );
+  pref->setItemProperty( "step", 0.1, viewcube_dur );
+  // .... -> show view cube axes
+  pref->addPreference( tr( "PREF_VIEWCUBE_AXES" ), occViewCubeGroup,
+               LightApp_Preferences::Bool, "OCCViewer", "viewcube_axes" );
+  // ... "View cube" group <<end>>
+
+  // ... "View cube default (OCCT) attributes" group <<start>>
+  int occViewCubeAttrsGroup = pref->addPreference( tr( "PREF_VIEWCUBE_CUSTOM" ), occGroup,
+               LightApp_Preferences::Auto, "OCCViewer", "viewcube_custom" );
+  pref->setItemProperty( "columns", 2, occViewCubeAttrsGroup );
+  // .... -> box color
+  pref->addPreference( tr( "PREF_VIEWCUBE_COLOR" ), occViewCubeAttrsGroup,
+               LightApp_Preferences::Color, "OCCViewer", "viewcube_color" );
+  // .... -> view cube size
+  int viewcube_size = pref->addPreference( tr( "PREF_VIEWCUBE_SIZE" ), occViewCubeAttrsGroup,
+               LightApp_Preferences::DblSpin, "OCCViewer", "viewcube_size" );
+  pref->setItemProperty( "min",  30.0, viewcube_size );
+  pref->setItemProperty( "max", 100.0, viewcube_size );
+  pref->setItemProperty( "step", 10.0, viewcube_size );
+  // .... -> text color
+  pref->addPreference( tr( "PREF_VIEWCUBE_TEXTCOLOR" ), occViewCubeAttrsGroup,
+               LightApp_Preferences::Color, "OCCViewer", "viewcube_text_color" );
+  // ... "View cube" group <<end>>
+
   // ... -> empty frame (for layout) <<start>>
   int occGen = pref->addPreference( "", occGroup, LightApp_Preferences::Frame );
   pref->setItemProperty( "margin",  0, occGen );
@@ -2858,7 +3271,7 @@ void LightApp_Application::createPreferences( LightApp_Preferences* pref )
   int vtkSelectionGroup = pref->addPreference( tr( "PREF_GROUP_SELECTION" ), vtkGroup );
   pref->setItemProperty( "columns", 2, vtkSelectionGroup );
   // .... -> preselection
-  int vtkPreselection = pref->addPreference( tr( "PREF_PRESELECTION" ),  vtkSelectionGroup, 
+  int vtkPreselection = pref->addPreference( tr( "PREF_PRESELECTION" ),  vtkSelectionGroup,
                                              LightApp_Preferences::Selector, "VTKViewer", "preselection" );
   aValuesList.clear();
   anIndicesList.clear();
@@ -2883,7 +3296,7 @@ void LightApp_Application::createPreferences( LightApp_Preferences* pref )
   int spacemousePref2 = pref->addPreference( tr( "PREF_SPACEMOUSE_FUNC_2" ), vtkSM,
                                              LightApp_Preferences::Selector, "VTKViewer",
                                              "spacemouse_func2_btn" );
-  // .... -> dominant / combined switch  
+  // .... -> dominant / combined switch
   int spacemousePref3 = pref->addPreference( tr( "PREF_SPACEMOUSE_FUNC_3" ), vtkSM,
                                              LightApp_Preferences::Selector, "VTKViewer",
                                              "spacemouse_func5_btn" ); //
@@ -3549,6 +3962,24 @@ void LightApp_Application::preferencesChanged( const QString& sec, const QString
   }
 #endif
 
+#ifndef DISABLE_OCCVIEWER
+  if ( sec == QString( "OCCViewer" ) && param.contains( "viewcube" ) )
+  {
+    QList<SUIT_ViewManager*> lst;
+    viewManagers( OCCViewer_Viewer::Type(), lst );
+    QListIterator<SUIT_ViewManager*> it( lst );
+    while ( it.hasNext() )
+    {
+      SUIT_ViewModel* vm = it.next()->getViewModel();
+      if ( !vm || !vm->inherits( "OCCViewer_Viewer" ) )
+        continue;
+
+      OCCViewer_Viewer* occVM = (OCCViewer_Viewer*)vm;
+      occVM->setViewCubeParamsFromPreferences();
+    }
+  }
+#endif
+
   if ( sec == QString( "3DViewer" ) && param == QString( "zooming_mode" ) )
   {
     int mode = resMgr->integerValue( "3DViewer", "zooming_mode", 0 );
@@ -4035,22 +4466,7 @@ void LightApp_Application::afterCloseDoc()
 */
 void LightApp_Application::updateModuleActions()
 {
-  QString modName;
-  if ( activeModule() )
-    modName = activeModule()->moduleName();
-
-  LightApp_ModuleAction* moduleAction =
-    qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
-  if ( moduleAction )
-    moduleAction->setActiveModule( modName );
-}
-
-void LightApp_Application::removeModuleAction( const QString& modName )
-{
-  LightApp_ModuleAction* moduleAction =
-    qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
-  if ( moduleAction )
-    moduleAction->removeModule( modName );
+  emit moduleActivated( activeModule() ? activeModule()->moduleName() : QString() );
 }
 
 bool LightApp_Application::checkModule( const QString& title )
@@ -4245,7 +4661,7 @@ void LightApp_Application::loadDockWindowsState()
   aResMgr->value( "windows_visibility", modName, aDefaultVisibility );
   bool hasDefaultVisibility = !aDefaultVisibility.isEmpty();
   aResMgr->setWorkingMode( prevMode );
-  
+
   if( !storeWin && !storeTb && aDefaultState.isEmpty() && !hasDefaultVisibility)
     return;
 
@@ -4270,13 +4686,13 @@ void LightApp_Application::loadDockWindowsState()
 
   QMap<QString, bool> *tbMap = 0;
   QMap<QString, bool> *dwMap = 0;
-  
+
   QMap<QString, bool> userTbMap, userDwMap;
   dockWindowsState( myWinVis[modName], userTbMap, userDwMap );
 
   QMap<QString, bool> defaultTbMap, defaultDwMap;
   if(hasDefaultVisibility) {
-    dockWindowsState( aDefaultVisibility, defaultTbMap, defaultDwMap);    
+    dockWindowsState( aDefaultVisibility, defaultTbMap, defaultDwMap);
   }
 
   if(storeTb) {
@@ -4298,9 +4714,9 @@ void LightApp_Application::loadDockWindowsState()
   if(tbMap) {
     QList<QToolBar*> tbList = findToolBars();
     for ( QList<QToolBar*>::iterator tit = tbList.begin(); tit != tbList.end(); ++tit )
-      { 
+      {
         QToolBar* tb = *tit;
-        if ( tbMap->contains( tb->objectName() ) ) {      
+        if ( tbMap->contains( tb->objectName() ) ) {
           tb->setVisible( (*tbMap)[tb->objectName()] );
         }
       }
@@ -4311,11 +4727,11 @@ void LightApp_Application::loadDockWindowsState()
     for ( QList<QDockWidget*>::iterator dit = dwList.begin(); dit != dwList.end(); ++dit )
       {
         QDockWidget* dw = *dit;
-        
+
         QObject* po = Qtx::findParent( dw, "QMainWindow" );
         if ( po != desktop() )
           continue;
-        
+
         if ( dwMap->contains( dw->objectName() ) )
           dw->setVisible( (*dwMap)[dw->objectName()] );
       }
@@ -4442,34 +4858,18 @@ void LightApp_Application::dockWindowsState( const QByteArray& arr, QMap<QString
   }
 }
 
-/*!
-  Adds icon names for modules
-*/
-void LightApp_Application::moduleIconNames( QMap<QString, QString>& iconMap ) const
+QPixmap LightApp_Application::moduleIcon( const QString& moduleTitle, const int size ) const
 {
-  iconMap.clear();
-
-  SUIT_ResourceMgr* resMgr = resourceMgr();
-  if ( !resMgr )
-    return;
-
-  QStringList modList;
-  modules( modList, false );
-
-  for ( QStringList::const_iterator it = modList.begin(); it != modList.end(); ++it )
+  QPixmap icon;
+  if ( resourceMgr() )
   {
-    QString modName = *it;
-    QString modIntr = moduleName( modName );
-    QString modIcon = resMgr->stringValue( modIntr, "icon", QString() );
-
-    if ( modIcon.isEmpty() )
-      continue;
-
-    if ( SUIT_Tools::extension( modIcon ).isEmpty() )
-      modIcon += QString( ".png" );
-
-    iconMap.insert( modName, modIcon );
+    QPixmap defaultIcon = resourceMgr()->loadPixmap( "LightApp", tr( "APP_MODULE_ICO" ), QPixmap( imageEmptyIcon ) );
+    QString iconName = resourceMgr()->stringValue( moduleName( moduleTitle ), "icon", QString() );
+    icon = resourceMgr()->loadPixmap( moduleName( moduleTitle ), iconName, defaultIcon );
+    if ( size > 0 )
+      icon = Qtx::scaleIcon( icon, size );
   }
+  return icon;
 }
 
 /*!
@@ -4853,6 +5253,9 @@ QStringList LightApp_Application::viewManagersTypes() const
  #else
   aTypesList<<VTKViewer_Viewer::Type();
  #endif
+#endif
+#ifndef DISABLE_PV3DVIEWER
+  aTypesList<<PV3DViewer_ViewModel::Type();
 #endif
   return aTypesList;
 }
@@ -5039,7 +5442,7 @@ void LightApp_Application::onDesktopMessage( const QString& message )
   }
   else if ( message.toLower().startsWith("register_module_in_study" ) ) {
     QString moduleName = message.split( sectionSeparator ).last();
-    // Check name of current activating module name in order to avoid ciclik 
+    // Check name of current activating module name in order to avoid ciclik
     // call because of messages
     if (!property("activateModule").toBool()) {
       CAM_Module* mod = module(moduleName);
@@ -5102,7 +5505,7 @@ void LightApp_Application::onInfoPanelShown()
 }
 
 /*!
-  Internal method. 
+  Internal method.
   Returns all top level toolbars.
   Note : Result list contains only main window toolbars, not including toolbars from viewers.
 */
@@ -5391,17 +5794,17 @@ bool LightApp_Application::checkExistingDoc( bool closeExistingDoc )
   if( activeStudy() ) {
     int answer = !activeStudy()->isModified() ? 1 :
                  SUIT_MessageBox::question( desktop(),
-                                           tr( "APPCLOSE_CAPTION" ),
-                                           tr( "STUDYCLOSE_DESCRIPTION" ),
-                                           tr( "APPCLOSE_SAVE" ),
-                                           tr( "APPCLOSE_CLOSE" ),
-                                           tr( "APPCLOSE_CANCEL" ), 0 );
+                                            tr( "APPCLOSE_CAPTION" ),
+                                            tr( "STUDYCLOSE_DESCRIPTION" ),
+                                            tr( "APPCLOSE_SAVE" ),
+                                            tr( "APPCLOSE_CLOSE" ),
+                                            tr( "APPCLOSE_CANCEL" ), 0 );
     if(answer == 0) {
       if ( activeStudy()->isSaved() ) {
         onSaveDoc();
-               if (closeExistingDoc) {
-                       closeDoc(false);
-               }
+                if (closeExistingDoc) {
+                        closeDoc(false);
+                }
       } else if ( onSaveAsDoc() ) {
          if (closeExistingDoc) {
            if( !closeDoc( false ) ) {
@@ -5414,7 +5817,7 @@ bool LightApp_Application::checkExistingDoc( bool closeExistingDoc )
     }
     else if( answer == 1 ) {
       if (closeExistingDoc) {
-       closeDoc( false );
+        closeDoc( false );
       }
     } else if( answer == 2 ) {
       result = false;
@@ -5441,3 +5844,98 @@ PyConsole_Interp* LightApp_Application::createPyInterp()
 }
 
 #endif // DISABLE_PYCONSOLE
+
+void LightApp_Application::createHelpItems( const QString& modTitle )
+{
+  if ( modTitle.isEmpty() )
+    return;
+
+  QString userGuide = "User's Guide";
+  QString devGuide = "Developer's Guide";
+
+  int helpMenu = createMenu( tr( "MEN_DESK_HELP" ), -1, -1, 1000 );
+
+  createMenu( userGuide, helpMenu, -1, 5 );
+  createMenu( devGuide, helpMenu, -1, 5 );
+
+  IMap <QString, QString> helpData;                                 // list of help files for the module
+  QString helpSubMenu;                                              // help submenu name (empty if not needed)
+  QString modName = moduleName( modTitle );                         // module name
+  if ( modName.isEmpty() ) modName = modTitle;                      // for KERNEL and GUI
+  QString rootDir = QString( "%1_ROOT_DIR" ).arg( modName );        // module root dir env variable
+  QString modDir = Qtx::getenv( rootDir.toUtf8().constData() );     // module root dir path
+  QString docSection;
+  if ( resourceMgr()->hasValue( modName, "documentation" ) )
+    docSection = resourceMgr()->stringValue( modName, "documentation" );
+  else if ( resourceMgr()->hasSection( modName + "_documentation" ) )
+    docSection = modName + "_documentation";
+  if ( !docSection.isEmpty() )
+  {
+    helpSubMenu = resourceMgr()->stringValue( docSection, "sub_menu", "" );
+    if ( helpSubMenu.contains( "%1" ) )
+      helpSubMenu = helpSubMenu.arg( modTitle );
+    foreach( QString paramName, resourceMgr()->parameters( docSection ) )
+    {
+      QString key = paramName.contains( "%1" ) ? paramName.arg( modTitle ) : paramName;
+      QString helpItem = getHelpItem( docSection, paramName );
+      if ( !helpItem.isEmpty() )
+        helpData.insert( key, helpItem );
+    }
+  }
+
+  if ( helpData.isEmpty() && !modDir.isEmpty() )
+  {
+    QStringList idxLst = QStringList() << modDir << "share" << "doc" << "salome" << "gui" << modName << "index.html";
+    QString indexFile = idxLst.join( QDir::separator() );          // index file
+    if ( QFile::exists( indexFile ) )
+      helpData.insert( tr( "%1 module Users's Guide" ).arg( modTitle ), indexFile );
+  }
+
+  IMapConstIterator<QString, QString > fileIt;
+  for ( fileIt = helpData.begin(); fileIt != helpData.end(); fileIt++ )
+  {
+    QString helpItemPath = fileIt.key();
+    // remove all '//' occurances
+    while ( helpItemPath.contains( "//" ) )
+      helpItemPath.replace( "//", "" );
+    // obtain submenus hierarchy if given
+    QStringList smenus = helpItemPath.split( "/" );
+    helpItemPath = smenus.takeLast();
+    // workaround for User's Guide and Developer's Guide to avoid having single item in module's submenu.
+    if ( helpItemPath == userGuide || helpItemPath == devGuide )
+    {
+      QString menuPath = smenus.join( "/" );
+      QStringList allKeys = helpData.keys();
+      QStringList total = allKeys.filter( QRegExp( QString( "^%1" ).arg( menuPath ) ) );
+      if ( total.count() == 1 && smenus.count() > 0 )
+        helpItemPath = smenus.takeLast();
+    }
+    QPixmap helpIcon = fileIt.value().startsWith( "http", Qt::CaseInsensitive ) ?
+      resourceMgr()->loadPixmap( "STD", tr( "ICON_WWW" ), false ) :
+      resourceMgr()->loadPixmap( "STD", tr( "ICON_HELP" ), false );
+    QAction* a = createAction( -1, helpItemPath, helpIcon, helpItemPath, helpItemPath,
+                               0, desktop(), false, this, SLOT( onHelpContentsModule() ) );
+    a->setData( fileIt.value() );
+    if ( !helpSubMenu.isEmpty() )
+      smenus.prepend( helpSubMenu );
+    // create sub-menus hierarchy
+    int menuId = helpMenu;
+    foreach ( QString subMenu, smenus )
+      menuId = createMenu( subMenu, menuId, -1, 5 );
+    createMenu( a, menuId, -1, ( menuId != helpMenu && ( helpItemPath == userGuide || helpItemPath == devGuide ) ) ? 0 : 5 );
+    if ( !myHelpItems.contains( modName ) )
+      myHelpItems[modName] = IdList();
+    myHelpItems[modName].append( actionId( a ) );
+  }
+}
+
+void LightApp_Application::removeHelpItems( const QString& modTitle )
+{
+  QString modName = moduleName( modTitle );
+  if ( myHelpItems.contains( modName ) )
+  {
+    foreach( int id, myHelpItems[modName] )
+      setMenuShown( id, false );
+    myHelpItems.remove( modName );
+  }
+}