Menu

#224 Keyboard shortcuts in TButtonGadget hint text

Owlet
pending
GUI (32)
1
2023-06-11
2023-04-22
No

The gadget framework automatically generates hint text for TButtonGadget if its ID matches a menu command, provided there is no hint text in the string resources with the same ID.

It would be very cool if the generated hint text also included the keyboard shortcut (accelerator) for the command. And it would be even cooler, if it would synthesise the key sequence for the command using the ampersand mnemonics, if there is no explicit accelerator for the command. For example, "Open (Alt+F,O)" for main menu command "&File | &Open", and "Options (Alt+T,O)" for main menu command "&Tools | &Options".

Discussion

  • Vidar Hasfjord

    Vidar Hasfjord - 2023-04-23
    • assigned_to: Vidar Hasfjord
    • Group: unspecified --> Owlet
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2023-04-23

    Except for the key sequence synthesis based on mnemonics, I've now implemented a solution in Owlet (not yet committed, though). This only required updating TDecoratedFrame::GetHintText as follows below. Note that I've changed the functionality a bit as well; the string resource tip text is now preferentially used, if it exists, rather than the other way around in the old code. This allows the user to override the auto-generated tip text, simply by creating a string resource.

    //
    /// Returns a hint text for the given command ID.
    /// 
    /// The function tries to load a string resource with the same ID. The string is assumed to have
    /// the following form: "status text\ntip text", where "\ntip text" is optional.
    /// 
    /// If \p hintType equals \ref htToolTip, the tip text in the string resource is returned, provided
    /// the string resource exists and contains a tip text. If a tip text is not found in the resource,
    /// the ID is looked up in the menu, and if found, a tip text is generated from the menu item text.
    /// 
    /// If \p hintType equals \ref htStatus, the status text in the string resource is returned,
    /// provided the string resource exists.
    /// 
    /// Otherwise an empty string is returned.
    //
    auto TDecoratedFrame::GetHintText(uint id, THintText hintType) const -> tstring
    {
      if (id == static_cast<uint>(-1)) return {};
    
      const auto loadHint = [&](uint id) -> tstring
      {
        const auto s = LoadString(id);
        return !s.empty() ? s :
          MergeModule ? MergeModule->LoadString(id) :
          tstring{};
      };
    
      if (hintType == htTooltip)
      {
        const auto loadTipText = [&](uint id) -> tstring
        {
          const auto hint = loadHint(id);
          if (hint.empty()) return hint;
          const auto i = hint.find(_T('\n')); // The tip text, if any, follows the newline.
          return i != hint.npos ? hint.substr(i + 1) : tstring{};
        };
    
        const auto getMenuString = [](HMENU menu, uint id) -> tstring
        { 
          return menu ? TMenu{menu}.GetMenuString(id, MF_BYCOMMAND) : tstring{};
        };
    
        if (const auto tip = loadTipText(id); !tip.empty())
        {
          OWL_TRACEX(OwlWin/1, _T("TDecoratedFrame::GetHintText: Loaded tip text: \"") << tip << _T('\"'));
          return tip;
        }
        else if (auto menuItem = getMenuString(GetMenu(), id); !menuItem.empty())
        {
          // Parse menu items of the form: "&Print...\tCtrl+P" or "F-Nine (&9)...\tF9", where the embedded
          // mnemonic, trailing mnemonic, ellipsis and accelerator (including the tab) are optional.
          //
          const auto re = tregex{_T(R"(^ *([^&]*?(?:&(.))?[^&]*?) *(?:\(&(.)\))?(?:\.\.\.)?(?:[\t\x08](.+))?$)")};
          auto m = tsmatch{};
          const auto r = regex_match(menuItem, m, re);
          OWL_WARNX(OwlWin, !r, _T("TDecoratedFrame::GetHintText: Parsing menu item \"") << menuItem << _T("\" failed."));
          if (!r) return {};
          const auto mainText = regex_replace(m[1].str(), tregex{_T('&')}, _T("")); // Remove ampersand.
          const auto embeddedMnemonic = m[2].str();
          const auto trailingMnemonic = m[3].str();
          const auto accelerator = m[4].str();
          OWL_TRACEX(OwlWin/1, _T("TDecoratedFrame::GetHintText: Parsed menu item: \"") << menuItem
            << _T("\", and got mainText: \"") << mainText
            << _T("\", embeddedMnemonic: \"") << embeddedMnemonic
            << _T("\", trailingMnemonic: \"") << trailingMnemonic
            << _T("\", accelerator: \"") << accelerator << _T('\"'));
    
          // Construct the hint text with the main text and the accelerator key.
          //
          const auto tip = accelerator.empty() ? mainText : mainText + _T(" (") + accelerator + _T(')');
          OWL_TRACEX(OwlWin/1, _T("TDecoratedFrame::GetHintText: Constructed tip text: \"") << tip << _T('\"'));
          return tip;
        }
      }
    
      else if (hintType == htStatus)
      {
        const auto hint = loadHint(id);
        OWL_WARNX(OwlWin, hint.empty(), _T("TDecoratedFrame::GetHintText: LoadString failed: (id = ") << id << _T(')'));
        if (hint.empty()) return {};
        const auto status = hint.substr(0, hint.find(_T('\n'))); // The status text precedes the newline, if any.
        OWL_TRACEX(OwlWin/1, _T("TDecoratedFrame::GetHintText: Loaded status text: \"") << status << _T('\"'));
        return status;
      }
    
      return {};
    }
    
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2023-04-25

    Here is code for synthesising key sequences, as proposed. Bing Chat helped me with this. In particular, to find the localised name of the Alt key, I didn't know about the Windows API function GetKeyNameText, which it suggested as a solution rather than creating another string resource (which may not have reflected the language of the user's keyboard).

    namespace
    {
    
      //
      // Helper function that constructs the given menu item's ancestral path from the given main menu.
      // Returns a container of labels from the main menu to the item itself, provided an item with the
      // given ID is found. Otherwise, an empty container is returned.
      //
      auto GetMenuItemPath_(const TMenu& menu, uint itemId) -> deque<tstring>
      {
        const auto count = menu.GetMenuItemCount();
        for (auto i = 0; i != count; ++i)
        {
          // If the item matches the given ID, return a path consisting of an entry for this item.
          // Otherwise, if the child is a submenu, get the path of the item within the submenu, then
          // if found, prepend the label for this item and return the result. 
          //
          if (itemId == menu.GetMenuItemID(i))
          {
            return {menu.GetMenuString(i, MF_BYPOSITION)};
          }
          else if (const auto hSubMenu = menu.GetSubMenu(i))
          {
            if (auto path = GetMenuItemPath_(TMenu{hSubMenu}, itemId); !path.empty())
            {
              path.push_front(menu.GetMenuString(i, MF_BYPOSITION));
              return path;
            }
          }
        }
        return {};
      }
    
      //
      // Helper function that generates the key presses needed to reach a given menu item. For example,
      // for menu item CM_FILESAVEAS with path {"&File", "Save &as..."}, the function return "Alt+F,A".
      //
      auto SynthesizeKeyboardSequence_(const TMenu& menu, uint id) -> tstring
      {
        const auto getMnemonic = [](const tstring& label) -> tchar
        {
          const auto pos = label.find(_T('&'));
          return (pos != label.npos && pos + 1 < label.size()) ?
            _totupper(label[pos + 1]) :
            _T('\0');
        };
    
        const auto getKeyName = [](uint vkCode) -> tstring
        {
          // Convert the given virtual-key code to a scan code using MapVirtualKey. Set bit 25 to
          // indicate that we do not care about left/right key variants. Then get the key name and
          // transform it from all-caps to capitalised form, e.g. "ALT" -> "Alt".
          //
          auto buf = array<TCHAR, 40>{};
          const auto scanCode = (::MapVirtualKey(vkCode, MAPVK_VK_TO_VSC) << 16) | (1 << 25);
          const auto n = ::GetKeyNameText(scanCode, data(buf), static_cast<int>(size(buf)));
          if (n <= 0) throw TXOwl{__FUNCTION__ ": GetKeyNameText failed"};
          transform(++begin(buf), begin(buf) + n, ++begin(buf), [](auto c) { return _totlower(c); });
          return tstring(data(buf), n);
        };
    
        // Get the path of labels from the root menu to the item itself. Then iterate over the path and
        // append the mnemonic character of each label to the sequence. For example, for the ancestral
        // path {"&File", "Save &as"}, the result should be "Alt+F,A".
        //
        auto sequence = tostringstream{};
        sequence << getKeyName(VK_MENU);
        auto divider = _T('+');
        const auto path = GetMenuItemPath_(menu, id);
        for (const auto& label : path)
        {
          const auto mnemonic = getMnemonic(label);
          if (!mnemonic) return {}; // If any label lacks a mnemonic, return an empty sequence.
          sequence << divider << mnemonic;
          divider = _T(',');
        }
        return sequence.str();
      };
    
    } // namespace
    
     
  • Vidar Hasfjord

    Vidar Hasfjord - 2023-06-11

    This feature was implemented in Owlet [r6387].

     

    Related

    Commit: [r6387]

  • Vidar Hasfjord

    Vidar Hasfjord - 2023-06-11
    • status: open --> pending
     

Anonymous
Anonymous

Add attachments
Cancel