/*
Copyright (c) 2023 Raspberry Pi
All rights reserved.

Some code taken from the lxpanel project

Copyright (c) 2006-2010 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
            2006-2008 Jim Huang <jserv.tw@gmail.com>
            2008 Fred Chien <fred@lxde.org>
            2009 Ying-Chun Liu (PaulLiu) <grandpaul@gmail.com>
            2009-2010 Marty Jack <martyj19@comcast.net>
            2010 Jürgen Hötzel <juergen@archlinux.org>
            2010-2011 Julien Lavergne <julien.lavergne@gmail.com>
            2012-2013 Henry Gebhardt <hsggebhardt@gmail.com>
            2012 Michael Rawson <michaelrawson76@gmail.com>
            2014 Max Krummenacher <max.oss.09@gmail.com>
            2014 SHiNE CsyFeK <csyfek@users.sourceforge.net>
            2014 Andriy Grytsenko <andrej@rep.kiev.ua>

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the copyright holder nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include <fcntl.h>
#include <libinput.h>
#include <libudev.h>
#include <libintl.h>
#include <linux/input.h>
#include <gtk/gtk.h>
#include <gtk-layer-shell.h>
#include "lxutils.h"

/*----------------------------------------------------------------------------*/
/* Macros and typedefs */
/*----------------------------------------------------------------------------*/

#define MENU_ICON_SPACE 6
#define BORDER_SIZE 1

typedef struct {
    GtkWidget *button;
    GtkMenu *menu;
    gulong chandle;
    gulong mhandle;
    double x;
    double y;
} kb_menu_t;

/*----------------------------------------------------------------------------*/
/* Global data */
/*----------------------------------------------------------------------------*/

press_t pressed;
double press_x, press_y;

gboolean touch_only;
int widget_icon_size;
gboolean is_pi_var;

static GtkWindow *panel, *popwindow;
static GtkWidget *clicksink;
static GtkLayerShellLayer orig_layer;
static int px, py, mw, mh, orient;

/*----------------------------------------------------------------------------*/
/* General public API - replaces functions from lxpanel */
/*----------------------------------------------------------------------------*/

GtkWindow *find_panel (GtkWidget *btn)
{
    GtkWidget *wid = btn;
    while (!GTK_IS_WINDOW (wid) || !gtk_layer_is_layer_window (GTK_WINDOW (wid)))
    {
        if (!GTK_IS_WIDGET (wid))
            return NULL;
        wid = gtk_widget_get_parent (wid);
    }
    return GTK_WINDOW (wid);
}

gboolean panel_at_bottom (GtkWidget *btn)
{
    GtkWindow *panel = find_panel (btn);
    return gtk_layer_get_anchor (panel, GTK_LAYER_SHELL_EDGE_BOTTOM);
}

int get_icon_size (GtkWidget *)
{
    return widget_icon_size;
}

void store_layer (GtkLayerShellLayer layer)
{
    orig_layer = layer;
}

GdkPixbuf *load_taskbar_pixbuf (GtkWidget *image, const char *icon_name)
{
    char *fname;
    GdkPixbuf *icon = NULL;
    int scale = gtk_widget_get_scale_factor (image);
    int size = get_icon_size (image);

    if (icon_name)
    {
        if (strstr (icon_name, "/"))
            icon = gdk_pixbuf_new_from_file_at_size (icon_name, size * scale, size * scale, NULL);
        else
        {
            icon = gtk_icon_theme_load_icon_for_scale (gtk_icon_theme_get_default (), icon_name,
                size, scale, GTK_ICON_LOOKUP_FORCE_SIZE, NULL);

            // fallback for packages using obsolete icon location
            if (!icon)
            {
                fname = g_strdup_printf ("/usr/share/pixmaps/%s", icon_name);
                icon = gdk_pixbuf_new_from_file_at_size (fname, size * scale, size * scale, NULL);
                g_free (fname);
            }
        }
    }
    if (!icon)
        icon = gtk_icon_theme_load_icon_for_scale (gtk_icon_theme_get_default (), "application-x-executable",
            size, scale, GTK_ICON_LOOKUP_FORCE_SIZE, NULL);
    return icon;
}

void set_image_from_pixbuf (GtkWidget *image, GdkPixbuf *pixbuf)
{
    int scale = gtk_widget_get_scale_factor (image);
    if (scale == 1) gtk_image_set_from_pixbuf (GTK_IMAGE (image), pixbuf);
    else
    {
        cairo_surface_t *cr = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL);
        gtk_image_set_from_surface (GTK_IMAGE (image), cr);
        cairo_surface_destroy (cr);
    }
}

void set_taskbar_icon (GtkWidget *image, const char *icon)
{
    GdkPixbuf *pixbuf = load_taskbar_pixbuf (image, icon);
    if (pixbuf)
    {
        set_image_from_pixbuf (image, pixbuf);
        g_object_unref (pixbuf);
    }
}

void set_menu_icon (GtkWidget *image, const char *icon, int size)
{
    if (!icon) return;
    int scale = gtk_widget_get_scale_factor (image);
    GdkPixbuf *pixbuf = gtk_icon_theme_load_icon_for_scale (gtk_icon_theme_get_default (), icon,
        (size >= 32 ? 24 : 16), scale, GTK_ICON_LOOKUP_FORCE_SIZE, NULL);
    if (pixbuf)
    {
        set_image_from_pixbuf (image, pixbuf);
        g_object_unref (pixbuf);
    }
}

GtkWidget *new_menu_item (const char *text, int maxlen, const char *iconname, int icon_size)
{
    GtkWidget *item = gtk_menu_item_new ();
    gtk_widget_set_name (item, "panelmenuitem");
    GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, MENU_ICON_SPACE);
    GtkWidget *label = gtk_label_new (text);
    GtkWidget *icon = gtk_image_new ();
    set_menu_icon (icon, iconname, icon_size);

    if (maxlen)
    {
        gtk_label_set_max_width_chars (GTK_LABEL (label), maxlen);
        gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
    }

    gtk_container_add (GTK_CONTAINER (item), box);
    gtk_container_add (GTK_CONTAINER (box), icon);
    gtk_container_add (GTK_CONTAINER (box), label);

    return item;
}

void update_menu_icon (GtkWidget *item, GtkWidget *image)
{
    GtkWidget *box = gtk_bin_get_child (GTK_BIN (item));
    GList *children = gtk_container_get_children (GTK_CONTAINER (box));
    GtkWidget *img = (GtkWidget *) children->data;
    gtk_container_remove (GTK_CONTAINER (box), img);
    gtk_box_pack_start (GTK_BOX (box), image, FALSE, FALSE, 0);
    gtk_box_reorder_child (GTK_BOX (box), image, 0);
}

const char *get_menu_label (GtkWidget *item)
{
    if (!GTK_IS_BIN (item)) return "";
    GtkWidget *box = gtk_bin_get_child (GTK_BIN (item));
    if (!box) return "";
    GList *children = gtk_container_get_children (GTK_CONTAINER (box));
    if (!children) return "";
    while (children->data)
    {
        if (GTK_IS_LABEL ((GtkWidget *) children->data))
            return gtk_label_get_text (GTK_LABEL ((GtkWidget *) children->data));
        children = children->next;
    }
    return "";
}

void append_menu_icon (GtkWidget *item, GtkWidget *image)
{
    GtkWidget *box = gtk_bin_get_child (GTK_BIN (item));
    gtk_box_pack_end (GTK_BOX (box), image, FALSE, FALSE, 0);
}

void revert_textdomain (void)
{
    textdomain (GETTEXT_PACKAGE);
}

/*----------------------------------------------------------------------------*/
/* Plugin graph */
/*----------------------------------------------------------------------------*/

/* Redraw entire graph */

static void graph_redraw (PluginGraph *graph, char *label)
{
    unsigned int fontsize, drawing_cursor, i;
    GdkPixbuf *pixbuf;

    uint scale = gtk_widget_get_scale_factor (graph->da);

    cairo_t *cr = cairo_create (graph->pixmap);
    cairo_set_line_width (cr, scale);
    
    /* Erase pixmap */
    cairo_rectangle (cr, 0, 0, graph->pixmap_width, graph->pixmap_height);
    cairo_set_source_rgba (cr, graph->background.blue, graph->background.green, graph->background.red, graph->background.alpha);
    cairo_fill (cr);

    /* Recompute pixmap */
    drawing_cursor = graph->ring_cursor;
    for (i = 0; i < graph->pixmap_width / scale; i++)
    {
        /* Draw one bar of the graph. */
        if (graph->samples[drawing_cursor] != 0.0)
        {
            cairo_set_source_rgba (cr, graph->colours[graph->samp_states[drawing_cursor]].blue, graph->colours[graph->samp_states[drawing_cursor]].green,
                graph->colours[graph->samp_states[drawing_cursor]].red, graph->colours[graph->samp_states[drawing_cursor]].alpha);

            cairo_move_to (cr, i * scale + 0.5, graph->pixmap_height - scale);
            cairo_line_to (cr, i * scale + 0.5, graph->pixmap_height - scale - graph->samples[drawing_cursor] * (graph->pixmap_height - 2 * scale));
            cairo_stroke (cr);
        }

        /* Increment and wrap drawing cursor */
        drawing_cursor += 1;
        if (drawing_cursor >= graph->pixmap_width / scale) drawing_cursor = 0;
    }

    /* Draw border in black */
    cairo_set_source_rgb (cr, 0, 0, 0);
    cairo_set_line_width (cr, scale);
    cairo_move_to (cr, scale - 1, scale - 1);
    cairo_line_to (cr, scale - 1, graph->pixmap_height - scale + 1);
    cairo_line_to (cr, graph->pixmap_width - scale + 1, graph->pixmap_height - scale + 1);
    cairo_line_to (cr, graph->pixmap_width - scale + 1, scale - 1);
    cairo_line_to (cr, scale - 1, scale - 1);
    cairo_stroke (cr);

    /* Apply label */
    fontsize = 12 * scale;
    if (graph->pixmap_width > 50 * scale) fontsize = graph->pixmap_height / 3;
    cairo_select_font_face (cr, "monospace", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size (cr, fontsize);
    cairo_set_source_rgb (cr, 0, 0, 0);
    cairo_text_extents_t extents;
    cairo_text_extents (cr, label, &extents);
    cairo_move_to (cr, (graph->pixmap_width - extents.width) / 2, (graph->pixmap_height + extents.height) / 2);
    cairo_show_text (cr, label);

    cairo_destroy (cr);

    /* Update image */
    pixbuf = gdk_pixbuf_new_from_data (cairo_image_surface_get_data (graph->pixmap), GDK_COLORSPACE_RGB, TRUE, 8, 
        graph->pixmap_width, graph->pixmap_height, graph->pixmap_width * 4, NULL, NULL);

    if (scale == 1) gtk_image_set_from_pixbuf (GTK_IMAGE (graph->da), pixbuf);
    else
    {
        cairo_surface_t *cr = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL);
        gtk_image_set_from_surface (GTK_IMAGE (graph->da), cr);
        cairo_surface_destroy (cr);
    }
    g_object_unref (pixbuf);
}

/* Initialise graph for a particular size */

void graph_reload (PluginGraph *graph, int icon_size, GdkRGBA background, GdkRGBA foreground, GdkRGBA throttle1, GdkRGBA throttle2)
{
    int scale = gtk_widget_get_scale_factor (graph->da);

    /* Load colours */
    graph->background = background;
    graph->colours[0] = foreground;
    graph->colours[1] = throttle1;
    graph->colours[2] = throttle2;

    /* Allocate pixmap and statistics buffer without border pixels. */
    guint new_pixmap_height = icon_size - (BORDER_SIZE << 1);
    guint new_pixmap_width = (new_pixmap_height * 3) >> 1;
    if (new_pixmap_width < 50) new_pixmap_width = 50;

    new_pixmap_width *= scale;
    new_pixmap_height *= scale;

    if ((new_pixmap_width > 0) && (new_pixmap_height > 0))
    {
        /* If statistics buffer does not exist or it changed size, reallocate and preserve existing data. */
        if ((graph->samples == NULL) || (new_pixmap_width != graph->pixmap_width))
        {
            float *new_samples = g_new0 (float, new_pixmap_width);
            int *new_samp_states = g_new0 (int, new_pixmap_width);
            if (graph->samples != NULL)
            {
                if (new_pixmap_width > graph->pixmap_width)
                {
                    /* New allocation is larger. Introduce new "oldest" samples of zero following the cursor. */
                    memcpy (&new_samples[0], &graph->samples[0], graph->ring_cursor * sizeof (float));
                    memcpy (&new_samples[new_pixmap_width - graph->pixmap_width + graph->ring_cursor], &graph->samples[graph->ring_cursor], (graph->pixmap_width - graph->ring_cursor) * sizeof (float));
                    memcpy (&new_samp_states[0], &graph->samp_states[0], graph->ring_cursor * sizeof (int));
                    memcpy (&new_samp_states[new_pixmap_width - graph->pixmap_width + graph->ring_cursor], &graph->samp_states[graph->ring_cursor], (graph->pixmap_width - graph->ring_cursor) * sizeof (int));
                }
                else if (graph->ring_cursor <= new_pixmap_width)
                {
                    /* New allocation is smaller, but still larger than the ring buffer cursor. Discard the oldest samples following the cursor. */
                    memcpy (&new_samples[0], &graph->samples[0], graph->ring_cursor * sizeof (float));
                    memcpy (&new_samples[graph->ring_cursor], &graph->samples[graph->pixmap_width - new_pixmap_width + graph->ring_cursor], (new_pixmap_width - graph->ring_cursor) * sizeof (float));
                    memcpy (&new_samp_states[0], &graph->samp_states[0], graph->ring_cursor * sizeof (int));
                    memcpy (&new_samp_states[graph->ring_cursor], &graph->samp_states[graph->pixmap_width - new_pixmap_width + graph->ring_cursor], (new_pixmap_width - graph->ring_cursor) * sizeof (int));
                }
                else
                {
                    /* New allocation is smaller, and also smaller than the ring buffer cursor. Discard all oldest samples following the ring buffer cursor and additional samples at the beginning of the buffer. */
                    memcpy (&new_samples[0], &graph->samples[graph->ring_cursor - new_pixmap_width], new_pixmap_width * sizeof (float));
                    memcpy (&new_samp_states[0], &graph->samp_states[graph->ring_cursor - new_pixmap_width], new_pixmap_width * sizeof (int));
                    graph->ring_cursor = 0;
                }
                g_free (graph->samples);
                g_free (graph->samp_states);
            }
            graph->samples = new_samples;
            graph->samp_states = new_samp_states;
        }

        /* Allocate or reallocate pixmap. */
        graph->pixmap_width = new_pixmap_width;
        graph->pixmap_height = new_pixmap_height;
        if (graph->pixmap) cairo_surface_destroy (graph->pixmap);
        graph->pixmap = cairo_image_surface_create (CAIRO_FORMAT_RGB24, graph->pixmap_width, graph->pixmap_height);

        /* Redraw pixmap at the new size. */
        graph_redraw (graph, "");
    }
}

/* Add new data point to the graph */

void graph_new_point (PluginGraph *graph, float value, int state, char *label)
{
    if (value < 0.0) value = 0.0;
    else if (value > 1.0) value = 1.0;
    graph->samples[graph->ring_cursor] = value;
    graph->samp_states[graph->ring_cursor] = state;

    graph->ring_cursor += 1;
    if (graph->ring_cursor >= graph->pixmap_width) graph->ring_cursor = 0;

    graph_redraw (graph, label);
}

void graph_init (PluginGraph *graph)
{
    graph->da = gtk_image_new ();
    graph->samples = NULL;
    graph->ring_cursor = 0;
}

void graph_free (PluginGraph *graph)
{
    if (graph->pixmap) cairo_surface_destroy (graph->pixmap);
    if (graph->samples) g_free (graph->samples);
    if (graph->samp_states) g_free (graph->samp_states);
    gtk_widget_destroy (graph->da);
}

/*----------------------------------------------------------------------------*/
/* Menu popup with keyboard handling */
/*----------------------------------------------------------------------------*/

static gboolean hide_prelight (GtkWidget *btn)
{
    if (btn && GTK_IS_WIDGET (btn)) gtk_widget_unset_state_flags (btn, GTK_STATE_FLAG_PRELIGHT);
    return FALSE;
}

static void menu_hidden (GtkWidget *, kb_menu_t *data)
{
    g_signal_handler_disconnect (data->menu, data->mhandle);
    gtk_layer_set_layer (panel, orig_layer);
    gtk_layer_set_keyboard_mode (panel, GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND);
    if (data->button) g_idle_add ((GSourceFunc) hide_prelight, data->button);
    g_free (data);
}

static int get_menu_padding (void)
{
    GtkWidget *men = gtk_menu_new ();
    GtkStyleContext *sc = gtk_widget_get_style_context (men);
    GtkBorder pad;
    gtk_style_context_get_padding (sc, gtk_style_context_get_state (sc), &pad);
    gtk_widget_destroy (men);
    return pad.left;
}

static void committed (GdkWindow *win, kb_menu_t *data)
{
    // spoof event just to suppress warnings...
    GdkEventButton *ev = (GdkEventButton *) gdk_event_new (GDK_NOTHING);
    ev->send_event = TRUE;
    gdk_event_set_device ((GdkEvent *) ev, gdk_seat_get_pointer (gdk_display_get_default_seat (gdk_display_get_default ())));

    gboolean bottom = gtk_layer_get_anchor (panel, GTK_LAYER_SHELL_EDGE_BOTTOM);
    int pad = get_menu_padding ();
    GValue val = G_VALUE_INIT;
    g_value_init (&val, G_TYPE_INT);
    g_value_set_int (&val, bottom ? -pad : pad);
    g_object_set_property ((GObject *) data->menu, "rect-anchor-dy", &val);

    g_signal_handler_disconnect (win, data->chandle);
    data->mhandle = g_signal_connect (data->menu, "hide", G_CALLBACK (menu_hidden), data);
    if (data->button)
    {
        gtk_menu_popup_at_widget (data->menu, data->button, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, (GdkEvent *) ev);
    }
    else
    {
        GdkRectangle rect;
        gtk_widget_get_allocation (GTK_WIDGET (panel), &rect);
        rect.x = data->x;
        rect.y = 0;
        rect.width = 0;
        gtk_menu_popup_at_rect (data->menu, gtk_widget_get_window (GTK_WIDGET (panel)), &rect, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, (GdkEvent *) ev);
    }
}

static void count_item (GtkWidget *, gpointer data)
{
    (* (int *) data)++;
}

gboolean check_menu (GtkWidget *menu)
{
    if (!GTK_IS_MENU (menu)) return FALSE;
    int count = 0;
    gtk_container_foreach (GTK_CONTAINER (menu), count_item, &count);
    if (count == 0) return FALSE;
    return TRUE;
}

void show_menu_with_kbd (GtkWidget *widget, GtkWidget *menu)
{
    close_popup ();

    kb_menu_t *data = g_new (kb_menu_t, 1);

    panel = find_panel (widget);

    if (GTK_IS_BUTTON (widget) || GTK_IS_EVENT_BOX (widget) || GTK_IS_BOX (widget)) data->button = widget;
    else data->button = NULL;
    data->menu = GTK_MENU (menu);
    data->x = -1.0;
    data->y = -1.0;

    gtk_layer_set_layer (panel, GTK_LAYER_SHELL_LAYER_TOP);
    gtk_layer_set_keyboard_mode (panel, GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE);
    data->chandle = g_signal_connect (gtk_widget_get_window (GTK_WIDGET (panel)), "committed", G_CALLBACK (committed), data);
}

void show_menu_with_kbd_at_xy (GtkWidget *widget, GtkWidget *menu, double x, double y)
{
    close_popup ();

    kb_menu_t *data = g_new (kb_menu_t, 1);

    panel = find_panel (widget);

    data->button = NULL;
    data->menu = GTK_MENU (menu);
    data->x = x;
    data->y = y;

    gtk_layer_set_layer (panel, GTK_LAYER_SHELL_LAYER_TOP);
    gtk_layer_set_keyboard_mode (panel, GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE);
    data->chandle = g_signal_connect (gtk_widget_get_window (GTK_WIDGET (panel)), "committed", G_CALLBACK (committed), data);
}

/*----------------------------------------------------------------------------*/
/* Window popup with close on click-away */
/*----------------------------------------------------------------------------*/

static void popup_hidden (GtkWidget *popup, kb_menu_t *data)
{
    gtk_widget_destroy (clicksink);
    g_signal_handler_disconnect (popup, data->mhandle);
    if (data->button) g_idle_add ((GSourceFunc) hide_prelight, data->button);
    g_free (data);
}

static gboolean handle_clickaway (GtkWidget *, GdkEventButton *, gpointer)
{
    close_popup ();
    return FALSE;
}

void popup_window_at_button (GtkWidget *window, GtkWidget *button)
{
    GdkDisplay *disp;
    GdkMonitor *mon;
    GdkRectangle rect;
    GtkCssProvider *prov;
    int i, pw, ph, wiz;
    gboolean bottom;
    FILE *fp;
    char *cmd, *mname;

    panel = find_panel (button);
    mon = gtk_layer_get_monitor (panel);

    clicksink = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_layer_init_for_window (GTK_WINDOW (clicksink));
    gtk_layer_set_anchor (GTK_WINDOW (clicksink), GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
    gtk_layer_set_anchor (GTK_WINDOW (clicksink), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE);
    gtk_layer_set_anchor (GTK_WINDOW (clicksink), GTK_LAYER_SHELL_EDGE_TOP, TRUE);
    gtk_layer_set_anchor (GTK_WINDOW (clicksink), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE);
    gtk_layer_set_monitor (GTK_WINDOW (clicksink), mon);
    gtk_widget_set_name (clicksink, "clicksink");

    prov = gtk_css_provider_new ();
    gtk_css_provider_load_from_data (prov, "#clicksink { background-color: transparent; }", -1, NULL);
    gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
        GTK_STYLE_PROVIDER (prov), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref (prov);

    gtk_widget_show (clicksink);
    gtk_window_present (GTK_WINDOW (clicksink));
    g_signal_connect (clicksink, "button-release-event", G_CALLBACK (handle_clickaway), NULL);

    close_popup ();

    popwindow = GTK_WINDOW (window);

    disp = gdk_display_get_default ();
    gtk_layer_init_for_window (popwindow);
    gtk_widget_show_all (window);

    // get the dimensions of the panel
    panel = find_panel (button);
    bottom = gtk_layer_get_anchor (panel, GTK_LAYER_SHELL_EDGE_BOTTOM);
    gtk_widget_get_allocation (GTK_WIDGET (panel), &rect);
    px = rect.width;
    py = rect.height;
    wiz = px;

    // get the dimensions of the popup itself and ensure the popup fits on the screen
    gtk_widget_get_allocation (window, &rect);
    pw = rect.width;
    ph = rect.height;
    px -= pw;

    // get the dimensions of the button - align left edge of popup with left edge of button
    gtk_widget_get_allocation (button, &rect);
    if (rect.x <= px) px = rect.x;

    // get the dimensions of the monitor - correct the y-coord of the plugin if at bottom
    gdk_monitor_get_geometry (mon, &rect);
    mh = rect.height;
    mw = rect.width;
    wiz -= mw;
    if (bottom) py = mh - py - ph;

    orient = 0;
    for (i = 0; i < gdk_display_get_n_monitors (disp); i++)
    {
        if (mon == gdk_display_get_monitor (disp, i))
        {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
            // yes, I know get_monitor_plug_name is deprecated, but the recommended replacement doesn't actually do the same thing...
            mname = gdk_screen_get_monitor_plug_name (gdk_display_get_default_screen (disp), i);
#pragma GCC diagnostic pop
            cmd = g_strdup_printf ("wlr-randr | sed -nr '/%s/,/^~ /{s/Transform:\\s*(.*)/\\1/p}' | tr -d ' '", mname);
            if ((fp = popen (cmd, "r")) != NULL)
            {
                if (fscanf (fp, "%d", &orient) != 1) orient = 0;
                pclose (fp);
            }
            g_free (cmd);
            g_free (mname);
        }
    }

    gtk_layer_set_layer (popwindow, GTK_LAYER_SHELL_LAYER_TOP);

    if (wiz < 0)
    {
        // special case if in wizard, where panel is at top right of screen
        gtk_layer_set_anchor (popwindow, GTK_LAYER_SHELL_EDGE_TOP, TRUE);
        gtk_layer_set_margin (popwindow, GTK_LAYER_SHELL_EDGE_TOP, get_menu_padding () + py);
        gtk_layer_set_anchor (popwindow, GTK_LAYER_SHELL_EDGE_RIGHT, TRUE);
        gtk_layer_set_margin (popwindow, GTK_LAYER_SHELL_EDGE_RIGHT, 0);
    }
    else
    {
        gtk_layer_set_anchor (popwindow, bottom ? GTK_LAYER_SHELL_EDGE_BOTTOM : GTK_LAYER_SHELL_EDGE_TOP, TRUE);
        gtk_layer_set_margin (popwindow, bottom ? GTK_LAYER_SHELL_EDGE_BOTTOM : GTK_LAYER_SHELL_EDGE_TOP, get_menu_padding ());
        gtk_layer_set_anchor (popwindow, GTK_LAYER_SHELL_EDGE_LEFT, TRUE);
        gtk_layer_set_margin (popwindow, GTK_LAYER_SHELL_EDGE_LEFT, px);
    }
    gtk_layer_set_monitor (popwindow, mon);
    gtk_layer_set_keyboard_mode (popwindow, GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE);

    gtk_window_present (popwindow);

    kb_menu_t *data = g_new (kb_menu_t, 1);
    data->button = button;
    data->mhandle = g_signal_connect (popwindow, "hide", G_CALLBACK (popup_hidden), data);
}

void close_popup (void)
{
    if (popwindow) gtk_widget_destroy (GTK_WIDGET (popwindow));
    popwindow = NULL;
}

/*----------------------------------------------------------------------------*/
/* Long press gestures */
/*----------------------------------------------------------------------------*/

void pass_right_click (GtkWidget *wid, double x, double y)
{
    GtkAllocation alloc;
    GdkEventButton *ev;
    GtkWidget *w;
    gboolean ret;

    gtk_widget_get_allocation (wid, &alloc);
    ev = (GdkEventButton *) gdk_event_new (GDK_BUTTON_PRESS);
    ev->send_event = TRUE;
    ev->button = 3;
    ev->window = gtk_widget_get_window (wid);
    ev->x_root = x + alloc.x;
    ev->y_root = y + alloc.y;
    gdk_event_set_device ((GdkEvent *) ev, gdk_seat_get_pointer (gdk_display_get_default_seat (gdk_display_get_default ())));
    w = wid;
    while (!GTK_IS_WINDOW (w)) w = gtk_widget_get_parent (w);
    g_signal_emit_by_name (w, "button-press-event", ev, &ret);
    ev->type = GDK_BUTTON_RELEASE;
    g_signal_emit_by_name (w, "button-release-event", ev, &ret);
    pressed = PRESS_LONG;
}

static void gesture_pressed (GtkGestureLongPress *, gdouble x, gdouble y, gpointer)
{
    pressed = PRESS_LONG;
    press_x = x;
    press_y = y;
}

static void gesture_end_default (GtkGestureLongPress *, GdkEventSequence *, GtkWidget *target)
{
    if (pressed == PRESS_LONG) pass_right_click (target, press_x, press_y);
}

GtkGesture *add_long_press (GtkWidget *target, GCallback callback, gpointer data)
{
    GtkGesture *gesture = gtk_gesture_long_press_new (target);
    gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), touch_only);
    g_signal_connect (gesture, "pressed", G_CALLBACK (gesture_pressed), NULL);
    if (callback) g_signal_connect (gesture, "end", G_CALLBACK (callback), data);
    else g_signal_connect (gesture, "end", G_CALLBACK (gesture_end_default), target);
    gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_BUBBLE);
    return gesture;
}

gboolean is_pi (void)
{
    return is_pi_var;
}

/* End of file */
/*----------------------------------------------------------------------------*/
