From: dish Date: Thu, 25 Jan 2024 08:27:48 +0000 (+0000) Subject: [bos #35160][EDF](2023-T1) Keyboard shortcuts. X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=549f0c197d43f56ccc684135fa0c5aa43c1f0a8a;p=modules%2Fgui.git [bos #35160][EDF](2023-T1) Keyboard shortcuts. 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. --- diff --git a/src/LightApp/LightApp_Module.cxx b/src/LightApp/LightApp_Module.cxx index b90aa42fa..fe3abbf30 100644 --- a/src/LightApp/LightApp_Module.cxx +++ b/src/LightApp/LightApp_Module.cxx @@ -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" ), diff --git a/src/LightApp/resources/LightApp.xml b/src/LightApp/resources/LightApp.xml index e29587f4b..ac829fc9c 100644 --- a/src/LightApp/resources/LightApp.xml +++ b/src/LightApp/resources/LightApp.xml @@ -248,6 +248,9 @@ +
+ +
@@ -283,8 +286,8 @@ - - + + @@ -295,174 +298,6 @@ - -
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
diff --git a/src/Qtx/QtxShortcutEdit.cxx b/src/Qtx/QtxShortcutEdit.cxx index eb7431d8e..fe845b574 100644 --- a/src/Qtx/QtxShortcutEdit.cxx +++ b/src/Qtx/QtxShortcutEdit.cxx @@ -37,6 +37,9 @@ #include #include +#include + +#include #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("" + theModuleName + "  " + 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 +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 theContainer, QWidget* theParent ) : QTreeWidget(theParent), -myShortcutContainer(theContainer ? theContainer : std::shared_ptr(new SUIT_ShortcutContainer())) +myShortcutContainer(theContainer ? theContainer : std::shared_ptr(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 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(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(moduleItem->child(childIdx)); + QtxShortcutTreeAction* const childItem = static_cast(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 actionIDsOfItems; - for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { - QtxShortcutTreeItem* const childItem = static_cast(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(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 QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const +std::pair QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const { for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { - QtxShortcutTreeItem* moduleItem = static_cast(topLevelItem(moduleIdx)); + QtxShortcutTreeFolder* moduleItem = static_cast(topLevelItem(moduleIdx)); if (moduleItem->myModuleID == theModuleID) - return std::pair(moduleItem, moduleIdx); + return std::pair(moduleItem, moduleIdx); } - return std::pair(nullptr, -1); + return std::pair(nullptr, -1); +} + +/*! \returns Children of theParentItem being sorted according to current sort mode and order. */ +std::set> QtxShortcutTree::getSortedChildren(QtxShortcutTreeFolder* theParentItem) +{ + QList> 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(mySortKey, mySortOrder)); + } + + static const QCollator collator; + const std::function 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> sortedChildren(comparator); + for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) { + QtxShortcutTreeAction* const childItem = static_cast(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>& 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(theItem); - // Do not react if folder-item is clicked. - if (item->isFolder()) - return; + { + QtxShortcutTreeItem* const item = static_cast(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(item->parent())->name(), item->name()); - myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(item->keySequence())); + QtxShortcutTreeAction* const actionItem = static_cast(theItem); + + myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID); + QString actionToolTip = actionItem->toolTip(QtxShortcutTree::ElementIdx::Name); + actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1); + myEditDialog->setModuleAndActionName( + static_cast(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> disabledActionIDs = myShortcutContainer->setShortcut(item->myModuleID, item->myInModuleActionID, newKeySequence, true /*override*/); + std::set> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/); /** { moduleID, {inModuleActionID, keySequence}[] }[] */ std::map> 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(moduleItem->child(childIdx)); + QtxShortcutTreeAction* const childItem = static_cast(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> 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> QtxShortcutTree::instances = std::map>(); -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 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 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 diff --git a/src/Qtx/QtxShortcutEdit.h b/src/Qtx/QtxShortcutEdit.h index 71e0d9d93..862c80d86 100644 --- a/src/Qtx/QtxShortcutEdit.h +++ b/src/Qtx/QtxShortcutEdit.h @@ -28,6 +28,8 @@ #include #include #include +#include + 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 shortcutContainer() const; + void sort(QtxShortcutTree::SortKey theKey, QtxShortcutTree::SortOrder theOrder); + private: void updateItems(bool theHighlightModified, bool theUpdateSyncTrees); - std::pair findModuleFolderItem(const QString& theModuleID) const; + std::pair findModuleFolderItem(const QString& theModuleID) const; + + std::set> getSortedChildren(QtxShortcutTreeFolder* theParentItem); + + void insertChild( + QtxShortcutTreeFolder* theParentItem, + std::set>& 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 myModuleIDs; + static const QList> DEFAULT_SORT_SCHEMA; + private: /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. */ const std::shared_ptr 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 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 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 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 diff --git a/src/SUIT/CMakeLists.txt b/src/SUIT/CMakeLists.txt index 10c9f0079..6c7c3f6af 100644 --- a/src/SUIT/CMakeLists.txt +++ b/src/SUIT/CMakeLists.txt @@ -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}) - diff --git a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md index 65702e721..d0cc5adc8 100644 --- a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md +++ b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md @@ -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 `
` 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 `
`. -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 diff --git a/src/SUIT/SUIT_ShortcutMgr.cxx b/src/SUIT/SUIT_ShortcutMgr.cxx index 501f050f9..bfae01ec3 100644 --- a/src/SUIT/SUIT_ShortcutMgr.cxx +++ b/src/SUIT/SUIT_ShortcutMgr.cxx @@ -33,6 +33,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include @@ -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 "/shortcut_mgr_dev/". */ @@ -100,6 +106,7 @@ static const QString SECTION_SHORTCUT_NAMES_PREFIX = QString("shortcut_translati #ifdef SHORTCUT_MGR_DEVTOOLS #include #include +#include #include #include "QtxMap.h" #include @@ -109,10 +116,10 @@ static const QString SECTION_SHORTCUT_NAMES_PREFIX = QString("shortcut_translati #include #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> 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& actionTranslations = itAction->second; - std::map& 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> 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& actionTranslations = itAction->second; - std::map& 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(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(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(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(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> myShortcutsOfMetaActions; - /** { moduleID, { actionID, {language, actionName} }[] }[] */ - std::map>> myTranslations; + /** { moduleID, { actionID, assets }[] }[] */ + std::map> myAssets; - /** { moduleID, { actionID, {language, actionName} }[] }[] */ - std::map>> myTranslationsOfMetaActions; + /** { moduleID, { actionID, assets }[] }[] */ + std::map> myAssetsOfMetaActions; #ifndef QT_NO_DOM // { filename, {file, domDoc} }[] std::map> myXMLFilesAndDocs; #endif // QT_NO_DOM + // { filename, {file, jsonDoc} }[] + std::map> 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> 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 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 SUIT_ShortcutMgr::getActionAssetsFromResources(const QString& theActionID) +{ + auto res = std::pair(false, SUIT_ActionAssets()); + SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr(); if (!resMgr) { Warning("SUIT_ShortcutMgr can't retrieve resource manager!"); - return std::pair(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(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(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(false, QString()); + res.first = true; + return res; +} - QString actionName; - const bool nameInCurLangExists = resMgr->value(sectionName, theLanguage, actionName); - if (!nameInCurLangExists) - return std::pair(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(true, actionName); + return resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG); } @@ -1331,105 +1559,90 @@ std::set SUIT_ShortcutMgr::getIDsOfInterferingModules(const QString& th return myShortcutContainer.getIDsOfInterferingModules(theModuleID); } -QString SUIT_ShortcutMgr::getModuleName(const QString& theModuleID) const +std::shared_ptr 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(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(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 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(nullptr); } - return getActionName(actionID); + return getActionAssets(actionID); } -QString SUIT_ShortcutMgr::getActionName(const QString& theActionID) const +std::shared_ptr 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(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& 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>& moduleActions = itModuleActions->second; - const auto itActions = moduleActions.find(inModuleActionID); - if (itActions == moduleActions.end()) - return inModuleActionID; - - const std::set& 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& 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(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(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 diff --git a/src/SUIT/SUIT_ShortcutMgr.h b/src/SUIT/SUIT_ShortcutMgr.h index 41ca842c4..c8debfb81 100644 --- a/src/SUIT/SUIT_ShortcutMgr.h +++ b/src/SUIT/SUIT_ShortcutMgr.h @@ -27,13 +27,16 @@ #include #include +#include #include #include +#include #include 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 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 getActionNameFromResources(const QString& theActionID, QString theLanguage = QString()); + \returns {assetsExist, assets}. */ + static std::pair 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 getIDsOfInterferingModules(const QString& theModuleID) const; - /*! \brief Retrieves module name translated to appropriate language. */ - QString getModuleName(const QString& theModuleID) const; + std::shared_ptr 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 getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const; + + std::shared_ptr 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>& 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: - -
- - - -
-
- - - -
+ } */ - 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> myActionNames; + /* {actionID, assets}[] */ + std::map> myActionAssets; + + /* {moduleID, assets}[] */ + mutable std::map> myModuleAssets; }; #if defined WIN32 diff --git a/src/SUIT/resources/action_assets.json b/src/SUIT/resources/action_assets.json new file mode 100644 index 000000000..eefb7012c --- /dev/null +++ b/src/SUIT/resources/action_assets.json @@ -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