Salome HOME
bos #29458 Salome on demand CR29458
authorvsr <vsr@opencascade.com>
Thu, 21 Apr 2022 13:07:06 +0000 (16:07 +0300)
committervsr <vsr@opencascade.com>
Fri, 1 Jul 2022 09:27:14 +0000 (12:27 +0300)
34 files changed:
CMakeLists.txt
SalomeGUIConfig.cmake.in
doc/salome/gui/images/modules_add.png [new file with mode: 0644]
doc/salome/gui/images/modules_remove.png [new file with mode: 0644]
doc/salome/gui/images/modules_toolbar.png [new file with mode: 0644]
doc/salome/gui/input/extend_salome.rst [new file with mode: 0644]
doc/salome/gui/input/howtos_and_best_practives.rst
src/CAM/CAM_Application.cxx
src/CAM/CAM_Application.h
src/LightApp/CMakeLists.txt
src/LightApp/LightApp_Application.cxx
src/LightApp/LightApp_Application.h
src/LightApp/LightApp_ModuleAction.cxx
src/LightApp/LightApp_ModuleAction.h
src/LightApp/LightApp_Preferences.cxx
src/LightApp/LightApp_Preferences.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_add_module.png [new file with mode: 0644]
src/LightApp/resources/icon_module.png
src/LightApp/resources/icon_module_big.png [deleted file]
src/LightApp/resources/icon_remove_module.png [new file with mode: 0644]
src/Qtx/Qtx.cxx
src/Qtx/Qtx.h
src/Qtx/QtxResourceMgr.cxx
src/Qtx/QtxResourceMgr.h
src/STD/STD_Application.cxx
src/STD/STD_Application.h
src/SUIT/SUIT_PreferenceMgr.cxx
src/SUIT/SUIT_PreferenceMgr.h
src/SalomeApp/SalomeApp_Application.cxx
src/SalomeApp/SalomeApp_Application.h

index 06d28fc0231a29c13592165317564b951f984586..069b7fea0aac9038ddf76b837da814bc82f54800 100644 (file)
@@ -63,6 +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)
 CMAKE_DEPENDENT_OPTION(SALOME_GUI_BUILD_FRENCH_DOC "Generate SALOME GUI French documentation" OFF
                        "SALOME_BUILD_DOC" OFF)
 
@@ -136,6 +137,11 @@ IF(SALOME_GUI_USE_OBSERVERS)
   ADD_DEFINITIONS(-DWITH_SALOMEDS_OBSERVER)
 ENDIF()
 
+# SALOME on demand
+IF(SALOME_ON_DEMAND)
+  ADD_DEFINITIONS(-DWITH_SALOME_ON_DEMAND)
+ENDIF()
+
 # Single-study GUI
 IF(SALOME_USE_SINGLE_DESKTOP)
   ADD_DEFINITIONS(-DSINGLE_DESKTOP)
index 6edcf645dfe89677bf7459162428d6b337610920..62159e8325466655ed7526e8e138426b4d6f9538 100644 (file)
@@ -64,7 +64,11 @@ SET(SALOME_USE_PYCONSOLE      @SALOME_USE_PYCONSOLE@)
 SET(SALOME_USE_SALOMEOBJECT   @SALOME_USE_SALOMEOBJECT@)
 SET(SALOME_GUI_USE_OBSERVERS  @SALOME_GUI_USE_OBSERVERS@)
 SET(SALOME_GUI_USE_OPENGL2    @SALOME_GUI_USE_OPENGL2@)
+SET(SALOME_ON_DEMAND          @SALOME_ON_DEMAND@)
 
+IF(SALOME_ON_DEMAND)
+  LIST(APPEND GUI_DEFINITIONS "-DWITH_SALOME_ON_DEMAND")
+ENDIF()
 IF(SALOME_GUI_LIGHT_ONLY)
   LIST(APPEND GUI_DEFINITIONS "-DGUI_DISABLE_CORBA")
 ENDIF() 
diff --git a/doc/salome/gui/images/modules_add.png b/doc/salome/gui/images/modules_add.png
new file mode 100644 (file)
index 0000000..0e37c5c
Binary files /dev/null and b/doc/salome/gui/images/modules_add.png differ
diff --git a/doc/salome/gui/images/modules_remove.png b/doc/salome/gui/images/modules_remove.png
new file mode 100644 (file)
index 0000000..d15caee
Binary files /dev/null and b/doc/salome/gui/images/modules_remove.png differ
diff --git a/doc/salome/gui/images/modules_toolbar.png b/doc/salome/gui/images/modules_toolbar.png
new file mode 100644 (file)
index 0000000..147cbf4
Binary files /dev/null and b/doc/salome/gui/images/modules_toolbar.png differ
diff --git a/doc/salome/gui/input/extend_salome.rst b/doc/salome/gui/input/extend_salome.rst
new file mode 100644 (file)
index 0000000..e7f967b
--- /dev/null
@@ -0,0 +1,85 @@
+.. _extend_salome: 
+
+******************************
+Add/remove modules dynamically
+******************************
+
+.. contents:: Table of Contents
+
+"SALOME on demand" feature provides a possibility to dynamically add or remove
+SALOME modules directly from GUI.
+
+.. _hp_salomex_format:
+
+Description file
+================
+
+**"SALOME on demand"** feature introduces notion of a module description file.
+This is the file in JSON format that has `.salomex` extension. The description
+file provides some parameters of the module; two parameters are mandatory:
+name of the module and path to its root directory.
+
+Example:
+
+.. code-block:: json
+
+    {
+      "name": "PYHELLO",
+      "root": "/home/user/salome/modules/PYHELLO"
+    }
+
+.. note:: In the description file, the `name` parameter is an **internal** name of the module,
+         in contrast to its **title** which is described in `SalomeApp.xml` or `LightApp.xml`
+         resource file.
+
+.. note:: One `.salomex` file may describe one SALOME module only.
+
+.. _hp_modules_toolbar:
+
+Modules toolbar
+===============
+
+The *Modules* toolbar provides two additional buttons, which allow adding or removing
+modules directly from GUI.
+
+.. figure:: ../images/modules_toolbar.png
+   :align: center
+   :alt: Modules toolbar
+
+.. |add| image:: ../images/modules_add.png
+  :height: 24px
+
+.. |remove| image:: ../images/modules_remove.png
+  :height: 24px
+
+.. _hp_modules_add:
+
+Adding modules
+--------------
+
+Pressing the first button |add| shows a standard *Open File* dialog that allows
+selecting one or more `.salomex` files. Each module correctly configured in the
+corresponding `.salomex` file is then added to the SALOME session and appears in the
+*Modules* toolbar. As soon as it is done, the user may work with the module in the same
+way as with any other module: all features are available after activating module from
+*Modules* toolbar.
+
+The modules added in this way become *persistent*. This means that after restarting
+SALOME these modules are available in the *Modules* toolbar.
+
+.. _hp_modules_remove:
+
+Removing modules
+----------------
+
+Pressing a small arrow near the second button |remove| shows a drop-down menu with
+the list of manually added modules, where the user may select a module to remove. As soon
+as user selects a module to remove, a message box is shown asking the user if it is
+necessary also to remove a directory, containing the module, from disk. If user
+confirms removal of the directory, it will be necessary to save the study, in order to
+avoid possible data loss.
+
+.. note:: The module libraries are not removed from RAM until exit from the current
+         SALOME session, so some its functionality may be still accessible, for example,
+         via Python API. However, using module functionality may have unpredicted
+         behavior.
index d281d87c3e692d43585111b71e4c8b19fd388844..837d8c071521fa56589e7cc60f84dd5103c52982 100644 (file)
@@ -13,4 +13,4 @@ and usage examples of SALOME platform.
        drag_and_drop.rst
        using_pluginsmanager.rst
        using_help_panel.rst
-
+       extend_salome.rst
index afd1faf37c681d16b4ccf52e97b1798a4cb07f32..2e6f51e43ca692251eeb798698a1a3c332c6aedb 100644 (file)
@@ -757,6 +757,7 @@ void CAM_Application::readModuleList()
   if ( !myInfoList.isEmpty() )
     return;
 
+  // we cannot use own resourceMgr() as this method can be called from constructor
   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
 
   QStringList modList;
@@ -801,88 +802,85 @@ void CAM_Application::readModuleList()
     modList = mods.split( ",", QString::SkipEmptyParts );
   }
 
-  for ( QStringList::const_iterator it = modList.begin(); it != modList.end(); ++it )
-  {
-    QString modName = (*it).trimmed();
-
-    if ( modName.isEmpty() )
-      continue;  // empty module name
-
-    if ( !moduleTitle( modName ).isEmpty() )
-      continue;  // already added
+  // extra modules loaded manually on previous session
+  // ...
 
-    if ( modName == "KERNEL" || modName == "GUI" )
-      continue; // omit KERNEL and GUI modules
+  foreach ( QString modName, modList )
+    appendModuleInfo( modName.trimmed() );
 
-    bool hasGui = resMgr->booleanValue( *it, "gui", true );
-
-    QString modTitle, modIcon, modLibrary, modDescription;
-
-    if ( hasGui )
-    {
-      // if module has GUI, check that it is present
-      modTitle = resMgr->stringValue( *it, "name", QString() );
-      if ( modTitle.isEmpty() )
+  if ( myInfoList.isEmpty() ) {
+    if ( desktop() && desktop()->isVisible() )
+      SUIT_MessageBox::warning( desktop(), tr( "Warning" ), tr( "Modules list is empty" ) );
+    else
       {
         printf( "****************************************************************\n" );
-        printf( "     Warning: module %s is improperly configured!\n", qPrintable(*it) );
-        printf( "     Module %s will not be available in GUI mode!\n", qPrintable(*it) );
+        printf( "*    Warning: modules list is empty.\n" );
         printf( "****************************************************************\n" );
-        continue;
       }
+  }
+}
 
-      modIcon = resMgr->stringValue( *it, "icon", QString() );
+bool CAM_Application::appendModuleInfo( const QString& modName )
+{
+  if ( modName.isEmpty() )
+    return false;  // empty module name
 
-      modDescription = resMgr->stringValue( *it, "description", QString() );
+  if ( !moduleTitle( modName ).isEmpty() )
+    return false;  // already added
 
-      modLibrary = resMgr->stringValue( *it, "library", QString() ).trimmed();
-      if ( !modLibrary.isEmpty() )
-      {
-        modLibrary = SUIT_Tools::file( modLibrary.trimmed() );
-#if defined(WIN32)
-        QString libExt = QString( "dll" );
-#elif defined(__APPLE__)
-        QString libExt = QString( "dylib" );
-#else
-        QString libExt = QString( "so" );
-#endif
-        if ( SUIT_Tools::extension( modLibrary ).toLower() == libExt )
-          modLibrary.truncate( modLibrary.length() - libExt.length() - 1 );
-#ifndef WIN32
-        QString prefix = QString( "lib" );
-        if ( modLibrary.startsWith( prefix ) )
-          modLibrary.remove( 0, prefix.length() );
-#endif
-      }
-      else
-        modLibrary = modName;
-    }
+  if ( modName == "KERNEL" || modName == "GUI" )
+    return false; // skip KERNEL and GUI modules
 
-    QString version = resMgr->stringValue( *it, "version", QString() );
+  // we cannot use own resourceMgr() as this method can be called from constructor
+  SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
 
-    QString modDisplayer = resMgr->stringValue( *it, "displayer", QString() );
+  // "gui" option explicitly says that module has GUI
+  bool hasGui = resMgr->booleanValue( modName, "gui", true );
 
-    ModuleInfo inf;
-    inf.name = modName;
-    inf.title = modTitle;
-    inf.status = hasGui ? stUnknown : stNoGui;
-    if ( hasGui ) inf.library = modLibrary;
-    inf.icon = modIcon;
-    inf.description = modDescription;
-    inf.displayer = modDisplayer;
-    inf.version = version;
-    myInfoList.append( inf );
+  ModuleInfo inf;
+
+  // module internal name
+  inf.name = modName;
+  // module version
+  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();
+  // 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
+    inf.description = resMgr->stringValue( modName, "description", QString() );
+    // library; if not specified, we use internal module name
+    inf.library = SUIT_Tools::libraryName( resMgr->stringValue( modName, "library", QString() ).trimmed() );
+    if ( inf.library.isEmpty() )
+      inf.library = modName;
   }
 
-  if ( myInfoList.isEmpty() ) {
-    if ( desktop() && desktop()->isVisible() )
-      SUIT_MessageBox::warning( desktop(), tr( "Warning" ), tr( "Modules list is empty" ) );
-    else
-      {
-        printf( "****************************************************************\n" );
-        printf( "*    Warning: modules list is empty.\n" );
-        printf( "****************************************************************\n" );
-      }
+  if ( inf.status != stInvalid )
+    myInfoList.append( inf );
+
+  return true;
+}
+
+void CAM_Application::removeModuleInfo( const QString& modName )
+{
+  QMutableListIterator<ModuleInfo> it( myInfoList );
+  while ( it.hasNext() )
+  {
+    ModuleInfo info = it.next();
+    if ( info.name == modName )
+    {
+      it.remove();
+      break;
+    }
   }
 }
 
index 3dc79da71e57e1171fd11ef7fdf0ba3775e3cf9e..daa7624e567e1e0d5a4233adc9802daaa32c0663 100644 (file)
@@ -97,21 +97,25 @@ protected:
 
   virtual bool        abortAllOperations();
 
-private:
-  void                readModuleList();
+protected:
+  bool                appendModuleInfo( const QString& );
+  void                removeModuleInfo( const QString& );
 
 private:
-  enum { stUnknown = 0, stNoGui, stInaccessible, stReady };
-  typedef struct { 
+  enum { stInvalid = -1, stUnknown = 0, stNoGui, stInaccessible, stReady };
+  struct ModuleInfo
+  {
     QString name, title, icon, library, version, description, displayer;
     int status;
-  } ModuleInfo;
-  typedef QList<ModuleInfo> ModuleInfoList;
+    ModuleInfo() : status( stInvalid ) {}
+  };
+  void                readModuleList();
 
 private:
+  typedef QList<ModuleInfo> ModuleInfoList;
+  static ModuleInfoList myInfoList;      //!< modules info list
   CAM_Module*           myModule;        //!< active module
   ModuleList            myModules;       //!< loaded modules list
-  static ModuleInfoList myInfoList;      //!< modules info list
   bool                  myAutoLoad;      //!< auto loading flag
   bool                  myBlocked;       //!< "blocked" flag, internal usage
 };
index efb0579fe6789801f3d2babf951a34b597884a96..993298328cd53b335fefdde0fa44364ee3bda945 100644 (file)
@@ -224,10 +224,11 @@ SET(_other_RESOURCES
   resources/icon_applogo.png
   resources/icon_default.png
   resources/icon_module.png
-  resources/icon_module_big.png
   resources/icon_select.png
   resources/icon_earth.png
   resources/icon_life_ring.png
+  resources/icon_add_module.png
+  resources/icon_remove_module.png
   resources/LightApp.xml
 )
 
index e64fef2c722b3ecc0af5ba931ed5b50cfc0b11e7..718efbd13a7afadb5b5605ae9bde1557c5043899 100644 (file)
@@ -323,6 +323,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.*/
@@ -625,8 +632,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
@@ -688,87 +693,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++;
-    }
-  }
-
-  // - Additional help items
+  foreach( QString aModule, aModuleList )
+    createHelpItems( aModule );
+  
+  // f) Additional help items
 
+  int id = LightApp_Application::UserID + FIRST_HELP_ID + 1000;
   createMenu( separator(), helpMenu, -1, 10 );
 
   QStringList addHelpItems = resMgr->parameters( "add_help" );
@@ -790,57 +725,24 @@ 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( onModuleAdding() ) );
+  connect( moduleAction, SIGNAL( removing( QString ) ),
+           this, SLOT( onModuleRemoving( QString ) ) );
 
   // New window
   int windowMenu = createMenu( tr( "MEN_DESK_WINDOW" ), -1, MenuWindowId, 100 );
@@ -910,26 +812,44 @@ 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 );
 }
 
+/*!
+  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 )
+  {
+    QStringList modList = resourceMgr()->stringValue( "launch", "user_modules" ).split( ";", QString::SkipEmptyParts );
+    foreach ( QString aModule, modList )
+      addUserModule(  aModule, resourceMgr()->stringValue( "user_modules", aModule ) );
+  }
+  else
+  {
+    moduleAction->setModeEnabled( LightApp_ModuleAction::AddRemove, false );
+  }
+}
+
 /*!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() );
@@ -942,17 +862,186 @@ 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 module adding action.*/
+void LightApp_Application::onModuleAdding()
+{
+  // 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
+    return;
+
+  // loop via selected configuration files
+  foreach( QString path, paths )
+  {
+    // read description file (.salomex) and check it's OK
+    QtxResourceMgr resMgr;
+    if ( !resMgr.addResource( path ) )
+    {
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_BAD_SALOMEX_FILE" ).arg( path ) );
+      continue;
+    }
+    // retrieve module name
+    QString name = resMgr.stringValue( "General", "name" ).trimmed();
+    if ( name.isEmpty() )
+    {
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_EMPTY_NAME" ).arg( path ) );
+      continue;
+    }
+    // retrieve root directory
+    QString root = resMgr.stringValue( "General", "root" ).trimmed();
+    if ( root.isEmpty() )
+    {
+      SUIT_MessageBox::warning( desktop(), tr( "WRN_WARNING" ), tr( "WRN_MODULE_EMPTY_ROOT" ).arg( path ) );
+      continue;
+    }
+    addUserModule( name, root, true );
+  }
+}
+
+/*Add user module.*/
+bool LightApp_Application::addUserModule( const QString& name, const QString& root, bool interactive )
+{
+  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;
+  }
+  // read XML configuration file
+  resourceMgr()->setConstant( QString( "%1_ROOT_DIR" ).arg( name ), root );
+  if ( !resourceMgr()->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
+  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
+  // 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 = resourceMgr()->stringValue( "launch", "user_modules" ).split( ";", QString::SkipEmptyParts );
+    customModules << name;
+    customModules.removeDuplicates();
+    resourceMgr()->setValue( "launch", "user_modules", customModules.join( ";" ) );
+    resourceMgr()->setValue( "user_modules", name, root );
+  }
+  return true;
+}
+
+/*!On module removing action.*/
+void LightApp_Application::onModuleRemoving( const QString& title )
+{
+  QString root = resourceMgr()->stringValue( "user_modules", moduleName( title ) );
+  QDir rootDirectory = QDir( root );
+
+  if ( rootDirectory.exists() )
+  {
+    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
+      }
+    }
+  }
+
+  if ( activeModule() && activeModule()->moduleName() == title )
+    activateModule( "" );
+
+  // remove from "Modules" menu and toolbar
+  LightApp_ModuleAction* moduleAction = qobject_cast<LightApp_ModuleAction*>( action( ModulesListId ) );
+  if ( moduleAction )
+  {
+    moduleAction->removeModule( title );
+  }
+  // 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)
+  updateWindows();
 }
 
 /*!Default module activation.*/
@@ -1220,7 +1309,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://" );
@@ -1335,7 +1424,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() );
       }
     }
   }
@@ -2260,8 +2349,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 )
   {
@@ -2273,10 +2360,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 )
@@ -2284,9 +2367,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
       }
     }
 
@@ -4045,22 +4126,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 )
@@ -4452,34 +4518,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;
 }
 
 /*!
@@ -5401,17 +5451,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 ) ) {
@@ -5424,7 +5474,7 @@ bool LightApp_Application::checkExistingDoc( bool closeExistingDoc )
     }
     else if( answer == 1 ) {
       if (closeExistingDoc) {
-       closeDoc( false );
+        closeDoc( false );
       }
     } else if( answer == 2 ) {
       result = false;
@@ -5451,3 +5501,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 );
+  }
+}
index c6e43cf47ee0b550a4e22fcb3acf66c3f7d70a22..aae6c005e511cd90db54f99e36d6072b461397db 100644 (file)
@@ -36,6 +36,7 @@
 #include <SUIT_DataObject.h>
 #include <CAM_Application.h>
 
+#include <QPixmap>
 #include <QPointer>
 #include <QStringList>
 
@@ -194,6 +195,7 @@ public:
 #endif
 
 signals:
+  void                                moduleActivated( const QString& );
   void                                studyOpened();
   void                                studySaved();
   void                                studyClosed();
@@ -217,6 +219,7 @@ public slots:
 protected:
   void                                showHelp( const QString& );
   virtual void                        createActions();
+  virtual void                        customize();
   virtual void                        createActionForViewer( const int id,
                                                              const int parentId,
                                                              const QString& suffix,
@@ -251,6 +254,8 @@ protected:
   virtual PyConsole_Interp*           createPyInterp();
 #endif
 
+  virtual void                        addCatalogue( const QString&, const QString& ) {}
+
 protected slots:
   virtual void                        onDesktopActivated();
   virtual void                        onViewManagerRemoved( SUIT_ViewManager* );
@@ -258,6 +263,8 @@ protected slots:
 
   void                                onNewWindow();
   virtual void                        onModuleActivation( const QString& );
+  void                                onModuleAdding();
+  void                                onModuleRemoving( const QString& );
   void                                onCloseView( SUIT_ViewManager* );
 
   virtual void                        onStudyCreated( SUIT_Study* );
@@ -293,7 +300,6 @@ protected:
   void                                updateWindows();
   void                                updateViewManagers();
   void                                updateModuleActions();
-  void                                removeModuleAction( const QString& );
 
   bool                                checkModule( const QString& );
 
@@ -309,7 +315,7 @@ protected:
   QString                             defaultModule() const;
   virtual void                        currentWindows( QMap<int, int>& ) const;
   void                                currentViewManagers( QStringList& ) const;
-  void                                moduleIconNames( QMap<QString, QString>& ) const;
+  QPixmap                             moduleIcon( const QString&, const int = -1 ) const;
 
   QDockWidget*                        windowDock( QWidget* ) const;
   QByteArray                          dockWindowsState( const QMap<QString, bool>&, const QMap<QString, bool>& ) const;
@@ -322,20 +328,25 @@ protected:
   void                                showPreferences( const QStringList& );
 
 private:
+  bool                                addUserModule( const QString&, const QString&, bool = false );
   void                                emptyPreferences( const QString& );
   QList<QToolBar*>                    findToolBars( const QStringList& names = QStringList() );
+  void                                createHelpItems( const QString& );
+  void                                removeHelpItems( const QString& );
 
   QByteArray                          processState(QByteArray& input,
-                                                  const bool processWin,
-                                                  const bool processTb,
-                                                  const bool isRestoring,
-                                                  QByteArray defaultState = QByteArray());
+                                                   const bool processWin,
+                                                   const bool processTb,
+                                                   const bool isRestoring,
+                                                   QByteArray defaultState = QByteArray());
 
 protected:
-  typedef QPointer<QWidget>         WinPtr;
-  typedef QMap<int, WinPtr>         WinMap;
-  typedef QMap<QString, QByteArray> WinVis;
-  typedef QMap<QString, QByteArray> WinGeom;
+  typedef QPointer<QWidget>          WinPtr;
+  typedef QMap<int, WinPtr>          WinMap;
+  typedef QMap<QString, QByteArray>  WinVis;
+  typedef QMap<QString, QByteArray>  WinGeom;
+  typedef QList<int>                 IdList;
+  typedef QMap<QString, IdList>      IdMap;
 
   enum { OpenReload = CAM_Application::OpenExist + 1 };
 
@@ -347,6 +358,7 @@ protected:
 
   WinMap                              myWin;
   WinVis                              myWinVis;
+  IdMap                               myHelpItems;
 
   SUIT_Accel*                         myAccel;
   QTimer*                             myAutoSaveTimer;
index d6a8ae9399a6d022b7f4d6913a35eb98a62b2562..f7a8f27af24f90aae581693599df408fbde66e56 100644 (file)
 //
 #include "LightApp_ModuleAction.h"
 
-#include <QtxComboBox.h>
 #include <QtxActionSet.h>
+#include <QtxComboBox.h>
+#include <QtxResourceMgr.h>
 #include <QVBoxLayout>
 #include <QApplication>
 #include <QEvent>
+#include <QMenu>
+#include <QSignalMapper>
 
 /*!
   \class LightApp_ModuleAction::ActionSet
@@ -146,7 +149,7 @@ QList<QtxComboBox*> LightApp_ModuleAction::ComboAction::widgets() const
 
   QList<QWidget*> wlist = createdWidgets();
   for ( QList<QWidget*>::const_iterator wit = wlist.begin(); wit != wlist.end(); ++wit )
-    lst += (*wit)->findChildren<QtxComboBox*>();
+    lst += qobject_cast<QtxComboBox*>(*wit);
 
   return lst;
 }
@@ -162,18 +165,11 @@ QWidget* LightApp_ModuleAction::ComboAction::createWidget( QWidget* parent )
   if ( !parent->inherits( "QToolBar" ) )
     return 0;
 
-  QWidget* dumb = new QWidget( parent );
-  QVBoxLayout* l = new QVBoxLayout( dumb );
-  l->setSpacing( 0 ); l->setMargin( 0 );
-  QtxComboBox* cb = new QtxComboBox( dumb );
+  QtxComboBox* cb = new QtxComboBox( parent );
   cb->setSizeAdjustPolicy( QComboBox::AdjustToContents );
   cb->setFocusPolicy( Qt::NoFocus );
-  l->addWidget( cb );
-  l->addSpacing( 3 );
-
   connect( cb, SIGNAL( activatedId( int ) ), this, SIGNAL( activatedId( int ) ) );
-
-  return dumb;
+  return cb;
 }
 
 /*!
@@ -205,63 +201,71 @@ private:
   \brief An action, representing the list of modules to be inserted to the
   toolbar.
 
-  This action is represented in the toolbar as combo box and a set of buttons 
-  for each module. In addition to the modules items, the combo box contains 
-  an item corresponding to the "neutral point" of the application 
-  (when there is no active module).
-  
-  The action can be constructed with up to two parameters, defining the text
-  and icon to be displayed for the "neutral point".
+  In the toolbar this action is represented as the combo box with the list of
+  available modules, and a set of buttons for each module. Additionally, combo box
+  contains an item representing "neutral point" of the application (i.e. no active module).
+
+  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 
   with setActiveModule() function. Use this method with empty string to turn
   to the "neutral point". To get active module, use activeModule() function.
 
-  When user activates/deactivates any module, the signal moduleActivated() 
+  When user activates/deactivates a module, the moduleActivated() signal
   is emitted.
 
-  The action can be represented in the toolbar in different modes:
-  * as combo box only (Qtx::ComboItem)
-  * as set of modules buttons only (Qtx::Buttons)
-  * as combo box followed by the set of modules buttons (Qtx::All)
-  * as none (Qtx::None)
-  By default, both combo box and buttons set are shown. Use method 
-  setMode() to change this behavior.
+  The action also provides an additional separate item "Add modules"; when
+  this button is pressed, a adding() signal is emitted. This signal
+  can be connected to a dedicated slot aimed to dynamically add a new module
+  into the application. In addition, a button "Remove module" shows a dropdown menu
+  with the list of user modules; when any item is selected, the removing() signal
+  is emitted. This signal may be connected to a slot aimed to dynamically remove
+  selected user module from application.
 
-  An action can be also added to the popup menu, but combo box is never shown
-  in this case, only modules buttons.
+  It is possible to customize which elements to show via the setMode() of setModeEnabled()
+  functions. By default, all elements are shown. The following choices are possible:
+
+  - LightApp_ModuleAction::Buttons: show separate items for all modules
+  - LightApp_ModuleAction::List: show combo box with list of modules (in toolbar only)
+  - LightApp_ModuleAction::AddRemove: show "Add modules" and "Remove modules" items
+  - LightApp_ModuleAction::All: show all items
 */
 
 /*!
   \brief Constructor
-
-  Creates an module action with "neutral point" item described by \a text.
-
-  \param text "neutral point" item's text
+  \param resMgr resource manager
   \param parent parent object
 */
-LightApp_ModuleAction::LightApp_ModuleAction( const QString& text, QObject* parent )
+LightApp_ModuleAction::LightApp_ModuleAction( QtxResourceMgr* resMgr, QObject* parent )
 : QtxAction( parent )
 {
-  setText( text );
-  init();
-}
+  setText( tr( "APP_NAME" ) );
+  setIcon( resMgr->loadPixmap( "LightApp", tr( "APP_DEFAULT_ICO" ), false ) );
+  setVisible( false );
 
-/*!
-  \brief Constructor
+  myMode = All;
+  myCombo = new ComboAction( this );
+  myAdd = new QtxAction( tr( "ADD_MODULE"),
+                        resMgr->loadPixmap( "LightApp", tr( "ICON_ADD_MODULE" ), false ),
+                        tr( "ADD_MODULE"),
+                        0, this );
+  myRemove = new QtxAction( tr( "REMOVE_MODULE"),
+                           resMgr->loadPixmap( "LightApp", tr( "ICON_REMOVE_MODULE" ), false ),
+                           tr( "REMOVE_MODULE"),
+                           0, this );
+  myRemove->setEnabled( false );
+  myRemove->setMenu( new QMenu() );
+  mySeparator = new QAction( this );
+  mySeparator->setSeparator( true );
+  mySet = new ActionSet( this );
 
-  Creates an module action with "neutral point" item described by \a text and \a ico.
+  myMapper = new QSignalMapper( this );
 
-  \param text "neutral point" item's text
-  \param ico "neutral point" item's icon
-  \param parent parent object
-*/
-LightApp_ModuleAction::LightApp_ModuleAction( const QString& text, const QIcon& ico, QObject* parent )
-: QtxAction( parent )
-{
-  setText( text );
-  setIcon( ico );
-  init();
+  connect( this,     SIGNAL( changed() ),          this, SLOT( onChanged() ) );
+  connect( myAdd,    SIGNAL( triggered( bool ) ),  this, SIGNAL( adding() ) );
+  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 ) ) );
 }
 
 /*!
@@ -341,9 +345,32 @@ QAction* LightApp_ModuleAction::moduleAction( const QString& name ) const
 */
 void LightApp_ModuleAction::insertModule( const QString& name, const QIcon& ico,
                                           const int idx )
+{
+  insertModule( name, ico, false, idx );
+}
+
+/*!
+  \brief Add module into the list.
+  \param name module name
+  \param ico module icon
+  \param isCustom \c false to insert regular module, \c true to insert user module
+  \param idx position in the module list (if -1, the module is added to the end of list)
+  \sa removeModule()
+*/
+void LightApp_ModuleAction::insertModule( const QString& name, const QIcon& ico,
+                                         bool isCustom, const int idx)
+
 {
   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 );
+  }
 
   mySet->insertAction( a, -1, idx );
   update();
@@ -360,7 +387,23 @@ void LightApp_ModuleAction::removeModule( const QString& name )
   if ( id == -1 )
     return;
 
+  QAction* a = moduleAction( name );
+  bool isCustom = a->data().toBool();
+
   mySet->removeAction( id );
+  if ( isCustom )
+  {
+    foreach ( QAction* ma, myRemove->menu()->actions() )
+    {
+      if ( ma->text() == name )
+      {
+        myRemove->menu()->removeAction( ma );
+        break;
+      }
+    }
+    myRemove->setEnabled( !myRemove->menu()->actions().isEmpty() );
+  }
+
   update();
 }
 
@@ -399,30 +442,38 @@ void LightApp_ModuleAction::setActiveModule( const QString& name )
 
 /*!
   \brief Set action display mode.
-
-  Action can be represented in the toolbar as
-  * combo box only (Qtx::ComboItem)
-  * set of modules buttons only (Qtx::Buttons)
-  * combo box followed by the set of modules buttons (Qtx::All)
-  * none (Qtx::None)
-
-  \param mode action display mode
+  \param mode action display options (combination of flags)
   \sa mode()
 */
-void LightApp_ModuleAction::setMode( const int mode )
+void LightApp_ModuleAction::setMode( const LightApp_ModuleAction::Mode& mode )
 {
   myMode = mode;
   update();
 }
 
+/*!
+  \brief Enable / disable action display mode.
+  \param mode action display options (combination of flags)
+  \param enabled \c true to enable mode, \c false to disable mode
+  \sa mode()
+*/
+void LightApp_ModuleAction::setModeEnabled( const LightApp_ModuleAction::Mode& mode, bool enabled )
+{
+  if ( enabled )
+    myMode |= mode;
+  else
+    myMode &= ~mode;
+  update();
+}
+
 /*!
   \brief Get action display mode.
   \param mode action display mode
   \sa setMode()
 */
-int LightApp_ModuleAction::mode() const
+bool LightApp_ModuleAction::isModeEnabled( const LightApp_ModuleAction::Mode& mode ) const
 {
-  return myMode;
+  return (bool)( myMode & mode );
 }
 
 /*!
@@ -433,6 +484,9 @@ void LightApp_ModuleAction::addedTo( QWidget* w )
 {
   if ( w->inherits( "QToolBar" ) )
     w->insertAction( this, myCombo );
+  w->insertAction( this, myAdd );
+  w->insertAction( this, myRemove );
+  w->insertAction( this, mySeparator );
   w->insertAction( this, mySet );
   update();
 }
@@ -447,6 +501,9 @@ void LightApp_ModuleAction::removedFrom( QWidget* w )
 {
   if ( w->inherits( "QToolBar" ) )
     w->removeAction( myCombo );
+  w->removeAction( myAdd );
+  w->removeAction( myRemove );
+  w->removeAction( mySeparator );
   w->removeAction( mySet );
 }
 
@@ -470,23 +527,6 @@ bool LightApp_ModuleAction::event( QEvent* e )
   \param name module name (empty string for neutral point)
 */
 
-/*!
-  \brief Initialize an action,
-  \internal
-*/
-void LightApp_ModuleAction::init()
-{
-  setVisible( false );
-
-  myMode = All;
-  myCombo = new ComboAction( this );
-  mySet = new ActionSet( this );
-
-  connect( this,    SIGNAL( changed() ),          this, SLOT( onChanged() ) );
-  connect( mySet,   SIGNAL( triggered( int ) ),   this, SLOT( onTriggered( int ) ) );
-  connect( myCombo, SIGNAL( activatedId( int ) ), this, SLOT( onComboActivated( int ) ) );
-}
-
 /*!
   \brief Update an action.
   \internal
@@ -497,7 +537,9 @@ void LightApp_ModuleAction::update()
   for ( QList<QtxComboBox*>::const_iterator it = lst.begin(); it != lst.end(); ++it )
     update( *it );
 
-  myCombo->setVisible( myMode & ComboItem );
+  myCombo->setVisible( myMode & List );
+  myAdd->setVisible( myMode & AddRemove );
+  myRemove->setVisible( myMode & AddRemove );
   mySet->setVisible( myMode & Buttons );
 }
 
index f669d681e8ed3a182d5ecaf2e07b7417928c1dba..acc92746eb9e36ad9ef8c0321e14d79c1138f65a 100644 (file)
@@ -32,6 +32,8 @@
 #endif
 
 class QtxComboBox;
+class QtxResourceMgr;
+class QSignalMapper;
 
 class LIGHTAPP_EXPORT LightApp_ModuleAction : public QtxAction
 {
@@ -43,11 +45,14 @@ private:
   class ActivateEvent;
 
 public:
-  enum { None = 0x00, Buttons = 0x01, ComboItem = 0x02, All = Buttons | ComboItem };
+  typedef enum { Buttons   = 0x01,
+                 List      = 0x02,
+                 AddRemove = 0x04,
+                 All       = Buttons | List | AddRemove
+  } Mode;
 
 public:
-  LightApp_ModuleAction( const QString&, QObject* = 0 );
-  LightApp_ModuleAction( const QString&, const QIcon&, QObject* = 0 );
+  LightApp_ModuleAction( QtxResourceMgr*, QObject* = 0 );
   virtual ~LightApp_ModuleAction();
 
   int              count() const;
@@ -59,13 +64,18 @@ public:
   QAction*         moduleAction( const QString& ) const;
 
   void             insertModule( const QString&, const QIcon&, const int = -1 );
+  void             insertModule( const QString&, const QIcon&, bool, const int = -1 );
   void             removeModule( const QString& );
 
   QString          activeModule() const;
+
+  void             setMode( const Mode& );
+  void             setModeEnabled( const Mode&, bool );
+  bool             isModeEnabled( const Mode& ) const;
+
+public slots:
   void             setActiveModule( const QString& );
 
-  void             setMode( const int );
-  int              mode() const;
 
 protected:
   virtual void     addedTo( QWidget* );
@@ -75,10 +85,10 @@ protected:
 
 signals:
   void             moduleActivated( const QString& );
+  void             adding();
+  void             removing( const QString& );
 
 private:
-  void             init();
-
   void             update();
   void             update( QtxComboBox* );
 
@@ -92,8 +102,12 @@ private slots:
 
 private:
   ComboAction*     myCombo;
+  QtxAction*       myAdd;
+  QtxAction*       myRemove;
   ActionSet*       mySet;
+  QAction*         mySeparator;
   int              myMode;
+  QSignalMapper*   myMapper;
 };
 
 class LightApp_ModuleAction::ComboAction : public QtxAction
index ea93eeb8dd6e06b81ce02a3b9f3eb95df14f23d5..2e1d352466965e38d4c97d20d1932638ea42077d 100644 (file)
@@ -61,6 +61,21 @@ int LightApp_Preferences::addPreference( const QString& mod, const QString& labe
   return id;
 }
 
+/*!
+  Remove module preferences.
+*/
+void LightApp_Preferences::removeModule( const QString& mod )
+{
+  QMutableMapIterator<int, QString> it( myPrefMod );
+  while ( it.hasNext() )
+  {
+    it.next();
+    if ( it.value() == mod )
+      it.remove();
+  }
+  removeItem( mod );
+}
+
 /*!
   Checks: is preferences has module with name \a mod.
 */
index f589541c29fe0297d6f4d9ef858a1721d526501e..91fcad6dceafc07b46a48ea7d5c190f9342be13e 100644 (file)
@@ -53,6 +53,7 @@ public:
                                       const QString& section = QString(), const QString& param = QString() );
 
   bool                 hasModule( const QString& ) const;
+  void                 removeModule( const QString& );
 
   void                 activateItem( const QString& );
   void                 activateItem( const QStringList& );
index d8a16f562571da35f2f52ba24a81c73c0742aed5..29f083f39d47302d943687176a282b300376073c 100644 (file)
         <source>ABOUT_SPLASH</source>
         <translation>icon_about.png</translation>
     </message>
-    <message>
-        <source>APP_MODULE_BIG_ICO</source>
-        <translation>icon_module_big.png</translation>
-    </message>
     <message>
         <source>APP_BASE_LOGO</source>
         <translation>icon_applogo.png</translation>
         <source>ICON_LIFE_RIGN</source>
         <translation>icon_life_ring.png</translation>
     </message>
+    <message>
+        <source>ICON_ADD_MODULE</source>
+        <translation>icon_add_module.png</translation>
+    </message>
+    <message>
+        <source>ICON_REMOVE_MODULE</source>
+        <translation>icon_remove_module.png</translation>
+    </message>
 </context>
 </TS>
index 1f810e27bf6ba2b1782196e90143b69d1a04fdf8..1a159cacfc06a7bfc6669c017902469930d823d9 100644 (file)
@@ -1149,6 +1149,62 @@ File does not exist</translation>
     <source>INFO_AVAILABLE_MODULES</source>
     <translation>Available modules</translation>
   </message>
+  <message>
+    <source>WRN_MODULE_BAD_SALOMEX_FILE</source>
+    <translation>Cannot read module description file:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_EMPTY_NAME</source>
+    <translation>Empty or invalid module name in:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_EMPTY_ROOT</source>
+    <translation>Module root directory is not set or incorrectly specified in:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_ROOT_DOES_NOT_EXIST</source>
+    <translation>Module root directory does not exist:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_DUPLICATED</source>
+    <translation>Module &quot;%1&quot; is already present in this session</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_BAD_RESDIR</source>
+    <translation>Bad or non-existing resources directory:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_CANNOT_READ_CFG</source>
+    <translation>Cannot read XML configuration file for the module from:
+%1.</translation>
+  </message>
+  <message>
+    <source>WRN_MODULE_BAD_CFG_FILE</source>
+    <translation>XML configuration file for the module &quot;%1&quot; is bad or incomplete</translation>
+  </message>
+  <message>
+    <source>TLT_REMOVE_MODULE</source>
+    <translation>Remove module</translation>
+  </message>
+  <message>
+    <source>QUE_REMOVE_MODULE_DIR</source>
+    <translation>Do you also want to remove module directory:
+%1?
+
+If you answer &quot;Yes&quot;, you may need to save your study before removal.</translation>
+  </message>
+  <message>
+    <source>WRN_CANNOT_REMOVE_DIR</source>
+    <translation>Cannot remove directory:
+%1.
+
+Continue?</translation>
+  </message>
 </context>
 <context>
     <name>LightApp_Module</name>
@@ -1269,6 +1325,14 @@ File does not exist</translation>
         <source>ACTIVATE_MODULE_TOP</source>
         <translation>Activate/deactivate %1 module</translation>
     </message>
+    <message>
+        <source>ADD_MODULE</source>
+        <translation>Add modules</translation>
+    </message>
+    <message>
+        <source>REMOVE_MODULE</source>
+        <translation>Remove modules</translation>
+    </message>
 </context>
 <context>
     <name>LightApp_PyEditor</name>
index 76415b674f9fc5a3bf44653a9784db0f97ce5250..29e2505d8ce9bbcbaf067ef98347e94b21811a96 100644 (file)
@@ -1149,6 +1149,62 @@ Le fichier n&apos;existe pas</translation>
       <source>INFO_AVAILABLE_MODULES</source>
       <translation>Modules disponibles</translation>
     </message>
+    <message>
+      <source>WRN_MODULE_BAD_SALOMEX_FILE</source>
+      <translation>Cannot read module description file:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_EMPTY_NAME</source>
+      <translation>Empty or invalid module name in:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_EMPTY_ROOT</source>
+      <translation>Module root directory is not set or incorrectly specified in:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_ROOT_DOES_NOT_EXIST</source>
+      <translation>Module root directory does not exist:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_DUPLICATED</source>
+      <translation>Module &quot;%1&quot; is already present in this session</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_BAD_RESDIR</source>
+      <translation>Bad or non-existing resources directory:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_CANNOT_READ_CFG</source>
+      <translation>Cannot read XML configuration file for the module from:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_BAD_CFG_FILE</source>
+      <translation>XML configuration file for the module &quot;%1&quot; is bad or incomplete</translation>
+    </message>
+    <message>
+      <source>TLT_REMOVE_MODULE</source>
+      <translation>Remove module</translation>
+    </message>
+    <message>
+      <source>QUE_REMOVE_MODULE_DIR</source>
+      <translation>Do you also want to remove module directory:
+%1?
+
+If you answer &quot;Yes&quot;, you may need to save your study before removal.</translation>
+    </message>
+    <message>
+      <source>WRN_CANNOT_REMOVE_DIR</source>
+      <translation>Cannot remove directory:
+%1.
+
+Continue?</translation>
+    </message>
 </context>
 <context>
     <name>LightApp_Module</name>
@@ -1269,6 +1325,14 @@ Le fichier n&apos;existe pas</translation>
         <source>ACTIVATE_MODULE_TOP</source>
         <translation>Activer/désactiver le module %1</translation>
     </message>
+    <message>
+        <source>ADD_MODULE</source>
+        <translation>Ajouter les modules</translation>
+    </message>
+    <message>
+        <source>REMOVE_MODULE</source>
+        <translation>Supprimer les modules</translation>
+    </message>
 </context>
 <context>
     <name>LightApp_PyEditor</name>
index e23d9b4b5c7310f5eb36be0081ea443a4c9a4231..50e43b8f3571bfcddfa8759a8dc5a8088abb84f8 100644 (file)
@@ -1147,6 +1147,62 @@ Pythonファイルは、文字、数字、アンダースコアが含まれて
       <source>INFO_AVAILABLE_MODULES</source>
       <translation type="unfinished">Available modules</translation>
     </message>
+    <message>
+      <source>WRN_MODULE_BAD_SALOMEX_FILE</source>
+      <translation>Cannot read module description file:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_EMPTY_NAME</source>
+      <translation>Empty or invalid module name in:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_EMPTY_ROOT</source>
+      <translation>Module root directory is not set or incorrectly specified in:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_ROOT_DOES_NOT_EXIST</source>
+      <translation>Module root directory does not exist:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_DUPLICATED</source>
+      <translation>Module &quot;%1&quot; is already present in this session</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_BAD_RESDIR</source>
+      <translation>Bad or non-existing resources directory:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_CANNOT_READ_CFG</source>
+      <translation>Cannot read XML configuration file for the module from:
+%1.</translation>
+    </message>
+    <message>
+      <source>WRN_MODULE_BAD_CFG_FILE</source>
+      <translation>XML configuration file for the module &quot;%1&quot; is bad or incomplete</translation>
+    </message>
+    <message>
+      <source>TLT_REMOVE_MODULE</source>
+      <translation>Remove module</translation>
+    </message>
+    <message>
+      <source>QUE_REMOVE_MODULE_DIR</source>
+      <translation>Do you also want to remove module directory:
+%1?
+
+If you answer &quot;Yes&quot;, you may need to save your study before removal.</translation>
+    </message>
+    <message>
+      <source>WRN_CANNOT_REMOVE_DIR</source>
+      <translation>Cannot remove directory:
+%1.
+
+Continue?</translation>
+    </message>
   </context>
   <context>
     <name>LightApp_Module</name>
@@ -1266,6 +1322,14 @@ Pythonファイルは、文字、数字、アンダースコアが含まれて
       <source>ACTIVATE_MODULE_TOP</source>
       <translation>モジュール %1 の有効/無効にします。</translation>
     </message>
+    <message>
+      <source>ADD_MODULE</source>
+      <translation>Add modules</translation>
+    </message>
+    <message>
+      <source>REMOVE_MODULE</source>
+      <translation>Remove modules</translation>
+    </message>
   </context>
   <context>
     <name>LightApp_PyEditor</name>
diff --git a/src/LightApp/resources/icon_add_module.png b/src/LightApp/resources/icon_add_module.png
new file mode 100644 (file)
index 0000000..ac8bb01
Binary files /dev/null and b/src/LightApp/resources/icon_add_module.png differ
index 298e581b069a441b92cfbade7cdf1b91262bce0d..c303e3bc3b8eeee5de09a1908018875f3391c96d 100644 (file)
Binary files a/src/LightApp/resources/icon_module.png and b/src/LightApp/resources/icon_module.png differ
diff --git a/src/LightApp/resources/icon_module_big.png b/src/LightApp/resources/icon_module_big.png
deleted file mode 100644 (file)
index c303e3b..0000000
Binary files a/src/LightApp/resources/icon_module_big.png and /dev/null differ
diff --git a/src/LightApp/resources/icon_remove_module.png b/src/LightApp/resources/icon_remove_module.png
new file mode 100644 (file)
index 0000000..037c826
Binary files /dev/null and b/src/LightApp/resources/icon_remove_module.png differ
index f050d032862e7589523d80ae4acb41473d58b9a3..919adfaf39eff28deac1151c0b1b8b9ea1e642a3 100644 (file)
@@ -439,22 +439,52 @@ QString Qtx::extension( const QString& path, const bool full )
   return full ? QFileInfo( path ).completeSuffix() : QFileInfo( path ).suffix();
 }
 
+/*!
+  \brief EXtract base library name.
+
+  The function removes platform-specific prefix (lib) and suffix (.dll/.so)
+  from the library file name.
+  For example, if \a str = "libmylib.so", "mylib" is returned..
+
+  \param libName library name
+  \return base library name
+*/
+QString Qtx::libraryName( const QString& libName )
+{
+  QString fullName = file( libName );
+#if defined(WIN32)
+  QString libExt = QString( "dll" );
+#elif defined(__APPLE__)
+  QString libExt = QString( "dylib" );
+#else
+  QString libExt = QString( "so" );
+#endif
+  if ( extension( fullName ).toLower() == libExt )
+    fullName.truncate( fullName.length() - libExt.length() - 1 );
+#ifndef WIN32
+  QString prefix = QString( "lib" );
+  if ( fullName.startsWith( prefix ) )
+    fullName.remove( 0, prefix.length() );
+#endif
+  return fullName;
+}
+
 /*!
   \brief Convert the given parameter to the platform-specific library name.
 
   The function appends platform-specific prefix (lib) and suffix (.dll/.so)
   to the library file name.
-  For example, if \a str = "mylib", "libmylib.so" is returned for Linux and
+  For example, if \a libName = "mylib", "libmylib.so" is returned for Linux and
   mylib.dll for Windows.
 
-  \param str short library name
+  \param libName short library name
   \return full library name
 */
-QString Qtx::library( const QString& str )
+QString Qtx::library( const QString& libName )
 {
-  QString path = dir( str, false );
-  QString name = file( str, false );
-  QString ext  = extension( str );
+  QString path = dir( libName, false );
+  QString name = file( libName, false );
+  QString ext  = extension( libName );
 
 #ifndef WIN32
   if ( !name.startsWith( "lib" ) )
@@ -556,6 +586,17 @@ QString Qtx::addSlash( const QString& path )
   return res;
 }
 
+/*!
+  \brief Return full path obtained by joining given components with
+  the native directory separator.
+  \param path Separate path components
+  \return complete path
+*/
+QString Qtx::joinPath( const QStringList& path )
+{
+  return path.join( QDir::separator() );
+}
+
 /*!
   \brief Convert text file from DOS format to UNIX.
 
index 0c28e383046cb5cceaace5793ba9ebb00f16b103..c602f574f4a031d2b1235541ddceb5f08c50b61e 100644 (file)
@@ -234,6 +234,7 @@ public:
   static QString     file( const QString&, const bool = true );
   static QString     extension( const QString&, const bool = false );
 
+  static QString     libraryName( const QString& );
   static QString     library( const QString& );
 
   static QString     tmpDir();
@@ -241,6 +242,7 @@ public:
   static bool        rmDir( const QString& );
   static bool        dos2unix( const QString& );
   static QString     addSlash( const QString& );
+  static QString     joinPath( const QStringList& );
 
   static QCompleter* pathCompleter( const PathType, const QString& = QString() );
   static QString     findEnvVar( const QString&, int&, int& );
index ae35b5007b8007b0abad937a850abc0972c32dbb..fdf1c7c828ce0456efd1b7a7b46dc8ef77a9c2b4 100644 (file)
@@ -30,6 +30,8 @@
 #include <QDir>
 #include <QFile>
 #include <QFileInfo>
+#include <QJsonDocument>
+#include <QJsonObject>
 #include <QRegExp>
 #include <QTextStream>
 #include <QApplication>
@@ -536,7 +538,7 @@ bool QtxResourceMgr::IniFormat::load( const QString& fname, QMap<QString, Sectio
 
 
 /*!
-  \brief Load resources from xml-file.
+  \brief Load resources from ini-file.
   \param fname resources file name
   \param secMap resources map to be filled in
   \param importHistory list of already imported resources files (to prevent import loops)
@@ -618,8 +620,8 @@ bool QtxResourceMgr::IniFormat::load( const QString& fname, QMap<QString, Sectio
       QString impFile = QDir::toNativeSeparators( Qtx::makeEnvVarSubst( data, Qtx::Always ) );
       QFileInfo impFInfo( impFile );
       if ( impFInfo.isRelative() )
-             impFInfo.setFile( aFinfo.absoluteDir(), impFile );
-    
+        impFInfo.setFile( aFinfo.absoluteDir(), impFile );
+
       QMap<QString, Section> impMap;
       if ( !load( impFInfo.absoluteFilePath(), impMap, importHistory ) )
       {
@@ -1032,6 +1034,222 @@ QString QtxResourceMgr::XmlFormat::valueAttribute() const
   return str;
 }
 
+/*!
+  \class QtxResourceMgr::JsonFormat
+  \internal
+  \brief Reader/writer for .json resources files.
+*/
+
+class QtxResourceMgr::JsonFormat : public Format
+{
+public:
+  JsonFormat();
+  ~JsonFormat();
+
+protected:
+  JsonFormat( const QString& );
+  virtual bool load( const QString&, QMap<QString, Section>& );
+  virtual bool save( const QString&, const QMap<QString, Section>& );
+
+private:
+  bool         load( const QString&, QMap<QString, Section>&, QSet<QString>& );
+};
+
+/*!
+  \brief Constructor.
+*/
+QtxResourceMgr::JsonFormat::JsonFormat()
+: QtxResourceMgr::JsonFormat( "json" )
+{
+}
+
+/*!
+  \brief Constructor.
+*/
+QtxResourceMgr::JsonFormat::JsonFormat( const QString& fmt )
+: Format( fmt )
+{
+}
+
+/*!
+  \brief Destructor.
+*/
+QtxResourceMgr::JsonFormat::~JsonFormat()
+{
+}
+
+/*!
+  \brief Load resources from json-file.
+  \param fname resources file name
+  \param secMap resources map to be filled in
+  \return \c true on success and \c false on error
+*/
+bool QtxResourceMgr::JsonFormat::load( const QString& fname, QMap<QString, Section>& secMap )
+{
+  QSet<QString> importHistory;
+  return load( fname, secMap, importHistory );
+}
+
+/*!
+  \brief Load resources from json-file.
+  \param fname resources file name
+  \param secMap resources map to be filled in
+  \param importHistory list of already imported resources files (to prevent import loops)
+  \return \c true on success or \c false on error
+*/
+bool QtxResourceMgr::JsonFormat::load( const QString& fname, QMap<QString, Section>& secMap, QSet<QString>& importHistory )
+{
+  QString aFName = fname.trimmed();
+  if ( !QFileInfo( aFName ).exists() )
+  {
+    if ( QFileInfo( aFName + ".json" ).exists() )
+      aFName += ".json";
+    else if ( QFileInfo( aFName + ".JSON" ).exists() )
+      aFName += ".JSON";
+    else
+      return false; // file does not exist
+  }
+  QFileInfo aFinfo( aFName );
+  aFName = aFinfo.canonicalFilePath();
+
+  if ( !importHistory.contains( aFName ) )
+    importHistory.insert( aFName );
+  else
+    return true;   // already imported (prevent import loops)
+
+  QFile file( aFName );
+  if ( !file.open( QFile::ReadOnly ) )
+    return false;  // file is not accessible
+
+  QJsonDocument document = QJsonDocument::fromJson( file.readAll() );
+  if ( document.isNull() )
+    return false;  // invalid json file
+
+  QJsonObject root = document.object();
+  foreach ( QString sectionName, root.keys() )
+  {
+    if ( sectionName == "import" )
+    {
+      QString impFile = root.value( sectionName ).toString();
+      if ( impFile.isEmpty() )
+        continue;
+      QString impPath = QDir::toNativeSeparators( Qtx::makeEnvVarSubst( impFile, Qtx::Always ) );
+      QFileInfo impFInfo( impPath );
+      if ( impFInfo.isRelative() )
+        impFInfo.setFile( aFinfo.absoluteDir(), impPath );
+      QMap<QString, Section> impMap;
+      if ( !load( impFInfo.absoluteFilePath(), impMap, importHistory ) )
+      {
+        qDebug() << "QtxResourceMgr: Error with importing file:" << impPath;
+      }
+      else
+      {
+        QMap<QString, Section>::const_iterator it = impMap.constBegin();
+        for ( ; it != impMap.constEnd() ; ++it )
+        {
+          if ( !secMap.contains( it.key() ) )
+          {
+            // insert full section
+            secMap.insert( it.key(), it.value() );
+          }
+          else
+          {
+            // insert all parameters from the section
+            Section::ConstIterator paramIt = it.value().begin();
+            for ( ; paramIt != it.value().end() ; ++paramIt )
+            {
+              if ( !secMap[it.key()].contains( paramIt.key() ) )
+                secMap[it.key()].insert( paramIt.key(), paramIt.value() );
+            }
+          }
+        }
+      }
+    }
+    else
+    {
+      QJsonObject section = root.value( sectionName ).toObject();
+      if ( !section.isEmpty() )
+      {
+       // case when a top-level item is a section
+        foreach ( QString parameterName, section.keys() )
+        {
+          // each value must be a string, number, or boolean
+          QJsonValue parameter = section.value( parameterName );
+          if ( parameter.isDouble() )
+            secMap[sectionName].insert( parameterName, QString::number( parameter.toDouble() ) );
+          else if ( parameter.isBool() )
+            secMap[sectionName].insert( parameterName, QString( parameter.toBool() ? "true" : "false" ) );
+          else if ( parameter.isString() )
+            secMap[sectionName].insert( parameterName, parameter.toString() );
+        }
+      }
+      else
+      {
+        QString parameterName = sectionName;
+        sectionName = "General"; // default section name for top-level items
+        // each value must be a string, number, or boolean
+        QJsonValue parameter = root.value( parameterName );
+        if ( parameter.isDouble() )
+          secMap[sectionName].insert( parameterName, QString::number( parameter.toDouble() ) );
+        else if ( parameter.isBool() )
+          secMap[sectionName].insert( parameterName, QString( parameter.toBool() ? "true" : "false" ) );
+        else if ( parameter.isString() )
+          secMap[sectionName].insert( parameterName, parameter.toString() );
+      }
+    }
+  }
+
+  if ( !secMap.isEmpty() )
+    qDebug() << "QtxResourceMgr: File" << fname << "is loaded successfully";
+  return true;
+}
+
+/*!
+  \brief Save resources to the json-file.
+  \param fname resources file name
+  \param secMap resources map
+  \return \c true on success and \c false on error
+*/
+bool QtxResourceMgr::JsonFormat::save( const QString& fname, const QMap<QString, Section>& secMap )
+{
+  if ( !Qtx::mkDir( QFileInfo( fname ).absolutePath() ) )
+    return false;
+
+  QFile file( fname );
+  if ( !file.open( QFile::WriteOnly ) )
+    return false;
+
+  QJsonObject root;
+  for ( QMap<QString, Section>::ConstIterator it = secMap.begin(); it != secMap.end(); ++it )
+  {
+    // note: we write all values as string, as it's enough to store resources as strings
+    // anyway resource manager converts values to strings when reading JSON file
+    QJsonObject section;
+    for ( Section::ConstIterator iter = it.value().begin(); iter != it.value().end(); ++iter )
+      section.insert( iter.key(), iter.value() );
+    root.insert( it.key(), section );
+  }
+
+  QJsonDocument document;
+  document.setObject( root );
+  file.write( document.toJson() );
+  file.close();
+  return true;
+}
+
+/*!
+  \class QtxResourceMgr::SalomexFormat
+  \internal
+  \brief Reader/writer for .salomex resources files. This is an alias for JSON format.
+*/
+
+class QtxResourceMgr::SalomexFormat : public JsonFormat
+{
+public:
+  SalomexFormat() : JsonFormat( "salomex" ) {}
+};
+
+
 /*!
   \class QtxResourceMgr::Format
   \brief Generic resources files reader/writer class.
@@ -1127,9 +1345,13 @@ bool QtxResourceMgr::Format::save( Resources* res )
   if ( !res )
     return false;
 
+  QtxResourceMgr* mgr = res->resMgr();
+
+  if ( mgr->appName().isEmpty() )
+    return false;
+
   Qtx::mkDir( Qtx::dir( res->myFileName ) );
 
-  QtxResourceMgr* mgr = res->resMgr();
   QString name = mgr ? mgr->userFileName( mgr->appName(), false ) : res->myFileName;
   return save( name, res->mySections );
 }
@@ -1168,7 +1390,7 @@ bool QtxResourceMgr::Format::save( Resources* res )
   (internationalization mechanism), load pixmaps and other resources from
   external files, etc.
 
-  Currently it supports .ini and .xml resources file formats. To implement
+  Currently it supports .ini, .xml, and .json resources file formats. To implement
   own resources file format, inherit from the Format class and implement virtual
   Format::load() and Format::save() methods.
 
@@ -1264,6 +1486,26 @@ QtxResourceMgr::QtxResourceMgr( const QString& appName, const QString& resVarTem
 
   installFormat( new XmlFormat() );
   installFormat( new IniFormat() );
+  installFormat( new JsonFormat() );
+  installFormat( new SalomexFormat() );
+
+  setOption( "translators", QString( "%P_msg_%L.qm|%P_images.qm" ) );
+}
+
+/*!
+  \brief Default constructor
+*/
+QtxResourceMgr::QtxResourceMgr()
+: myCheckExist( true ),
+  myDefaultPix( 0 ),
+  myIsPixmapCached( true ),
+  myHasUserValues( false ),
+  myWorkingMode( IgnoreUserValues )
+{
+  installFormat( new XmlFormat() );
+  installFormat( new IniFormat() );
+  installFormat( new JsonFormat() );
+  installFormat( new SalomexFormat() );
 
   setOption( "translators", QString( "%P_msg_%L.qm|%P_images.qm" ) );
 }
@@ -1339,7 +1581,7 @@ QStringList QtxResourceMgr::dirList() const
 */
 void QtxResourceMgr::initialize( const bool autoLoad ) const
 {
-  if ( !myResources.isEmpty() )
+  if ( !myResources.isEmpty() || appName().isEmpty() )
     return;
 
   QtxResourceMgr* that = (QtxResourceMgr*)this;
@@ -2189,7 +2431,7 @@ void QtxResourceMgr::setCurrentFormat( const QString& fmt )
   myFormats.removeAll( form );
   myFormats.prepend( form );
 
-  if ( myResources.isEmpty() )
+  if ( myResources.isEmpty() || appName().isEmpty() )
     return;
 
   ResList::Iterator resIt = myResources.begin();
@@ -2382,6 +2624,42 @@ bool QtxResourceMgr::save()
   return result;
 }
 
+/*!
+  \brief Load resource from given file.
+*/
+bool QtxResourceMgr::addResource( const QString& fname )
+{
+  if ( fname.isEmpty() )
+    return false;
+
+  QFileInfo fi( fname );
+
+  if ( fi.exists() && fi.isDir() && !appName().isEmpty() )
+    fi.setFile( QDir( fname ).filePath( globalFileName( appName() ) ) );
+
+  if ( !fi.exists() )
+    return false;
+
+  QString dirName = fi.absolutePath();
+  if ( myDirList.contains( dirName ) )
+    return true; // file already loaded
+
+  Format* fmt = format( fi.suffix() );
+  if ( !fmt )
+    return false;
+
+  Resources* resource = new Resources( this, fi.absoluteFilePath() );
+  if ( !fmt->load( resource ) )
+  {
+    delete resource;
+    return false;
+  }
+
+  myDirList << dirName;
+  myResources << resource;
+  return true;
+}
+
 /*!
   \brief Get all sections names.
   \return list of section names
@@ -2744,7 +3022,8 @@ void QtxResourceMgr::loadLanguage( const QString& pref, const QString& preferabl
   initialize( true );
 
   QMap<QChar, QString> substMap;
-  substMap.insert( 'A', appName() );
+  if ( !appName().isEmpty() )
+    substMap.insert( 'A', appName() );
 
   QString lang = language( preferableLanguage );
 
index 9c9ced174926ac903be482917424950bf3cc5d74..3694ae9d6968c42ddf0a022ccad36de9442edd7b 100644 (file)
@@ -53,6 +53,8 @@ class QTX_EXPORT QtxResourceMgr
 {
   class IniFormat;
   class XmlFormat;
+  class JsonFormat;
+  class SalomexFormat;
   class Resources;
 
 public:
@@ -71,6 +73,7 @@ public:
   } WorkingMode;
 
 public:
+  QtxResourceMgr();
   QtxResourceMgr( const QString&, const QString& = QString() );
   virtual ~QtxResourceMgr();
 
@@ -168,6 +171,7 @@ public:
   bool             load();
   bool             import( const QString& );
   bool             save();
+  bool             addResource( const QString& );
 
   QStringList      sections() const;
   QStringList      sections(const QRegExp&) const;
index 1fa73dfe1a7f106cb16580801a066862cd421c55..27a0c1722797f83cbf8c5af76e86f54165ec0fbb 100644 (file)
@@ -88,6 +88,7 @@ QString STD_Application::applicationName() const
 void STD_Application::start()
 {
   createActions();
+  customize();
 
   updateDesktopTitle();
   updateCommandsStatus();
@@ -270,6 +271,13 @@ void STD_Application::createActions()
   createTool( EditPasteId, stdTBar );
 }
 
+/*!
+  Customize actions.
+*/
+void STD_Application::customize()
+{
+}
+
 /*!Opens new application*/
 void STD_Application::onNewDoc()
 {
@@ -561,13 +569,13 @@ bool STD_Application::openAction( const int choice, const QString& aName )
 }
 
 /*!Save document if all ok, else error message.*/
-void STD_Application::onSaveDoc()
+bool STD_Application::onSaveDoc()
 {
   if ( !activeStudy() )
-    return;
+    return false;
 
   if ( !abortAllOperations() )
-    return;
+    return false;
 
   bool isOk = false;
   if ( activeStudy()->isSaved() )
@@ -594,7 +602,8 @@ void STD_Application::onSaveDoc()
   if ( isOk )
     studySaved( activeStudy() );
   else
-    onSaveAsDoc();
+    isOk = onSaveAsDoc();
+  return isOk;
 }
 
 /*! \retval \c true, if document saved successfully, else \c false.*/
index 25273d5f1bb95dd989868205dac42790bd60271c..4a3631c1df31cfd160f85876abef081022293fcb 100644 (file)
@@ -125,7 +125,7 @@ public slots:
   virtual bool          onNewDoc( const QString& );
 
   virtual void          onCloseDoc( bool ask = true );
-  virtual void          onSaveDoc();
+  virtual bool          onSaveDoc();
   virtual bool          onSaveAsDoc();
 
   virtual void          onOpenDoc();
@@ -154,6 +154,7 @@ protected:
 
 protected:
   virtual void          createActions();
+  virtual void          customize();
   virtual void          updateCommandsStatus();
 
   virtual void          setDesktop( SUIT_Desktop* );
index b19b0797d409a75329ce0095ec88ae5ed374261b..4b2c482a640074a023ff5facc0cbce7a7bfe2dd3 100644 (file)
@@ -174,6 +174,18 @@ int SUIT_PreferenceMgr::addItem( const QString& title, const int pId,
   return item ? item->id() : -1;
 }
 
+void SUIT_PreferenceMgr::removeItem( const QString& title )
+{
+  if ( myRoot )
+  {
+    QtxPreferenceItem* item = myRoot->findItem( title, false );
+    if ( item ) {
+      QtxPagePrefMgr::removeItem( item );
+      delete item;
+    }
+  }
+}
+
 QVariant SUIT_PreferenceMgr::optionValue( const QString& name ) const
 {
   QVariant val = QtxPagePrefMgr::optionValue( name );
index b78490e8d538c46360197a6d012e60a377eb368e..8e150a2b4c7ba45a47dacee5038b2030ebb2cf7b 100644 (file)
@@ -53,6 +53,7 @@ public:
 
   int                addItem( const QString&, const int pId = -1, const PrefItemType = Auto,
                               const QString& = QString(), const QString& = QString() );
+  void               removeItem( const QString& );
 
 protected:
   virtual QVariant   optionValue( const QString& ) const;
index 3e60d32823eb8505255bf210b33cbca40fc028a7..1e9492add077acccabba332ef823d802d49aa05a 100644 (file)
 
 #include <SALOMEDS_Tool.hxx>
 
+#include <SALOMEconfig.h>
+#include CORBA_CLIENT_HEADER(SALOME_ModuleCatalog)
+
 std::unique_ptr<SALOME_NamingService_Abstract> SalomeApp_Application::_ns;
 
 /*!Internal class that updates object browser item properties */
@@ -2156,3 +2159,19 @@ void SalomeApp_Application::ensureShaperIsActivated()
       onDesktopMessage("register_module_in_study/Shaper");
   }
 }
+
+void SalomeApp_Application::addCatalogue( const QString& moduleName, const QString& catalogue )
+{
+  CORBA::Object_var obj = namingService()->Resolve( "/Kernel/ModulCatalog" );
+  SALOME_ModuleCatalog::ModuleCatalog_var moduleCatalogue = SALOME_ModuleCatalog::ModuleCatalog::_narrow( obj );
+  QFileInfo fi( catalogue );
+  if ( !CORBA::is_nil( moduleCatalogue ) && fi.isFile() )
+  {
+    SALOME_ModuleCatalog::ListOfComponents_var known = moduleCatalogue->GetComponentList();
+    bool loaded = false;
+    for ( int i = 0; i < (int)known->length() && !loaded; i++ )
+      loaded = QString( known[i].in() ) == moduleName;
+    if ( !loaded )
+      moduleCatalogue->ImportXmlCatalogFile( catalogue.toUtf8().constData() );
+  }
+}
index c0e84fc3711f42662f6038fce114d8b25aa65483..f96590866b32f3f14572fd3ad3f2bb611b18c92e 100644 (file)
@@ -178,6 +178,8 @@ protected:
   virtual bool                        canOpenDoc( const QString& );
   virtual void                        afterCloseDoc();
 
+  virtual void                        addCatalogue( const QString&, const QString& );
+
 private slots:
   void                                onDeleteInvalidReferences();
   void                                onDblClick( SUIT_DataObject* );