Salome HOME
First integration of Salome On Demand
authorKonstantin Leontev <konstantin.leontev@opencascade.com>
Thu, 15 Dec 2022 16:08:36 +0000 (19:08 +0300)
committerAnthony Geay <anthony.geay@edf.fr>
Tue, 18 Apr 2023 14:14:32 +0000 (16:14 +0200)
15 files changed:
CMakeLists.txt
src/CAM/CAM_Application.cxx
src/CAM/CMakeLists.txt
src/LightApp/CMakeLists.txt
src/LightApp/LightApp_Application.cxx
src/LightApp/LightApp_Application.h
src/LightApp/LightApp_ExtInfoDlg.cxx [new file with mode: 0644]
src/LightApp/LightApp_ExtInfoDlg.h [new file with mode: 0644]
src/LightApp/LightApp_ModuleAction.cxx
src/LightApp/LightApp_ModuleAction.h
src/LightApp/resources/LightApp_images.ts
src/LightApp/resources/LightApp_msg_en.ts
src/LightApp/resources/LightApp_msg_fr.ts
src/LightApp/resources/LightApp_msg_ja.ts
src/LightApp/resources/icon_info_module.png [new file with mode: 0644]

index 5812423287aed6b136b0dfe9389431a99b645fd4..1683205037cdf536dbabee9b4ad8bcd6a18f07ed 100644 (file)
@@ -63,7 +63,7 @@ SET(BUILD_SHARED_LIBS TRUE)
 OPTION(SALOME_BUILD_DOC "Generate SALOME GUI documentation" ON)
 OPTION(SALOME_BUILD_TESTS "Build SALOME tests" ON)
 OPTION(SALOME_GUI_USE_OBSERVERS "Use study observers in GUI (advanced)" ON)
-OPTION(SALOME_ON_DEMAND "Activate SALOME on demand feature" OFF)
+OPTION(SALOME_ON_DEMAND "Activate SALOME on demand feature" ON)
 CMAKE_DEPENDENT_OPTION(SALOME_GUI_BUILD_FRENCH_DOC "Generate SALOME GUI French documentation" OFF
                        "SALOME_BUILD_DOC" OFF)
 
@@ -112,7 +112,7 @@ IF(NOT SALOME_LIGHT_ONLY)
   FIND_PACKAGE(SalomeOmniORBPy REQUIRED)
 ELSE()
   ADD_DEFINITIONS("-DGUI_DISABLE_CORBA")
-ENDIF() 
+ENDIF()
 IF(SALOME_BUILD_TESTS)
   ENABLE_TESTING()
   FIND_PACKAGE(SalomeCppUnit)
@@ -202,8 +202,8 @@ ENDIF()
 
 # - VTK viewer: VTK
 IF(SALOME_USE_VTKVIEWER)
-  # Required components are listed in the FindSalomeVTK.cmake file: 
-  FIND_PACKAGE(SalomeVTK) 
+  # Required components are listed in the FindSalomeVTK.cmake file:
+  FIND_PACKAGE(SalomeVTK)
   SALOME_LOG_OPTIONAL_PACKAGE(VTK SALOME_USE_VTKVIEWER)
   # VSR: TODO: since ParaView 5.8 the following is not needed, as OPENGL2 is hardcoded: to be removed (everywhere)
   ADD_DEFINITIONS("-DVTK_OPENGL2")
@@ -263,18 +263,18 @@ SET(SALOME_INSTALL_BINS "${SALOME_INSTALL_BINS}" CACHE PATH "Install path: SALOM
 SET(SALOME_INSTALL_LIBS "${SALOME_INSTALL_LIBS}" CACHE PATH "Install path: SALOME libs")
 SET(SALOME_INSTALL_IDLS "${SALOME_INSTALL_IDLS}" CACHE PATH "Install path: SALOME IDL files")
 SET(SALOME_INSTALL_HEADERS "${SALOME_INSTALL_HEADERS}" CACHE PATH "Install path: SALOME headers")
-SET(SALOME_INSTALL_SCRIPT_SCRIPTS "${SALOME_INSTALL_SCRIPT_SCRIPTS}" CACHE PATH 
+SET(SALOME_INSTALL_SCRIPT_SCRIPTS "${SALOME_INSTALL_SCRIPT_SCRIPTS}" CACHE PATH
    "Install path: SALOME scripts")
-SET(SALOME_INSTALL_SCRIPT_DATA "${SALOME_INSTALL_SCRIPT_DATA}" CACHE PATH 
+SET(SALOME_INSTALL_SCRIPT_DATA "${SALOME_INSTALL_SCRIPT_DATA}" CACHE PATH
    "Install path: SALOME script data")
-SET(SALOME_INSTALL_SCRIPT_PYTHON "${SALOME_INSTALL_SCRIPT_PYTHON}" CACHE PATH 
+SET(SALOME_INSTALL_SCRIPT_PYTHON "${SALOME_INSTALL_SCRIPT_PYTHON}" CACHE PATH
    "Install path: SALOME Python scripts")
 SET(SALOME_INSTALL_PYTHON "${SALOME_INSTALL_PYTHON}" CACHE PATH "Install path: SALOME Python stuff")
-SET(SALOME_INSTALL_PYTHON_SHARED "${SALOME_INSTALL_PYTHON_SHARED}" CACHE PATH 
+SET(SALOME_INSTALL_PYTHON_SHARED "${SALOME_INSTALL_PYTHON_SHARED}" CACHE PATH
    "Install path: SALOME Python shared modules")
 SET(SALOME_INSTALL_CMAKE "${SALOME_INSTALL_CMAKE}" CACHE PATH "Install path: SALOME CMake files")
-SET(SALOME_INSTALL_CMAKE_LOCAL "${SALOME_INSTALL_CMAKE_LOCAL}" CACHE PATH 
-    "Install path: local SALOME CMake files") 
+SET(SALOME_INSTALL_CMAKE_LOCAL "${SALOME_INSTALL_CMAKE_LOCAL}" CACHE PATH
+    "Install path: local SALOME CMake files")
 SET(SALOME_INSTALL_AMCONFIG_LOCAL "${SALOME_INSTALL_AMCONFIG_LOCAL}" CACHE PATH
   "Install path: local SALOME config files (obsolete, to be removed)")
 
@@ -282,11 +282,11 @@ SET(SALOME_INSTALL_RES "${SALOME_INSTALL_RES}" CACHE PATH "Install path: SALOME
 SET(SALOME_INSTALL_DOC "${SALOME_INSTALL_DOC}" CACHE PATH "Install path: SALOME documentation")
 
 # Specific to GUI:
-SET(SALOME_GUI_INSTALL_RES_DATA "${SALOME_INSTALL_RES}/gui" CACHE PATH 
+SET(SALOME_GUI_INSTALL_RES_DATA "${SALOME_INSTALL_RES}/gui" CACHE PATH
     "Install path: SALOME GUI specific data")
-SET(SALOME_GUI_INSTALL_RES_SCRIPTS "${SALOME_INSTALL_RES}/gui" CACHE PATH 
+SET(SALOME_GUI_INSTALL_RES_SCRIPTS "${SALOME_INSTALL_RES}/gui" CACHE PATH
     "Install path: SALOME GUI scripts")
-SET(SALOME_GUI_INSTALL_PLUGINS share/salome/plugins/gui CACHE PATH 
+SET(SALOME_GUI_INSTALL_PLUGINS share/salome/plugins/gui CACHE PATH
     "Install path: SALOME GUI plugins")
 
 MARK_AS_ADVANCED(SALOME_INSTALL_BINS SALOME_INSTALL_LIBS SALOME_INSTALL_IDLS SALOME_INSTALL_HEADERS)
@@ -301,9 +301,9 @@ SALOME_ACCUMULATE_ENVIRONMENT(PYTHONPATH NOCHECK ${CMAKE_INSTALL_PREFIX}/${SALOM
                                                  ${CMAKE_INSTALL_PREFIX}/${SALOME_INSTALL_PYTHON}
                                                  ${CMAKE_INSTALL_PREFIX}/${SALOME_INSTALL_LIBS}
                                                  ${CMAKE_INSTALL_PREFIX}/${SALOME_INSTALL_PYTHON_SHARED})
-SALOME_ACCUMULATE_ENVIRONMENT(LD_LIBRARY_PATH NOCHECK ${CMAKE_INSTALL_PREFIX}/${SALOME_INSTALL_LIBS}) 
-                                              
-# Sources 
+SALOME_ACCUMULATE_ENVIRONMENT(LD_LIBRARY_PATH NOCHECK ${CMAKE_INSTALL_PREFIX}/${SALOME_INSTALL_LIBS})
+
+# Sources
 # ========
 IF(NOT SALOME_LIGHT_ONLY)
   ADD_SUBDIRECTORY(idl)
@@ -328,45 +328,45 @@ INCLUDE(CMakePackageConfigHelpers)
 
 # List of targets in this project we want to make visible to the rest of the world.
 # They all have to be INSTALL'd with the option "EXPORT ${PROJECT_NAME}TargetGroup"
-SET(_${PROJECT_NAME}_exposed_targets 
+SET(_${PROJECT_NAME}_exposed_targets
     CAM CASCatch DDS Event LightApp LogWindow ObjBrowser
     QDS qtx SalomePrs SalomeStyle std SUITApp suit ViewerTools ViewerData
     ImageComposer
 )
 
 IF(SALOME_USE_OCCVIEWER OR SALOME_USE_VTKVIEWER OR SALOME_USE_GLVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        OpenGLUtils)
 ENDIF(SALOME_USE_OCCVIEWER OR SALOME_USE_VTKVIEWER OR SALOME_USE_GLVIEWER)
 
 # SALOME object specific targets:
 IF(SALOME_USE_SALOMEOBJECT)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        SalomeObject)
 ENDIF(SALOME_USE_SALOMEOBJECT)
 
 # GL specific targets:
 IF(SALOME_USE_GLVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        GLViewer)
 ENDIF(SALOME_USE_GLVIEWER)
 
 # VTK specific targets:
 IF(SALOME_USE_VTKVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        VTKViewer )
   IF(SALOME_USE_SALOMEOBJECT)
-    LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+    LIST(APPEND _${PROJECT_NAME}_exposed_targets
          SVTK)
   ENDIF(SALOME_USE_SALOMEOBJECT)
 ENDIF(SALOME_USE_VTKVIEWER)
 
 # OCC specific targets:
 IF(SALOME_USE_OCCVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        OCCViewer)
   IF(SALOME_USE_SALOMEOBJECT)
-    LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+    LIST(APPEND _${PROJECT_NAME}_exposed_targets
          SOCC)
   ENDIF(SALOME_USE_SALOMEOBJECT)
 ENDIF(SALOME_USE_OCCVIEWER)
@@ -383,29 +383,29 @@ ENDIF(SALOME_USE_PV3DVIEWER)
 
 # Plot2d specific targets:
 IF(SALOME_USE_PLOT2DVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        Plot2d)
   IF(SALOME_USE_SALOMEOBJECT)
-    LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+    LIST(APPEND _${PROJECT_NAME}_exposed_targets
          SPlot2d)
   ENDIF(SALOME_USE_SALOMEOBJECT)
 ENDIF(SALOME_USE_PLOT2DVIEWER)
 
 # Qx specific targets:
 IF(SALOME_USE_QXGRAPHVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        QxScene)
 ENDIF(SALOME_USE_QXGRAPHVIEWER)
 
 # GraphicsView specific targets:
 IF(SALOME_USE_GRAPHICSVIEW)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        GraphicsView)
 ENDIF(SALOME_USE_GRAPHICSVIEW)
 
 # PyEditor/Viewer specific targets:
 IF(SALOME_USE_PYVIEWER)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        PyEditor PyViewer)
 ENDIF(SALOME_USE_PYVIEWER)
 
@@ -416,7 +416,7 @@ ENDIF()
 
 # Python-based packages specific targets:
 IF(SALOME_USE_PYCONSOLE)
-  LIST(APPEND _${PROJECT_NAME}_exposed_targets 
+  LIST(APPEND _${PROJECT_NAME}_exposed_targets
        PyInterp PyConsole SalomePyQtGUILight)
   IF(SALOME_USE_PLOT2DVIEWER)
     LIST(APPEND _${PROJECT_NAME}_exposed_targets SalomePyQt)
@@ -453,34 +453,34 @@ SET(OPENGL_ROOT_DIR "${OPENGL_ROOT_DIR}")
 SET(PARAVIEW_ROOT_DIR "${PARAVIEW_ROOT_DIR}")
 SET(VTK_ROOT_DIR "${VTK_ROOT_DIR}")
 SET(QWT_ROOT_DIR "${QWT_ROOT_DIR}")
+
 SET(CONF_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}/include" "${PROJECT_BINARY_DIR}/include")
 
 # Build variables that will be expanded when configuring Salome<MODULE>Config.cmake:
 SALOME_CONFIGURE_PREPARE(OpenCASCADE OpenGL Qt5 PyQt5 Qwt SIP ParaView VTK)
 
-CONFIGURE_PACKAGE_CONFIG_FILE(${PROJECT_NAME}Config.cmake.in 
+CONFIGURE_PACKAGE_CONFIG_FILE(${PROJECT_NAME}Config.cmake.in
     ${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
     INSTALL_DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}"
     PATH_VARS CONF_INCLUDE_DIRS SALOME_INSTALL_CMAKE_LOCAL CMAKE_INSTALL_PREFIX
-       KERNEL_ROOT_DIR SIP_ROOT_DIR QT_ROOT_DIR PYQT_ROOT_DIR OPENCASCADE_ROOT_DIR 
+       KERNEL_ROOT_DIR SIP_ROOT_DIR QT_ROOT_DIR PYQT_ROOT_DIR OPENCASCADE_ROOT_DIR
        OPENGL_ROOT_DIR PARAVIEW_ROOT_DIR VTK_ROOT_DIR QWT_ROOT_DIR)
 
 #   - in the install tree (VSR 16/08/2013: TEMPORARILY COMMENT THIS - TO REMOVE?):
-#       Get the relative path of the include directory so 
+#       Get the relative path of the include directory so
 #       we can register it in the generated configuration files:
 #SET(CONF_INCLUDE_DIRS "${CMAKE_INSTALL_PREFIX}/${INSTALL_INCLUDE_DIR}")
-#CONFIGURE_PACKAGE_CONFIG_FILE(adm_local/cmake_files/${PROJECT_NAME}Config.cmake.in 
+#CONFIGURE_PACKAGE_CONFIG_FILE(adm_local/cmake_files/${PROJECT_NAME}Config.cmake.in
 #    ${PROJECT_BINARY_DIR}/to_install/${PROJECT_NAME}Config.cmake
 #    INSTALL_DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}"
-#    PATH_VARS CONF_INCLUDE_DIRS SALOME_INSTALL_CMAKE_LOCAL CMAKE_INSTALL_PREFIX 
-#       KERNEL_ROOT_DIR SIP_ROOT_DIR QT_ROOT_DIR PYQT_ROOT_DIR OPENCASCADE_ROOT_DIR 
+#    PATH_VARS CONF_INCLUDE_DIRS SALOME_INSTALL_CMAKE_LOCAL CMAKE_INSTALL_PREFIX
+#       KERNEL_ROOT_DIR SIP_ROOT_DIR QT_ROOT_DIR PYQT_ROOT_DIR OPENCASCADE_ROOT_DIR
 #       OPENGL_ROOT_DIR PARAVIEW_ROOT_DIR VTK_ROOT_DIR QWT_ROOT_DIR)
 
 WRITE_BASIC_PACKAGE_VERSION_FILE(${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
     VERSION ${${PROJECT_NAME_UC}_VERSION}
     COMPATIBILITY AnyNewerVersion)
-  
+
 # Install the CMake configuration files:
 INSTALL(FILES
   "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
@@ -488,5 +488,5 @@ INSTALL(FILES
   DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}")
 
 # Install the export set for use with the install-tree
-INSTALL(EXPORT ${PROJECT_NAME}TargetGroup DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}" 
+INSTALL(EXPORT ${PROJECT_NAME}TargetGroup DESTINATION "${SALOME_INSTALL_CMAKE_LOCAL}"
         FILE ${PROJECT_NAME}Targets.cmake)
index 51e76fcfa5945d24fafbc85990716f170b1b55d5..c3800cba7f318c2564e75251b4b8936d5d1c45c7 100644 (file)
@@ -46,6 +46,8 @@
 #include <cstdio>
 #include <iostream>
 
+#include <utilities.h>
+
 namespace
 {
 class BusyLocker
@@ -822,6 +824,9 @@ void CAM_Application::readModuleList()
 
 bool CAM_Application::appendModuleInfo( const QString& modName )
 {
+  MESSAGE("Start to append module info for a given module name: ");
+  SCRUTE(modName.toStdString());
+
   if ( modName.isEmpty() )
     return false;  // empty module name
 
@@ -834,9 +839,6 @@ bool CAM_Application::appendModuleInfo( const QString& modName )
   // we cannot use own resourceMgr() as this method can be called from constructor
   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
 
-  // "gui" option explicitly says that module has GUI
-  bool hasGui = resMgr->booleanValue( modName, "gui", true );
-
   ModuleInfo inf;
 
   // module internal name
@@ -845,15 +847,32 @@ bool CAM_Application::appendModuleInfo( const QString& modName )
   inf.version = resMgr->stringValue( modName, "version", QString() ).trimmed();
   // displayer, if module does not have GUI, displayer may be delegated to other module
   inf.displayer = resMgr->stringValue( modName, "displayer", QString() ).trimmed();
+
+  // "gui" option explicitly says that module has GUI
+  // Now trying to get the "gui" option value, we always get a default one,
+  // then we can't rely on it.
+  bool hasGui = resMgr->booleanValue(modName, "gui", false);
+
+  // Additional check if the module actually has a title and icon.
+  // Module with GUI must explicitly specify title (GUI name).
+  inf.title = resMgr->stringValue(modName, "name", QString()).trimmed();
+  const bool hasTitle = !inf.title.isEmpty();
+  SCRUTE(hasGui);
+  SCRUTE(hasTitle);
+  if (hasGui && !hasTitle)
+  {
+    MESSAGE("Invalid config! The module has gui option, but doesn't have a title.");
+    return false;
+  }
+
+  // While we can't rely on gui option, use a title to make a decision about gui.
+  hasGui = hasTitle;
+
   // status; if module has GUI, availability will be checked on activation
   inf.status = hasGui ? stUnknown : stNoGui;
 
   if ( hasGui )
   {
-    // module with GUI must explicitly specify title (GUI name)
-    inf.title = resMgr->stringValue( modName, "name", QString() ).trimmed();
-    if ( inf.title.isEmpty() )
-      inf.status = stInvalid;
     // icon
     inf.icon = resMgr->stringValue( modName, "icon", QString() ).trimmed();
     // description, for Info panel
@@ -864,8 +883,17 @@ bool CAM_Application::appendModuleInfo( const QString& modName )
       inf.library = modName;
   }
 
-  if ( inf.status != stInvalid )
-    myInfoList.append( inf );
+  // At this point we should have only valid inf object.
+  myInfoList.append(inf);
+
+  SCRUTE(inf.name.toStdString());
+  SCRUTE(inf.version.toStdString());
+  SCRUTE(inf.displayer.toStdString());
+  SCRUTE(inf.status);
+  SCRUTE(inf.title.toStdString());
+  SCRUTE(inf.icon.toStdString());
+  SCRUTE(inf.description.toStdString());
+  SCRUTE(inf.library.toStdString());
 
   return true;
 }
index fbc1ab9f24223e89937c41d3ed13219e2c914431..5590f86c0478ec7505fdb9469cc0b8152840373b 100644 (file)
@@ -34,7 +34,11 @@ INCLUDE_DIRECTORIES(
 ADD_DEFINITIONS(${QT_DEFINITIONS})
 
 # libraries to link to
-SET(_link_LIBRARIES ${QT_LIBRARIES} qtx suit std)
+SET(_link_LIBRARIES
+  ${QT_LIBRARIES}
+  ${KERNEL_SALOMELocalTrace}
+  qtx suit std
+  )
 
 # --- headers ---
 
index 084f12931ea5535318c7952cb38d38722129bfbd..b53f59329cadb3c69b858900d0aad4d70be938c4 100644 (file)
@@ -106,9 +106,11 @@ SET(_link_LIBRARIES
   ${OpenCASCADE_FoundationClasses_LIBRARIES}
   ${QT_LIBRARIES}
   ${HDF5_LIBRARIES}
-  CASCatch qtx suit std SalomeStyle SalomePrs CAM LogWindow ObjBrowser Event 
+  CASCatch qtx suit std SalomeStyle SalomePrs CAM LogWindow ObjBrowser Event
   ${KERNEL_SalomeHDFPersist} ${KERNEL_SALOMELocalTrace}
+  ${GRAPHVIZ_LIBRARIES}
 )
+
 IF(SALOME_USE_SALOMEOBJECT)
   LIST(APPEND _link_LIBRARIES SalomeObject)
 ENDIF()
@@ -158,7 +160,7 @@ ENDIF()
 # --- headers ---
 
 # header files / to be processed by moc
-SET(_moc_HEADERS   
+SET(_moc_HEADERS
   LightApp_AboutDlg.h
   LightApp_Application.h
   LightApp_DataModel.h
@@ -176,6 +178,7 @@ SET(_moc_HEADERS
   LightApp_Study.h
   LightApp_SwitchOp.h
   LightApp_WgViewModel.h
+  LightApp_ExtInfoDlg.h
 )
 IF(SALOME_USE_GLVIEWER)
   LIST(APPEND _moc_HEADERS LightApp_GLSelector.h)
@@ -246,6 +249,7 @@ SET(_other_RESOURCES
   resources/icon_life_ring.png
   resources/icon_add_module.png
   resources/icon_remove_module.png
+  resources/icon_info_module.png
   resources/LightApp.xml
 )
 
@@ -286,6 +290,7 @@ SET(_other_SOURCES
   LightApp_Study.cxx
   LightApp_SwitchOp.cxx
   LightApp_WgViewModel.cxx
+  LightApp_ExtInfoDlg.cxx
 )
 IF(SALOME_USE_GLVIEWER)
   LIST(APPEND _other_SOURCES LightApp_GLSelector.cxx)
index 4758aa40e0d85764065ab849bcf113cc330fb8e9..27b2ce9943a7673cd67e80fed38bb5d3424eafa8 100644 (file)
@@ -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"
@@ -264,6 +264,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_ ) {
@@ -714,7 +719,7 @@ void LightApp_Application::createActions()
 
   foreach( QString aModule, aModuleList )
     createHelpItems( aModule );
-  
+
   // f) Additional help items
 
   int id = LightApp_Application::UserID + FIRST_HELP_ID + 1000;
@@ -754,9 +759,13 @@ void LightApp_Application::createActions()
   connect( moduleAction, SIGNAL( moduleActivated( const QString& ) ),
            this, SLOT( onModuleActivation( const QString& ) ) );
   connect( moduleAction, SIGNAL( adding() ),
-           this, SLOT( onModuleAdding() ) );
+           this, SLOT( onExtAdding() ) );
   connect( moduleAction, SIGNAL( removing( QString ) ),
-           this, SLOT( onModuleRemoving( 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 );
@@ -837,6 +846,49 @@ void LightApp_Application::createActions()
   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.
 */
@@ -888,41 +940,79 @@ void LightApp_Application::onModuleActivation( const QString& modTitle )
     activateModule( modTitle );
 }
 
-/*!On module adding action.*/
-void LightApp_Application::onModuleAdding()
+/*!On extension adding action.*/
+void LightApp_Application::onExtAdding()
 {
-  // show dialog to browse configuration file
-  QStringList filters = ( QStringList() << tr( "Config files") + " (*.salomex)" << tr( "All files" ) + " (*)" );
-  QStringList paths = getOpenFileNames( QString(), filters.join( ";;" ), QString(), desktop() );
-  if ( paths.isEmpty() ) // cancelled
+  // 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;
+  }
 
-  // loop via selected configuration files
-  foreach( QString path, paths )
+  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>(action(ModulesListId));
+  if (!moduleAction)
   {
-    // read description file (.salomex) and check it's OK
-    QtxResourceMgr resMgr;
-    if ( !resMgr.addResource( path ) )
+    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");
+
+  // 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_MODULE_BAD_SALOMEX_FILE" ).arg( path ) );
+      SUIT_MessageBox::warning(desktop(), tr("WRN_WARNING"), tr("WRN_FAILED_UNPACK_EXTENSION").arg(path) );
       continue;
     }
-    // retrieve module name
-    QString name = resMgr.stringValue( "General", "name" ).trimmed();
-    if ( name.isEmpty() )
+
+    // Iterate all the components (modules) for this extension
+    for (Py_ssize_t pos = 0; pos < PyList_Size(unpackedModules); ++pos)
     {
-      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_EMPTY_NAME" ).arg( path ) );
-      continue;
+      auto moduleNameItem = PyList_GetItem(unpackedModules, pos);
+      QString moduleName(PyUnicode_AsUTF8(moduleNameItem));
+      SCRUTE(moduleName.toStdString());
+
+      addUserModule(moduleName, SalomeExtDir, true);
     }
-    // retrieve root directory
-    QString root = resMgr.stringValue( "General", "root" ).trimmed();
-    if ( root.isEmpty() )
+
+    // Add an extension to GUI
+    if (moduleAction)
     {
-      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_EMPTY_ROOT" ).arg( path ) );
-      continue;
+      QFileInfo extFileInfo(path);
+      QString extName = extFileInfo.baseName();
+      moduleAction->insertExtension(extName);
     }
-    addUserModule( name, root, true );
   }
+
+  // Udate actions only once after all of them were already inserted
+  moduleAction->updateExtActions();
 }
 
 /*Add user module.*/
@@ -951,9 +1041,12 @@ bool LightApp_Application::addUserModule( const QString& name, const QString& ro
       SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_BAD_RESDIR" ).arg( resDir ) );
     return false;
   }
+
+  SUIT_ResourceMgr* resMgr = resourceMgr();
+
   // read XML configuration file
-  resourceMgr()->setConstant( QString( "%1_ROOT_DIR" ).arg( name ), root );
-  if ( !resourceMgr()->addResource( resDir ) ) // cannot read configuration
+  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 ) );
@@ -966,12 +1059,26 @@ bool LightApp_Application::addUserModule( const QString& name, const QString& ro
       SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_BAD_CFG_FILE" ).arg( name ) );
     return false;
   }
+
   // load translations
-  resourceMgr()->loadLanguage( name );
-  // append module to the menu / toolbar
-  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
-  if ( moduleAction )
-    moduleAction->insertModule( moduleTitle( name ), moduleIcon( moduleTitle( name ), 20 ), true ); // scale icon to 20x20 pix
+  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())
+  {
+    // 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 ) ) )
@@ -995,72 +1102,145 @@ bool LightApp_Application::addUserModule( const QString& name, const QString& ro
   // save module in the resource manager
   if ( interactive )
   {
-    QStringList customModules = resourceMgr()->stringValue( "launch", "user_modules" ).split( ";", QString::SkipEmptyParts );
+    QStringList customModules = resMgr->stringValue("launch", "user_modules").split(";", QString::SkipEmptyParts);
     customModules << name;
     customModules.removeDuplicates();
-    resourceMgr()->setValue( "launch", "user_modules", customModules.join( ";" ) );
-    resourceMgr()->setValue( "user_modules", name, root );
+    resMgr->setValue( "launch", "user_modules", customModules.join( ";" ) );
+    resMgr->setValue( "user_modules", name, root );
   }
   return true;
 }
 
-/*!On module removing action.*/
-void LightApp_Application::onModuleRemoving( const QString& title )
+/*!Remove user module from UI.*/
+void LightApp_Application::removeUserModule(const QString& moduleInnerName, LightApp_ModuleAction* moduleAction)
 {
-  QString root = resourceMgr()->stringValue( "user_modules", moduleName( title ) );
-  QDir rootDirectory = QDir( root );
+  MESSAGE("Remove a module from UI...");
+  SCRUTE(moduleInnerName.toStdString());
 
-  if ( rootDirectory.exists() )
+  // 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)
   {
-    int answer = SUIT_MessageBox::question( desktop(),
-                                           tr( "TLT_REMOVE_MODULE" ),
-                                           tr( "QUE_REMOVE_MODULE_DIR" ).arg( root ),
-                                           SUIT_MessageBox::Yes | SUIT_MessageBox::No | SUIT_MessageBox::Cancel,
-                                           SUIT_MessageBox::No );
-    if ( answer == SUIT_MessageBox::Cancel )
-      return; // cancelled
-    if ( answer == SUIT_MessageBox::Yes )
-    {
-      if ( activeStudy() && activeStudy()->isModified() && !onSaveDoc() )
-        // doc is not saved, or saving cancelled
-        return;
-      if ( !rootDirectory.removeRecursively() )
-      {
-        // canont remove directory
-        if ( SUIT_MessageBox::question( desktop(),
-                                        tr( "WRN_WARNING" ),
-                                        tr( "WRN_CANNOT_REMOVE_DIR" ).arg( root ),
-                                        SUIT_MessageBox::Yes | SUIT_MessageBox::No,
-                                        SUIT_MessageBox::No ) == SUIT_MessageBox::No )
-        return; // removal is cancelled
-      }
-    }
+    moduleAction->removeModule(moduleUserName);
   }
 
-  if ( activeModule() && activeModule()->moduleName() == title )
-    activateModule( "" );
+  // Remove Help menu items
+  removeHelpItems(moduleUserName);
 
-  // remove from "Modules" menu and toolbar
-  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
-  if ( moduleAction )
+  // 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)
   {
-    moduleAction->removeModule( title );
+    MESSAGE("Removing of an extension was cancelled");
+    return; // cancelled
   }
-  // remove Help menu items
-  removeHelpItems( title );
-  // remove Preferences
-  LightApp_Preferences* prefs = preferences();
-  if ( prefs )
-    prefs->removeModule( title );
-  // remove settings
-  QStringList customModules = resourceMgr()->stringValue( "launch", "user_modules" ).split( ";", QString::SkipEmptyParts );
-  customModules.removeAll( moduleName( title ) );
-  resourceMgr()->setValue( "launch", "user_modules", customModules.join( ";" ) );
-  removeModuleInfo( moduleName( title ) );
-  // update windows (in particular, Info panel)
+  
+  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.*/
 QString LightApp_Application::defaultModule() const
 {
@@ -1153,16 +1333,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();
@@ -1355,7 +1535,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();
@@ -3038,7 +3218,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();
@@ -3063,7 +3243,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" ); //
@@ -4428,7 +4608,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;
 
@@ -4453,13 +4633,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) {
@@ -4481,9 +4661,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()] );
         }
       }
@@ -4494,11 +4674,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()] );
       }
@@ -5209,7 +5389,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);
@@ -5272,7 +5452,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.
 */
index 9861ff964d03163b851a9bc2da1b551b0a5477c1..763eede7bab23cd5e08aa8d813b8a8385ccb0c32 100644 (file)
@@ -51,6 +51,7 @@ class LightApp_Preferences;
 class LightApp_SelectionMgr;
 class LightApp_FullScreenHelper;
 class LightApp_DataObject;
+class LightApp_ModuleAction;
 class SUIT_DataBrowser;
 class SUIT_Study;
 class SUIT_Accel;
@@ -219,6 +220,7 @@ public slots:
 protected:
   void                                showHelp( const QString& );
   virtual void                        createActions();
+  virtual void                        addExtensionsActions(LightApp_ModuleAction* moduleAction);
   virtual void                        customize();
   virtual void                        createActionForViewer( const int id,
                                                              const int parentId,
@@ -263,8 +265,9 @@ protected slots:
 
   void                                onNewWindow();
   virtual void                        onModuleActivation( const QString& );
-  void                                onModuleAdding();
-  void                                onModuleRemoving( const QString& );
+  void                                onExtAdding();
+  void                                onExtRemoving( const QString& );
+  void                                onShowExtInfo();
   void                                onCloseView( SUIT_ViewManager* );
 
   virtual void                        onStudyCreated( SUIT_Study* );
@@ -329,6 +332,7 @@ protected:
 
 private:
   bool                                addUserModule( const QString&, const QString&, bool = false );
+  void                                removeUserModule(const QString& moduleInnerName, LightApp_ModuleAction* moduleAction);
   void                                emptyPreferences( const QString& );
   QList<QToolBar*>                    findToolBars( const QStringList& names = QStringList() );
   void                                createHelpItems( const QString& );
diff --git a/src/LightApp/LightApp_ExtInfoDlg.cxx b/src/LightApp/LightApp_ExtInfoDlg.cxx
new file mode 100644 (file)
index 0000000..9589a8f
--- /dev/null
@@ -0,0 +1,310 @@
+// Copyright (C) 2007-2022  CEA/DEN, EDF R&D, OPEN CASCADE
+//
+// Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
+// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+// File:      LightApp_ExtInfoDlg.cxx
+// Author:    Konstantin Leontev
+//
+#include "LightApp_ExtInfoDlg.h"
+#include "utilities.h"
+
+// Prevent slot compilation error
+#pragma push_macro("slots")
+#undef slots
+#include "PyInterp_Utils.h"
+#pragma pop_macro("slots")
+
+#include <QHBoxLayout>
+
+// Show extensions info
+#include <QTableWidget>
+#include <QTableWidgetItem>
+#include <QHeaderView>
+
+// Render dependency tree
+#include <QSvgWidget>
+#include <QByteArray>
+#include <graphviz/gvc.h>
+
+
+/*! RAII wrapper to graphviz Agraph_t.*/
+class GraphWrapper
+{
+  public:
+    virtual ~GraphWrapper();
+
+    void init(char* name, Agdesc_t desc = Agstrictdirected, Agdisc_t* disc = nullptr);
+
+    Agraph_t* getGraph() const { return m_graph; }
+    GVC_t* getGvc() const { return m_gvc; }
+
+  private:
+    Agraph_t* m_graph = nullptr;
+    GVC_t* m_gvc = nullptr;
+};
+
+GraphWrapper::~GraphWrapper()
+{
+  if (m_graph)
+  {
+    agclose(m_graph);
+    m_graph = nullptr;
+  }
+
+  if (m_gvc)
+  {
+    gvFreeContext(m_gvc);
+    m_gvc = nullptr;
+  }
+}
+
+void GraphWrapper::init(char *name, Agdesc_t desc /* = Agstrictdirected */, Agdisc_t* disc /* = nullptr */)
+{
+  m_graph = agopen(name, desc, disc);
+  m_gvc = gvContext();
+}
+
+/*!Constructor.*/
+LightApp_ExtInfoDlg::LightApp_ExtInfoDlg(QWidget* parent)
+: QtxDialog(parent, true, true, ButtonFlags::OK)
+{
+  MESSAGE("Start creating a dialog...\n");
+
+  setObjectName("salome_ext_info_dialog");
+  setWindowTitle(tr("EXT_INFO_CAPTION"));
+  setSizeGripEnabled(true);
+  setButtonPosition(ButtonPosition::Center, ButtonFlags::OK);
+
+  auto extInfoWiget = getExtListWidget(mainFrame());
+  auto extTreeWiget = getExtTreeWidget(mainFrame());
+
+  auto layout = new QHBoxLayout(mainFrame());
+  layout->addWidget(extInfoWiget);
+  layout->addWidget(extTreeWiget);
+}
+
+/*!Destructor.*/
+LightApp_ExtInfoDlg::~LightApp_ExtInfoDlg()
+{
+  //! Do nothing.
+}
+
+/*! Fill the given widget with info about installed extensions */
+bool LightApp_ExtInfoDlg::fillExtListWidget(QTableWidget* extListWidget) const
+{
+  MESSAGE("Getting info from SalomeOnDemandTK.extension_query...\n");
+
+  // Import Python module that manages SALOME extensions
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionQuery = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_query");
+  auto extRootDir = getenv("SALOME_APPLICATION_DIR");
+  PyObjWrapper extInfoDict = PyObject_CallMethod(extensionQuery, (char*)"ext_info_dict", (char*)"s", extRootDir);
+  if (!extInfoDict)
+  {
+    PyErr_Print();
+    return false;
+  }
+
+  // Check if we have any info to display
+  Py_ssize_t rowCount = PyDict_Size(extInfoDict);
+  if (!rowCount)
+  {
+    MESSAGE("Didn't find any extensions! Return.\n");
+    return false;
+  }
+
+  extListWidget->setRowCount(rowCount);
+  const int columnCount = extListWidget->columnCount();
+
+  auto makeTableWidgetItem = [](PyObject* itemText) -> QTableWidgetItem*
+  {
+    const char* itemTextStr = PyUnicode_AsUTF8(itemText);
+    SCRUTE(itemTextStr);
+
+    return new QTableWidgetItem(itemTextStr);
+  };
+
+  PyObject* keyName = nullptr;
+  PyObject* infoList = nullptr;
+  Py_ssize_t keyNamePos = 0;
+
+  // Iterate name:info_list dictionary
+  while (PyDict_Next(extInfoDict, &keyNamePos, &keyName, &infoList))
+  {
+    auto widgetItem = makeTableWidgetItem(keyName);
+
+    // keyNamePos is already 1 on the first iteration, so we need to decrease it 
+    extListWidget->setItem(keyNamePos - 1, 0, widgetItem);
+
+    // Iterate an extension info list
+    for (Py_ssize_t infoPos = 0; infoPos < PyList_Size(infoList); ++infoPos)
+    {
+      if (infoPos >= columnCount)
+      {
+        MESSAGE("Number of info items is greater than column count! Skip.\n");
+        break;
+      }
+
+      auto info = PyList_GetItem(infoList, infoPos);
+      widgetItem = makeTableWidgetItem(info);
+
+      // keyNamePos started from 1 instead of 0, so decrease
+      // info need to be filled from column 1, so increase
+      extListWidget->setItem(keyNamePos - 1, infoPos + 1, widgetItem);
+    }
+  }
+
+  return true;
+}
+
+/*! Return widget with info about installed extensions */
+QWidget* LightApp_ExtInfoDlg::getExtListWidget(QWidget* parent) const
+{
+  MESSAGE("Make a widget to display extensions info...\n");
+
+  auto extListWidget = new QTableWidget(parent);
+
+  // Setup the table params
+  const QStringList headerLabels = {
+    "Name", "Description", "Author", "Components", "Size"
+    };
+
+  extListWidget->setColumnCount(headerLabels.count());
+  extListWidget->setHorizontalHeaderLabels(headerLabels);
+
+  // Fill it with data about extensions
+  if (fillExtListWidget(extListWidget))
+  {
+    // Tune an appearance
+    extListWidget->sortItems(0);
+    extListWidget->horizontalHeader()->setStretchLastSection(true);
+    extListWidget->resizeColumnsToContents();
+    extListWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+  }
+
+  return extListWidget;
+}
+
+/*! Fill a given extension tree graph with nodes and edges */
+bool LightApp_ExtInfoDlg::fillExtTreeGraph(const GraphWrapper& graph) const
+{
+  MESSAGE("Start to get info from SalomeOnDemandTK.extension_query...\n");
+
+  // Import Python module that manages SALOME extensions
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionQuery = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_query");
+  auto extRootDir = getenv("SALOME_APPLICATION_DIR");
+  PyObjWrapper dependencyTree = PyObject_CallMethod(extensionQuery, (char*)"dependency_tree", (char*)"s", extRootDir);
+  if (!dependencyTree)
+  {
+    PyErr_Print();
+    return false;
+  }
+
+  Agraph_t* extTreeGraph = graph.getGraph();
+
+  // Python declarations
+  PyObject* extName = nullptr;
+  PyObject* dependantsList = nullptr;
+  Py_ssize_t pos = 0;
+
+  // Iterate the tree
+  while (PyDict_Next(dependencyTree, &pos, &extName, &dependantsList))
+  {
+    // Create a parent node if it doesn't already exist
+    auto parentName = PyUnicode_AsUTF8(extName);
+    Agnode_t* parentNode = agnode(extTreeGraph, const_cast<char*>(parentName), true);
+    SCRUTE(parentName);
+
+    // Iterate a list of dependants
+    for (Py_ssize_t depPos = 0; depPos < PyList_Size(dependantsList); ++depPos)
+    {
+      auto dependant = PyList_GetItem(dependantsList, depPos);
+
+      // Create a child node if it doesn't already exist
+      auto dependantName = PyUnicode_AsUTF8(dependant);
+      Agnode_t* dependantNode = agnode(extTreeGraph, const_cast<char*>(dependantName), true);
+      SCRUTE(dependantName);
+
+      // Make an edge
+      std::string edgeNameStr(parentName);
+      edgeNameStr += dependantName;
+      auto edgeName = edgeNameStr.c_str();
+      agedge(extTreeGraph, parentNode, dependantNode, const_cast<char*>(edgeName), true);
+      SCRUTE(edgeName);
+    }
+  }
+
+  return true;
+}
+
+/*! Render dependency tree to array of bytes */
+QByteArray LightApp_ExtInfoDlg::renderExtTreeGraph(const GraphWrapper& graph) const
+{
+  Agraph_t* extTreeGraph = graph.getGraph();
+  GVC_t* gvc = graph.getGvc();
+
+  // Layout and render to buffer
+  MESSAGE("Layout dependency tree...\n");
+  int res = gvLayout(gvc, extTreeGraph, "dot");
+  if (res)
+  {
+    MESSAGE("gvLayout failed!\n");
+    return {};
+  }
+
+  MESSAGE("Render dependency tree...\n");
+  char* buffer = nullptr;
+  unsigned int bufferLength = 0;
+  res = gvRenderData(gvc, extTreeGraph, "svg", &buffer, &bufferLength);
+  if (res)
+  {
+    MESSAGE("gvRenderData failed!\n");
+    return {};
+  }
+
+  QByteArray renderedGraph(buffer, bufferLength);
+  
+  // Clean up layout and buffer
+  gvFreeLayout(gvc, extTreeGraph);
+  gvFreeRenderData(buffer);
+
+  return renderedGraph;
+}
+
+/*! Return widget with an image of dependency tree fot installed extensions */
+QWidget* LightApp_ExtInfoDlg::getExtTreeWidget(QWidget* parent) const
+{
+  MESSAGE("Start to make a widget to show dependency tree...\n");
+
+  auto extTreeWidget = new QSvgWidget(parent);
+
+  // Graph to be filled up from python data
+  GraphWrapper extTreeGraph;
+  char graphName[] = "extTreeGraph";
+  extTreeGraph.init(graphName);
+  if (fillExtTreeGraph(extTreeGraph))
+  {
+    extTreeWidget->load(renderExtTreeGraph(extTreeGraph));
+  }
+
+  return extTreeWidget;
+}
diff --git a/src/LightApp/LightApp_ExtInfoDlg.h b/src/LightApp/LightApp_ExtInfoDlg.h
new file mode 100644 (file)
index 0000000..b4d9323
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright (C) 2007-2022  CEA/DEN, EDF R&D, OPEN CASCADE
+//
+// Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
+// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See https://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+// File:      LightApp_ExtInfoDlg.h
+// Author:    Konstantin Leontev
+//
+#ifndef LIGHTAPP_EXTINFODLG_H
+#define LIGHTAPP_EXTINFODLG_H
+
+#include "LightApp.h"
+
+#include <QtxDialog.h>
+
+class QTableWidget;
+class GraphWrapper;
+class QByteArray;
+
+/*!
+  \class LightApp_ExtInfoDlg
+  LightApp dialog with info about loaded extensions
+*/
+class LIGHTAPP_EXPORT LightApp_ExtInfoDlg : public QtxDialog
+{
+  Q_OBJECT
+
+public:
+  LightApp_ExtInfoDlg(QWidget* = 0);
+  virtual ~LightApp_ExtInfoDlg();
+
+private:
+  QWidget* getExtListWidget(QWidget* parent) const;
+  QWidget* getExtTreeWidget(QWidget* parent) const;
+
+  bool fillExtListWidget(QTableWidget* extListWidget) const;
+  bool fillExtTreeGraph(const GraphWrapper& graph) const;
+  QByteArray renderExtTreeGraph(const GraphWrapper& graph) const;
+};
+
+#endif
index f7a8f27af24f90aae581693599df408fbde66e56..60bc3f02cd13624492f00bd1b81587a8de86e63d 100644 (file)
 #include <QMenu>
 #include <QSignalMapper>
 
+#include <utilities.h>
+
+// Prevent slot compilation error
+#pragma push_macro("slots")
+#undef slots
+#include "PyInterp_Utils.h"
+#pragma pop_macro("slots")
+
 /*!
   \class LightApp_ModuleAction::ActionSet
   \brief Internal class to represent list of modules buttons.
@@ -53,7 +61,7 @@ public:
   \param parent parent object
 */
 LightApp_ModuleAction::ActionSet::ActionSet( QObject* parent )
-: QtxActionSet( parent ) 
+: QtxActionSet( parent )
 {
 }
 
@@ -207,7 +215,7 @@ private:
 
   In menu, the action is represented as a plain list of items, one per module.
 
-  Only one module can be active at the moment. It can be set programmatically 
+  Only one module can be active at the moment. It can be set programmatically
   with setActiveModule() function. Use this method with empty string to turn
   to the "neutral point". To get active module, use activeModule() function.
 
@@ -255,6 +263,10 @@ LightApp_ModuleAction::LightApp_ModuleAction( QtxResourceMgr* resMgr, QObject* p
                            0, this );
   myRemove->setEnabled( false );
   myRemove->setMenu( new QMenu() );
+  myInfo = new QtxAction( tr( "INFO_MODULE"),
+                           resMgr->loadPixmap( "LightApp", tr( "ICON_INFO_MODULE" ), false ),
+                           tr( "INFO_MODULE"),
+                           0, this );
   mySeparator = new QAction( this );
   mySeparator->setSeparator( true );
   mySet = new ActionSet( this );
@@ -263,6 +275,7 @@ LightApp_ModuleAction::LightApp_ModuleAction( QtxResourceMgr* resMgr, QObject* p
 
   connect( this,     SIGNAL( changed() ),          this, SLOT( onChanged() ) );
   connect( myAdd,    SIGNAL( triggered( bool ) ),  this, SIGNAL( adding() ) );
+  connect( myInfo,   SIGNAL( triggered( bool ) ),  this, SIGNAL( showExtInfo() ) );
   connect( mySet,    SIGNAL( triggered( int ) ),   this, SLOT( onTriggered( int ) ) );
   connect( myCombo,  SIGNAL( activatedId( int ) ), this, SLOT( onComboActivated( int ) ) );
   connect( myMapper, SIGNAL( mapped( QString ) ),  this, SIGNAL( removing( QString ) ) );
@@ -363,14 +376,11 @@ void LightApp_ModuleAction::insertModule( const QString& name, const QIcon& ico,
 {
   QtxAction* a = new QtxAction( name, ico, name, 0, this, true );
   a->setStatusTip( tr( "ACTIVATE_MODULE_TOP" ).arg( name ) );
-  a->setData( isCustom );
-  if ( isCustom )
-  {
-    myRemove->setEnabled( true );
-    QAction* inserted = myRemove->menu()->addAction( name );
-    connect( inserted, SIGNAL( triggered() ), myMapper, SLOT( map() ) );
-    myMapper->setMapping( inserted, name );
-  }
+  // Commented because the next call mySet->insertAction() overrides it with
+  // action id int != 0 value, so test a->data().toBool() is always true after that.
+  // Leave it here to mention that we need other approach to mark module as custom.
+  // a->setData( isCustom );
 
   mySet->insertAction( a, -1, idx );
   update();
@@ -383,34 +393,94 @@ void LightApp_ModuleAction::insertModule( const QString& name, const QIcon& ico,
 */
 void LightApp_ModuleAction::removeModule( const QString& name )
 {
+  MESSAGE("Start to remove module...");
+
   int id = mySet->moduleId( name );
+  SCRUTE(id);
   if ( id == -1 )
+  {
+    MESSAGE("Can't get a module's id! Return");
     return;
+  }
+
+  MESSAGE("Remove action by id...");
+  mySet->removeAction(id);
+
+  update();
+
+  MESSAGE("Module was removed");
+}
+
+/*!
+  \brief Add an installed extension. Now only to the Remove button's menu.
+  \param name an extension's name
+  \sa removeExtension()
+*/
+void LightApp_ModuleAction::insertExtension(const QString& name)
+{
+  MESSAGE("Insert an extension's action...");
+  SCRUTE(name.toStdString());
 
-  QAction* a = moduleAction( name );
-  bool isCustom = a->data().toBool();
+  myRemove->setEnabled(true);
 
-  mySet->removeAction( id );
-  if ( isCustom )
+  // Find a place to insert in the alphabetical order
+  QAction* insertBefore = nullptr;
+  foreach(QAction* curAction, myRemove->menu()->actions())
   {
-    foreach ( QAction* ma, myRemove->menu()->actions() )
+    int compareRes = QString::compare(curAction->text(), name, Qt::CaseInsensitive);
+    if (!compareRes)
     {
-      if ( ma->text() == name )
-      {
-        myRemove->menu()->removeAction( ma );
-        break;
-      }
+      return; // already added
+    }
+    else if (compareRes > 0)
+    {
+      insertBefore = curAction;
+
+      SCRUTE(insertBefore->text().toStdString());
+      break;
+    }
+  }
+
+  QAction* inserted = new QAction(name);
+
+  myRemove->menu()->insertAction(insertBefore, inserted);
+  connect(inserted, SIGNAL(triggered()), myMapper, SLOT(map()));
+  myMapper->setMapping(inserted, name);
+
+  MESSAGE("An extension's action was inserted");
+}
+
+/*!
+  \brief Remove an installed extension.
+  \param name an extension's name
+  \sa insertExtension()
+*/
+void LightApp_ModuleAction::removeExtension(const QString& name)
+{
+  MESSAGE("Remove an extension's action...");
+  SCRUTE(name.toStdString());
+
+  foreach(QAction* ma, myRemove->menu()->actions())
+  {
+    if (ma->text() == name)
+    {
+      myRemove->menu()->removeAction(ma);
+
+      MESSAGE("Extension's action was removed");
+      break;
     }
-    myRemove->setEnabled( !myRemove->menu()->actions().isEmpty() );
   }
 
+  myRemove->setEnabled(!myRemove->menu()->actions().isEmpty());
+
+  updateExtActions();
   update();
 }
 
 /*!
   \brief Get active module.
 
-  If there is no active module ("neutral point"), then the null string 
+  If there is no active module ("neutral point"), then the null string
   is returned.
 
   \return active module name
@@ -486,6 +556,7 @@ void LightApp_ModuleAction::addedTo( QWidget* w )
     w->insertAction( this, myCombo );
   w->insertAction( this, myAdd );
   w->insertAction( this, myRemove );
+  w->insertAction( this, myInfo );
   w->insertAction( this, mySeparator );
   w->insertAction( this, mySet );
   update();
@@ -503,6 +574,7 @@ void LightApp_ModuleAction::removedFrom( QWidget* w )
     w->removeAction( myCombo );
   w->removeAction( myAdd );
   w->removeAction( myRemove );
+  w->removeAction( myInfo );
   w->removeAction( mySeparator );
   w->removeAction( mySet );
 }
@@ -537,9 +609,20 @@ void LightApp_ModuleAction::update()
   for ( QList<QtxComboBox*>::const_iterator it = lst.begin(); it != lst.end(); ++it )
     update( *it );
 
+
   myCombo->setVisible( myMode & List );
-  myAdd->setVisible( myMode & AddRemove );
-  myRemove->setVisible( myMode & AddRemove );
+  if ( QString::compare(getenv("SALOME_ON_DEMAND"),"HIDE", Qt::CaseInsensitive) != 0)
+  {
+    myAdd->setVisible( myMode & AddRemove );
+    myRemove->setVisible( myMode & AddRemove );
+    myInfo->setVisible( myMode & All );
+  }
+  else
+  {
+    myAdd->setVisible(false);
+    myRemove->setVisible( false );
+    myInfo->setVisible( false );
+  }
   mySet->setVisible( myMode & Buttons );
 }
 
@@ -557,7 +640,7 @@ void LightApp_ModuleAction::update( QtxComboBox* cb )
   int curId = mySet->moduleId( active() );
   QList<QAction*> alist = mySet->actions();
   cb->clear();
-  
+
   cb->addItem( icon(), text() );
   cb->setId( 0, -1 );
 
@@ -573,6 +656,59 @@ void LightApp_ModuleAction::update( QtxComboBox* cb )
   cb->blockSignals( blocked );
 }
 
+/*!
+  \brief Update extension actions based on dependencies.
+  \internal
+  \param
+*/
+void LightApp_ModuleAction::updateExtActions()
+{
+  MESSAGE("Check dependencies to update extensions actions...");
+
+  // It should be set on the app start
+  auto extRootDir = getenv("SALOME_APPLICATION_DIR");
+  if (!extRootDir)
+  {
+    MESSAGE("Cannot get SALOME_APPLICATION_DIR env variable! Cancel adding selected extensions.");
+    return;
+  }
+  SCRUTE(extRootDir);
+
+  // Import Python module that manages SALOME extensions.
+  PyLockWrapper lck; // acquire GIL
+  PyObjWrapper extensionQuery = PyImport_ImportModule((char*)"SalomeOnDemandTK.extension_query");
+  PyObjWrapper extCanRemoveDict = PyObject_CallMethod(extensionQuery, (char*)"ext_canremove_flags", (char*)"s", extRootDir);
+  if (!extCanRemoveDict || extCanRemoveDict == Py_None)
+  {
+    MESSAGE("Couldn't get <ext>:<can_remove> dictionary from SalomeOnDemandTK.extension_query! Return.");
+    return;
+  }
+
+  // Iterate extensions' actions to disable ones we can't remove because of dependencies.
+  foreach(QAction* curAction, myRemove->menu()->actions())
+  {
+    const std::string action_name = curAction->text().toStdString();
+    SCRUTE(action_name);
+
+    PyObject* canRemoveObject = PyDict_GetItemString(extCanRemoveDict, action_name.c_str());
+    if (!canRemoveObject)
+    {
+      MESSAGE("Couldn't get can remove flag from dictionary! Skip.");
+      continue;
+    }
+
+    const int isTrueRes = PyObject_IsTrue(canRemoveObject);
+    if (isTrueRes == -1)
+    {
+      MESSAGE("PyObject_IsTrue() failed. Using false value instead.");
+    }
+    const bool canRemove = isTrueRes == 1;
+    SCRUTE(canRemove);
+
+    curAction->setEnabled(canRemove);
+  }
+}
+
 /*!
   \brief Get an action corresponding to the active module.
   \internal
@@ -641,7 +777,7 @@ void LightApp_ModuleAction::onTriggered( int id )
 /*!
   \brief Called when action state is changed.
   \internal
-  
+
   This slot is used to prevent making the parent action visible.
 */
 void LightApp_ModuleAction::onChanged()
@@ -662,4 +798,4 @@ void LightApp_ModuleAction::onChanged()
 void LightApp_ModuleAction::onComboActivated( int id )
 {
   QApplication::postEvent( this, new ActivateEvent( QEvent::MaxUser, id ) );
-} 
+}
index acc92746eb9e36ad9ef8c0321e14d79c1138f65a..0b5fbdc3d0d81250d3813a472db0322a84412e67 100644 (file)
@@ -66,6 +66,9 @@ public:
   void             insertModule( const QString&, const QIcon&, const int = -1 );
   void             insertModule( const QString&, const QIcon&, bool, const int = -1 );
   void             removeModule( const QString& );
+  void             insertExtension(const QString&);
+  void             removeExtension(const QString&);
+  void             updateExtActions();
 
   QString          activeModule() const;
 
@@ -87,6 +90,7 @@ signals:
   void             moduleActivated( const QString& );
   void             adding();
   void             removing( const QString& );
+  void             showExtInfo();
 
 private:
   void             update();
@@ -104,6 +108,7 @@ private:
   ComboAction*     myCombo;
   QtxAction*       myAdd;
   QtxAction*       myRemove;
+  QtxAction*       myInfo;
   ActionSet*       mySet;
   QAction*         mySeparator;
   int              myMode;
index 29f083f39d47302d943687176a282b300376073c..904c079a2fdf2653af2376ceed6a8552ad54eefb 100644 (file)
@@ -43,5 +43,9 @@
         <source>ICON_REMOVE_MODULE</source>
         <translation>icon_remove_module.png</translation>
     </message>
+    <message>
+        <source>ICON_INFO_MODULE</source>
+        <translation>icon_info_module.png</translation>
+    </message>
 </context>
 </TS>
index 3bb663ce7a35f6273c7931a3e66ba4ad57c203c3..6423f6563a8dfb127daf16d4f4e6bbb12328562f 100644 (file)
@@ -26,6 +26,10 @@ CEA/DEN, CEDRAT, EDF R&amp;D, LEG, PRINCIPIA R&amp;D, BUREAU VERITAS</translatio
         <source>ABOUT_MODULE_INFOS</source>
         <translation>Modules information</translation>
     </message>
+    <message>
+        <source>EXT_INFO_CAPTION</source>
+        <translation>Extensions information</translation>
+    </message>
     <message>
         <source>ABOUT_UNKNOWN_VERSION</source>
         <translation>Unknown</translation>
@@ -1243,6 +1247,36 @@ If you answer &quot;Yes&quot;, you may need to save your study before removal.</
 
 Continue?</translation>
   </message>
+    <message>
+        <source>WRN_FAILED_UNPACK_EXTENSION</source>
+        <translation>Failed to unpack a salome extension from file: %1.</translation>
+    </message>
+    <message>
+        <source>WRN_FAILED_REMOVE_EXTENSION</source>
+        <translation>Failed to remove a salome extension: %1.</translation>
+    </message>
+    <message>
+        <source>WRN_SALOME_APPLICATION_DIR</source>
+        <translation>Cannot get SALOME_APPLICATION_DIR env variable!
+Cancel current extensions operation.
+        </translation>
+    </message>
+    <message>
+        <source>TLT_REMOVE_EXTENSION</source>
+        <translation>Remove extension</translation>
+    </message>
+    <message>
+        <source>QUE_REMOVE_EXTENSION</source>
+        <translation>Do you want to completely remove an extension %1 and all its modules and files?
+If you answer &quot;Ok&quot;, you may need to save your study before removal.
+Continue?
+        </translation>
+    </message>
+    <message>
+        <source>WRN_CANCEL_REMOVE_EXTENSION_UNSAVE</source>
+        <translation>Removing of a salome extension %1 was cancelled because current doc is unsaved.
+        </translation>
+    </message>
 </context>
 <context>
     <name>LightApp_Module</name>
@@ -1371,6 +1405,10 @@ Continue?</translation>
         <source>REMOVE_MODULE</source>
         <translation>Remove modules</translation>
     </message>
+    <message>
+        <source>INFO_MODULE</source>
+        <translation>Module information</translation>
+    </message>
 </context>
 <context>
     <name>LightApp_PyEditor</name>
index 29e2505d8ce9bbcbaf067ef98347e94b21811a96..da3460b3b93f8e8d7864cf39b01d8adeae138679 100644 (file)
@@ -26,6 +26,10 @@ CEA/DEN, CEDRAT, EDF R&amp;D, LEG, PRINCIPIA R&amp;D, BUREAU VERITAS</translatio
         <source>ABOUT_MODULE_INFOS</source>
         <translation>Modules</translation>
     </message>
+    <message>
+        <source>EXT_INFO_CAPTION</source>
+        <translation>Informations sur les extensions</translation>
+    </message>
     <message>
         <source>ABOUT_UNKNOWN_VERSION</source>
         <translation>Inconnue</translation>
@@ -1173,6 +1177,36 @@ Le fichier n&apos;existe pas</translation>
       <source>WRN_MODULE_DUPLICATED</source>
       <translation>Module &quot;%1&quot; is already present in this session</translation>
     </message>
+    <message>
+        <source>WRN_FAILED_UNPACK_EXTENSION</source>
+        <translation>Échec de la décompression d'une extension salome à partir d'un fichier: %1.</translation>
+    </message>
+    <message>
+        <source>WRN_FAILED_REMOVE_EXTENSION</source>
+        <translation>Échec de la suppression d'une extension Salomé: %1.</translation>
+    </message>
+    <message>
+        <source>WRN_SALOME_APPLICATION_DIR</source>
+        <translation>Impossible d'obtenir la variable d'environnement SALOME_APPLICATION_DIR!
+Annulez l'opération d'extensions en cours.
+        </translation>
+    </message>
+    <message>
+        <source>TLT_REMOVE_EXTENSION</source>
+        <translation>Supprimer l'extension</translation>
+    </message>
+    <message>
+        <source>QUE_REMOVE_EXTENSION</source>
+        <translation>Voulez-vous supprimer complètement une extension %1 et tous ses modules et fichiers?
+Si vous répondez &quot;Ok&quot;, vous devrez peut-être enregistrer votre étude avant de la supprimer.
+Continuer?
+        </translation>
+    </message>
+    <message>
+        <source>WRN_CANCEL_REMOVE_EXTENSION_UNSAVE</source>
+        <translation>La suppression d'une extension salome %1 a été annulée car la documentation actuelle n'est pas enregistrée.
+        </translation>
+    </message>
     <message>
       <source>WRN_MODULE_BAD_RESDIR</source>
       <translation>Bad or non-existing resources directory:
@@ -1333,6 +1367,10 @@ Continue?</translation>
         <source>REMOVE_MODULE</source>
         <translation>Supprimer les modules</translation>
     </message>
+    <message>
+        <source>INFO_MODULE</source>
+        <translation>Information sur le module</translation>
+    </message>
 </context>
 <context>
     <name>LightApp_PyEditor</name>
index 50e43b8f3571bfcddfa8759a8dc5a8088abb84f8..ebda4f5a19d71df2be2ed922e2aee09e81a67947 100644 (file)
@@ -26,6 +26,10 @@ CEA/DEN, CEDRAT, EDF R&amp;D, LEG, PRINCIPIA R&amp;D, BUREAU VERITAS</translatio
       <source>ABOUT_MODULE_INFOS</source>
       <translation>モジュール情報</translation>
     </message>
+    <message>
+        <source>EXT_INFO_CAPTION</source>
+        <translation>内線情報</translation>
+    </message>
     <message>
       <source>ABOUT_UNKNOWN_VERSION</source>
       <translation>不明</translation>
@@ -1203,6 +1207,36 @@ If you answer &quot;Yes&quot;, you may need to save your study before removal.</
 
 Continue?</translation>
     </message>
+    <message>
+      <source>WRN_FAILED_UNPACK_EXTENSION</source>
+      <translation>ファイルからサロメ拡張機能を解凍できませんでした: %1.</translation>
+    </message>
+    <message>
+      <source>WRN_FAILED_REMOVE_EXTENSION</source>
+      <translation>サロメ エクステンションの削除に失敗しました: %1.</translation>
+    </message>
+    <message>
+        <source>WRN_SALOME_APPLICATION_DIR</source>
+        <translation>SALOME_APPLICATION_DIR 環境変数を取得できません!
+現在の拡張操作をキャンセルします.
+        </translation>
+    </message>
+    <message>
+        <source>TLT_REMOVE_EXTENSION</source>
+        <translation>拡張子を削除</translation>
+    </message>
+    <message>
+        <source>QUE_REMOVE_EXTENSION</source>
+        <translation>拡張機能を完全に削除しますか %1 およびそのすべてのモジュールとファイル?
+&quot;Ok&quot; と答えた場合、削除する前にスタディを保存する必要があるかもしれません。
+続く?
+        </translation>
+    </message>
+    <message>
+        <source>WRN_CANCEL_REMOVE_EXTENSION_UNSAVE</source>
+        <translation>サロメ拡張の除去 %1 現在のドキュメントが保存されていないため、キャンセルされました。
+        </translation>
+    </message>
   </context>
   <context>
     <name>LightApp_Module</name>
diff --git a/src/LightApp/resources/icon_info_module.png b/src/LightApp/resources/icon_info_module.png
new file mode 100644 (file)
index 0000000..7b70fb3
Binary files /dev/null and b/src/LightApp/resources/icon_info_module.png differ