diff --git a/metadata/meson.build b/metadata/meson.build
index 056f18f..932be1e 100644
--- a/metadata/meson.build
+++ b/metadata/meson.build
@@ -9,5 +9,6 @@ install_data('join-views.xml', install_dir: wayfire.get_variable(pkgconfig: 'met
install_data('keycolor.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
install_data('mag.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
install_data('showrepaint.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
+install_data('window-menu.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
install_data('water.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
install_data('workspace-names.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
diff --git a/metadata/window-menu.xml b/metadata/window-menu.xml
new file mode 100644
index 0000000..1dcbf0e
--- /dev/null
+++ b/metadata/window-menu.xml
@@ -0,0 +1,15 @@
+
+
+
+ <_short>Window Menu
+ Window Management
+
+
+
+
diff --git a/src/meson.build b/src/meson.build
index f27e8dc..0413e04 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -46,6 +46,10 @@ water = shared_module('water', 'water.cpp',
dependencies: [wayfire, wlroots, wfconfig],
install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))
+window_menu = shared_module('window-menu', 'window-menu.cpp',
+ dependencies: [wayfire, wlroots, wfconfig],
+ install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))
+
workspace_names = shared_module('workspace-names', 'workspace-names.cpp',
dependencies: [wayfire, wlroots, wfconfig, cairo],
install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))
diff --git a/src/window-menu.cpp b/src/window-menu.cpp
new file mode 100644
index 0000000..58f5cc3
--- /dev/null
+++ b/src/window-menu.cpp
@@ -0,0 +1,162 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2020 Scott Moreau
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class wayfire_window_menu : public wf::plugin_interface_t
+{
+ /* The command should be set to a client that shows a menu window. */
+ wf::option_wrapper_t command{"window-menu/command"};
+ wf::option_wrapper_t app_id{"window-menu/app_id"};
+ wf::point_t position_offset;
+ wayfire_view menu_view = nullptr;
+ wayfire_view target_view = nullptr;
+
+ public:
+ void init() override
+ {
+ grab_interface->name = "window-menu";
+ grab_interface->capabilities = 0;
+
+ output->connect_signal("view-show-window-menu", &show_window_menu);
+ }
+
+ wf::signal_connection_t view_mapped{[this] (wf::signal_data_t *data)
+ {
+ auto view = get_signaled_view(data);
+ if (target_view && (view->get_app_id() == std::string(app_id)))
+ {
+ view->set_decoration(nullptr);
+
+ auto vg = target_view->get_output_geometry();
+ auto position = wf::point_t{vg.x, vg.y} + position_offset;
+
+ vg = view->get_wm_geometry();
+ auto og = output->get_relative_geometry();
+ int padding = 20;
+ og.x += padding;
+ og.y += padding;
+ og.width -= padding * 2 + vg.width;
+ og.height -= padding * 2 + vg.height;
+ if ((og.width <= 0) || (og.height <= 0))
+ {
+ return;
+ }
+
+ wlr_box box{og.x, og.y, og.width, og.height};
+ wf::pointf_t p{(float)position.x, (float)position.y};
+ p = target_view->transform_point(p);
+ wlr_box_closest_point(&box, p.x, p.y, &p.x, &p.y);
+ position.x = (int)p.x;
+ position.y = (int)p.y;
+
+ ((wf::view_mapped_signal*)data)->is_positioned = true;
+ view->move(position.x, position.y);
+
+ /* Place above other views */
+ output->workspace->add_view(view, wf::LAYER_UNMANAGED);
+
+ menu_view = view;
+ }
+ }
+ };
+
+ wf::signal_connection_t on_button{[=] (wf::signal_data_t *data)
+ {
+ auto ev = static_cast<
+ wf::input_event_signal*>(data);
+
+ if (ev->event->state != WLR_BUTTON_PRESSED)
+ {
+ return;
+ }
+
+ auto view = wf::get_core().get_cursor_focus_view();
+ if (!menu_view || !view)
+ {
+ return;
+ }
+
+ /* Check if the client of the views match, in case it's a
+ * subsurface/menu, meaning the views won't match but
+ * the underlying client object will */
+ if (menu_view->get_client() != view->get_client())
+ {
+ menu_view->close();
+ }
+ }
+ };
+
+ wf::signal_connection_t view_unmapped{[this] (wf::signal_data_t *data)
+ {
+ auto view = get_signaled_view(data);
+ if (view && (menu_view == view))
+ {
+ menu_view = target_view = nullptr;
+ output->disconnect_signal(&view_mapped);
+ output->disconnect_signal(&view_unmapped);
+ wf::get_core().disconnect_signal(&on_button);
+ }
+ }
+ };
+
+ wf::signal_connection_t show_window_menu{[this] (wf::signal_data_t *data)
+ {
+ if (target_view || menu_view)
+ {
+ return;
+ }
+
+ position_offset =
+ ((wf::view_show_window_menu_signal*)data)->relative_position;
+ if (wf::get_core().run(std::string(command)) == -1)
+ {
+ return;
+ }
+
+ /* Showing menu for this view */
+ target_view = get_signaled_view(data);
+ output->connect_signal("view-mapped", &view_mapped);
+ output->connect_signal("view-unmapped", &view_unmapped);
+ wf::get_core().connect_signal("pointer_button", &on_button);
+ }
+ };
+
+ void fini() override
+ {
+ if (menu_view)
+ {
+ menu_view->close();
+ }
+ }
+};
+
+DECLARE_WAYFIRE_PLUGIN(wayfire_window_menu);