Skip to content
This repository was 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
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions appshell/appshell_extensions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ class ProcessMessageDelegate : public ClientHandler::ProcessMessageDelegate {
bool enabled = argList->GetBool(2);
bool checked = argList->GetBool(3);
error = NativeMenuModel::getInstance(getMenuParent(browser)).setMenuItemState(command, enabled, checked);
if (error == NO_ERROR) {
error = SetMenuItemState(browser, command, enabled, checked);

Choose a reason for hiding this comment

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

this seems to be common code (i.e. windows, mac and win).
Isn't the menu item state being set in line 608?

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 NativeMenuModel::getInstance(getMenuParent(browser)).setMenuItemState(command, enabled, checked); does not work for Linux as it only sets a flag, so I have added SetMenuState function which is meant only for Linux and for case of Windows and MAC it is No Op

}
}
} else if (message_name == "SetMenuTitle") {
// Parameters:
Expand Down
285 changes: 261 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,13 @@
#include <sys/stat.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <gdk/gdkkeysyms.h>
#include <algorithm>
#include <sstream>
#include <vector>
#include <iostream>

GtkWidget* _menuWidget;
extern CefRefPtr<ClientHandler> g_handler;

// Supported browsers (order matters):
// - google-chorme
Expand Down Expand Up @@ -652,69 +657,256 @@ 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.empty()) {
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);
}
}


static 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;
}


static int32 ParseShortcut(CefRefPtr<CefBrowser> browser, GtkWidget* entry, ExtensionString& key, ExtensionString& commandId) {
if (key.size()) {
GdkModifierType modifier = GdkModifierType(0);
guint keyVal = 0;
GtkAccelGroup *accel_group = NULL;
accel_group = gtk_accel_group_new();

std::vector<std::string> tokens;
std::istringstream f(key);
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);

// 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] == "\\" || 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()) {
// If fallback also fails, then
// we append the shorcut to menu title
// e.g. MENU_TITLE (Ctrl + A)
ExtensionString menuTitle;
GetMenuTitle(browser, commandId, menuTitle);
menuTitle += "\t( " + GetDisplayKeyString(key) + " )";
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));
ExtensionString commandId = model.getCommandId(tag);
model.setOsItem(tag, entry);
ParseShortcut(browser, entry, key, 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 @@ -723,18 +915,63 @@ int32 GetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString commandId,
return NO_ERROR;
}

int32 SetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString command, bool& enabled, bool& checked)
{
// TODO: Implement functionality for checked
NativeMenuModel& model = NativeMenuModel::getInstance(getMenuParent(browser));
int tag = model.getTag(command);
if (tag == kTagNotFound) {
return ERR_NOT_FOUND;
}
GtkWidget* menuItem = (GtkWidget*) model.getOsItem(tag);
gtk_widget_set_sensitive(menuItem, enabled);
return NO_ERROR;
}

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), newTitle.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);
ParseShortcut(browser, entry, shortcut, commandId);
return NO_ERROR;
}

Expand Down
5 changes: 5 additions & 0 deletions appshell/appshell_extensions_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,11 @@ int32 GetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString commandId,
return NO_ERROR;
}

int32 SetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString command, bool& enabled, bool& checked)
{
return NO_ERROR;
}

int32 SetMenuTitle(CefRefPtr<CefBrowser> browser, ExtensionString command, ExtensionString itemTitle) {
NSString* itemTitleStr = [[[NSString alloc] initWithUTF8String:itemTitle.c_str()] autorelease];
int32 tag = NativeMenuModel::getInstance(getMenuParent(browser)).getTag(command);
Expand Down
2 changes: 2 additions & 0 deletions appshell/appshell_extensions_platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ int32 RemoveMenuItem(CefRefPtr<CefBrowser> browser, const ExtensionString& comma

int32 GetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString commandId, bool& enabled, bool& checked, int& index);

int32 SetMenuItemState(CefRefPtr<CefBrowser> browser, ExtensionString command, bool& enabled, bool& checked);

int32 SetMenuTitle(CefRefPtr<CefBrowser> browser, ExtensionString commandId, ExtensionString menuTitle);

int32 GetMenuTitle(CefRefPtr<CefBrowser> browser, ExtensionString commandId, ExtensionString& menuTitle);
Expand Down
Loading