diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6d9d58529..47bdd0048 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -84,6 +84,7 @@
ui/userWidget.js
ui/viewSelector.js
ui/windowAttentionHandler.js
+ ui/windowMenu.js
ui/windowManager.js
ui/workspace.js
ui/workspaceSwitcherPopup.js
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index ae6a20717..0acf4f609 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -15,6 +15,7 @@ const WorkspaceSwitcherPopup = imports.ui.workspaceSwitcherPopup;
const Main = imports.ui.main;
const ModalDialog = imports.ui.modalDialog;
const Tweener = imports.ui.tweener;
+const WindowMenu = imports.ui.windowMenu;
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
const WINDOW_ANIMATION_TIME = 0.25;
@@ -480,6 +481,7 @@ const WindowManager = new Lang.Class({
this._shellwm.connect('switch-workspace', Lang.bind(this, this._switchWorkspace));
this._shellwm.connect('show-tile-preview', Lang.bind(this, this._showTilePreview));
this._shellwm.connect('hide-tile-preview', Lang.bind(this, this._hideTilePreview));
+ this._shellwm.connect('show-window-menu', Lang.bind(this, this._showWindowMenu));
this._shellwm.connect('minimize', Lang.bind(this, this._minimizeWindow));
this._shellwm.connect('maximize', Lang.bind(this, this._maximizeWindow));
this._shellwm.connect('unmaximize', Lang.bind(this, this._unmaximizeWindow));
@@ -669,6 +671,8 @@ const WindowManager = new Lang.Class({
this._dimWindow(this._dimmedWindows[i]);
}));
+ this._windowMenuManager = new WindowMenu.WindowMenuManager();
+
if (Main.sessionMode.hasWorkspaces)
this._workspaceTracker = new WorkspaceTracker(this);
@@ -1159,6 +1163,10 @@ const WindowManager = new Lang.Class({
this._tilePreview.hide();
},
+ _showWindowMenu: function(shellwm, window) {
+ this._windowMenuManager.showForWindow(window);
+ },
+
_startAppSwitcher : function(display, screen, window, binding) {
/* prevent a corner case where both popups show up at once */
if (this._workspaceSwitcherPopup != null)
diff --git a/js/ui/windowMenu.js b/js/ui/windowMenu.js
new file mode 100644
index 000000000..673827701
--- /dev/null
+++ b/js/ui/windowMenu.js
@@ -0,0 +1,145 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*
+
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Meta = imports.gi.Meta;
+const St = imports.gi.St;
+const Shell = imports.gi.Shell;
+
+const BoxPointer = imports.ui.boxpointer;
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+
+const WindowMenu = new Lang.Class({
+ Name: 'WindowMenu',
+ Extends: PopupMenu.PopupMenu,
+
+ _init: function(window) {
+ this.parent(Main.layoutManager.dummyCursor, 0, St.Side.TOP);
+
+ this.actor.add_style_class_name('window-menu');
+
+ Main.layoutManager.uiGroup.add_actor(this.actor);
+ this.actor.hide();
+
+ this._buildMenu(window);
+ },
+
+ _buildMenu: function(window) {
+ let type = window.get_window_type();
+
+ let item;
+
+ item = this.addAction(_("Minimize"), Lang.bind(this, function(event) {
+ window.minimize();
+ }));
+ if (!window.can_minimize())
+ item.setSensitive(false);
+
+ if (window.get_maximized()) {
+ item = this.addAction(_("Unmaximize"), Lang.bind(this, function() {
+ window.unmaximize(Meta.MaximizeFlags.BOTH);
+ }));
+ } else {
+ item = this.addAction(_("Maximize"), Lang.bind(this, function() {
+ window.maximize(Meta.MaximizeFlags.BOTH);
+ }));
+ }
+ if (!window.can_maximize())
+ item.setSensitive(false);
+
+ item = this.addAction(_("Move"), Lang.bind(this, function(event) {
+ window.begin_grab_op(Meta.GrabOp.KEYBOARD_MOVING, true, event.get_time());
+ }));
+ if (!window.allows_move())
+ item.setSensitive(false);
+
+ item = this.addAction(_("Resize"), Lang.bind(this, function(event) {
+ window.begin_grab_op(Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN, true, event.get_time());
+ }));
+ if (!window.allows_resize())
+ item.setSensitive(false);
+
+ if (!window.titlebar_is_onscreen() && type != Meta.WindowType.DOCK && type != Meta.WindowType.DESKTOP) {
+ this.addAction(_("Move Titlebar Onscreen"), Lang.bind(this, function(event) {
+ window.shove_titlebar_onscreen();
+ }));
+ }
+
+ item = this.addAction("Always on Top", Lang.bind(this, function() {
+ if (window.is_above())
+ window.unmake_above();
+ else
+ window.make_above();
+ }));
+ if (window.is_above())
+ item.setOrnament(PopupMenu.Ornament.DOT);
+ if (window.get_maximized() ||
+ type == Meta.WindowType.DOCK ||
+ type == Meta.WindowType.DESKTOP ||
+ type == Meta.WindowType.SPLASHSCREEN)
+ item.setSensitive(false);
+
+ if (!Meta.prefs_get_workspaces_only_on_primary() || window.is_on_primary_monitor()) {
+ let isSticky = window.is_on_all_workspaces();
+
+ item = this.addAction(_("Always on Visible Workspace"), Lang.bind(this, function() {
+ if (isSticky)
+ window.unstick();
+ else
+ window.stick();
+ }));
+ if (isSticky)
+ item.setOrnament(PopupMenu.Ornament.DOT);
+ if (window.is_always_on_all_workspaces())
+ item.setSensitive(false);
+
+ let nWorkspaces = global.screen.n_workspaces;
+
+ if (!isSticky) {
+ let workspace = window.get_workspace();
+ let idx = workspace.index();
+ if (idx > 0) {
+ this.addAction(_("Move to Workspace Up"), Lang.bind(this, function(event) {
+ window.change_workspace(workspace.get_neighbor(Meta.MotionDirection.UP));
+ }));
+ }
+ if (idx < nWorkspaces) {
+ this.addAction(_("Move to Workspace Down"), Lang.bind(this, function(event) {
+ window.change_workspace(workspace.get_neighbor(Meta.MotionDirection.DOWN));
+ }));
+ }
+ }
+ }
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ item = this.addAction(_("Close"), Lang.bind(this, function(event) {
+ window.close(event.get_time());
+ }));
+ if (!window.can_close())
+ item.setSensitive(false);
+ }
+});
+
+const WindowMenuManager = new Lang.Class({
+ Name: 'WindowMenuManager',
+
+ _init: function() {
+ this._manager = new PopupMenu.PopupMenuManager({ actor: Main.layoutManager.dummyCursor });
+ },
+
+ showForWindow: function(window) {
+ let menu = new WindowMenu(window);
+ this._manager.addMenu(menu);
+
+ let [x, y] = global.get_pointer();
+ Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0);
+ menu.open(BoxPointer.PopupAnimation.NONE);
+ menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
+ menu.connect('open-state-changed', Lang.bind(this, function(menu_, isOpen) {
+ if (!isOpen)
+ menu.destroy();
+ }));
+ },
+});
diff --git a/src/gnome-shell-plugin.c b/src/gnome-shell-plugin.c
index 71503bca0..75a3bddf0 100644
--- a/src/gnome-shell-plugin.c
+++ b/src/gnome-shell-plugin.c
@@ -73,7 +73,8 @@ static void gnome_shell_plugin_show_tile_preview (MetaPlugin *plugin,
MetaRectangle *tile_rect,
int tile_monitor);
static void gnome_shell_plugin_hide_tile_preview (MetaPlugin *plugin);
-
+static void gnome_shell_plugin_show_window_menu (MetaPlugin *plugin,
+ MetaWindow *window);
static gboolean gnome_shell_plugin_xevent_filter (MetaPlugin *plugin,
XEvent *event);
@@ -140,6 +141,7 @@ gnome_shell_plugin_class_init (GnomeShellPluginClass *klass)
plugin_class->show_tile_preview = gnome_shell_plugin_show_tile_preview;
plugin_class->hide_tile_preview = gnome_shell_plugin_hide_tile_preview;
+ plugin_class->show_window_menu = gnome_shell_plugin_show_window_menu;
plugin_class->xevent_filter = gnome_shell_plugin_xevent_filter;
plugin_class->keybinding_filter = gnome_shell_plugin_keybinding_filter;
@@ -303,6 +305,13 @@ gnome_shell_plugin_hide_tile_preview (MetaPlugin *plugin)
_shell_wm_hide_tile_preview (get_shell_wm ());
}
+static void
+gnome_shell_plugin_show_window_menu (MetaPlugin *plugin,
+ MetaWindow *window)
+{
+ _shell_wm_show_window_menu (get_shell_wm (), window);
+}
+
static gboolean
gnome_shell_plugin_xevent_filter (MetaPlugin *plugin,
XEvent *xev)
diff --git a/src/shell-wm-private.h b/src/shell-wm-private.h
index 9df4adab3..35cbcb0f7 100644
--- a/src/shell-wm-private.h
+++ b/src/shell-wm-private.h
@@ -40,6 +40,8 @@ void _shell_wm_show_tile_preview (ShellWM *wm,
MetaRectangle *tile_rect,
int tile_monitor);
void _shell_wm_hide_tile_preview (ShellWM *wm);
+void _shell_wm_show_window_menu (ShellWM *wm,
+ MetaWindow *window);
gboolean _shell_wm_filter_keybinding (ShellWM *wm,
MetaKeyBinding *binding);
diff --git a/src/shell-wm.c b/src/shell-wm.c
index f2c484d6d..0d51606d7 100644
--- a/src/shell-wm.c
+++ b/src/shell-wm.c
@@ -28,6 +28,7 @@ enum
KILL_WINDOW_EFFECTS,
SHOW_TILE_PREVIEW,
HIDE_TILE_PREVIEW,
+ SHOW_WINDOW_MENU,
FILTER_KEYBINDING,
CONFIRM_DISPLAY_CHANGE,
@@ -135,6 +136,13 @@ shell_wm_class_init (ShellWMClass *klass)
0,
NULL, NULL, NULL,
G_TYPE_NONE, 0);
+ shell_wm_signals[SHOW_WINDOW_MENU] =
+ g_signal_new ("show-window-menu",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ META_TYPE_WINDOW);
shell_wm_signals[FILTER_KEYBINDING] =
g_signal_new ("filter-keybinding",
G_TYPE_FROM_CLASS (klass),
@@ -288,6 +296,12 @@ _shell_wm_hide_tile_preview (ShellWM *wm)
g_signal_emit (wm, shell_wm_signals[HIDE_TILE_PREVIEW], 0);
}
+void
+_shell_wm_show_window_menu (ShellWM *wm,
+ MetaWindow *window)
+{
+ g_signal_emit (wm, shell_wm_signals[SHOW_WINDOW_MENU], 0, window);
+}
void
_shell_wm_minimize (ShellWM *wm,