Salome HOME
[bos #35160][EDF](2023-T1) Keyboard shortcuts.
authordish <dmitrii.shvydkoi@opencascade.com>
Thu, 25 Jan 2024 08:27:48 +0000 (08:27 +0000)
committerdish <dmitrii.shvydkoi@opencascade.com>
Thu, 25 Jan 2024 10:50:35 +0000 (10:50 +0000)
Extend set of action assets being loaded in advance. Add sorting of tree items in ShortcutMgr by action name.
NOTE: Shortcut manager can display icons of actions, but paths to them must be typed in JSON asset files, but they are not.

src/LightApp/LightApp_Module.cxx
src/LightApp/resources/LightApp.xml
src/Qtx/QtxShortcutEdit.cxx
src/Qtx/QtxShortcutEdit.h
src/SUIT/CMakeLists.txt
src/SUIT/SUIT_ShortcutMgr. ReadMe.md
src/SUIT/SUIT_ShortcutMgr.cxx
src/SUIT/SUIT_ShortcutMgr.h
src/SUIT/resources/action_assets.json [new file with mode: 0644]

index b90aa42fa6a1d8a7bcfc34c323e89dda2d621566..fe3abbf30febaacc0e2e4e784f4ec0301ee30ef4 100644 (file)
@@ -455,9 +455,9 @@ QtxPopupMgr* LightApp_Module::popupMgr()
 
     QAction
       *disp = createAction( -1, tr( "TOP_SHOW" ), p, tr( "MEN_SHOW" ), tr( "STB_SHOW" ),
-                            0, d, false, this, SLOT( onShowHide() ), QString("#General/Objects(s)/Show") ),
+                            0, d, false, this, SLOT( onShowHide() ), QString("#General/Object(s)/Show") ),
       *erase = createAction( -1, tr( "TOP_HIDE" ), p, tr( "MEN_HIDE" ), tr( "STB_HIDE" ),
-                             0, d, false, this, SLOT( onShowHide() ) , QString("#General/Objects(s)/Hide") ),
+                             0, d, false, this, SLOT( onShowHide() ) , QString("#General/Object(s)/Hide") ),
       *dispOnly = createAction( -1, tr( "TOP_DISPLAY_ONLY" ), p, tr( "MEN_DISPLAY_ONLY" ), tr( "STB_DISPLAY_ONLY" ),
                                 0, d, false, this, SLOT( onShowHide() ) ),
       *eraseAll = createAction( -1, tr( "TOP_ERASE_ALL" ), p, tr( "MEN_ERASE_ALL" ), tr( "STB_ERASE_ALL" ),
index e29587f4b669d5c0eef290a37a11bd6272f99d02..ac829fc9c807be2be2a2f4b41f4be8ca90efbfce 100644 (file)
     <parameter name="animation" value="0"/>
     <parameter name="size" value="300"/>
   </section>
+  <section name="action_assets">
+    <parameter name="${GUI_ROOT_DIR}/share/salome/resources/gui/action_assets.json" value=""/>
+  </section>
   <!--Salome shortcut settings
        See SUIT_ShortcutMgr for details.
   -->
     <parameter name="PRP_CREATE_NEW_WINDOW_FOR_VIEWER_6" value="Alt+A"/>
     <parameter name="PRP_CREATE_NEW_WINDOW_FOR_VIEWER_7" value="Alt+Y"/>
     <parameter name="PRP_CREATE_NEW_WINDOW_FOR_VIEWER_8" value="Alt+3"/>
-    <parameter name="#General/Objects(s)/Show" value="Ctrl+Alt+S"/>
-    <parameter name="#General/Objects(s)/Hide" value="Ctrl+Alt+H"/>
+    <parameter name="#General/Object(s)/Show" value="Ctrl+Alt+S"/>
+    <parameter name="#General/Object(s)/Hide" value="Ctrl+Alt+H"/>
     <parameter name="#Viewers/View/Set X+" value="Ctrl+Alt+B"/>
     <parameter name="#Viewers/View/Set X-" value="Ctrl+Alt+F"/>
     <parameter name="#Viewers/View/Set Y+" value="Ctrl+Alt+J"/>
     <parameter name="#Viewers/View/Rotate clockwise" value=""/>
     <parameter name="#Viewers/View/Reset" value="Ctrl+Alt+E"/>
   </section>
-  <!--Names of actions for shortcut editor
-       See SUIT_ShortcutMgr for details
-  -->
-  <section name="shortcut_translations:/PRP_DESK_CONNECT">
-      <parameter name="en" value="Connect active study"/>
-      <parameter name="fr" value="Connecter l'étude en cours"/>
-      <parameter name="ja" value="アクティブスタディの接続"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_DISCONNECT">
-      <parameter name="en" value="Disconnect the current study"/>
-      <parameter name="fr" value="Déconnecter l'étude en cours"/>
-      <parameter name="ja" value="カレントスタディの切断"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_FILE_LOAD_SCRIPT">
-      <parameter name="en" value="Load python script from file"/>
-      <parameter name="fr" value="Exécuter un script Python à partir d'un fichier"/>
-      <parameter name="ja" value="ファイルからPythonスクリプトを読込み"/>
-  </section>
-      <section name="shortcut_translations:/PRP_DESK_FILE_DUMP_STUDY">
-      <parameter name="en" value="Dump study to the python script"/>
-      <parameter name="fr" value="Génèrer le script python de l'étude"/>
-      <parameter name="ja" value="Pythonスクリプトにスタディをダンプする"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_CATALOG_GENERATOR">
-    <parameter name="en" value="Generate XML catalog of a component's interface"/>
-    <parameter name="fr" value="Générer un catalogue XML de l'interface du composant"/>
-    <parameter name="ja" value="コンポーネントインターフェイスのXMLカタログを生成"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_PREFERENCES">
-      <parameter name="en" value="Open preference window"/>
-      <parameter name="fr" value="Ouvrir la fenêtre des préférences"/>
-      <parameter name="ja" value="設定ウィンドウを開く"/>
-  </section>
-  <section name="shortcut_translations:/PRP_RENAME">
-      <parameter name="en" value="Rename active window"/>
-      <parameter name="fr" value="Renommer la fenêtre active"/>
-      <parameter name="ja" value="アクティブなウィンドウの名前を変更"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CLOSE">
-      <parameter name="en" value="Close active window"/>
-      <parameter name="fr" value="Fermer la fenêtre active"/>
-      <parameter name="ja" value="アクティブ ウィンドウを閉じる"/>
-  </section>
-  <section name="shortcut_translations:/PRP_FULLSCREEN">
-      <parameter name="en" value="Switch to full screen mode"/>
-      <parameter name="fr" value="Basculer en mode plein écran"/>
-      <parameter name="ja" value="全画面表示モードに切り替え"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_WINDOW_HSPLIT">
-      <parameter name="en" value="Split the active window on two horizontal parts"/>
-      <parameter name="fr" value="Diviser la fenêtre actuelle en deux parties horizontales"/>
-      <parameter name="ja" value="現在のウィンドウを 2つに水平分割"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_WINDOW_VSPLIT">
-      <parameter name="en" value="Split the active window on two vertical parts"/>
-      <parameter name="fr" value="Diviser la fenêtre actuelle en deux parties verticales"/>
-      <parameter name="ja" value="現在のウィンドウを2つに上下分割"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_VIEW_STATUSBAR">
-      <parameter name="en" value="Toggle status bar view on/off"/>
-      <parameter name="fr" value="Activer ou désactiver la barre de status"/>
-      <parameter name="ja" value="ステータスバーの有効/無効"/>
-  </section>
-  <section name="shortcut_translations:/PRP_DESK_HELP_ABOUT">
-      <parameter name="en" value="Show 'About' dialog"/>
-      <parameter name="fr" value="Montrer la boîte de dialogue 'A propos'"/>
-      <parameter name="ja" value="ソフト情報の表示"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_0">
-    <parameter name="en" value="Create new GL 2D view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène GL"/>
-    <parameter name="ja" value="新しい GL 2D view(G) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_1">
-    <parameter name="en" value="Create new Plot 2D view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène Plot2d"/>
-    <parameter name="ja" value="新しい Plot 2D View(P) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_2">
-    <parameter name="en" value="Create new OCC 3D view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène OCC"/>
-    <parameter name="ja" value="新しい OCC 3D View(O) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_3">
-    <parameter name="en" value="Create new VTK 3D view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène VTK"/>
-    <parameter name="ja" value="新しい VTK 3D View(K) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_4">
-    <parameter name="en" value="Create new QxScene 2D view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène QxScene"/>
-    <parameter name="ja" value="新しい シーン QxScene(S) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_5">
-    <parameter name="en" value="Create new Graphics view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène Graphiques"/>
-    <parameter name="ja" value="新しい グラフィックの表示 (r) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_6">
-    <parameter name="en" value="Create new ParaView view"/>
-    <parameter name="fr" value="Créer une nouvelle Scène ParaView"/>
-    <parameter name="ja" value="新しい ParaView 表示 (w) を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_7">
-    <parameter name="en" value="Create new Python view"/>
-    <parameter name="fr" value="Créer une nouvelle Vue Python"/>
-    <parameter name="ja" value="新しい Python view を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_8">
-    <parameter name="en" value="Create new ParaView3D view"/>
-    <parameter name="fr" value="Créer une nouvelle ParaView3D view"/>
-    <parameter name="ja" value="新しい ParaView3D view を作成します。"/>
-  </section>
-  <section name="shortcut_translations:/#General/Objects(s)/Show">
-      <parameter name="en" value="Show"/>
-      <parameter name="fr" value="Afficher"/>
-      <parameter name="ja" value="表示"/>
-  </section>
-  <section name="shortcut_translations:/#General/Objects(s)/Hide">
-      <parameter name="en" value="Hide"/>
-      <parameter name="fr" value="Cacher"/>
-      <parameter name="ja" value="非表示"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Set X+">
-      <parameter name="en" value="Back view"/>
-      <parameter name="fr" value="Vue arrière"/>
-      <parameter name="ja" value="背面図"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Set X-">
-      <parameter name="en" value="Front view"/>
-      <parameter name="fr" value="Vue de devant"/>
-      <parameter name="ja" value="正面"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Set Y+">
-      <parameter name="en" value="Left view"/>
-      <parameter name="fr" value="Vue de gauche"/>
-      <parameter name="ja" value="左側のビュー"/>
-  </section>
-   <section name="shortcut_translations:/#Viewers/View/Set Y-">
-      <parameter name="en" value="Right view"/>
-      <parameter name="fr" value="Vue de droite"/>
-      <parameter name="ja" value="右側のビュー"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Set Z+">
-      <parameter name="en" value="Bottom view"/>
-      <parameter name="fr" value="Vue de dessous"/>
-      <parameter name="ja" value="下から表示します。"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Set Z-">
-      <parameter name="en" value="Top view"/>
-      <parameter name="fr" value="Vue de dessus"/>
-      <parameter name="ja" value="上から見る"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Rotate anticlockwise">
-      <parameter name="en" value="Rotate view counterclockwise"/>
-      <parameter name="fr" value="Tourner la vue à gauche"/>
-      <parameter name="ja" value="表示を左に"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Rotate clockwise">
-      <parameter name="en" value="Rotate view clockwise"/>
-      <parameter name="fr" value="Tourner à droite"/>
-      <parameter name="ja" value="右のビューを回転させる"/>
-  </section>
-  <section name="shortcut_translations:/#Viewers/View/Reset">
-      <parameter name="en" value="Reset view point"/>
-      <parameter name="fr" value="Restaurer le point de vue"/>
-      <parameter name="ja" value="ビューのポイントを復元します。"/>
-  </section>
   <section name="ExternalBrowser" >
     <!-- External HELP browser settings -->
     <parameter name="winapplication"       value="C:\Program Files\Internet Explorer\iexplore.exe" />
index eb7431d8eafea66fc637e2fe983d49f5881cbbb4..fe845b574fc887a1406cbf6dabd6bb0c9cd57cc3 100644 (file)
@@ -37,6 +37,9 @@
 
 #include <QKeyEvent>
 #include <QKeySequence>
+#include <QCollator>
+
+#include <algorithm>
 
 
 #define COLUMN_SIZE  500
@@ -261,9 +264,10 @@ void QtxEditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID,
 const QString& QtxEditKeySequenceDialog::moduleID() const { return myModuleID; }
 const QString& QtxEditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; }
 
-void QtxEditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName)
+void QtxEditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip)
 {
   myActionName->setText("<b>" + theModuleName + "</b>&nbsp;&nbsp;" + theActionName);
+  myActionName->setToolTip(theActionToolTip);
 }
 
 void QtxEditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence)
@@ -348,6 +352,26 @@ void QtxEditKeySequenceDialog::onConfirm()
 }
 
 
+/*! \brief Compensates lack of std::distance(), which is introduced in C++17.
+\returns -1, if theIt does not belong to the  */
+template <class Container>
+size_t indexOf(
+  const Container& theContainer,
+  const typename Container::iterator& theIt
+) {
+  auto it = theContainer.begin();
+  size_t distance = 0;
+  while (it != theContainer.end()) {
+    if (it == theIt)
+      return distance;
+
+    it++;
+    distance++;
+  }
+  return -1;
+}
+
+
 /*! \param theContainer Share the same container between several trees,
 to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr.
 Pass nullptr to create non-synchronized tree. */
@@ -355,7 +379,8 @@ QtxShortcutTree::QtxShortcutTree(
   std::shared_ptr<SUIT_ShortcutContainer> theContainer,
   QWidget* theParent
 ) : QTreeWidget(theParent),
-myShortcutContainer(theContainer ? theContainer : std::shared_ptr<SUIT_ShortcutContainer>(new SUIT_ShortcutContainer()))
+myShortcutContainer(theContainer ? theContainer : std::shared_ptr<SUIT_ShortcutContainer>(new SUIT_ShortcutContainer())),
+mySortKey(QtxShortcutTree::SortKey::Name), mySortOrder(QtxShortcutTree::SortOrder::Ascending)
 {
   setColumnCount(2);
   setSelectionMode(QAbstractItemView::SingleSelection);
@@ -369,6 +394,7 @@ myShortcutContainer(theContainer ? theContainer : std::shared_ptr<SUIT_ShortcutC
     setHeaderLabels(labelMap.values());
   }
   setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead.
+  setSortingEnabled(false);
   setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
   setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
   myEditDialog = new QtxEditKeySequenceDialog(this);
@@ -436,10 +462,31 @@ std::shared_ptr<const SUIT_ShortcutContainer> QtxShortcutTree::shortcutContainer
   return myShortcutContainer;
 }
 
+/*! \brief Does not sort modules. */
+void QtxShortcutTree::sort(QtxShortcutTree::SortKey theKey, QtxShortcutTree::SortOrder theOrder)
+{
+  if (theKey == mySortKey && theOrder == mySortOrder)
+    return;
+
+  mySortKey == theKey;
+  mySortOrder = theOrder;
+
+  for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
+    const auto moduleItem = static_cast<QtxShortcutTreeFolder*>(topLevelItem(moduleIdx));
+    const auto sortedChildren = getSortedChildren(moduleItem);
+    moduleItem->takeChildren();
+
+    for (const auto childItem : sortedChildren) {
+      moduleItem->addChild(childItem);
+    }
+  }
+}
+
 /*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */
 void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees)
 {
   const auto shortcutMgr = SUIT_ShortcutMgr::get();
+  const QString lang = SUIT_ShortcutMgr::getLang();
 
   for (const QString& moduleID : myModuleIDs) {
     const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID);
@@ -447,39 +494,40 @@ void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncT
       // Do not display empty module.
       const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
       if (moduleItemAndIdx.second >= 0)
-        takeTopLevelItem(moduleItemAndIdx.second);
+        delete takeTopLevelItem(moduleItemAndIdx.second);
 
       continue;
     }
 
     const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
-    QtxShortcutTreeItem* moduleItem = moduleItemAndIdx.first;
+    QtxShortcutTreeFolder* moduleItem = moduleItemAndIdx.first;
     if (!moduleItem) {
-      moduleItem = QtxShortcutTreeItem::createFolderItem(moduleID);
-      moduleItem->setName(shortcutMgr->getModuleName(moduleID));
+      moduleItem = new QtxShortcutTreeFolder(moduleID);
+      moduleItem->setAssets(shortcutMgr->getModuleAssets(moduleID), lang);
       addTopLevelItem(moduleItem);
       moduleItem->setFlags(Qt::ItemIsEnabled);
 
+      auto sortedChildren = getSortedChildren(moduleItem);
       for (const auto& shortcut : moduleShortcuts) {
         const QString& inModuleActionID = shortcut.first;
         const QKeySequence& keySequence = shortcut.second;
         const QString keySequenceString = keySequence.toString();
 
-        auto shortcutItem = QtxShortcutTreeItem::createShortcutItem(moduleID, inModuleActionID);
-        if (!shortcutItem) {
+        auto actionItem = QtxShortcutTreeAction::create(moduleID, inModuleActionID);
+        if (!actionItem) {
           ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
           continue;
         }
 
-        shortcutItem->setName(shortcutMgr->getActionName(moduleID, inModuleActionID));
-        shortcutItem->setKeySequence(keySequenceString);
+        actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang);
+        actionItem->setKeySequence(keySequenceString);
 
         if (theHighlightModified) {
           const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
-          shortcutItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
+          actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
         }
 
-        moduleItem->addChild(shortcutItem);
+        insertChild(moduleItem, sortedChildren, actionItem);
       }
 
       moduleItem->setExpanded(true); // Make tree expanded on first show.
@@ -487,7 +535,7 @@ void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncT
     else /* if the tree has the module-item */ {
       for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
         // Update exisiting items of a module.
-        QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
+        QtxShortcutTreeAction* const childItem = static_cast<QtxShortcutTreeAction*>(moduleItem->child(childIdx));
         const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID);
         if (itShortcut == moduleShortcuts.end()) {
           // Shortcut of the item has been removed from myShortcutContainer - impossible.
@@ -509,41 +557,31 @@ void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncT
       // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot
       // to add shortcuts for registered actions to resource files.
       if (moduleItem->childCount() < moduleShortcuts.size()) {
-        // Module shortcuts and tree items must be ordered with the same comparator. Now it is std::less(inModuleActionID_A, inModuleActionID_B).
-        std::set<QString> actionIDsOfItems;
-        for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
-          QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
-          actionIDsOfItems.emplace(childItem->myInModuleActionID);
-        }
-
+        auto sortedChildren = getSortedChildren(moduleItem);
         for (const auto& shortcut : moduleShortcuts) {
           const QString& inModuleActionID = shortcut.first;
-          const QKeySequence& keySequence = shortcut.second;
-
-          auto itNewActionID = actionIDsOfItems.emplace(inModuleActionID).first;
-          int newItemIdx = 0;
-          // Replace this with std::distance if C++ >= 17.
-          auto it = actionIDsOfItems.begin();
-          while (it != itNewActionID) {
-            it++;
-            newItemIdx++;
+          const auto predicate = [&inModuleActionID](const QtxShortcutTreeItem* const theItem) -> bool {
+            return static_cast<const QtxShortcutTreeAction* const>(theItem)->myInModuleActionID == inModuleActionID;
+          };
+
+          if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) {
+            const auto actionItem = QtxShortcutTreeAction::create(moduleID, inModuleActionID);
+            if (!actionItem) {
+              ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
+              continue;
+            }
+
+            const QKeySequence& keySequence = shortcut.second;
+            actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang);
+            actionItem->setKeySequence(keySequence.toString());
+
+            if (theHighlightModified) {
+              const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
+              actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
+            }
+
+            insertChild(moduleItem, sortedChildren, actionItem);
           }
-
-          const auto shortcutItem = QtxShortcutTreeItem::createShortcutItem(moduleID, inModuleActionID);
-          if (!shortcutItem) {
-            ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
-            continue;
-          }
-
-          shortcutItem->setName(shortcutMgr->getActionName(moduleID, inModuleActionID));
-          shortcutItem->setKeySequence(keySequence.toString());
-
-          if (theHighlightModified) {
-            const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
-            shortcutItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
-          }
-
-          moduleItem->insertChild(newItemIdx, shortcutItem);
         }
       }
     }
@@ -565,26 +603,83 @@ void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncT
 
 /*! \returns Pointer and index of top-level item.
 If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */
-std::pair<QtxShortcutTreeItem*, int> QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const
+std::pair<QtxShortcutTreeFolder*, int> QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const
 {
   for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
-    QtxShortcutTreeItem* moduleItem = static_cast<QtxShortcutTreeItem*>(topLevelItem(moduleIdx));
+    QtxShortcutTreeFolder* moduleItem = static_cast<QtxShortcutTreeFolder*>(topLevelItem(moduleIdx));
     if (moduleItem->myModuleID == theModuleID)
-      return std::pair<QtxShortcutTreeItem*, int>(moduleItem, moduleIdx);
+      return std::pair<QtxShortcutTreeFolder*, int>(moduleItem, moduleIdx);
   }
-  return std::pair<QtxShortcutTreeItem*, int>(nullptr, -1);
+  return std::pair<QtxShortcutTreeFolder*, int>(nullptr, -1);
+}
+
+/*! \returns Children of theParentItem being sorted according to current sort mode and order. */
+std::set<QtxShortcutTreeItem*, std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)>> QtxShortcutTree::getSortedChildren(QtxShortcutTreeFolder* theParentItem)
+{
+  QList<std::pair<QtxShortcutTree::SortKey, QtxShortcutTree::SortOrder>> sortSchema = QtxShortcutTree::DEFAULT_SORT_SCHEMA;
+  {
+    for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) {
+      if (itSameKey->first == mySortKey) {
+        sortSchema.erase(itSameKey);
+        break;
+      }
+    }
+    sortSchema.push_front(std::pair<QtxShortcutTree::SortKey, QtxShortcutTree::SortOrder>(mySortKey, mySortOrder));
+  }
+
+  static const QCollator collator;
+  const std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)> comparator =
+  [this, sortSchema, &collator](const QtxShortcutTreeItem* theItemA, const QtxShortcutTreeItem* theItemB) {
+    int res = 0;
+    for (const auto& keyAndOrder : sortSchema) {
+      int res = 0;
+      res = collator.compare(theItemA->getValue(keyAndOrder.first), theItemB->getValue(keyAndOrder.first));
+      if (res != 0)
+        return keyAndOrder.second == QtxShortcutTree::SortOrder::Ascending ? res < 0 : res > 0;
+    }
+    return false;
+  };
+
+  std::set<QtxShortcutTreeItem*, std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)>> sortedChildren(comparator);
+  for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) {
+    QtxShortcutTreeAction* const childItem = static_cast<QtxShortcutTreeAction*>(theParentItem->child(childIdx));
+    sortedChildren.emplace(childItem);
+  }
+  return sortedChildren;
+}
+
+/*! \brief Inserts theChildItem to theParentItem and theSortedChildren.
+Does not check whether theSortedChildren are actually child items of theParentItem.
+Does not check whether current item sort schema is same as one of theSortedChildren. */
+void QtxShortcutTree::insertChild(
+  QtxShortcutTreeFolder* theParentItem,
+  std::set<QtxShortcutTreeItem*, std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)>>& theSortedChildren,
+  QtxShortcutTreeItem* theChildItem
+) {
+  auto emplaceRes = theSortedChildren.emplace(theChildItem);
+  theParentItem->insertChild(indexOf(theSortedChildren, emplaceRes.first), theChildItem);
 }
 
 void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx)
 {
-  QtxShortcutTreeItem* const item = static_cast<QtxShortcutTreeItem*>(theItem);
-  // Do not react if folder-item is clicked.
-  if (item->isFolder())
-    return;
+  {
+    QtxShortcutTreeItem* const item = static_cast<QtxShortcutTreeItem*>(theItem);
+    // Do not react if folder-item is clicked.
+    if (item->type() != QtxShortcutTreeItem::Type::Action)
+      return;
+  }
 
-  myEditDialog->setModuleAndActionID(item->myModuleID, item->myInModuleActionID);
-  myEditDialog->setModuleAndActionName(static_cast<QtxShortcutTreeItem*>(item->parent())->name(), item->name());
-  myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(item->keySequence()));
+  QtxShortcutTreeAction* const actionItem = static_cast<QtxShortcutTreeAction*>(theItem);
+
+  myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID);
+  QString actionToolTip = actionItem->toolTip(QtxShortcutTree::ElementIdx::Name);
+  actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1);
+  myEditDialog->setModuleAndActionName(
+    static_cast<QtxShortcutTreeItem*>(actionItem->parent())->name(),
+    actionItem->name(),
+    actionToolTip
+  );
+  myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(actionItem->keySequence()));
   myEditDialog->updateConflictsMessage();
   const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted;
 
@@ -594,11 +689,11 @@ void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColId
   const QKeySequence newKeySequence = myEditDialog->editedKeySequence();
 
   /** { moduleID, inModuleActionID }[] */
-  std::set<std::pair<QString, QString>> disabledActionIDs = myShortcutContainer->setShortcut(item->myModuleID, item->myInModuleActionID, newKeySequence, true /*override*/);
+  std::set<std::pair<QString, QString>> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/);
 
   /** { moduleID, {inModuleActionID, keySequence}[] }[] */
   std::map<QString, std::map<QString, QString>> changes;
-  changes[item->myModuleID][item->myInModuleActionID] = newKeySequence.toString();
+  changes[actionItem->myModuleID][actionItem->myInModuleActionID] = newKeySequence.toString();
   for (const auto moduleAndActionID : disabledActionIDs) {
     changes[moduleAndActionID.first][moduleAndActionID.second] = QString();
   }
@@ -617,7 +712,7 @@ void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColId
 
     // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences.
     for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
-      QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
+      QtxShortcutTreeAction* const childItem = static_cast<QtxShortcutTreeAction*>(moduleItem->child(childIdx));
       const auto itChange = moduleChanges.find(childItem->myInModuleActionID);
       if (itChange == moduleChanges.end()) {
         // The shortcut has not been changed.
@@ -632,73 +727,157 @@ void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColId
   }
 }
 
+/*static*/ const QList<std::pair<QtxShortcutTree::SortKey, QtxShortcutTree::SortOrder>> QtxShortcutTree::DEFAULT_SORT_SCHEMA =
+{
+  {QtxShortcutTree::SortKey::Name, QtxShortcutTree::SortOrder::Ascending},
+  {QtxShortcutTree::SortKey::ToolTip, QtxShortcutTree::SortOrder::Ascending},
+  {QtxShortcutTree::SortKey::KeySequence, QtxShortcutTree::SortOrder::Ascending},
+  {QtxShortcutTree::SortKey::ID, QtxShortcutTree::SortOrder::Ascending}
+};
+
 /*static*/ std::map<SUIT_ShortcutContainer*, std::set<QtxShortcutTree*>> QtxShortcutTree::instances =
 std::map<SUIT_ShortcutContainer*, std::set<QtxShortcutTree*>>();
 
 
-QtxShortcutTreeItem::QtxShortcutTreeItem(const QString& theModuleID, const QString& theInModuleActionID)
-: QTreeWidgetItem(), myModuleID(theModuleID), myInModuleActionID(theInModuleActionID)
+
+QtxShortcutTreeItem::QtxShortcutTreeItem(const QString& theModuleID)
+: QTreeWidgetItem(), myModuleID(theModuleID)
 { }
 
-/*static*/ QtxShortcutTreeItem* QtxShortcutTreeItem::createFolderItem(const QString& theModuleID)
+QString QtxShortcutTreeItem::name() const
 {
-  auto item = new QtxShortcutTreeItem(theModuleID, QString());
+  return text(QtxShortcutTree::ElementIdx::Name);
+}
 
-  QFont font = item->font(QtxShortcutTree::ElementIdx::Name);
-  font.setBold(true);
-  item->setFont(QtxShortcutTree::ElementIdx::Name, font);
 
-  return item;
+QtxShortcutTreeFolder::QtxShortcutTreeFolder(const QString& theModuleID)
+: QtxShortcutTreeItem(theModuleID)
+{
+  QFont f = font(QtxShortcutTree::ElementIdx::Name);
+  f.setBold(true);
+  setFont(QtxShortcutTree::ElementIdx::Name, f);
+  setText(QtxShortcutTree::ElementIdx::Name, theModuleID);
 }
 
-/*! \returns nullptr if theInModuleActionID is empty. */
-/*static*/ QtxShortcutTreeItem* QtxShortcutTreeItem::createShortcutItem(const QString& theModuleID, const QString& theInModuleActionID)
+void QtxShortcutTreeFolder::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
 {
-  if (theInModuleActionID.isEmpty()) {
-    ShCutDbg("QtxShortcutTreeItem: attempt to create item with empty action ID.");
-    return nullptr;
+  if (!theAssets)
+    return;
+
+  setIcon(QtxShortcutTree::ElementIdx::Name, theAssets->myIcon);
+
+  const auto& ldaMap = theAssets->myLangDependentAssets;
+  if (ldaMap.empty()) {
+    setText(QtxShortcutTree::ElementIdx::Name, myModuleID);
+    return;
   }
 
-  auto item = new QtxShortcutTreeItem(theModuleID, theInModuleActionID);
-  item->setToolTip(QtxShortcutTree::ElementIdx::KeySequence, QtxShortcutTree::tr("Double click to edit key sequence."));
-  return item;
+  auto itLDA = ldaMap.find(theLang);
+  if (itLDA == ldaMap.end())
+    itLDA = ldaMap.begin();
+
+  const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
+  const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName;
+  setText(QtxShortcutTree::ElementIdx::Name, name);
 }
 
-bool QtxShortcutTreeItem::isFolder() const
+QString QtxShortcutTreeFolder::getValue(QtxShortcutTree::SortKey theKey) const
 {
-  return myInModuleActionID.isEmpty();
+  switch (theKey) {
+    case QtxShortcutTree::SortKey::ID:
+      return myModuleID;
+    case QtxShortcutTree::SortKey::Name:
+      return name();
+    case QtxShortcutTree::SortKey::ToolTip:
+      return name();
+    default:
+      return QString();
+  }
 }
 
-/*! \brief Highlights text at ElementIdx::KeySequence. */
-void QtxShortcutTreeItem::highlightKeySequenceAsModified(bool theHighlight)
-{
-  static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen);
-  static const QBrush fgHighlitingBrush = QBrush(Qt::white);
-  static const QBrush noBrush = QBrush();
 
-  setBackground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush);
-  setForeground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush);
+QtxShortcutTreeAction::QtxShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID)
+: QtxShortcutTreeItem(theModuleID), myInModuleActionID(theInModuleActionID)
+{
+  setText(QtxShortcutTree::ElementIdx::Name, theInModuleActionID);
+  setToolTip(
+    QtxShortcutTree::ElementIdx::Name,
+    theInModuleActionID + (theInModuleActionID.at(theInModuleActionID.length()-1) == "." ? "\n" : ".\n") + QtxShortcutTree::tr("Double click to edit key sequence.")
+  );
+  setToolTip(QtxShortcutTree::ElementIdx::KeySequence, QtxShortcutTree::tr("Double click to edit key sequence."));
 }
 
-void QtxShortcutTreeItem::setName(const QString& theName)
+/*static*/ QtxShortcutTreeAction* QtxShortcutTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID)
 {
-  const QString& name = theName.isEmpty() ? myInModuleActionID : theName;
-  setText(QtxShortcutTree::ElementIdx::Name, name);
-  if (!isFolder())
-    setToolTip(QtxShortcutTree::ElementIdx::Name, name + (name.at(name.length()-1) == "." ? "\n" : ".\n") + QtxShortcutTree::tr("Double click to edit key sequence."));
+  if (theInModuleActionID.isEmpty()) {
+    ShCutDbg("QtxShortcutTreeItem: attempt to create item with empty action ID.");
+    return nullptr;
+  }
+
+  return new QtxShortcutTreeAction(theModuleID, theInModuleActionID);
 }
 
-QString QtxShortcutTreeItem::name() const
+void QtxShortcutTreeAction::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
 {
-  return text(QtxShortcutTree::ElementIdx::Name);
+  if (!theAssets)
+    return;
+
+  setIcon(QtxShortcutTree::ElementIdx::Name, theAssets->myIcon);
+
+  const auto& ldaMap = theAssets->myLangDependentAssets;
+  if (ldaMap.empty()) {
+    setText(QtxShortcutTree::ElementIdx::Name, myInModuleActionID);
+    return;
+  }
+
+  auto itLDA = ldaMap.find(theLang);
+  if (itLDA == ldaMap.end())
+    itLDA = ldaMap.begin();
+
+  const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
+  const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName;
+  setText(QtxShortcutTree::ElementIdx::Name, name);
+
+  const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip;
+  setToolTip(
+    QtxShortcutTree::ElementIdx::Name,
+    actionToolTip + (actionToolTip.at(actionToolTip.length()-1) == "." ? "\n" : ".\n") + QtxShortcutTree::tr("Double click to edit key sequence.")
+  );
+}
+
+QString QtxShortcutTreeAction::getValue(QtxShortcutTree::SortKey theKey) const
+{
+  switch (theKey) {
+    case QtxShortcutTree::SortKey::ID:
+      return myInModuleActionID;
+    case QtxShortcutTree::SortKey::Name:
+      return name();
+    case QtxShortcutTree::SortKey::ToolTip:
+      return toolTip(QtxShortcutTree::ElementIdx::Name);
+    case QtxShortcutTree::SortKey::KeySequence:
+      return keySequence();
+    default:
+      return QString();
+  }
 }
 
-void QtxShortcutTreeItem::setKeySequence(const QString& theKeySequence)
+void QtxShortcutTreeAction::setKeySequence(const QString& theKeySequence)
 {
   setText(QtxShortcutTree::ElementIdx::KeySequence, theKeySequence);
 }
 
-QString QtxShortcutTreeItem::keySequence() const
+QString QtxShortcutTreeAction::keySequence() const
 {
   return text(QtxShortcutTree::ElementIdx::KeySequence);
+}
+
+/*! \brief Highlights text at ElementIdx::KeySequence. */
+void QtxShortcutTreeAction::highlightKeySequenceAsModified(bool theHighlight)
+{
+  static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen);
+  static const QBrush fgHighlitingBrush = QBrush(Qt::white);
+  static const QBrush noBrush = QBrush();
+
+  setBackground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush);
+  setForeground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush);
 }
\ No newline at end of file
index 71e0d9d938b9727959f834289796c858d0a4b101..862c80d8610ad936e80ee3670cc5201fc065822c 100644 (file)
@@ -28,6 +28,8 @@
 #include <memory>
 #include <map>
 #include <set>
+#include <functional>
+
 
 class QLineEdit;
 class QLabel;
@@ -77,6 +79,8 @@ private:
 
 class QtxShortcutTree;
 class QtxShortcutTreeItem;
+class QtxShortcutTreeFolder;
+class QtxShortcutTreeAction;
 class QTextEdit;
 
 
@@ -94,7 +98,7 @@ public:
   const QString& moduleID() const;
   const QString& inModuleActionID() const;
 
-  void setModuleAndActionName(const QString& theModuleName, const QString& theActionName);
+  void setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip = "");
 
   void setConfirmedKeySequence(const QKeySequence& theSequence);
   QKeySequence editedKeySequence() const;
@@ -125,7 +129,19 @@ class QTX_EXPORT QtxShortcutTree : public QTreeWidget
 public:
   enum ElementIdx {
     Name = 0,
-    KeySequence = 1, // Empty, if item is used as folder.
+    KeySequence = 1, // Empty, if item is folder item.
+  };
+
+  enum class SortKey {
+    ID,
+    Name,
+    ToolTip,
+    KeySequence,
+  };
+
+  enum class SortOrder {
+    Ascending,
+    Descending
   };
 
   QtxShortcutTree(
@@ -142,23 +158,38 @@ public:
 
   std::shared_ptr<const SUIT_ShortcutContainer> shortcutContainer() const;
 
+  void sort(QtxShortcutTree::SortKey theKey, QtxShortcutTree::SortOrder theOrder);
+
 private:
   void updateItems(bool theHighlightModified, bool theUpdateSyncTrees);
-  std::pair<QtxShortcutTreeItem*, int> findModuleFolderItem(const QString& theModuleID) const;
+  std::pair<QtxShortcutTreeFolder*, int> findModuleFolderItem(const QString& theModuleID) const;
+
+  std::set<QtxShortcutTreeItem*, std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)>> getSortedChildren(QtxShortcutTreeFolder* theParentItem);
+
+  void insertChild(
+    QtxShortcutTreeFolder* theParentItem,
+    std::set<QtxShortcutTreeItem*, std::function<bool(QtxShortcutTreeItem*, QtxShortcutTreeItem*)>>& theSortedChildren,
+    QtxShortcutTreeItem* theChildItem
+  );
 
 private slots:
   void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx);
 
 public:
-  /** Keeps IDs of modules, which will be shown on setShortcutsFromManager(). */
+  /** Keeps IDs of modules, which will are shown on setShortcutsFromManager(). */
   std::set<QString> myModuleIDs;
 
+  static const QList<std::pair<QtxShortcutTree::SortKey, QtxShortcutTree::SortOrder>> DEFAULT_SORT_SCHEMA;
+
 private:
   /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. */
   const std::shared_ptr<SUIT_ShortcutContainer> myShortcutContainer;
 
   QtxEditKeySequenceDialog* myEditDialog;
 
+  QtxShortcutTree::SortKey mySortKey;
+  QtxShortcutTree::SortOrder mySortOrder;
+
   /**
    * Ensures that, if several QtxShortcutTree instances coexist,
    * all of them are updated when one of them applies pending changes to SUIT_ShortcutMgr.
@@ -174,25 +205,61 @@ private:
 
 class QtxShortcutTreeItem : public QTreeWidgetItem
 {
-private:
-  QtxShortcutTreeItem(const QString& theModuleID, const QString& theInModuleActionID);
+public:
+  enum Type {
+    Folder = 0,
+    Action = 1,
+  };
+
+protected:
+  QtxShortcutTreeItem(const QString& theModuleID);
 
 public:
-  static QtxShortcutTreeItem* createFolderItem(const QString& theModuleID);
-  static QtxShortcutTreeItem* createShortcutItem(const QString& theModuleID, const QString& theInModuleActionID);
   virtual ~QtxShortcutTreeItem() = default;
+  virtual QtxShortcutTreeItem::Type type() const = 0;
 
-  bool isFolder() const;
-  void highlightKeySequenceAsModified(bool theHighlight);
-
-  void setName(const QString& theName);
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang) = 0;
   QString name() const;
 
+  virtual QString getValue(QtxShortcutTree::SortKey theKey) const = 0;
+
+public:
+  const QString myModuleID;
+};
+
+
+class QtxShortcutTreeFolder : public QtxShortcutTreeItem
+{
+public:
+  QtxShortcutTreeFolder(const QString& theModuleID);
+  virtual ~QtxShortcutTreeFolder() = default;
+  virtual QtxShortcutTreeItem::Type type() const { return QtxShortcutTreeItem::Type::Folder; };
+
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang);
+
+  virtual QString getValue(QtxShortcutTree::SortKey theKey) const;
+};
+
+
+class QtxShortcutTreeAction : public QtxShortcutTreeItem
+{
+private:
+  QtxShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID);
+
+public:
+  static QtxShortcutTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID);
+  virtual ~QtxShortcutTreeAction() = default;
+  virtual QtxShortcutTreeItem::Type type() const { return QtxShortcutTreeItem::Type::Action; };
+
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang);
+
+  virtual QString getValue(QtxShortcutTree::SortKey theKey) const;
+
   void setKeySequence(const QString& theKeySequence);
   QString keySequence() const;
+  void highlightKeySequenceAsModified(bool theHighlight);
 
-  const QString myModuleID;
-  const QString myInModuleActionID; // Empty, if item is used as folder.
+  const QString myInModuleActionID;
 };
 
 #endif // QTXSHORTCUTTREE_H
index 10c9f0079f6fe70e58149fb71da67dddfb2640e7..6c7c3f6af0a215c70d008c62d050b781df445c5c 100644 (file)
@@ -37,28 +37,28 @@ SET(_link_LIBRARIES ${PLATFORM_LIBS} ${QT_LIBRARIES} qtx ObjBrowser)
 # --- headers ---
 
 # header files / to be processed by moc
-SET(_moc_HEADERS   
-  SUIT_Accel.h  
-  SUIT_ActionOperation.h 
-  SUIT_Application.h 
-  SUIT_AutoRotate.h 
-  SUIT_DataBrowser.h 
-  SUIT_DataObject.h 
-  SUIT_Desktop.h 
-  SUIT_FileDlg.h 
+SET(_moc_HEADERS
+  SUIT_Accel.h
+  SUIT_ActionOperation.h
+  SUIT_Application.h
+  SUIT_AutoRotate.h
+  SUIT_DataBrowser.h
+  SUIT_DataObject.h
+  SUIT_Desktop.h
+  SUIT_FileDlg.h
   SUIT_LicenseDlg.h
   SUIT_MessageBox.h
-  SUIT_Operation.h 
-  SUIT_PopupClient.h 
-  SUIT_PreferenceMgr.h 
-  SUIT_SelectionMgr.h 
-  SUIT_Session.h 
+  SUIT_Operation.h
+  SUIT_PopupClient.h
+  SUIT_PreferenceMgr.h
+  SUIT_SelectionMgr.h
+  SUIT_Session.h
   SUIT_ShortcutMgr.h
   SUIT_Study.h
-  SUIT_TreeModel.h 
-  SUIT_ViewManager.h 
-  SUIT_ViewModel.h 
-  SUIT_ViewWindow.h 
+  SUIT_TreeModel.h
+  SUIT_ViewManager.h
+  SUIT_ViewModel.h
+  SUIT_ViewWindow.h
 )
 
 # header files / no moc processing
@@ -97,6 +97,7 @@ SET(_other_RESOURCES
   resources/icon_visibility_on.png
   resources/icon_visibility_off.png
   resources/view_sync.png
+  resources/action_assets.json
 )
 
 # --- sources ---
@@ -153,4 +154,3 @@ INSTALL(FILES ${suit_HEADERS} DESTINATION ${SALOME_INSTALL_HEADERS})
 QT_INSTALL_TS_RESOURCES("${_ts_RESOURCES}" "${SALOME_GUI_INSTALL_RES_DATA}")
 
 INSTALL(FILES ${_other_RESOURCES} DESTINATION ${SALOME_GUI_INSTALL_RES_DATA})
-
index 65702e7215a6ee4d0f01ab30c0cdd84dc6caf899..d0cc5adc8c796e0685f9f80d6633a012be80c753 100644 (file)
@@ -6,15 +6,15 @@ Hot keys must be considered as resources, being shared between all components of
 
 To (de)serialize shortcut preferences without dependence on language environment, shortcuts must be stored as pairs {action ID, key sequence}, where action IDs must be application-unique.
 
-Since desktop shortcuts may also be changed and interfere with shortcuts of modules, `QtxShortcutTree` should always display desktop shortcuts and shortcuts of all modules altogether, even if some modules are inactive. It means, that `QtxShortcutTree` must be fed not only with shortcut data {action ID, key sequence}[], but also with dictionaries {action ID, action name}[].
+Since desktop shortcuts may also be changed and interfere with shortcuts of modules, `QtxShortcutTree` should always display desktop shortcuts and shortcuts of all modules altogether, even if some modules are inactive. It means, that `QtxShortcutTree` must be fed not only with shortcut data {action ID, key sequence}[], but also with dictionaries {action ID, action name}[]. `QtxShortcutTree` also requires other action assets - tool tip and icon path.
 
-Names of actions may be retrieved from instances of actions, but there is a pitfall: if a module has not been activated yet, its actions have not been initialized either.
-Qt Linguist is no help in this case too. To retrieve an action name using `QObject::tr(actionID)`, the `tr(const char*)` method must be called with instance of the class, which is designated as a context for the actionID in *.ts files. And contexts are usually descendants of SUIT_Application and CAM_Module. Again, until a module instance is created, there is no way for `SUIT_ShortcutMgr` to get even a name of a context-class, which an action with an ID belongs to, without any additional data. Straightforward mechanism for dynamic translation from action IDs to action names has been devised: for all actions, which are bound by default or may be bound by user to hotkeys, dictionaries must be placed into resource files. People who do/refine localizations should keep this in mind and also process entries like `<section name="shortcut_translations:action ID">` of resource files.
+Assets of actions may be retrieved from instances of actions, but there is a pitfall: if a module has not been activated yet, its actions have not been initialized either.
+Qt Linguist is no help in this case too. To retrieve an action name using `QObject::tr(actionID)`, the `tr(const char*)` method must be called with instance of the class, which is designated as a context for the actionID in *.ts files. And contexts are usually descendants of SUIT_Application and CAM_Module. Again, until a module instance is created, there is no way for `SUIT_ShortcutMgr` to get even a name of a context-class, which an action with an ID belongs to, without any additional data. Straightforward mechanism for loading of action assets in advance has been devised: for all actions, which are bound by default or may be bound by user to hotkeys, assets must be placed into asset files. People who do/refine localizations should keep this in mind and also process JSON files, which are referred in resource files in sections `<section name="action_assets">`.
 
-To alleviate process of composing resources, a development tool `DevTools` has been made. It grabs all shortcuts and status tips of intercepted at runtime actions with valid IDs and dumps results into XML files with identical to project-conventional resource files structure. Run modules/features of interest, switch application language, and if IDs or names in the selected language of some actions of the module are not added yet to preference files, these dump files will be appended with new data – shortcuts and translations, if the last ones are provided in *.ts files.
-The tool also logs assets (text, tool tip, status tip, bound key sequence, etc.) of intercepted actions with invalid IDs. The intent is as follows: run modules/features of interest at several languages in exactly the same sequence of actions, paste content of resulting language-dedicated *.csv files to corresponding sheet of  [“ShortcutMgr. Resource generator.xlsx”](../../tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx). Then come up with proper IDs for the actions, type the action IDs and their module IDs into corresponding columns and take away ready resource items.
+To alleviate process of composing resource and asset files, a development tool `DevTools` has been made. It grabs all shortcuts and assets (except icon path) of intercepted at runtime actions with valid IDs and dumps shortcuts into XML files with identical to project-conventional resource files structure and assets into properly-formatted JSON files. Run modules/features of interest, switch application language, and if IDs or names in the selected language of some actions of the module are not added yet to preference files, these dump files will be appended with new data – shortcuts and assets, if the last ones are provided in *.ts files.
+The tool also logs assets of intercepted actions with invalid IDs. The intent is as follows: run modules/features of interest at several languages in exactly the same sequence of actions, paste content of resulting language-dedicated *.csv files to corresponding sheet of  [“ShortcutMgr. Resource generator.xlsx”](../../tools/DevTools/ShortcutMgr/ShortcutMgr. Resource generator.xlsx). Then come up with proper IDs for the actions, type the action IDs and their module IDs into corresponding columns and take away ready resource and asset items.
 
-Shortcuts and translations for all actions of SHAPER and GEOM and those actions of desktop, which were bound (hardcoded) to non-empty key sequences, have been added to resource files. Now they are available for hot key remapping via GUI, no conflicts guaranteed. Any hardcoded shortcut is disabled, unless the same shortcut persists in resource files.
+Shortcuts and assets for all actions of SHAPER and GEOM and those actions of desktop, which were bound (hardcoded) to non-empty key sequences, have been added to resource files. Now they are available for hot key remapping via GUI, no conflicts guaranteed. Any hardcoded shortcut is disabled, unless the same shortcut persists in resource files.
 
 ## Possible conflicts between shortcuts of desktop and modules, except SHAPER and GEOM
 
index 501f050f903b4b411496625baec2844134188c4c..bfae01ec37a16085c86d705c67fba665d7801f2e 100644 (file)
 #include <QActionEvent>
 #include <QKeySequence>
 #include <QDebug>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QFile>
+#include <QProcessEnvironment>
+#include <QRegExp>
+#include <Qtx.h>
 
 #include <list>
 
@@ -82,17 +89,16 @@ const QString DEFAULT_LANG = QString("en");
 const QStringList LANG_PRIORITY_LIST = QStringList({DEFAULT_LANG, "fr"});
 const QString LANG_SECTION = QString("language");
 
-/** Prefix of names of sections in preference files with shortcut actions' names. */
-static const QString SECTION_SHORTCUT_NAMES_PREFIX = QString("shortcut_translations");
+static const QString SECTION_NAME_ACTION_ASSET_FILE_PATHS = QString("action_assets");
 
 
 
 /**
- * Uncomment this, to start collecting all shortcuts and action name translations (1),
- * from instances of QtxActions, if a shortcut or name translation is absent in resource files.
+ * Uncomment this, to start collecting all shortcuts and action assets (1),
+ * from instances of QtxActions, if a shortcut or action assets are absent in resource/asset files.
  *
  * (1) Set required language in the application settings and run features of interest.
- * For all actions from these features, their statusTip()s will be dumped to appropriate places in dump files.
+ * For all actions from these features, their assets will be dumped to appropriate places in dump files.
  *
  * Content of dump files is appended on every run. Files are located in "<APP_DIR>/shortcut_mgr_dev/".
 */
@@ -100,6 +106,7 @@ static const QString SECTION_SHORTCUT_NAMES_PREFIX = QString("shortcut_translati
 #ifdef SHORTCUT_MGR_DEVTOOLS
 #include <QDir>
 #include <QFile>
+#include <QFileInfo>
 #include <QTextStream>
 #include "QtxMap.h"
 #include <functional>
@@ -109,10 +116,10 @@ static const QString SECTION_SHORTCUT_NAMES_PREFIX = QString("shortcut_translati
 #include <QDomNode>
 #endif // QT_NO_DOM
 
-/*! \brief Generates XML files with appearing in runtime shortcuts,
+/*! \brief Generates XML files with appearing at runtime shortcuts,
     using key sequences of QActions passed to the shortcut manager,
-    and translations, using QAction::statusTip(), of QtxActions passed to the shortcut manager.
-    Content of these files can be easily copied to resource files. */
+    and JSON files with assets of QtxActions passed to the shortcut manager.
+    Content of these files can be easily copied to resource/asset files. */
 class DevTools
 {
 private:
@@ -127,6 +134,11 @@ public:
       delete fileNameAndPtrs.second.second;
       delete fileNameAndPtrs.second.first;
     }
+
+    for (const auto& fileNameAndPtrs : myJSONFilesAndDocs) {
+      delete fileNameAndPtrs.second.second;
+      delete fileNameAndPtrs.second.first;
+    }
   }
 
   static DevTools* get() {
@@ -163,60 +175,39 @@ public:
     }
   }
 
-  void collectTranslation(
+  void collectAssets(
     const QString& theModuleID,
     const QString& theInModuleActionID,
     const QString& theLang,
-    const QString& theActionName
+    const QAction* theAction
   ) {
     if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) {
       QString actionID = SUIT_ShortcutMgr::makeActionID(ROOT_MODULE_ID, theInModuleActionID);
-      // { actionID, {lang, actionName}[] } []
-      auto& moduleTranslations = myTranslationsOfMetaActions[theModuleID];
+      // { actionID, assets } []
+      auto& moduleAssets = myAssetsOfMetaActions[theModuleID];
 
-      // {lang, actionName}[]
-      auto& actionTranslations = moduleTranslations[actionID];
-      actionTranslations[theLang] = theActionName;
+      auto& actionAssets = moduleAssets[actionID];
+      actionAssets.myLangDependentAssets[theLang].myName = theAction->text();
+      actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip();
 
-      const QString fileName = theModuleID + DevTools::TRANSLATIONS_OF_META_SUFFIX;
-      std::map<QString, std::map<QString, QString>> sections;
-      for (auto itAction = moduleTranslations.begin(); itAction != moduleTranslations.end(); itAction++) {
-        const QString sectionName = SECTION_SHORTCUT_NAMES_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + ROOT_MODULE_ID;
-
-        // {lang, actionName}[]
-        std::map<QString, QString>& actionTranslations = itAction->second;
-        std::map<QString, QString>& section = sections[sectionName];
-        for (auto itTranslation = actionTranslations.begin(); itTranslation != actionTranslations.end(); itTranslation++) {
-          section[itTranslation->first] = itTranslation->second;
-        }
-      }
-      writeToXMLFile(fileName, sections);
+      const QString fileName = theModuleID + DevTools::ASSETS_OF_META_SUFFIX;
+      writeToJSONFile(fileName, actionID, actionAssets);
     }
     else {
       QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
-      // { actionID, {lang, actionName}[] } []
-      auto& moduleTranslations = myTranslations[theModuleID];
-      // {lang, actionName}[]
-      auto& actionTranslations = moduleTranslations[actionID];
-      actionTranslations[theLang] = theActionName;
+      // { actionID, assets } []
+      auto& moduleAssets = myAssets[theModuleID];
 
-      const QString fileName = theModuleID + DevTools::TRANSLATIONS_SUFFIX;
-      std::map<QString, std::map<QString, QString>> sections;
-      for (auto itAction = moduleTranslations.begin(); itAction != moduleTranslations.end(); itAction++) {
-        const QString sectionName = SECTION_SHORTCUT_NAMES_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + itAction->first;
-
-        // {lang, actionName}[]
-        std::map<QString, QString>& actionTranslations = itAction->second;
-        std::map<QString, QString>& section = sections[sectionName];
-        for (auto itTranslation = actionTranslations.begin(); itTranslation != actionTranslations.end(); itTranslation++) {
-          section[itTranslation->first] = itTranslation->second;
-        }
-      }
-      writeToXMLFile(fileName, sections);
+      auto& actionAssets = moduleAssets[actionID];
+      actionAssets.myLangDependentAssets[theLang].myName = theAction->text();
+      actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip();
+
+      const QString fileName = theModuleID + DevTools::ASSETS_SUFFIX;
+      writeToJSONFile(fileName, actionID, actionAssets);
     }
   }
 
-  void collectShortcutAndTranslation(const QtxAction* const theAction)
+  void collectShortcutAndAssets(const QtxAction* const theAction)
   {
     const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theAction->ID());
     if (moduleIDAndActionID.second.isEmpty())
@@ -225,7 +216,7 @@ public:
     if (!SUIT_ShortcutMgr::get()->getShortcutContainer().hasShortcut(moduleIDAndActionID.first, moduleIDAndActionID.second))
       collectShortcut(moduleIDAndActionID.first, moduleIDAndActionID.second, theAction->shortcut());
 
-    { // Collect action name (translation) in current language, if it is not provided in resource files.
+    { // Collect action assets, if they are not provided in asset files.
       SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
       if (!resMgr) {
         Warning("DevTools for SUIT_ShortcutMgr can't retrieve resource manager!");
@@ -236,10 +227,11 @@ public:
       if (lang.isEmpty())
         return;
 
-      if (SUIT_ShortcutMgr::getActionNameFromResources(theAction->ID(), lang).first)
+      const auto& assetsInResources = SUIT_ShortcutMgr::getActionAssetsFromResources(theAction->ID());
+      if (assetsInResources.first && assetsInResources.second.myLangDependentAssets.find(lang) != assetsInResources.second.myLangDependentAssets.end())
         return;
 
-      collectTranslation(moduleIDAndActionID.first, moduleIDAndActionID.second, lang, theAction->statusTip());
+      collectAssets(moduleIDAndActionID.first, moduleIDAndActionID.second, lang, theAction);
     }
   }
 
@@ -354,6 +346,68 @@ private:
 #endif // QT_NO_DOM
   }
 
+  /*! Appends new entries to content of dump files. */
+  bool writeToJSONFile(const QString& theFileName, const QString& theActionID, const SUIT_ActionAssets& theAssets)
+  {
+    const auto itFileAndDoc = myJSONFilesAndDocs.find(theFileName);
+    if (itFileAndDoc == myJSONFilesAndDocs.end()) {
+      const QString fullPath = DevTools::SAVE_PATH + theFileName + ".json";
+      if (!Qtx::mkDir(QFileInfo(fullPath).absolutePath())) {
+        myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
+        return false;
+      }
+
+      const bool fileExisted = QFileInfo::exists(fullPath);
+      QFile* file = new QFile(fullPath);
+      if (!file->open(QFile::ReadWrite | QIODevice::Text)) {
+        delete file;
+        myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
+        return false;
+      }
+
+      QJsonParseError jsonError;
+      QJsonDocument* document = new QJsonDocument(QJsonDocument::fromJson(file->readAll(), &jsonError));
+      if (jsonError.error != QJsonParseError::NoError && fileExisted) {
+        Warning("SUIT_ShortcutMgr: error during parsing of action asset dump file \"" + fullPath + "\"!");
+        delete file;
+        delete document;
+        myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
+        return false;
+      }
+
+      if (!document->isObject()) {
+        document->setObject(QJsonObject());
+        file->resize(0);
+        QTextStream outstream(file);
+        outstream << document->toJson(QJsonDocument::Indented);
+      }
+
+      myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(file, document);
+    }
+    else if (itFileAndDoc->second.first == nullptr) {
+      return false;
+    }
+
+    const auto fileAndDoc = myJSONFilesAndDocs[theFileName];
+    QFile* const file = fileAndDoc.first;
+    QJsonDocument* const document = fileAndDoc.second;
+
+    QJsonObject rootJSON = document->object();
+    QJsonObject actionAssetsJSON = rootJSON[theActionID].toObject();
+    SUIT_ActionAssets actionAssets;
+    actionAssets.fromJSON(actionAssetsJSON);
+    actionAssets.merge(theAssets, true /*theOverride*/);
+    actionAssets.toJSON(actionAssetsJSON);
+    rootJSON[theActionID] = actionAssetsJSON;
+    document->setObject(rootJSON);
+
+    file->resize(0);
+    QTextStream outstream(file);
+    outstream << document->toJson(QJsonDocument::Indented);
+
+    return true;
+  }
+
 public:
   void collectAssetsOfActionWithInvalidID(const QAction* const theAction)
   {
@@ -394,8 +448,8 @@ public:
   static const QString SAVE_PATH;
   static const QString SHORTCUTS_SUFFIX;
   static const QString SHORTCUTS_OF_META_SUFFIX;
-  static const QString TRANSLATIONS_SUFFIX;
-  static const QString TRANSLATIONS_OF_META_SUFFIX;
+  static const QString ASSETS_SUFFIX;
+  static const QString ASSETS_OF_META_SUFFIX;
   static const QString INVALID_ID_ACTIONS_SUFFIX;
 
   static DevTools* instance;
@@ -407,27 +461,29 @@ public:
   /** { moduleID, { inModuleActionID, keySequence }[] }[]. keySequence can be empty. */
   std::map<QString, std::map<QString, QString>> myShortcutsOfMetaActions;
 
-  /** { moduleID, { actionID, {language, actionName} }[] }[] */
-  std::map<QString, std::map<QString, std::map<QString, QString>>> myTranslations;
+  /** { moduleID, { actionID, assets }[] }[] */
+  std::map<QString, std::map<QString, SUIT_ActionAssets>> myAssets;
 
-  /** { moduleID, { actionID, {language, actionName} }[] }[] */
-  std::map<QString, std::map<QString, std::map<QString, QString>>> myTranslationsOfMetaActions;
+  /** { moduleID, { actionID, assets }[] }[] */
+  std::map<QString, std::map<QString, SUIT_ActionAssets>> myAssetsOfMetaActions;
 
 #ifndef QT_NO_DOM
   // { filename, {file, domDoc} }[]
   std::map<QString, std::pair<QFile*, QDomDocument*>> myXMLFilesAndDocs;
 #endif // QT_NO_DOM
+  // { filename, {file, jsonDoc} }[]
+  std::map<QString, std::pair<QFile*, QJsonDocument*>> myJSONFilesAndDocs;
 
   QFile* myActionsWithInvalidIDsFile;
 };
 /*static*/ DevTools* DevTools::instance = nullptr;
 /*static*/ const QString DevTools::SAVE_PATH = "shortcut_mgr_dev/";
+/*static*/ const QString DevTools::INVALID_ID_ACTIONS_SUFFIX = "_actions_with_invalid_IDs";
+/*static*/ const QString DevTools::XML_SECTION_TOKENS_SEPARATOR = ":";
 /*static*/ const QString DevTools::SHORTCUTS_SUFFIX = "_shortcuts";
 /*static*/ const QString DevTools::SHORTCUTS_OF_META_SUFFIX = "_shortcuts_of_meta_actions";
-/*static*/ const QString DevTools::TRANSLATIONS_SUFFIX = "_translations";
-/*static*/ const QString DevTools::TRANSLATIONS_OF_META_SUFFIX = "_translations_of_meta_actions";
-/*static*/ const QString DevTools::XML_SECTION_TOKENS_SEPARATOR = ":";
-/*static*/ const QString DevTools::INVALID_ID_ACTIONS_SUFFIX = "_actions_with_invalid_IDs";
+/*static*/ const QString DevTools::ASSETS_SUFFIX = "_assets";
+/*static*/ const QString DevTools::ASSETS_OF_META_SUFFIX = "_assets_of_meta_actions";
 #endif // SHORTCUT_MGR_DEVTOOLS
 
 
@@ -686,6 +742,108 @@ QString SUIT_ShortcutContainer::toString() const
   return text;
 }
 
+/*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME = "name";
+/*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP = "tooltip";
+
+bool SUIT_ActionAssets::LangDependentAssets::fromJSON(const QJsonObject& theJsonObject)
+{
+  myName    = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME].toString();
+  myToolTip = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP].toString();
+
+  if (myName.isEmpty())
+    myName = myToolTip;
+
+  return !myName.isEmpty();
+}
+
+void SUIT_ActionAssets::LangDependentAssets::toJSON(QJsonObject& oJsonObject) const
+{
+  oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME] = myName;
+  oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP] = myToolTip;
+}
+
+/*static*/ const QString SUIT_ActionAssets::STRUCT_ID = "SUIT_ActionAssets";
+/*static*/ const QString SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS = "langDependentAssets";
+/*static*/ const QString SUIT_ActionAssets::PROP_ID_ICON_PATH = "iconPath";
+
+bool SUIT_ActionAssets::fromJSON(const QJsonObject& theJsonObject)
+{
+  myLangDependentAssets.clear();
+
+  auto lda = SUIT_ActionAssets::LangDependentAssets();
+  const auto& langToLdaJson = theJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS].toObject();
+  for (const QString& lang : langToLdaJson.keys()) {
+    if (!lda.fromJSON(langToLdaJson[lang].toObject()))
+      continue;
+
+    myLangDependentAssets[lang] = lda;
+  }
+
+  myIconPath = theJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH].toString();
+
+  return !myLangDependentAssets.empty();
+}
+
+void SUIT_ActionAssets::toJSON(QJsonObject& oJsonObject) const
+{
+  auto langDependentAssetsJSON = QJsonObject();
+
+  auto langDependentAssetsItemJSON = QJsonObject();
+  for (const auto& langAndLDA : myLangDependentAssets) {
+    langAndLDA.second.toJSON(langDependentAssetsItemJSON);
+    langDependentAssetsJSON[langAndLDA.first] = langDependentAssetsItemJSON;
+  }
+  oJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS] = langDependentAssetsJSON;
+
+  oJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH] = myIconPath;
+}
+
+QString SUIT_ActionAssets::toString() const
+{
+  QJsonObject jsonObject;
+  toJSON(jsonObject);
+  return QString::fromStdString(QJsonDocument(jsonObject).toJson(QJsonDocument::Indented).toStdString());
+}
+
+QStringList SUIT_ActionAssets::getLangs() const
+{
+  QStringList langs;
+
+  for (const auto& langAndAssets : myLangDependentAssets) {
+    langs.push_back(langAndAssets.first);
+  }
+
+  return langs;
+}
+
+void SUIT_ActionAssets::clearAllLangsExcept(const QString& theLang)
+{
+  for (auto it = myLangDependentAssets.begin(); it != myLangDependentAssets.end();) {
+    if (it->first == theLang)
+      it++;
+    else
+      it = myLangDependentAssets.erase(it);
+  }
+}
+
+void SUIT_ActionAssets::merge(const SUIT_ActionAssets& theOther, bool theOverride)
+{
+  for (const auto& otherLangAndLDA : theOther.myLangDependentAssets) {
+    const QString& lang = otherLangAndLDA.first;
+    const auto& otherLDA = otherLangAndLDA.second;
+    auto& thisLDA = myLangDependentAssets[lang];
+
+    if (thisLDA.myName.isEmpty() || theOverride && !otherLDA.myName.isEmpty())
+      thisLDA.myName = otherLDA.myName;
+
+    if (thisLDA.myToolTip.isEmpty() || theOverride && !otherLDA.myToolTip.isEmpty())
+      thisLDA.myToolTip = otherLDA.myToolTip;
+  }
+
+  if (theOverride)
+    myIconPath = theOther.myIconPath;
+}
+
 std::map<QString, std::map<QString, QKeySequence>> SUIT_ShortcutContainer::merge(
   const SUIT_ShortcutContainer& theOther,
   bool theOverride,
@@ -1030,36 +1188,106 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr()
   ShCutDbg() && ShCutDbg("theContainer holds following shortcuts:\n" + theContainer.toString());
 }
 
-/*static*/ std::pair<bool, QString> SUIT_ShortcutMgr::getActionNameFromResources(const QString& theActionID, QString theLanguage)
+QString substituteBashVars(const QString& theString)
+{
+  QString res = theString;
+  const auto env = QProcessEnvironment::systemEnvironment();
+  int pos = 0;
+  QRegExp rx("\\$\\{([^\\}]+)\\}"); // Match substrings enclosed by "${" and "}".
+  rx.setMinimal(true); // Set search to non-greedy.
+  while((pos = rx.indexIn(res, pos)) != -1) {
+    QString capture = rx.cap(1);
+    QString subst = env.value(capture);
+    ShCutDbg("capture = " + capture);
+    ShCutDbg("subst   = " + subst);
+    res.replace("${" + capture + "}", subst);
+    pos += rx.matchedLength();
+  }
+  return res;
+}
+
+QString substitutePowerShellVars(const QString& theString)
 {
+  QString res = theString;
+  int pos = 0;
+  QRegExp rx("%([^%]+)%"); // Match substrings enclosed by "%".
+  rx.setMinimal(true); // Set search to non-greedy.
+  while((pos = rx.indexIn(res, pos)) != -1) {
+    QString capture = rx.cap(1);
+    QString subst = Qtx::getenv(capture.toUtf8().constData());
+    ShCutDbg("capture = " + capture);
+    ShCutDbg("subst   = " + subst);
+    res.replace("%" + capture + "%", subst);
+    pos += rx.matchedLength();
+  }
+  return res;
+}
+
+QString substituteVars(const QString& theString)
+{
+  QString str = substituteBashVars(theString);
+  return substitutePowerShellVars(str);
+}
+
+/*static*/ std::pair<bool, SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssetsFromResources(const QString& theActionID)
+{
+  auto res = std::pair<bool, SUIT_ActionAssets>(false, SUIT_ActionAssets());
+
   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
   if (!resMgr) {
     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
-    return std::pair<bool, QString>(false, QString());
+    return res;
   }
 
-  if (theLanguage.isEmpty())
-    theLanguage = resMgr->stringValue(LANG_SECTION, LANG_SECTION);
+  QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS);
+  for (const QString& actionAssetFilePath : actionAssetFilePaths) {
+    const QString path = substituteVars(actionAssetFilePath);
+    QFile actionAssetFile(path);
+    if (!actionAssetFile.open(QIODevice::ReadOnly)) {
+      Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!");
+      continue;
+    }
 
-  if (theLanguage.isEmpty())
-    return std::pair<bool, QString>(false, QString());
+    QJsonParseError jsonError;
+    QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError);
+    actionAssetFile.close();
+    if(jsonError.error != QJsonParseError::NoError) {
+      Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!");
+      continue;
+    }
+
+    if(!document.isObject()) {
+      Warning("SUIT_ShortcutMgr: empty action asset file \"" + path + "\"!");
+      continue;
+    }
+
+    QJsonObject object = document.object();
+    if (object.keys().indexOf(theActionID) == -1)
+      continue;
+
+    SUIT_ActionAssets actionAssets;
+    if (!actionAssets.fromJSON(object[theActionID].toObject())) {
+      ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + theActionID + "\".");
+      continue;
+    }
 
-  QStringList actionIDs = resMgr->subSections(SECTION_SHORTCUT_NAMES_PREFIX, false);
-  if (actionIDs.indexOf(theActionID) == -1)
-    return std::pair<bool, QString>(false, QString());
+    res.second.merge(actionAssets, true);
+  }
 
-  const QString sectionName = SECTION_SHORTCUT_NAMES_PREFIX + resMgr->sectionsToken() + theActionID;
-  QStringList availableActionNameLangs = resMgr->parameters(sectionName);
-  if (availableActionNameLangs.indexOf(theLanguage) == -1)
-    return std::pair<bool, QString>(false, QString());
+  res.first = true;
+  return res;
+}
 
-  QString actionName;
-  const bool nameInCurLangExists = resMgr->value(sectionName, theLanguage, actionName);
 
-  if (!nameInCurLangExists)
-    return std::pair<bool, QString>(false, QString());
+/*static*/ QString SUIT_ShortcutMgr::getLang()
+{
+  SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
+  if (!resMgr) {
+    Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
+    return DEFAULT_LANG;
+  }
 
-  return std::pair<bool, QString>(true, actionName);
+  return resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG);
 }
 
 
@@ -1331,105 +1559,90 @@ std::set<QString> SUIT_ShortcutMgr::getIDsOfInterferingModules(const QString& th
   return myShortcutContainer.getIDsOfInterferingModules(theModuleID);
 }
 
-QString SUIT_ShortcutMgr::getModuleName(const QString& theModuleID) const
+std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getModuleAssets(const QString& theModuleID) const
 {
-  return theModuleID == ROOT_MODULE_ID ? tr("General") : theModuleID;
+  const auto itModuleAssets = myModuleAssets.find(theModuleID);
+  if (itModuleAssets == myModuleAssets.end()) {
+    auto assets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets());
+    auto lda = SUIT_ActionAssets::LangDependentAssets();
+    lda.myName = theModuleID; // At least something meaningful.
 
-  /*
-  // TODO ? The SUIT_ShortcutMgr should be renamed and moved to CAM folder.
-  // Because the CAM_Application class is the closest to SUIT_Application in the inheritance hierarchy,
-  // who has concept of application module. Enabling this chunk of code, due to the presence of CAM_Application,
-  // requires to heap up cyclic dependencies in compilation units.
-  // Or it is reasond to add like-action-name-resources to preference files.
+    assets->myLangDependentAssets.emplace(SUIT_ShortcutMgr::getLang(), lda);
+    return assets;
+  }
+  return itModuleAssets->second;
+}
+
+QString SUIT_ShortcutMgr::getModuleName(const QString& theModuleID, const QString& theLang) const
+{
+  const auto assets = getModuleAssets(theModuleID);
+  const auto& ldaMap = assets->myLangDependentAssets;
+  if (ldaMap.empty())
+    return theModuleID;
 
-  SUIT_Application* aSUIT_Application = SUIT_Session::session()->activeApplication();
-  const auto aCAM_Application = dynamic_cast<CAM_Application*>(aSUIT_Application);
-  if (aCAM_Application)
-    return aCAM_Application->moduleTitle(theModuleID);
-  */
+  auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang);
+  if (itLang == ldaMap.end())
+    itLang = ldaMap.begin(); // Get name in any language.
 
-  // At least something meaningful.
-  return theModuleID;
+  const auto& name = itLang->second.myName;
+  return name.isEmpty() ? theModuleID : name;
 }
 
-QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID) const
+std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const
 {
   const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
   if (actionID.isEmpty()) {
-    ShCutDbg() && ShCutDbg("Can't get action name: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid.");
-    return actionID;
+    ShCutDbg() && ShCutDbg("Can't get action assets: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid.");
+    return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
   }
-  return getActionName(actionID);
+  return getActionAssets(actionID);
 }
 
-QString SUIT_ShortcutMgr::getActionName(const QString& theActionID) const
+std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const QString& theActionID) const
 {
-  const auto moduleAndInModuleActionIDs = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID);
-  if (moduleAndInModuleActionIDs.second.isEmpty()) {
-    ShCutDbg() && ShCutDbg("Can't get action name using invalid action ID \"" + theActionID + "\".");
-    return QString();
+  const auto it = myActionAssets.find(theActionID);
+  if (it == myActionAssets.end())
+    return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
+  else
+    return it->second;
+}
+
+QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang) const
+{
+  const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
+  if (actionID.isEmpty()) {
+    ShCutDbg() && ShCutDbg("Can't get action name: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid.");
+    return actionID;
   }
 
-  const auto itActionNames = myActionNames.find(theActionID);
-  if (itActionNames != myActionNames.end() && !itActionNames->second.empty()) {
-    QString lang;
-    SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
-    if (!resMgr) {
-      Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
-      lang = DEFAULT_LANG;
-    }
-    else {
-      lang = resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG);
-    }
+  const auto itActionAssets = myActionAssets.find(actionID);
+  if (itActionAssets != myActionAssets.end() && !itActionAssets->second->myLangDependentAssets.empty()) {
+    const auto& ldaMap = itActionAssets->second->myLangDependentAssets;
+    if (ldaMap.empty())
+      return theInModuleActionID;
 
-    QStringList langPriorityList = LANG_PRIORITY_LIST;
-    langPriorityList.push_front(lang);
-    langPriorityList.removeDuplicates();
+    auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang);
+    if (itLang == ldaMap.end())
+      itLang = ldaMap.begin(); // Get name in any language.
 
-    const std::map<QString, QString>& translations = itActionNames->second;
-    for (const QString& lang : langPriorityList) {
-      const auto itTranslation = translations.find(lang);
-      if (itTranslation != translations.end()) {
-        return itTranslation->second;
-      }
-    }
-    return translations.begin()->second;
+    const auto& name = itLang->second.myName;
+    return name.isEmpty() ? theInModuleActionID : name;
   }
-  else /* if action ID has no loaded name in any language. */ {
-    // Try to get action->toolTip() and use it as a name.
+  else /* if action assets have not been loaded. */ {
+    // Try to get action->text() and use it as a name.
 
     // Pitfall of the approach: at the time this code block is called, the action may not exist.
     // Moreover, an action with such an ID may not even have been created at the time of calling this method.
-    // Thus, even buffering of names of every action ever created at runtime does not guarantee,
-    // that the name will be available at any point in the life of the application,
-    // unless the name is added to dedicated section in a preference file.
-
-    const QString& moduleID = moduleAndInModuleActionIDs.first;
-    const QString& inModuleActionID = moduleAndInModuleActionIDs.second;
-
-    if (SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID)) {
-      for (const auto& actionAndID : myActionIDs) {
-        if (actionAndID.second.second == inModuleActionID)
-          return actionAndID.first->toolTip();
-      }
-      return inModuleActionID;
-    }
-    else {
-      const auto itModuleActions = myActions.find(moduleID);
-      if (itModuleActions == myActions.end())
-        return inModuleActionID;
-
-      const std::map<QString, std::set<QAction*>>& moduleActions = itModuleActions->second;
-      const auto itActions = moduleActions.find(inModuleActionID);
-      if (itActions == moduleActions.end())
-        return inModuleActionID;
-
-      const std::set<QAction*>& actions = itActions->second;
-      if (actions.empty())
-        return inModuleActionID;
-
-      return (*actions.begin())->toolTip();
+    // Thus, even buffering of assets of every action ever created at runtime does not guarantee,
+    // that the assets will be available at any point in the life of the application,
+    // unless the assets are added to dedicated section in an asset file.
+
+    const auto actions = getActions(theModuleID, theInModuleActionID);
+    for (const auto& action : actions) {
+      if (!action->text().isEmpty())
+        return action->text();
     }
+    return theInModuleActionID;
   }
 }
 
@@ -1476,7 +1689,7 @@ bool SUIT_ShortcutMgr::eventFilter(QObject* theObject, QEvent* theEvent)
 #endif//SHORTCUT_MGR_DBG
 #ifdef SHORTCUT_MGR_DEVTOOLS
         {
-          DevTools::get()->collectShortcutAndTranslation(aQtxAction);
+          DevTools::get()->collectShortcutAndAssets(aQtxAction);
           const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(aQtxAction->ID());
           if (moduleIDAndActionID.second.isEmpty())
             DevTools::get()->collectAssetsOfActionWithInvalidID(aQtxAction);
@@ -1553,7 +1766,7 @@ void SUIT_ShortcutMgr::setShortcutsFromPreferences()
   SUIT_ShortcutContainer container;
   SUIT_ShortcutMgr::fillContainerFromPreferences(container, false /*theDefaultOnly*/);
   mergeShortcutContainer(container, true /*theOverrde*/, false /*theTreatAbsentIncomingAsDisabled*/);
-  setActionNamesFromResources();
+  setAssetsFromResources();
 
   ShCutDbg() && ShCutDbg("ShortcutMgr has been initialized.");
 }
@@ -1585,9 +1798,9 @@ void SUIT_ShortcutMgr::setShortcutsFromPreferences()
   }
 }
 
-void SUIT_ShortcutMgr::setActionNamesFromResources(QString theLanguage)
+void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
 {
-  ShCutDbg() && ShCutDbg("Retrieving action names (translations) from preference files.");
+  ShCutDbg() && ShCutDbg("Retrieving action assets.");
 
   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
   if (!resMgr) {
@@ -1602,36 +1815,125 @@ void SUIT_ShortcutMgr::setActionNamesFromResources(QString theLanguage)
   langPriorityList.push_front(theLanguage);
   langPriorityList.removeDuplicates();
 
-  QStringList actionIDs = resMgr->subSections(SECTION_SHORTCUT_NAMES_PREFIX, false);
-  for (const QString& actionID : actionIDs) {
-    // {language, actionName}[]
-    std::map<QString, QString>& actionTranslations = myActionNames[actionID];
-    const QString sectionName = SECTION_SHORTCUT_NAMES_PREFIX + resMgr->sectionsToken() + actionID;
-    QStringList availableActionNameLangs = resMgr->parameters(sectionName);
-
-    QString actionName = actionID;
-    const bool nameInCurLangExists = resMgr->value(sectionName, theLanguage, actionName);
-    if (nameInCurLangExists) {
-      actionTranslations[theLanguage] = actionName;
+  QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS);
+#ifdef SHORTCUT_MGR_DBG
+  ShCutDbg("Asset files: " + actionAssetFilePaths.join(", ") + ".");
+#endif
+  for (const QString& actionAssetFilePath : actionAssetFilePaths) {
+    const QString path = substituteVars(actionAssetFilePath);
+    QFile actionAssetFile(path);
+    if (!actionAssetFile.open(QIODevice::ReadOnly)) {
+      Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!");
+      continue;
     }
-    else {
-      bool nameInlinguaFrancaExists = false;
-      QString usedLanguage = QString();
-      for (int i = 1; i < langPriorityList.length(); i++) {
-        nameInlinguaFrancaExists = resMgr->value(sectionName, langPriorityList[i], actionName);
-        if (nameInlinguaFrancaExists) {
-          usedLanguage = langPriorityList[i];
-          break;
+
+    QJsonParseError jsonError;
+    QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError);
+    actionAssetFile.close();
+    if(jsonError.error != QJsonParseError::NoError) {
+        Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!");
+        continue;
+    }
+
+    if(document.isObject()) {
+      QJsonObject object = document.object();
+      SUIT_ActionAssets actionAssets;
+      for (const QString& actionID : object.keys()) {
+        if (!SUIT_ShortcutMgr::isActionIDValid(actionID)) {
+          ShCutDbg("Action asset file \"" + path + "\" contains invalid action ID \"" + actionID + "\".");
+          continue;
+        }
+
+        if (!actionAssets.fromJSON(object[actionID].toObject())) {
+          ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + actionID + "\".");
+          continue;
+        }
+
+        const bool nameInCurLangExists = actionAssets.myLangDependentAssets.find(theLanguage) != actionAssets.myLangDependentAssets.end();
+        if (nameInCurLangExists) {
+          actionAssets.clearAllLangsExcept(theLanguage);
+        }
+        else {
+          bool nameInLinguaFrancaExists = false;
+          QString usedLanguage = QString();
+          for (int i = 1; i < langPriorityList.length(); i++) {
+            nameInLinguaFrancaExists = actionAssets.myLangDependentAssets.find(langPriorityList[i]) != actionAssets.myLangDependentAssets.end();
+            if (nameInLinguaFrancaExists) {
+              usedLanguage = langPriorityList[i];
+              actionAssets.clearAllLangsExcept(usedLanguage);
+              break;
+            }
+          }
+
+          #ifdef SHORTCUT_MGR_DBG
+          if (nameInLinguaFrancaExists)
+            ShCutDbg("Can't find assets for action with ID \"" + actionID + "\" at current (" + theLanguage + ") language. Assets in " + usedLanguage + " is used for the action." );
+          else {
+            ShCutDbg("Can't find assets for action with ID \"" + actionID + "\". Tried " + langPriorityList.join(", ") + " languages." );
+            continue;
+          }
+          #endif
         }
+
+        auto itAssets = myActionAssets.find(actionID);
+        if (itAssets == myActionAssets.end()) {
+          auto pAssets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets(actionAssets));
+          itAssets = myActionAssets.emplace(actionID, pAssets).first;
+        }
+        else
+          itAssets->second->merge(actionAssets, true);
+
+        const auto& assets = itAssets->second;
+        if (!assets->myIconPath.isEmpty() && assets->myIcon.isNull())
+          assets->myIcon = QIcon(substituteVars(assets->myIconPath));
       }
-      actionTranslations[theLanguage] = actionName;
+    }
+  }
 
-#ifdef SHORTCUT_MGR_DBG
-      if (nameInlinguaFrancaExists)
-        ShCutDbg("Can't find in preference files a name for action with ID \"" + actionID + "\" at current (" + theLanguage + ") language. " + usedLanguage + " is used for the name." );
-      else
-        ShCutDbg("Can't find in preference files a name for action with ID \"" + actionID + "\". Also tried " + langPriorityList.join(", ") + " languages." );
-#endif
+  #ifdef SHORTCUT_MGR_DBG
+  ShCutDbg("Parsed assets: ");
+  QJsonObject object;
+  for (const auto& actionIDAndAssets : myActionAssets) {
+    actionIDAndAssets.second->toJSON(object);
+    QJsonDocument doc(object);
+    QString strJson = doc.toJson(QJsonDocument::Indented);
+    ShCutDbg(actionIDAndAssets.first + " : " +  strJson);
+  }
+  #endif
+
+  // Fill myModuleAssets.
+  for (const auto& moduleID : myShortcutContainer.getIDsOfAllModules()) {
+    const auto assets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets());
+    auto& lda = assets->myLangDependentAssets[DEFAULT_LANG];
+
+    if (moduleID == ROOT_MODULE_ID) {
+      lda.myName = tr("General");
+
+      { // Load icon.
+        QString dirPath;
+        if (resMgr->value("resources", "LightApp", dirPath)) {
+          assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + "icon_default.png";
+          assets->myIcon = QIcon(substituteVars(assets->myIconPath));
+        }
+      }
+    }
+    else {
+      QString moduleName = moduleID;
+      resMgr->value(moduleID, "name", moduleName);
+      lda.myName = moduleName;
+
+      resMgr->value(moduleID, "description", lda.myToolTip);
+
+      { // Load icon.
+        QString dirPath;
+        QString fileName;
+        if (resMgr->value("resources", moduleID, dirPath) && resMgr->value(moduleID, "icon", fileName)) {
+          assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + fileName;
+          assets->myIcon = QIcon(substituteVars(assets->myIconPath));
+        }
+      }
     }
+
+    myModuleAssets.emplace(moduleID, std::move(assets));
   }
 }
\ No newline at end of file
index 41ca842c4d1e71ee5bd1bea5a30dfeb764d72654..c8debfb81b1094fb5681980cfbe95fb7954c80b6 100644 (file)
 
 #include <QObject>
 #include <QString>
+#include <QIcon>
 #include <map>
 #include <set>
+#include <memory>
 #include <utility>
 
 class QAction;
 class QtxAction;
 class QKeySequence;
+class QJsonObject;
 
 #if defined WIN32
 #pragma warning( disable: 4251 )
@@ -142,6 +145,43 @@ private:
 };
 
 
+/*! \brief GUI-related assets. */
+struct SUIT_EXPORT SUIT_ActionAssets
+{
+  struct LangDependentAssets
+  {
+    static const QString PROP_ID_NAME;
+    static const QString PROP_ID_TOOLTIP;
+
+    bool fromJSON(const QJsonObject& theJsonObject);
+    void toJSON(QJsonObject& oJsonObject) const;
+
+    QString myName;
+    QString myToolTip;
+  };
+
+  static const QString STRUCT_ID;
+  static const QString PROP_ID_LANG_DEPENDENT_ASSETS;
+  static const QString PROP_ID_ICON_PATH;
+
+  bool fromJSON(const QJsonObject& theJsonObject);
+  void toJSON(QJsonObject& oJsonObject) const;
+  QString toString() const;
+
+  QStringList getLangs() const;
+  void clearAllLangsExcept(const QString& theLang);
+
+  /*! \param theOverride If true, values of theOther override conflicting values of this. */
+  void merge(const SUIT_ActionAssets& theOther, bool theOverride);
+
+  std::map<QString, LangDependentAssets> myLangDependentAssets;
+  QString myIconPath;
+
+  /*! Is not serialized. */
+  QIcon myIcon;
+};
+
+
 /*!
   \class SUIT_ShortcutMgr
   \brief Handles action shortcut customization.
@@ -242,8 +282,11 @@ public:
   static void fillContainerFromPreferences(SUIT_ShortcutContainer& theContainer, bool theDefaultOnly);
 
   /*! \brief Checks the resource manager directly.
-  \returns {nameExists, name}. */
-  static std::pair<bool, QString> getActionNameFromResources(const QString& theActionID, QString theLanguage = QString());
+  \returns {assetsExist, assets}. */
+  static std::pair<bool, SUIT_ActionAssets> getActionAssetsFromResources(const QString& theActionID);
+
+  /*! \returns Language being set in resource manager. */
+  static QString getLang();
 
 
   /*! \brief Add theAction to map of managed actions. */
@@ -314,16 +357,17 @@ public:
   if the module is root (theModuleID is empty) - returns all module IDs, otherwise returns ["", theModuleID]. */
   std::set<QString> getIDsOfInterferingModules(const QString& theModuleID) const;
 
-  /*! \brief Retrieves module name translated to appropriate language. */
-  QString getModuleName(const QString& theModuleID) const;
+  std::shared_ptr<const SUIT_ActionAssets> getModuleAssets(const QString& theModuleID) const;
 
-  /*! \brief Retrieves at runtime action name translated to appropriate language,
-  if the language was loaded using \ref setActionNamesFromResources(QString). */
-  QString getActionName(const QString& theModuleID, const QString& theInModuleActionID) const;
+  /*! \brief Retrieves module name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */
+  QString getModuleName(const QString& theModuleID, const QString& theLang = "") const;
 
-  /*! \brief Retrieves at runtime action name translated to appropriate language,
-  if the language was loaded using \ref setActionNamesFromResources(QString). */
-  QString getActionName(const QString& theActionID) const;
+  std::shared_ptr<const SUIT_ActionAssets> getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const;
+
+  std::shared_ptr<const SUIT_ActionAssets> getActionAssets(const QString& theActionID) const;
+
+  /*! \brief Retrieves action name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */
+  QString getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang = "") const;
 
 private slots:
   /*!
@@ -378,30 +422,28 @@ private:
   \param theShortcuts { moduleID, { inModuleActionID, keySequence }[] }[]. Empty inModuleActionIDs are ignored. */
   static void saveShortcutsToPreferences(const std::map<QString, std::map<QString, QKeySequence>>& theShortcutsInversed);
 
-  /*! Fills myActionNames from preference files in theLanguage.
-  \param theLanguage If default, fills names in current language.
-  If a name in requested language is not found, seeks for the name EN in and then in FR.
+  /*! Fills myActionAssets from asset files in theLanguage.
+  \param theLanguage If default, fills assets in current language.
+  If an asset in requested language is not found, seeks for the asset EN in and then in FR.
 
-  In case if resource file is XML, it should look like this:
-  <!--
-  <section name="<action ID>"> Note, that apllication-unique must be typed, not in-module action ID.
+  Asset files must be structured like this:
+  {
     ...
-    <parameter name="<language>" value="action name">
+    actionID : {
+      "langDependentAssets": {
+        ...
+        lang: {
+          "name": name,
+          "tooltip": tooltip
+        },
+        ...
+      },
+      "iconPath": iconPath
+    },
     ...
-  </section>
-  -->
-  <section name="shortcut_translations:/#Viewers/Reset view">
-      <parameter name="en" value="Reset View Point"/>
-      <parameter name="fr" value="Restaurer le point de vue"/>
-      <parameter name="ja" value="ビューのポイントを復元します。"/>
-  </section>
-  <section name="shortcut_translations:GEOM/Isolines/Increase number">
-      <parameter name="en" value="Increase number of isolines"/>
-      <parameter name="fr" value="Augmenter le nombre d'isolignes"/>
-      <parameter name="ja" value="等値線の数を増やす"/>
-  </section>
+  }
   */
-  void setActionNamesFromResources(QString theLanguage = QString());
+  void setAssetsFromResources(QString theLanguage = QString());
 
 private:
   static SUIT_ShortcutMgr* myShortcutMgr;
@@ -420,8 +462,11 @@ private:
   Sets of moduleIDs and inModuleActionIDs may NOT be equal for myActions and myShortcutContainer.
   */
 
-  /** { actionID, {language, actionName} }[] */
-  std::map<QString, std::map<QString, QString>> myActionNames;
+  /* {actionID, assets}[] */
+  std::map<QString, std::shared_ptr<SUIT_ActionAssets>> myActionAssets;
+
+  /* {moduleID, assets}[] */
+  mutable std::map<QString, std::shared_ptr<SUIT_ActionAssets>> myModuleAssets;
 };
 
 #if defined WIN32
diff --git a/src/SUIT/resources/action_assets.json b/src/SUIT/resources/action_assets.json
new file mode 100644 (file)
index 0000000..eefb701
--- /dev/null
@@ -0,0 +1,699 @@
+{
+    "/#TOT_DESK_EDIT_COPY": {
+        "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/copy.png",
+        "langDependentAssets": {
+            "en": {
+                "name": "Copy",
+                "tooltip": "Copy the selection to the Clipboard"
+            },
+            "fr": {
+                "name": "Copier",
+                "tooltip": "Copier la sélection dans le presse-papiers"
+            },
+            "ja": {
+                "name": "コピー",
+                "tooltip": "選択範囲をクリップボードにコピー"
+            }
+        }
+    },
+    "/#Viewers/View/Reset": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Reset",
+                "tooltip": "Reset View Point"
+            },
+            "fr": {
+                "name": "Restaurer",
+                "tooltip": "Restaurer le point de vue"
+            },
+            "ja": {
+                "name": "復元",
+                "tooltip": "ビューのポイントを復元します。"
+            }
+        }
+    },
+    "/#Viewers/View/Rotate anticlockwise": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Rotate counterclockwise",
+                "tooltip": "Rotate view counterclockwise"
+            },
+            "fr": {
+                "name": "Tourner la vue à gauche",
+                "tooltip": "Tourner la vue à gauche"
+            },
+            "ja": {
+                "name": "表示を左に",
+                "tooltip": "表示を左に"
+            }
+        }
+    },
+    "/#Viewers/View/Rotate clockwise": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Rotate clockwise",
+                "tooltip": "Rotate View Clockwise"
+            },
+            "fr": {
+                "name": "Tourner la vue à droite",
+                "tooltip": "Tourner la vue à droite"
+            },
+            "ja": {
+                "name": "右のビューを回転させる",
+                "tooltip": "右のビューを回転させる"
+            }
+        }
+    },
+    "/#Viewers/View/Set X+": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "+OX",
+                "tooltip": "+OX View"
+            },
+            "fr": {
+                "name": "+OX",
+                "tooltip": "Vue +OX"
+            },
+            "ja": {
+                "name": "+OX",
+                "tooltip": "+OX View"
+            }
+        }
+    },
+    "/#Viewers/View/Set X-": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "-OX",
+                "tooltip": "-OX View"
+            },
+            "fr": {
+                "name": "-OX",
+                "tooltip": "Vue -OX"
+            },
+            "ja": {
+                "name": "-OX",
+                "tooltip": "-OX View"
+            }
+        }
+    },
+    "/#Viewers/View/Set Y+": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "+OY",
+                "tooltip": "+OY View"
+            },
+            "fr": {
+                "name": "+OY",
+                "tooltip": "Vue +OY"
+            },
+            "ja": {
+                "name": "+OY",
+                "tooltip": "+OY View"
+            }
+        }
+    },
+    "/#Viewers/View/Set Y-": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "-OY",
+                "tooltip": "-OY View"
+            },
+            "fr": {
+                "name": "-OY",
+                "tooltip": "Vue -OY"
+            },
+            "ja": {
+                "name": "-OY",
+                "tooltip": "-OY View"
+            }
+        }
+    },
+    "/#Viewers/View/Set Z+": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "+OZ",
+                "tooltip": "+OZ View"
+            },
+            "fr": {
+                "name": "+OZ",
+                "tooltip": "Vue +OZ"
+            },
+            "ja": {
+                "name": "+OZ",
+                "tooltip": "+OZ View"
+            }
+        }
+    },
+    "/#Viewers/View/Set Z-": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "-OZ",
+                "tooltip": "-OZ View"
+            },
+            "fr": {
+                "name": "-OZ",
+                "tooltip": "Vue -OZ"
+            },
+            "ja": {
+                "name": "-OZ",
+                "tooltip": "-OZ View"
+            }
+        }
+    },
+    "/#General/Object(s)/Hide": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Hide",
+                "tooltip": "Hide"
+            },
+            "fr": {
+                "name": "Cacher",
+                "tooltip": "Cacher"
+            },
+            "ja": {
+                "name": "非表示",
+                "tooltip": "非表示"
+            }
+        }
+    },
+    "/#General/Object(s)/Show": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Show",
+                "tooltip": "Show"
+            },
+            "fr": {
+                "name": "Afficher",
+                "tooltip": "Afficher"
+            },
+            "ja": {
+                "name": "表示",
+                "tooltip": "表示"
+            }
+        }
+    },
+    "/PRP_CLOSE": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Close",
+                "tooltip": "Close active window"
+            },
+            "fr": {
+                "name": "Fermer",
+                "tooltip": "Fermer la fenêtre active"
+            },
+            "ja": {
+                "name": "閉じる",
+                "tooltip": "アクティブ ウィンドウを閉じる"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_0": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "GL 2D view",
+                "tooltip": "Create new GL 2D view"
+            },
+            "fr": {
+                "name": "Scène GL ",
+                "tooltip": "Créer une nouvelle Scène GL "
+            },
+            "ja": {
+                "name": "GL 2D view",
+                "tooltip": "新しい GL 2D view を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_1": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Plot 2D view",
+                "tooltip": "Create new Plot 2D view"
+            },
+            "fr": {
+                "name": "Scène Plot2d ",
+                "tooltip": "Créer une nouvelle Scène Plot2d "
+            },
+            "ja": {
+                "name": "Plot 2D View",
+                "tooltip": "新しい Plot 2D View を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_2": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "OCC 3D view",
+                "tooltip": "Create new OCC 3D view"
+            },
+            "fr": {
+                "name": "Scène OCC",
+                "tooltip": "Créer une nouvelle Scène OCC"
+            },
+            "ja": {
+                "name": "OCC 3D View",
+                "tooltip": "新しい OCC 3D View を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_3": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "VTK 3D view",
+                "tooltip": "Create new VTK 3D view"
+            },
+            "fr": {
+                "name": "Scène VTK",
+                "tooltip": "Créer une nouvelle Scène VTK"
+            },
+            "ja": {
+                "name": "VTK 3D View",
+                "tooltip": "新しい VTK 3D View を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_4": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "QxScene 2D view",
+                "tooltip": "Create new QxScene 2D view"
+            },
+            "fr": {
+                "name": "Scène QxScene",
+                "tooltip": "Créer une nouvelle Scène QxScene"
+            },
+            "ja": {
+                "name": "シーン QxScene",
+                "tooltip": "新しい シーン QxScene を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_5": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Graphics view",
+                "tooltip": "Create new Graphics view"
+            },
+            "fr": {
+                "name": "Scène Graphiques",
+                "tooltip": "Créer une nouvelle Scène Graphiques"
+            },
+            "ja": {
+                "name": "グラフィックの表示",
+                "tooltip": "新しい グラフィックの表示 を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_6": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "ParaView view",
+                "tooltip": "Create new ParaView view"
+            },
+            "fr": {
+                "name": "Scène ParaView",
+                "tooltip": "Créer une nouvelle Scène ParaView"
+            },
+            "ja": {
+                "name": "ParaView 表示 ",
+                "tooltip": "新しい ParaView 表示  を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_7": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Python view",
+                "tooltip": "Create new Python view"
+            },
+            "fr": {
+                "name": "Vue Python",
+                "tooltip": "Créer une nouvelle Vue Python"
+            },
+            "ja": {
+                "name": "Python view",
+                "tooltip": "新しい Python view を作成します。"
+            }
+        }
+    },
+    "/PRP_CREATE_NEW_WINDOW_FOR_VIEWER_8": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "ParaView3D view",
+                "tooltip": "Create new ParaView3D view"
+            },
+            "fr": {
+                "name": "ParaView3D view",
+                "tooltip": "Créer une nouvelle ParaView3D view"
+            },
+            "ja": {
+                "name": "ParaView3D view",
+                "tooltip": "新しい ParaView3D view を作成します。"
+            }
+        }
+    },
+    "/PRP_DESK_CATALOG_GENERATOR": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Catalog Generator",
+                "tooltip": "Generates XML catalog of a component's interface"
+            },
+            "fr": {
+                "name": "Genérateur de catalogue",
+                "tooltip": "Génére un catalogue XML de l'interface du composant"
+            },
+            "ja": {
+                "name": "カタログ ジェネレーター",
+                "tooltip": "コンポーネントインターフェイスのXMLカタログを生成"
+            }
+        }
+    },
+    "/PRP_DESK_CONNECT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Connect",
+                "tooltip": "Connect active study"
+            },
+            "fr": {
+                "name": "Connecter",
+                "tooltip": "Connecter l'étude en cours"
+            },
+            "ja": {
+                "name": "接続",
+                "tooltip": "アクティブスタディの接続"
+            }
+        }
+    },
+    "/PRP_DESK_DISCONNECT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Disconnect",
+                "tooltip": "Disconnect the current study"
+            },
+            "fr": {
+                "name": "Déconnecter",
+                "tooltip": "Déconnecter l'étude en cours"
+            },
+            "ja": {
+                "name": "切断",
+                "tooltip": "カレントスタディの切断"
+            }
+        }
+    },
+    "/PRP_DESK_FILE_DUMP_STUDY": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Dump Study...",
+                "tooltip": "Dumps study to the python script"
+            },
+            "fr": {
+                "name": "Générer le script de l'étude...",
+                "tooltip": "Génère le script python de l'étude"
+            },
+            "ja": {
+                "name": "スクリプトを保存",
+                "tooltip": "Pythonスクリプトにスタディをダンプする"
+            }
+        }
+    },
+    "/PRP_DESK_FILE_LOAD_SCRIPT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Load Script...",
+                "tooltip": "Loads python script from file"
+            },
+            "fr": {
+                "name": "Exécuter un script...",
+                "tooltip": "Exécute un script Python à partir d'un fichier"
+            },
+            "ja": {
+                "name": "スクリプトを読込み...",
+                "tooltip": "ファイルからPythonスクリプトを読込み"
+            }
+        }
+    },
+    "/PRP_DESK_HELP_ABOUT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "About...",
+                "tooltip": "Shows 'About' dialog"
+            },
+            "fr": {
+                "name": "A propos de...",
+                "tooltip": "Montre la boîte de dialogue 'A propos'"
+            },
+            "ja": {
+                "name": "バージョン情報...",
+                "tooltip": "ソフト情報の表示"
+            }
+        }
+    },
+    "/PRP_DESK_PREFERENCES": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Preferences...",
+                "tooltip": "Allow to change the preferences"
+            },
+            "fr": {
+                "name": "Préférences...",
+                "tooltip": "Permettre de changer les préférences"
+            },
+            "ja": {
+                "name": "環境設定...",
+                "tooltip": "設定を変更することができます。"
+            }
+        }
+    },
+    "/PRP_DESK_VIEW_STATUSBAR": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Status Bar",
+                "tooltip": "Toggles status bar view on/off"
+            },
+            "fr": {
+                "name": "Barre de status",
+                "tooltip": "Activer ou désactiver la barre de status"
+            },
+            "ja": {
+                "name": "ステータス バー",
+                "tooltip": "ステータスバーの有効/無効"
+            }
+        }
+    },
+    "/PRP_DESK_WINDOW_HSPLIT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Split Horizontally",
+                "tooltip": "Splits the active window on two horizontal parts"
+            },
+            "fr": {
+                "name": "Séparation horizontale",
+                "tooltip": "Diviser la fenêtre actuelle en deux parties horizontales"
+            },
+            "ja": {
+                "name": "水平分割",
+                "tooltip": "現在のウィンドウを 2つに水平分割"
+            }
+        }
+    },
+    "/PRP_DESK_WINDOW_VSPLIT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Split Vertically",
+                "tooltip": "Splits the active window on two vertical parts"
+            },
+            "fr": {
+                "name": "Séparation verticale",
+                "tooltip": "Diviser la fenêtre actuelle en deux parties verticales"
+            },
+            "ja": {
+                "name": "垂直分割",
+                "tooltip": "現在のウィンドウを2つに上下分割"
+            }
+        }
+    },
+    "/PRP_FULLSCREEN": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Full screen",
+                "tooltip": "Switch to full screen mode"
+            },
+            "fr": {
+                "name": "Plein écran",
+                "tooltip": "Basculer en mode plein écran"
+            },
+            "ja": {
+                "name": "全画面表示",
+                "tooltip": "全画面表示モードに切り替え"
+            }
+        }
+    },
+    "/PRP_RENAME": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Rename",
+                "tooltip": "Rename active window"
+            },
+            "fr": {
+                "name": "Renommer",
+                "tooltip": "Renommer la fenêtre active"
+            },
+            "ja": {
+                "name": "名前変更",
+                "tooltip": "アクティブなウィンドウの名前を変更"
+            }
+        }
+    },
+    "/TOT_DESK_EDIT_PASTE": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Paste",
+                "tooltip": "Inserts the Clipboard content at the insertion point"
+            },
+            "fr": {
+                "name": "Coller",
+                "tooltip": "Insérer le contenu du presse-papiers au point d'insertion"
+            },
+            "ja": {
+                "name": "貼り付け",
+                "tooltip": "クリップボードの内容を挿入"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_CLOSE": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Close",
+                "tooltip": "Closes the active document"
+            },
+            "fr": {
+                "name": "Fermer",
+                "tooltip": "Ferme le document actuel"
+            },
+            "ja": {
+                "name": "閉じる",
+                "tooltip": "現在のドキュメントを閉じる"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_EXIT": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Exit",
+                "tooltip": "Exits the application"
+            },
+            "fr": {
+                "name": "Quitter",
+                "tooltip": "Quitte l'application"
+            },
+            "ja": {
+                "name": "終了",
+                "tooltip": "アプリケーションを終了"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_NEW": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "New",
+                "tooltip": "Create a new document"
+            },
+            "fr": {
+                "name": "Nouveau",
+                "tooltip": "Créer une nouvelle étude"
+            },
+            "ja": {
+                "name": "新規作成",
+                "tooltip": "新しいドキュメントを作成"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_OPEN": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Open...",
+                "tooltip": "Open an existing document"
+            },
+            "fr": {
+                "name": "Ouvrir...",
+                "tooltip": "Ouvre une étude existant"
+            },
+            "ja": {
+                "name": "開く...",
+                "tooltip": "既存のドキュメントを開く"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_SAVE": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Save",
+                "tooltip": "Save the active document"
+            },
+            "fr": {
+                "name": "Enregistrer",
+                "tooltip": "Sauvegarder l'étude actuelle"
+            },
+            "ja": {
+                "name": "保存",
+                "tooltip": "現在のドキュメントを保存"
+            }
+        }
+    },
+    "/TOT_DESK_FILE_SAVEAS": {
+        "iconPath": "",
+        "langDependentAssets": {
+            "en": {
+                "name": "Save As...",
+                "tooltip": "Saves the active document with a new name"
+            },
+            "fr": {
+                "name": "Enregistrer sous...",
+                "tooltip": "Sauvegarder le document actuel sous un nouveau nom"
+            },
+            "ja": {
+                "name": "別名保存...",
+                "tooltip": "現在のドキュメントを新しい名前で保存"
+            }
+        }
+    }
+}
\ No newline at end of file