Skip to content
This repository has been archived by the owner on Sep 2, 2021. It is now read-only.

Added support of Native Menus in Linux #602

Merged
merged 13 commits into from
May 3, 2017
271 changes: 247 additions & 24 deletions appshell/appshell_extensions_gtk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include "appshell_extensions.h"
#include "appshell_extensions_platform.h"
#include "native_menu_model.h"
#include "client_handler.h"

#include <errno.h>
Expand All @@ -36,8 +36,12 @@
#include <sys/stat.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <gdk/gdkkeysyms.h>
#include <algorithm>
#include <sstream>
#include <vector>

GtkWidget* _menuWidget;
extern CefRefPtr<ClientHandler> g_handler;

// Supported browsers (order matters):
// - google-chorme
Expand Down Expand Up @@ -652,69 +656,253 @@ int32 GetPendingFilesToOpen(ExtensionString& files)
{
}

GtkWidget* GetMenuBar(CefRefPtr<CefBrowser> browser)
static GtkWidget* GetMenuBar(CefRefPtr<CefBrowser> browser)
{
GtkWidget* window = (GtkWidget*)getMenuParent(browser);
GtkWidget* widget;
GList *children, *iter;
GtkWidget* menuBar = NULL;

children = gtk_container_get_children(GTK_CONTAINER(window));
for(iter = children; iter != NULL; iter = g_list_next(iter)) {
widget = (GtkWidget*)iter->data;

if (GTK_IS_MENU_BAR(widget))
return widget;
if (GTK_IS_MENU_BAR(widget)) {
menuBar = widget;
break;
}
}

return NULL;
g_list_free(children);

return menuBar;
}

static int GetPosition(const ExtensionString& positionString, const ExtensionString& relativeId,
GtkWidget* container, NativeMenuModel& model)
{
if (positionString == "" || positionString == "last")
return -1;
else if (positionString == "first")
return 0;
else if (relativeId.size() > 0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use empty() instead

int relativeTag = model.getTag(relativeId);
GtkWidget* relativeMenuItem = (GtkWidget*) model.getOsItem(relativeTag);
int position = 0;
GList* children = gtk_container_get_children(GTK_CONTAINER(container));
GList* iter;
for (iter = children; iter != NULL; iter = g_list_next(iter)) {
if (iter->data == relativeMenuItem)
break;
position++;
}
if (iter == NULL)
position = -1;
else if (positionString == "before")
;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't you instead set position as 0 here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't set position to 0 here, as in https://github.com/adobe/brackets-shell/pull/602/files#diff-10aa13d798996c2d964d38b1f328ae62R698 we are incrementing value of position, so setting it to 0 will always add the menu in the first index, which is not desired.

else if (positionString == "after")
position++;
else if (positionString == "firstInSection") {
for (; iter != NULL; iter = g_list_previous(iter)) {
if (GTK_IS_SEPARATOR_MENU_ITEM(iter->data))
break;
position--;
}
position++;
}
else if (positionString == "lastInSection") {
for (; iter != NULL; iter = g_list_next(iter)) {
if (GTK_IS_SEPARATOR_MENU_ITEM(iter->data))
break;
position++;
}
}
else
position = -1;
g_list_free(children);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you point to the documentation in GTK where they specify that you need to explicitly free it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@eyelash eyelash May 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also https://developer.gnome.org/gtk3/stable/GtkContainer.html#gtk-container-get-children says:

Returns a newly-allocated list of the container’s non-internal children.

and the tooltip of [transfer-container] says Free data container after the code is done.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked the tooltip indeed conveys that we need to free.
I looked at documentation of https://developer.gnome.org/gtk3/stable/GtkContainer.html#gtk_container_get_focus_chain and noticed the explicit free mentioned there.

return position;
}
else
return -1;
}

int32 AddMenu(CefRefPtr<CefBrowser> browser, ExtensionString title, ExtensionString command,
ExtensionString position, ExtensionString relativeId)
ExtensionString positionString, ExtensionString relativeId)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't these parameters be passed by const ref?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are actually implementation of interface which is used by all platforms, so changing this would mean changing in all the platforms implementation.
We can take this for later PR.

{
// if (tag == kTagNotFound) {
// tag = NativeMenuModel::getInstance(getMenuParent(browser)).getOrCreateTag(command, ExtensionString());
// } else {
// // menu already there
// return NO_ERROR;
// }
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int tag = model.getTag(command);
if (tag == kTagNotFound) {
tag = model.getOrCreateTag(command, ExtensionString());
} else {
// menu is already there
return NO_ERROR;
}

GtkWidget* menuBar = GetMenuBar(browser);
GtkWidget* menuWidget = gtk_menu_new();
GtkWidget* menuHeader = gtk_menu_item_new_with_label(title.c_str());
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuHeader), menuWidget);
gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), menuHeader);
model.setOsItem(tag, menuHeader);
int position = GetPosition(positionString, relativeId, menuBar, model);
if (position >= 0)
gtk_menu_shell_insert(GTK_MENU_SHELL(menuBar), menuHeader, position);
else
gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), menuHeader);
gtk_widget_show(menuHeader);

// FIXME add lookup for menu widgets
_menuWidget = menuWidget;

return NO_ERROR;
}

void FakeCallback() {
static void Callback(GtkMenuItem* menuItem, gpointer userData) {
if (g_handler.get() && g_handler->GetBrowserId()) {
int tag = GPOINTER_TO_INT(userData);
ExtensionString commandId = NativeMenuModel::getInstance(getMenuParent(g_handler->GetBrowser())).getCommandId(tag);
CefRefPtr<CommandCallback> callback = new EditCommandCallback(g_handler->GetBrowser(), commandId);
g_handler->SendJSCommand(g_handler->GetBrowser(), commandId, callback);
}
}


ExtensionString GetDisplayKeyString(const ExtensionString& keyStr)
{
ExtensionString result = keyStr;

// We get a keyStr that looks like "Ctrl-O", which is the format we
// need for the accelerator table. For displaying in the menu, though,
// we have to change it to "Ctrl+O". Careful not to change the final
// character, though, so "Ctrl--" ends up as "Ctrl+-".
if (result.size() > 0) {
replace(result.begin(), result.end() - 1, '-', '+');
}
return result;
}


int32 ParseShortcut(CefRefPtr<CefBrowser> browser, GtkWidget* entry, ExtensionString& key, GdkModifierType* modifier, guint* keyVal, ExtensionString& commandId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could probably be static if it is not used outside this file.

if (key.size()) {
GtkAccelGroup *accel_group = NULL;
accel_group = gtk_accel_group_new();

std::vector<std::string> tokens;
std::istringstream f(key.c_str());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the .c_str() necessary here?

std::string s;
while (getline(f, s, '-')) {
tokens.push_back(s);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether the tokens could be parsed here directly instead of copying them to a vector first and the parsing the vector.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I have used tokens in creating shortcut of type A and also in the fallback, so I won't be able to have fallback if there is no tokens vector.

}
std::string shortcut = "";
// convert shortcut format
// e.g. Ctrl+A converts to <Ctrl>A whose entry is stored in accelerator table
for (int i=0;i<tokens.size();i++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it better to take the size() in a local and use that instead?

if (i != tokens.size() - 1) {
shortcut += "<";
}
shortcut += tokens[i];
if (i != tokens.size() - 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the last token excluded?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually in this we are changing the token string from Ctrl+A to A whose entry is stored in accelerator table, so for the last token there is no need to enclose it in <>

shortcut += ">";
}
}
gchar* val = (gchar*)shortcut.c_str();
gtk_accelerator_parse (val, keyVal, modifier);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: everywhere else there is no space between function name and parameters.


// Fallback
if (!(*keyVal)) {
for (int i=0;i<tokens.size();i++) {
if (tokens[i] == "Ctrl") {
*modifier = GdkModifierType(*modifier | GDK_CONTROL_MASK);
} else if (tokens[i] == "Alt") {
*modifier = GdkModifierType(*modifier | GDK_MOD1_MASK);
} else if (tokens[i] == "Shift") {
*modifier = GdkModifierType(*modifier | GDK_SHIFT_MASK);
} else if (tokens[i] == "Enter") {
*keyVal = GDK_KEY_KP_Enter;
} else if (tokens[i] == "Up" || tokens[i] == "\u2191") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to use macros or constants instead of these string literals

*keyVal = GDK_KEY_uparrow;
} else if (tokens[i] == "Down" || tokens[i] == "\u2193") {
*keyVal = GDK_KEY_downarrow;
} else if (tokens[i] == "Right") {
*keyVal = GDK_KEY_rightarrow;
} else if (tokens[i] == "Left") {
*keyVal = GDK_KEY_leftarrow;
} else if (tokens[i] == "Space") {
*keyVal = GDK_KEY_KP_Space;
} else if (tokens[i] == "PageUp") {
*keyVal = GDK_KEY_Page_Up;
} else if (tokens[i] == "PageDown") {
*keyVal = GDK_KEY_Page_Down;
} else if (tokens[i] == "[" || tokens[i] == "]" || tokens[i] == "+" || tokens[i] == "." || tokens[i] == "," || tokens[i] == "\\") {
*keyVal = tokens[i][0];
} else if (tokens[i] == "−") {
*keyVal = GDK_KEY_minus;
}
}
}
if (*keyVal) {
gtk_widget_add_accelerator(entry, "activate", accel_group, *keyVal, *modifier, GTK_ACCEL_VISIBLE);
} else if (shortcut.size()) {
ExtensionString menuTitle;
GetMenuTitle(browser, commandId, menuTitle);
menuTitle += "\t( " + shortcut + " )";
gtk_menu_item_set_label(GTK_MENU_ITEM(entry), menuTitle.c_str());
}
}
return NO_ERROR;
}

int32 AddMenuItem(CefRefPtr<CefBrowser> browser, ExtensionString parentCommand, ExtensionString itemTitle,
ExtensionString command, ExtensionString key, ExtensionString displayStr,
ExtensionString position, ExtensionString relativeId)
ExtensionString positionString, ExtensionString relativeId)
{
GtkWidget* entry = gtk_menu_item_new_with_label(itemTitle.c_str());
g_signal_connect(entry, "activate", FakeCallback, NULL);
// FIXME add lookup for menu widgets
gtk_menu_shell_append(GTK_MENU_SHELL(_menuWidget), entry);
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int parentTag = model.getTag(parentCommand);
if (parentTag == kTagNotFound) {
return ERR_NOT_FOUND;
}

int tag = model.getTag(command);
if (tag == kTagNotFound) {
tag = model.getOrCreateTag(command, parentCommand);
} else {
return NO_ERROR;
}

GtkWidget* entry;
if (itemTitle == "---")
entry = gtk_separator_menu_item_new();
else
entry = gtk_menu_item_new_with_label(itemTitle.c_str());
g_signal_connect(entry, "activate", G_CALLBACK(Callback), GINT_TO_POINTER(tag));
GdkModifierType modifier = GdkModifierType(0);
guint keyVal = 0;
ExtensionString commandId = model.getCommandId(tag);
model.setOsItem(tag, entry);
ParseShortcut(browser, entry, key, &modifier, &keyVal, commandId);
GtkWidget* menuHeader = (GtkWidget*) model.getOsItem(parentTag);
GtkWidget* menuWidget = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menuHeader));
int position = GetPosition(positionString, relativeId, menuWidget, model);
if (position >= 0)
gtk_menu_shell_insert(GTK_MENU_SHELL(menuWidget), entry, position);
else
gtk_menu_shell_append(GTK_MENU_SHELL(menuWidget), entry);
gtk_widget_show(entry);

return NO_ERROR;
}

int32 RemoveMenu(CefRefPtr<CefBrowser> browser, const ExtensionString& commandId)
{
return NO_ERROR;
// works for menu and menu item
return RemoveMenuItem(browser, commandId);
}

int32 RemoveMenuItem(CefRefPtr<CefBrowser> browser, const ExtensionString& commandId)
{
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int tag = model.getTag(commandId);
if (tag == kTagNotFound)
return ERR_NOT_FOUND;
GtkWidget* menuItem = (GtkWidget*) model.getOsItem(tag);
model.removeMenuItem(commandId);
gtk_widget_destroy(menuItem);
return NO_ERROR;
}

Expand All @@ -725,16 +913,51 @@ int32 GetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString commandId,

int32 SetMenuTitle(CefRefPtr<CefBrowser> browser, ExtensionString commandId, ExtensionString menuTitle)
{
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int tag = model.getTag(commandId);
if (tag == kTagNotFound)
return ERR_NOT_FOUND;
GtkWidget* menuItem = (GtkWidget*) model.getOsItem(tag);
ExtensionString shortcut;
GetMenuTitle(browser, commandId, shortcut);
size_t pos = shortcut.find("\t");
if (pos != -1) {
shortcut = shortcut.substr(pos);
} else {
shortcut = "";
}

ExtensionString newTitle = menuTitle;
if (shortcut.length() > 0) {
newTitle += shortcut;
}
gtk_menu_item_set_label(GTK_MENU_ITEM(menuItem), menuTitle.c_str());
return NO_ERROR;
}

int32 GetMenuTitle(CefRefPtr<CefBrowser> browser, ExtensionString commandId, ExtensionString& menuTitle)
{
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int tag = model.getTag(commandId);
if (tag == kTagNotFound)
return ERR_NOT_FOUND;
GtkWidget* menuItem = (GtkWidget*) model.getOsItem(tag);
menuTitle = gtk_menu_item_get_label(GTK_MENU_ITEM(menuItem));
return NO_ERROR;
}

int32 SetMenuItemShortcut(CefRefPtr<CefBrowser> browser, ExtensionString commandId, ExtensionString shortcut, ExtensionString displayStr)
{
NativeMenuModel model = NativeMenuModel::getInstance(getMenuParent(browser));
int32 tag = model.getTag(commandId);
if (tag == kTagNotFound) {
return ERR_NOT_FOUND;
}
GtkWidget* entry = (GtkWidget*) model.getOsItem(tag);
GdkModifierType modifier = GdkModifierType(0);
guint keyVal = 0;
ParseShortcut(browser, entry, shortcut, &modifier, &keyVal, commandId);

return NO_ERROR;
}

Expand Down