- add sources.
[platform/framework/web/crosswalk.git] / src / ui / views / controls / menu / menu_win.cc
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/views/controls/menu/menu_win.h"
6
7 #include <string>
8
9 #include "base/logging.h"
10 #include "base/stl_util.h"
11 #include "base/strings/string_util.h"
12 #include "ui/base/accelerators/accelerator.h"
13 #include "ui/base/l10n/l10n_util.h"
14 #include "ui/base/l10n/l10n_util_win.h"
15 #include "ui/events/keycodes/keyboard_codes.h"
16 #include "ui/gfx/canvas.h"
17 #include "ui/gfx/font.h"
18 #include "ui/gfx/rect.h"
19 #include "ui/gfx/win/window_impl.h"
20 #include "ui/views/layout/layout_constants.h"
21
22 namespace views {
23
24 // The width of an icon, including the pixels between the icon and
25 // the item label.
26 const int kIconWidth = 23;
27 // Margins between the top of the item and the label.
28 const int kItemTopMargin = 3;
29 // Margins between the bottom of the item and the label.
30 const int kItemBottomMargin = 4;
31 // Margins between the left of the item and the icon.
32 const int kItemLeftMargin = 4;
33 // The width for displaying the sub-menu arrow.
34 const int kArrowWidth = 10;
35
36 // Current active MenuHostWindow. If NULL, no menu is active.
37 static MenuHostWindow* active_host_window = NULL;
38
39 // The data of menu items needed to display.
40 struct MenuWin::ItemData {
41   string16 label;
42   gfx::ImageSkia icon;
43   bool submenu;
44 };
45
46 namespace {
47
48 static int ChromeGetMenuItemID(HMENU hMenu, int pos) {
49   // The built-in Windows GetMenuItemID doesn't work for submenus,
50   // so here's our own implementation.
51   MENUITEMINFO mii = {0};
52   mii.cbSize = sizeof(mii);
53   mii.fMask = MIIM_ID;
54   GetMenuItemInfo(hMenu, pos, TRUE, &mii);
55   return mii.wID;
56 }
57
58 // MenuHostWindow -------------------------------------------------------------
59
60 // MenuHostWindow is the HWND the HMENU is parented to. MenuHostWindow is used
61 // to intercept right clicks on the HMENU and notify the delegate as well as
62 // for drawing icons.
63 //
64 class MenuHostWindow : public gfx::WindowImpl {
65  public:
66   MenuHostWindow(MenuWin* menu, HWND parent_window) : menu_(menu) {
67     int extended_style = 0;
68     // If the menu needs to be created with a right-to-left UI layout, we must
69     // set the appropriate RTL flags (such as WS_EX_LAYOUTRTL) property for the
70     // underlying HWND.
71     if (menu_->delegate()->IsRightToLeftUILayout())
72       extended_style |= l10n_util::GetExtendedStyles();
73     set_window_style(WS_CHILD);
74     set_window_ex_style(extended_style);
75     Init(parent_window, gfx::Rect());
76   }
77
78   ~MenuHostWindow() {
79     DestroyWindow(hwnd());
80   }
81
82   BEGIN_MSG_MAP_EX(MenuHostWindow);
83     MSG_WM_RBUTTONUP(OnRButtonUp)
84     MSG_WM_MEASUREITEM(OnMeasureItem)
85     MSG_WM_DRAWITEM(OnDrawItem)
86   END_MSG_MAP();
87
88  private:
89   // NOTE: I really REALLY tried to use WM_MENURBUTTONUP, but I ran into
90   // two problems in using it:
91   // 1. It doesn't contain the coordinates of the mouse.
92   // 2. It isn't invoked for menuitems representing a submenu that have children
93   //   menu items (not empty).
94
95   void OnRButtonUp(UINT w_param, const CPoint& loc) {
96     int id;
97     if (menu_->delegate() && FindMenuIDByLocation(menu_, loc, &id))
98       menu_->delegate()->ShowContextMenu(menu_, id, gfx::Point(loc), true);
99   }
100
101   void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) {
102     MenuWin::ItemData* data =
103         reinterpret_cast<MenuWin::ItemData*>(lpmis->itemData);
104     if (data != NULL) {
105       gfx::Font font;
106       lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth +
107           kItemLeftMargin + views::kItemLabelSpacing -
108           GetSystemMetrics(SM_CXMENUCHECK);
109       if (data->submenu)
110         lpmis->itemWidth += kArrowWidth;
111       // If the label contains an accelerator, make room for tab.
112       if (data->label.find(L'\t') != string16::npos)
113         lpmis->itemWidth += font.GetStringWidth(L" ");
114       lpmis->itemHeight = font.GetHeight() + kItemBottomMargin + kItemTopMargin;
115     } else {
116       // Measure separator size.
117       lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2;
118       lpmis->itemWidth = 0;
119     }
120   }
121
122   void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) {
123     HDC hDC = lpdis->hDC;
124     COLORREF prev_bg_color, prev_text_color;
125
126     // Set background color and text color
127     if (lpdis->itemState & ODS_SELECTED) {
128       prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_HIGHLIGHT));
129       prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
130     } else {
131       prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU));
132       if (lpdis->itemState & ODS_DISABLED)
133         prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT));
134       else
135         prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT));
136     }
137
138     if (lpdis->itemData) {
139       MenuWin::ItemData* data =
140           reinterpret_cast<MenuWin::ItemData*>(lpdis->itemData);
141
142       // Draw the background.
143       HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC));
144       FillRect(hDC, &lpdis->rcItem, hbr);
145       DeleteObject(hbr);
146
147       // Draw the label.
148       RECT rect = lpdis->rcItem;
149       rect.top += kItemTopMargin;
150       // Should we add kIconWidth only when icon.width() != 0 ?
151       rect.left += kItemLeftMargin + kIconWidth;
152       rect.right -= views::kItemLabelSpacing;
153       UINT format = DT_TOP | DT_SINGLELINE;
154       // Check whether the mnemonics should be underlined.
155       BOOL underline_mnemonics;
156       SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0);
157       if (!underline_mnemonics)
158         format |= DT_HIDEPREFIX;
159       gfx::Font font;
160       HGDIOBJ old_font =
161           static_cast<HFONT>(SelectObject(hDC, font.GetNativeFont()));
162
163       // If an accelerator is specified (with a tab delimiting the rest of the
164       // label from the accelerator), we have to justify the fist part on the
165       // left and the accelerator on the right.
166       // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the
167       //                 window system UI font and will not hit here.
168       string16 label = data->label;
169       string16 accel;
170       string16::size_type tab_pos = label.find(L'\t');
171       if (tab_pos != string16::npos) {
172         accel = label.substr(tab_pos);
173         label = label.substr(0, tab_pos);
174       }
175       DrawTextEx(hDC, const_cast<wchar_t*>(label.data()),
176                  static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL);
177       if (!accel.empty())
178         DrawTextEx(hDC, const_cast<wchar_t*>(accel.data()),
179                    static_cast<int>(accel.size()), &rect,
180                    format | DT_RIGHT, NULL);
181       SelectObject(hDC, old_font);
182
183       // Draw the icon after the label, otherwise it would be covered
184       // by the label.
185       gfx::ImageSkiaRep icon_image_rep = data->icon.GetRepresentation(1.0f);
186       if (data->icon.width() != 0 && data->icon.height() != 0) {
187         gfx::Canvas canvas(icon_image_rep, false);
188         skia::DrawToNativeContext(
189             canvas.sk_canvas(), hDC, lpdis->rcItem.left + kItemLeftMargin,
190             lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top -
191                 data->icon.height()) / 2, NULL);
192       }
193
194     } else {
195       // Draw the separator
196       lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3;
197       DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP);
198     }
199
200     SetBkColor(hDC, prev_bg_color);
201     SetTextColor(hDC, prev_text_color);
202   }
203
204   bool FindMenuIDByLocation(MenuWin* menu, const CPoint& loc, int* id) {
205     int index = MenuItemFromPoint(NULL, menu->menu_, loc);
206     if (index != -1) {
207       *id = ChromeGetMenuItemID(menu->menu_, index);
208       return true;
209     } else {
210       for (std::vector<MenuWin*>::iterator i = menu->submenus_.begin();
211            i != menu->submenus_.end(); ++i) {
212         if (FindMenuIDByLocation(*i, loc, id))
213           return true;
214       }
215     }
216     return false;
217   }
218
219   // The menu that created us.
220   MenuWin* menu_;
221
222   DISALLOW_COPY_AND_ASSIGN(MenuHostWindow);
223 };
224
225 }  // namespace
226
227 // static
228 Menu* Menu::Create(Delegate* delegate,
229                    AnchorPoint anchor,
230                    gfx::NativeView parent) {
231   return new MenuWin(delegate, anchor, parent);
232 }
233
234 // static
235 Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) {
236   return new views::MenuWin(::GetSystemMenu(parent, FALSE));
237 }
238
239 MenuWin::MenuWin(Delegate* d, AnchorPoint anchor, HWND owner)
240     : Menu(d, anchor),
241       menu_(CreatePopupMenu()),
242       owner_(owner),
243       is_menu_visible_(false),
244       owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) {
245   DCHECK(delegate());
246 }
247
248 MenuWin::MenuWin(HMENU hmenu)
249     : Menu(NULL, TOPLEFT),
250       menu_(hmenu),
251       owner_(NULL),
252       is_menu_visible_(false),
253       owner_draw_(false) {
254   DCHECK(menu_);
255 }
256
257 MenuWin::~MenuWin() {
258   STLDeleteContainerPointers(submenus_.begin(), submenus_.end());
259   STLDeleteContainerPointers(item_data_.begin(), item_data_.end());
260   DestroyMenu(menu_);
261 }
262
263 void MenuWin::AddMenuItemWithIcon(int index,
264                                   int item_id,
265                                   const string16& label,
266                                   const gfx::ImageSkia& icon) {
267   owner_draw_ = true;
268   Menu::AddMenuItemWithIcon(index, item_id, label, icon);
269 }
270
271 Menu* MenuWin::AddSubMenuWithIcon(int index,
272                                   int item_id,
273                                   const string16& label,
274                                   const gfx::ImageSkia& icon) {
275   MenuWin* submenu = new MenuWin(this);
276   submenus_.push_back(submenu);
277   AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL);
278   return submenu;
279 }
280
281 void MenuWin::AddSeparator(int index) {
282   MENUITEMINFO mii;
283   mii.cbSize = sizeof(mii);
284   mii.fMask = MIIM_FTYPE;
285   mii.fType = MFT_SEPARATOR;
286   InsertMenuItem(menu_, index, TRUE, &mii);
287 }
288
289 void MenuWin::EnableMenuItemByID(int item_id, bool enabled) {
290   UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
291   EnableMenuItem(menu_, item_id, MF_BYCOMMAND | enable_flags);
292 }
293
294 void MenuWin::EnableMenuItemAt(int index, bool enabled) {
295   UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED;
296   EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags);
297 }
298
299 void MenuWin::SetMenuLabel(int item_id, const string16& label) {
300   MENUITEMINFO mii = {0};
301   mii.cbSize = sizeof(mii);
302   mii.fMask = MIIM_STRING;
303   mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
304   mii.cch = static_cast<UINT>(label.size());
305   SetMenuItemInfo(menu_, item_id, false, &mii);
306 }
307
308 bool MenuWin::SetIcon(const gfx::ImageSkia& icon, int item_id) {
309   if (!owner_draw_)
310     owner_draw_ = true;
311
312   const int num_items = GetMenuItemCount(menu_);
313   int sep_count = 0;
314   for (int i = 0; i < num_items; ++i) {
315     if (!(GetMenuState(menu_, i, MF_BYPOSITION) & MF_SEPARATOR)) {
316       if (ChromeGetMenuItemID(menu_, i) == item_id) {
317         item_data_[i - sep_count]->icon = icon;
318         // When the menu is running, we use SetMenuItemInfo to let Windows
319         // update the item information so that the icon being displayed
320         // could change immediately.
321         if (active_host_window) {
322           MENUITEMINFO mii;
323           mii.cbSize = sizeof(mii);
324           mii.fMask = MIIM_FTYPE | MIIM_DATA;
325           mii.fType = MFT_OWNERDRAW;
326           mii.dwItemData =
327               reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
328           SetMenuItemInfo(menu_, item_id, false, &mii);
329         }
330         return true;
331       }
332     } else {
333       ++sep_count;
334     }
335   }
336
337   // Continue searching for the item in submenus.
338   for (size_t i = 0; i < submenus_.size(); ++i) {
339     if (submenus_[i]->SetIcon(icon, item_id))
340       return true;
341   }
342
343   return false;
344 }
345
346 void MenuWin::RunMenuAt(int x, int y) {
347   SetMenuInfo();
348
349   delegate()->MenuWillShow();
350
351   // NOTE: we don't use TPM_RIGHTBUTTON here as it breaks selecting by way of
352   // press, drag, release. See bugs 718 and 8560.
353   UINT flags =
354       GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE;
355   is_menu_visible_ = true;
356   DCHECK(owner_);
357   // In order for context menus on menus to work, the context menu needs to
358   // share the same window as the first menu is parented to.
359   bool created_host = false;
360   if (!active_host_window) {
361     created_host = true;
362     active_host_window = new MenuHostWindow(this, owner_);
363   }
364   UINT selected_id =
365       TrackPopupMenuEx(menu_, flags, x, y, active_host_window->hwnd(), NULL);
366   if (created_host) {
367     delete active_host_window;
368     active_host_window = NULL;
369   }
370   is_menu_visible_ = false;
371
372   // Execute the chosen command
373   if (selected_id != 0)
374     delegate()->ExecuteCommand(selected_id);
375 }
376
377 void MenuWin::Cancel() {
378   DCHECK(is_menu_visible_);
379   EndMenu();
380 }
381
382 int MenuWin::ItemCount() {
383   return GetMenuItemCount(menu_);
384 }
385
386 void MenuWin::AddMenuItemInternal(int index,
387                                   int item_id,
388                                   const string16& label,
389                                   const gfx::ImageSkia& icon,
390                                   MenuItemType type) {
391   AddMenuItemInternal(index, item_id, label, icon, NULL, type);
392 }
393
394 void MenuWin::AddMenuItemInternal(int index,
395                                   int item_id,
396                                   const string16& label,
397                                   const gfx::ImageSkia& icon,
398                                   HMENU submenu,
399                                   MenuItemType type) {
400   DCHECK(type != SEPARATOR) << "Call AddSeparator instead!";
401
402   if (!owner_draw_ && !icon.isNull())
403     owner_draw_ = true;
404
405   if (label.empty() && !delegate()) {
406     // No label and no delegate; don't add an empty menu.
407     // It appears under some circumstance we're getting an empty label
408     // (l10n_util::GetStringUTF16(IDS_TASK_MANAGER) returns ""). This shouldn't
409     // happen, but I'm working over the crash here.
410     NOTREACHED();
411     return;
412   }
413
414   MENUITEMINFO mii;
415   mii.cbSize = sizeof(mii);
416   mii.fMask = MIIM_FTYPE | MIIM_ID;
417   if (submenu) {
418     mii.fMask |= MIIM_SUBMENU;
419     mii.hSubMenu = submenu;
420   }
421
422   // Set the type and ID.
423   if (!owner_draw_) {
424     mii.fType = MFT_STRING;
425     mii.fMask |= MIIM_STRING;
426   } else {
427     mii.fType = MFT_OWNERDRAW;
428   }
429
430   if (type == RADIO)
431     mii.fType |= MFT_RADIOCHECK;
432
433   mii.wID = item_id;
434
435   // Set the item data.
436   MenuWin::ItemData* data = new ItemData;
437   item_data_.push_back(data);
438   data->submenu = submenu != NULL;
439
440   string16 actual_label(label.empty() ? delegate()->GetLabel(item_id) : label);
441
442   // Find out if there is a shortcut we need to append to the label.
443   ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
444   if (delegate() && delegate()->GetAcceleratorInfo(item_id, &accelerator)) {
445     actual_label += L'\t';
446     actual_label += accelerator.GetShortcutText();
447   }
448   labels_.push_back(actual_label);
449
450   if (owner_draw_) {
451     if (icon.width() != 0 && icon.height() != 0)
452       data->icon = icon;
453     else
454       data->icon = delegate()->GetIcon(item_id);
455   } else {
456     mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str());
457   }
458
459   InsertMenuItem(menu_, index, TRUE, &mii);
460 }
461
462 MenuWin::MenuWin(MenuWin* parent)
463     : Menu(parent->delegate(), parent->anchor()),
464       menu_(CreatePopupMenu()),
465       owner_(parent->owner_),
466       is_menu_visible_(false),
467       owner_draw_(parent->owner_draw_) {
468 }
469
470 void MenuWin::SetMenuInfo() {
471   const int num_items = GetMenuItemCount(menu_);
472   int sep_count = 0;
473   for (int i = 0; i < num_items; ++i) {
474     MENUITEMINFO mii_info;
475     mii_info.cbSize = sizeof(mii_info);
476     // Get the menu's original type.
477     mii_info.fMask = MIIM_FTYPE;
478     GetMenuItemInfo(menu_, i, MF_BYPOSITION, &mii_info);
479     // Set item states.
480     if (!(mii_info.fType & MF_SEPARATOR)) {
481       const int id = ChromeGetMenuItemID(menu_, i);
482
483       MENUITEMINFO mii;
484       mii.cbSize = sizeof(mii);
485       mii.fMask = MIIM_STATE | MIIM_FTYPE | MIIM_DATA | MIIM_STRING;
486       // We also need MFT_STRING for owner drawn items in order to let Windows
487       // handle the accelerators for us.
488       mii.fType = MFT_STRING;
489       if (owner_draw_)
490         mii.fType |= MFT_OWNERDRAW;
491       // If the menu originally has radiocheck type, we should follow it.
492       if (mii_info.fType & MFT_RADIOCHECK)
493         mii.fType |= MFT_RADIOCHECK;
494       mii.fState = GetStateFlagsForItemID(id);
495
496       // Validate the label. If there is a contextual label, use it, otherwise
497       // default to the static label
498       string16 label;
499       if (!delegate()->GetContextualLabel(id, &label))
500         label = labels_[i - sep_count];
501
502       if (owner_draw_) {
503         item_data_[i - sep_count]->label = label;
504         mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]);
505       }
506       mii.dwTypeData = const_cast<wchar_t*>(label.c_str());
507       mii.cch = static_cast<UINT>(label.size());
508       SetMenuItemInfo(menu_, i, true, &mii);
509     } else {
510       // Set data for owner drawn separators. Set dwItemData NULL to indicate
511       // a separator.
512       if (owner_draw_) {
513         MENUITEMINFO mii;
514         mii.cbSize = sizeof(mii);
515         mii.fMask = MIIM_FTYPE;
516         mii.fType = MFT_SEPARATOR | MFT_OWNERDRAW;
517         mii.dwItemData = NULL;
518         SetMenuItemInfo(menu_, i, true, &mii);
519       }
520       ++sep_count;
521     }
522   }
523
524   for (size_t i = 0; i < submenus_.size(); ++i)
525     submenus_[i]->SetMenuInfo();
526 }
527
528 UINT MenuWin::GetStateFlagsForItemID(int item_id) const {
529   // Use the delegate to get enabled and checked state.
530   UINT flags =
531     delegate()->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED;
532
533   if (delegate()->IsItemChecked(item_id))
534     flags |= MFS_CHECKED;
535
536   if (delegate()->IsItemDefault(item_id))
537     flags |= MFS_DEFAULT;
538
539   return flags;
540 }
541
542 DWORD MenuWin::GetTPMAlignFlags() const {
543   // The manner in which we handle the menu alignment depends on whether or not
544   // the menu is displayed within a mirrored view. If the UI is mirrored, the
545   // alignment needs to be fliped so that instead of aligning the menu to the
546   // right of the point, we align it to the left and vice versa.
547   DWORD align_flags = TPM_TOPALIGN;
548   switch (anchor()) {
549     case TOPLEFT:
550       if (delegate()->IsRightToLeftUILayout()) {
551         align_flags |= TPM_RIGHTALIGN;
552       } else {
553         align_flags |= TPM_LEFTALIGN;
554       }
555       break;
556
557     case TOPRIGHT:
558       if (delegate()->IsRightToLeftUILayout()) {
559         align_flags |= TPM_LEFTALIGN;
560       } else {
561         align_flags |= TPM_RIGHTALIGN;
562       }
563       break;
564
565     default:
566       NOTREACHED();
567       return 0;
568   }
569   return align_flags;
570 }
571
572 }  // namespace views