1: /*
     2:  ============================================================================
     3:  xoblite™ -> an advanced "extended shell" for Microsoft® Windows® 11
     4:  Copyright © 2002-2026 Karl Henrik Henriksson [qwilk/@xoblite]
     5:  Copyright © 2001-2004 The Blackbox for Windows Development Team
     6:  http://xoblite.net/ + https://github.com/xoblite/
     7:  ============================================================================
     8: 
     9:   Blackbox for Windows is free software, released under the
    10:   GNU General Public License (GPL version 2 or later), with an extension
    11:   that allows linking of proprietary modules under a controlled interface.
    12:   What this means is that plugins etc. are allowed to be released
    13:   under any license the author wishes. Please note, however, that the
    14:   original Blackbox gradient math code used in Blackbox for Windows
    15:   is available under the BSD license.
    16: 
    17:   http://www.fsf.org/licenses/gpl.html
    18:   http://www.fsf.org/licenses/gpl-faq.html#LinkingOverControlledInterface
    19:   http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
    20: 
    21:   This program is distributed in the hope that it will be useful,
    22:   but WITHOUT ANY WARRANTY; without even the implied warranty of
    23:   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    24:   GNU General Public License for more details.
    25: 
    26:   For additional license information, please read the included license.html.
    27: 
    28:  ============================================================================
    29: */
    30: 
    31: #include "MenuItem.h" 
    32: #include "MenuCommon.h" 
    33: #include "PreviewItem.h" 
    34: #include "..\Settings\Settings.h" 
    35: 
    36: extern MenuCommon *pMenuCommon;
    37: extern PreviewItem* pPreviewItem;
    38: extern Settings *pSettings;
    39: 
    40: //===========================================================================
    41: 
    42: MenuItem::MenuItem(int type, char* pszTitleANSI, wchar_t* pszTitleUnicode, char* pszCommand, char* pszArgument, bool isSelected)
    43: {
    44:     itemType = type;
    45: 
    46:     if (pszTitleANSI == NULL) m_pszTitleANSI = _strdup("");
    47:     else m_pszTitleANSI = _strdup(pszTitleANSI);
    48: 
    49:     if (pszTitleUnicode == NULL)
    50:     {
    51:         if (!pSettings->disableUnicodeParsing)
    52:         {
    53:             wchar_t itemTitleUnicode[MAX_LINE_LENGTH];
    54:             MultiByteToWideChar(CP_UTF8, 0, pszTitleANSI, strlen(pszTitleANSI)+1, itemTitleUnicode, MAX_LINE_LENGTH);
    55:             m_pszTitleUnicode = _wcsdup(itemTitleUnicode);
    56:         }
    57:         else m_pszTitleUnicode = _wcsdup(L"");
    58:     }
    59:     else m_pszTitleUnicode = _wcsdup(pszTitleUnicode);
    60: 
    61:     //====================
    62: 
    63:     m_pParent = NULL;
    64:     m_nWidth = 0;
    65:     m_nHeight = 0;
    66: 
    67:     m_bActive = false;
    68:     m_nTimerId = 0;
    69: 
    70:     m_selectionGroup = 0; // Default -> Not part of any selection group (i.e. a group of mutually exclusive boolean items)
    71: 
    72:     SetSortPriority(PRIORITY_NORMAL); // Default sort priority unless defined
    73: 
    74:     //====================
    75: 
    76:     if (type == MENUITEM_COMMAND || type == MENUITEM_BOOLEAN || type == MENUITEM_EDITINT)
    77:     {
    78:         // Creates a command-type menu item object (being either a regular, a boolean or an integer editing item)
    79:         // pszCommand -> The command associated with this menu item
    80:         // pszArgument -> Optional argument to pszCommand
    81:         // pszTitle -> The text to be drawn on the menu item
    82: 
    83:         m_pszCommand = pszCommand ? _strdup(pszCommand) : _strdup("");
    84:         m_pszArgument = pszArgument ? _strdup(pszArgument) : _strdup("");
    85:         m_pszOriginalTitleANSI = pszTitleANSI ? _strdup(pszTitleANSI) : _strdup("");
    86:         m_pszOriginalTitleUnicode = pszTitleUnicode ? _wcsdup(pszTitleUnicode) : _wcsdup(L"");
    87:         m_isSelected = isSelected;
    88: 
    89:         if (type == MENUITEM_EDITINT) // Used by MakeMenuItemInt...
    90:         {
    91:             char command[MAX_LINE_LENGTH], value[10], min[10], max[10];
    92:             LPSTR tokens[3];
    93:             tokens[0] = command;
    94:             tokens[1] = value;
    95:             tokens[2] = min;
    96:             command[0] = value[0] = min[0] = max[0] = '\0';
    97:             BBTokenize (m_pszCommand, tokens, 3, max);
    98: 
    99:             m_pszCommand = command ? _strdup(command) : _strdup("");
   100: 
   101:             m_currentValue = atoi(value);
   102:             m_minValue = atoi(min);
   103:             m_maxValue = atoi(max);
   104: 
   105:             char itemstringANSI[MAX_LINE_LENGTH];
   106:             sprintf_s(itemstringANSI, sizeofArray(itemstringANSI), "%s <%d>", m_pszOriginalTitleANSI, m_currentValue);
   107:             SetTitleANSI(itemstringANSI);
   108: 
   109:             wchar_t itemStringUnicode[MAX_LINE_LENGTH];
   110:             swprintf(itemStringUnicode, sizeof(itemStringUnicode), L"%s <%d>", m_pszOriginalTitleUnicode, m_currentValue);
   111:             SetTitleUnicode(itemStringUnicode);
   112:         }
   113:     }
   114: 
   115:     //====================
   116: 
   117:     else
   118:     {
   119:         m_pszCommand = NULL;
   120:         m_pszArgument = NULL;
   121:         m_pszOriginalTitleANSI = NULL;
   122:         m_pszOriginalTitleUnicode = NULL;
   123: //      m_isSelected = false;
   124:         m_isSelected = isSelected;
   125: 
   126:         m_currentValue = m_minValue = m_maxValue = 0;
   127:     }
   128: 
   129:     //====================
   130: 
   131:     // Set a higher sort priority for [separator] items to make
   132:     // sort before any regular menu items (i.e. between folders
   133:     // regular menu items)
   134: //  if (itemType == MENUITEM_SEPARATOR) SetSortPriority(PRIORITY_SEPARATOR);
   135: //  else if (itemType == MENUITEM_HEADER) SetSortPriority(PRIORITY_HEADER);
   136: }
   137: 
   138: //===========================================================================
   139: 
   140: MenuItem::~MenuItem()
   141: {
   142:     ItemKillTimer();
   143: 
   144:     if (m_pszTitleANSI) free(m_pszTitleANSI);
   145:     m_pszTitleANSI = NULL;
   146:     if (m_pszTitleUnicode) free(m_pszTitleUnicode);
   147:     m_pszTitleUnicode = NULL;
   148: 
   149:     if (m_pszOriginalTitleANSI) free(m_pszOriginalTitleANSI);
   150:     m_pszOriginalTitleANSI = NULL;
   151:     if (m_pszOriginalTitleUnicode) free(m_pszOriginalTitleUnicode);
   152:     m_pszOriginalTitleUnicode = NULL;
   153: 
   154:     if (m_pszCommand) free(m_pszCommand);
   155:     m_pszCommand = NULL;
   156:     if (m_pszArgument) free(m_pszArgument);
   157:     m_pszArgument = NULL;
   158: }
   159: 
   160: //===========================================================================
   161: 
   162: LRESULT MenuItem::NcHitTest(int x, int y)
   163: {
   164:     if (itemType == MENUITEM_HEADER || itemType == MENUITEM_FOOTER)
   165:     {
   166:         if (IsOver(x, y) && (!m_pParent->GetParent() || m_pParent->IsPinned())) return HTCAPTION;
   167:     }
   168: 
   169:     return HTCLIENT;
   170: }
   171: 
   172: //===========================================================================
   173: 
   174: int MenuItem::IsOver(int x, int y)
   175: {
   176:     if ((x >= 0) && (x <= GetWidth()) && (y >= m_nTop) &&  (y <= m_nTop + GetHeight()))
   177:         return true;
   178:     else
   179:         return false;
   180: }
   181: 
   182: //===========================================================================
   183: 
   184: void MenuItem::Mouse(UINT nMsg, POINT pt)
   185: {
   186:     // Save the mouse position...
   187:     // (this may be used by MenuCommon to detect where on the
   188:     // item you clicked, e.g. for integer editing items)
   189:     m_mousePos.x = pt.x;
   190:     m_mousePos.y = pt.y;
   191: 
   192:     //====================
   193: 
   194:     RECT r;
   195:     GetItemRect(&r);
   196: 
   197:     if (PtInRect(&r, pt))
   198:     {
   199:         m_nLastMouseButton = nMsg;
   200: 
   201:         switch (nMsg)
   202:         {
   203:             case WM_LBUTTONDOWN:
   204:             case WM_RBUTTONDOWN:
   205:             {
   206:                 if (itemType == MENUITEM_EDITINT) ItemSetTimer();
   207:                 break;
   208:             }
   209: 
   210:             case WM_LBUTTONUP:
   211:             case WM_NCLBUTTONUP:
   212:             {
   213:                 ItemKillTimer();
   214:                 Invoke(1);
   215:                 break;
   216:             }
   217: 
   218:             case WM_RBUTTONUP:
   219:             case WM_NCRBUTTONUP:
   220:             {
   221:                 ItemKillTimer();
   222: //              if ((itemType == MENUITEM_HEADER) && (GetAsyncKeyState(VK_MENU) & 0x8000)) m_pParent->TogglePinned();
   223:                 if ((itemType == MENUITEM_HEADER || itemType == MENUITEM_FOOTER) && (GetAsyncKeyState(VK_MENU) & 0x8000)) m_pParent->TogglePinned();
   224:                 else Invoke(2);
   225:                 break;
   226:             }
   227: 
   228:             case WM_MBUTTONUP:
   229:             case WM_NCMBUTTONUP:
   230:             case WM_LBUTTONDBLCLK:
   231:             case WM_NCLBUTTONDBLCLK:
   232:             {
   233:                 // Mid click or left doubleclick on menu title or grip
   234:                 // -> Toggle pinned state for the menu...
   235: //              if (itemType == MENUITEM_HEADER) m_pParent->TogglePinned();
   236:                 if (itemType == MENUITEM_HEADER || itemType == MENUITEM_FOOTER) m_pParent->TogglePinned();
   237:                 else Invoke(3);
   238:                 break;
   239:             }
   240: 
   241:             case WM_MOUSEMOVE:
   242:             case WM_NCMOUSEMOVE:
   243:             {
   244:                 Active(true);
   245:                 break;
   246:             }
   247: 
   248:             default:
   249:                 break;
   250:         }
   251:     }
   252:     else Active(false);
   253: }
   254: 
   255: //===========================================================================
   256: 
   257: bool MenuItem::Active(bool bActive)
   258: {
   259:     if (itemType != MENUITEM_COMMAND && bActive) pPreviewItem->Hide();
   260: 
   261:     if (itemType == MENUITEM_HEADER || itemType == MENUITEM_SEPARATOR || itemType == MENUITEM_NOP
   262:         || itemType == MENUITEM_MARGINPAD || itemType == MENUITEM_FOOTER)
   263:     {
   264:         return false;
   265:     }
   266: 
   267:     //====================
   268: 
   269:     // Has the menu item active state changed?
   270:     if (m_bActive != bActive)
   271:     {
   272:         if (bActive)
   273:         {
   274:             // We only redraw the window when a new item becomes *active*...
   275:             // (i.e. to avoid double redraws as active state moves from one item to another)
   276:             pMenuCommon->FindActiveAndDeactivate(m_pParent, false);
   277:             m_bActive = true;
   278:             m_pParent->UpdateMenuWindow();
   279: 
   280:             // Should we also display an image preview for the new active item?
   281:             if (bActive && (itemType == MENUITEM_COMMAND) && !_stricmp(m_pszCommand, "[exec]"))
   282:             {
   283:                 char path[MAX_PATH];
   284:                 strcpy_s(path, sizeofArray(path), m_pszArgument);
   285:                 if (strchr(path, '\"')) StrRemoveEncap(path);
   286:                 if (strchr(path, '$')) ReplaceShellFolders(path);
   287:                 if (strchr(path, '%')) ReplaceEnvVars(path);
   288:                 int pointer = strnlen_s(path, sizeofArray(path)) - 4;
   289: 
   290:                 if ((path[1] == ':') && (pointer > 0))
   291:                 {
   292:                     if (!_stricmp(&path[pointer], ".jpg") || !_stricmp(&path[pointer], ".png") || !_stricmp(&path[pointer], ".bmp") || !_stricmp(&path[pointer], "jpeg"))
   293:                     {
   294:                         int screenWidth = GetSystemMetrics(SM_CXSCREEN);
   295:                         int previewItemWidth = pPreviewItem->GetWidth();
   296: 
   297:                         RECT parentRect;
   298:                         GetWindowRect(m_pParent->GetWindow(), &parentRect);
   299: 
   300:                         int gap = 2 * pSettings->scalingFactorHiDPI;
   301: 
   302:                         int x = parentRect.right + gap;
   303:                         int y = parentRect.top + m_nTop + (m_nHeight / 2);
   304: 
   305:                         // If there isn't enough space for the preview item on the right hand
   306:                         // side of the parent menu, we move it to the left hand side instead...
   307:                         if ((x + previewItemWidth) > screenWidth) x = parentRect.left - gap - previewItemWidth;
   308: 
   309:                         // Show the preview item after a short delay...
   310:                         // (nb. we do this to get smoother fast scrolling through parent menus,
   311:                         //  i.e. no loading of preview images on every single entry while scrolling)
   312:                         // + Note: If width and height are both set to 0 like here, PreviewItem will
   313:                         // auto-resize itself to match the aspect ratio of the to-be-previewed image.
   314:                         pPreviewItem->Show(x, y, 0, 0, path, 200);
   315:                     }
   316:                     else pPreviewItem->Hide();
   317:                 }
   318:                 else pPreviewItem->Hide();
   319:             }
   320:         }
   321:         else m_bActive = false;
   322:     }
   323: 
   324:     //====================
   325: 
   326:     if (!m_bActive) ItemKillTimer();
   327: 
   328:     //====================
   329: 
   330:     return m_bActive;
   331: }
   332: 
   333: //===========================================================================
   334: 
   335: void MenuItem::ItemSetTimer()
   336: {
   337:     if (m_nTimerId == 0)
   338:     {
   339:         // Wait 500 milliseconds until we start changing the value...
   340: //      SendMessage(GetBBWnd(), BB_CONSOLEMESSAGE, (WPARAM)CONSOLE_WARNING_MESSAGE, (LPARAM)"Setting timer...");
   341:         m_nTimerId = (UINT)this;
   342:         SetTimer(GetWindow(), m_nTimerId, 500, NULL);
   343:     }
   344: }
   345: 
   346: //====================
   347: 
   348: void MenuItem::Timer(int nTimer)
   349: {
   350:     // Execute timer messages...
   351:     if ((UINT)nTimer == m_nTimerId)
   352:     {
   353:         // Update value and reset timer to 100 milliseconds... 
   354: //      SendMessage(GetBBWnd(), BB_CONSOLEMESSAGE, (WPARAM)CONSOLE_WARNING_MESSAGE, (LPARAM)this->m_pszTitle);
   355:         KillTimer(GetWindow(), m_nTimerId);
   356:         if (m_nLastMouseButton == WM_RBUTTONDOWN) pMenuCommon->Invoke(this, 2);
   357:         else pMenuCommon->Invoke(this, 1);
   358:         SetTimer(GetWindow(), m_nTimerId, 100, NULL);
   359:     }
   360: }
   361: 
   362: //====================
   363: 
   364: void MenuItem::ItemKillTimer()
   365: {
   366:     if (m_nTimerId != 0)
   367:     {
   368: //      SendMessage(GetBBWnd(), BB_CONSOLEMESSAGE, (WPARAM)CONSOLE_WARNING_MESSAGE, (LPARAM)"Killing timer...");
   369:         KillTimer(GetWindow(), m_nTimerId);
   370:         m_nTimerId = 0;
   371:     }
   372: }
   373: 
   374: //===========================================================================
   375: 
   376: void MenuItem::Invoke(int button)
   377: {
   378:     if ((itemType == MENUITEM_HEADER) && (button == 2))
   379:     {
   380: //      if (!m_pParent->IsPinned()) pMenuCommon->Hide();
   381: //      else if (m_pParent->m_pszFolderPath != NULL)
   382:         if (m_pParent->m_pszFolderPath != NULL)
   383:         {
   384:             // ##### TODO: Move to MenuCommon along with similar code in SpecialFolder.cpp #####
   385:             char folder[MAX_LINE_LENGTH], arg[MAX_LINE_LENGTH];
   386:             strcpy_s(folder, sizeofArray(folder), m_pParent->m_pszFolderPath);
   387: 
   388:             if (strchr(folder, '|'))
   389:             {
   390:                 // If this is a merged [path] item we use
   391:                 // the first path when right clicking on it...
   392:                 Tokenize(folder, arg, "|");
   393:                 strcpy_s(folder, sizeofArray(folder), arg); // Note: Just using arg as a temp variable here...
   394:             }
   395: 
   396:             if (folder[0] == '\"') StrRemoveEncap(folder);
   397:             strcpy_s(arg, sizeofArray(arg), "/e,");
   398:             strcat_s(arg, sizeofArray(arg), folder);
   399: 
   400:             BBExecute(GetDesktopWindow(), NULL, "explorer.exe", arg, folder, SW_SHOWNORMAL, false);
   401:             PlaySoundFX(SFX_MENU_CLICK);
   402:         }
   403:     }
   404:     else if (itemType == MENUITEM_COMMAND || itemType == MENUITEM_BOOLEAN || itemType == MENUITEM_EDITINT) pMenuCommon->Invoke(this, button);
   405: }
   406: 
   407: //===========================================================================
   408: 
   409: void MenuItem::GetItemRect(RECT* r)
   410: {
   411:     r->left = m_nLeft;
   412:     r->top = m_nTop;
   413:     r->right = m_nLeft + m_nWidth;
   414:     r->bottom = m_nTop + m_nHeight;
   415: }
   416: 
   417: //====================
   418: 
   419: void MenuItem::GetTitleRect(RECT* r)
   420: {
   421:     if (itemType == MENUITEM_HEADER || itemType == MENUITEM_FOOTER) // Header and grip items
   422:     {
   423: //      r->left = m_nLeft + pSettings->MenuTitle->borderWidth + pSettings->bevelWidth + 2;
   424:         r->left = m_nLeft + pSettings->MenuTitle->borderWidth + 3;
   425:         r->top = m_nTop;
   426: //      r->right = m_nLeft + m_nWidth - pSettings->MenuTitle->borderWidth - pSettings->bevelWidth - 2;
   427:         r->right = m_nLeft + m_nWidth - pSettings->MenuTitle->borderWidth - 3;
   428:         r->bottom = m_nTop + m_nHeight;
   429:         if (!_stricmp(pSettings->MenuTitle->Font, "nu")) r->bottom--;
   430:     }
   431:     else // Normal items
   432:     {
   433:         r->left = m_nLeft + pMenuCommon->m_nLeftIndent;
   434:         r->top = m_nTop;
   435:         r->right = m_nLeft + m_nWidth - pMenuCommon->m_nRightIndent;
   436:         r->bottom = m_nTop + m_nHeight;
   437:     }
   438: }
   439: 
   440: //===========================================================================
   441: 
   442: UINT MenuItem::GetDrawTextFormat()
   443: {
   444:     if (itemType == MENUITEM_HEADER) // Header items
   445:         return pSettings->MenuTitle->Justify | DT_VCENTER | DT_NOPREFIX | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOCLIP;
   446:     if (itemType == MENUITEM_FOOTER) // Grip items
   447:         return pSettings->MenuGrip->Justify | DT_VCENTER | DT_NOPREFIX | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOCLIP;
   448:     else // Normal items
   449:         return pSettings->MenuFrame->Justify | DT_VCENTER | DT_NOPREFIX | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOCLIP;
   450: }
   451: 
   452: //===========================================================================
   453: 
   454: void MenuItem::SetTitleANSI(char* pszTitleANSI)
   455: {
   456:     if (m_pszTitleANSI) free(m_pszTitleANSI);
   457:     m_pszTitleANSI = pszTitleANSI ? _strdup(pszTitleANSI) : _strdup("");;
   458: }
   459: 
   460: void MenuItem::SetTitleUnicode(wchar_t* pszTitleUnicode)
   461: {
   462:     if (m_pszTitleUnicode) free(m_pszTitleUnicode);
   463:     m_pszTitleUnicode = pszTitleUnicode ? _wcsdup(pszTitleUnicode) : _wcsdup(L"");;
   464: }
   465: 
   466: //===========================================================================
   467: 
   468: void MenuItem::Attached(Menu* pMenu)
   469: {
   470:     m_pParent = pMenu;
   471: }
   472: 
   473: //===========================================================================
   474: 
   475: HWND MenuItem::GetWindow()
   476: {
   477:     if (m_pParent) return m_pParent->GetWindow();
   478:     else return NULL;
   479: }
   480: 
   481: //===========================================================================
   482: 
   483: LRESULT MenuItem::Command(WPARAM wParam, LPARAM lParam)
   484: {
   485:     return 0;
   486: }
   487: 
   488: //===========================================================================
   489: // Functions: Compare
   490: // Purpose: Compares the title strings of menu items,
   491: //          and returns true if m1 should be sorted before m2
   492: //===========================================================================
   493: 
   494: bool MenuItem::Compare(MenuItem* m1, MenuItem* m2)
   495: {
   496:     // Menu item priorities:
   497:     // MENUITEM_HEADER              -> 255
   498:     // MENUITEM_MARGINPAD (top)     -> 254
   499:     // MENUITEM_FOLDER              -> 4
   500:     // MENUITEM_SEPARATOR           -> 3
   501:     // === Regular items ===        -> 2
   502:     // MENUITEM_MARGINPAD (bottom)  -> 1
   503:     // MENUITEM_FOOTER              -> 0
   504: 
   505:     int m1_sort = m1->GetSortPriority();
   506:     int m2_sort = m2->GetSortPriority();
   507: 
   508:     if (m1_sort == m2_sort)
   509:     {
   510:         if (m1->m_pszTitleUnicode != NULL)
   511:         {
   512:             if (m2->m_pszTitleUnicode != NULL) return _wcsicmp(m1->m_pszTitleUnicode, m2->m_pszTitleUnicode) < 0; // -> Both m1 and m2 are Unicode items, do a regular string compare...
   513:             else return true; // -> m1 being a Unicode item is sorted before m2 being an ANSI item...
   514:         }
   515:         else if (m2->m_pszTitleUnicode != NULL) return false;
   516:         else return _stricmp(m1->GetSortString(), m2->GetSortString()) < 0; // -> Both m1 and m2 are ANSI items, do a regular string compare...
   517:     }
   518:     else return m1_sort > m2_sort;
   519: }
   520: 
   521: //===========================================================================
   522: // Functions: ToggleSelected
   523: // Purpose: ...
   524: //===========================================================================
   525: 
   526: void MenuItem::ToggleSelected()
   527: {
   528:     if (m_isSelected) m_isSelected = false;
   529:     else m_isSelected = true;
   530: }
   531: 
   532: //===========================================================================
   533: // Functions: AddToSelectionGroup
   534: // Purpose: ...
   535: //===========================================================================
   536: 
   537: void MenuItem::AddToSelectionGroup(int group)
   538: {
   539:     m_selectionGroup = group;
   540: }
   541: 
   542: //===========================================================================
   543: