toaruos/userspace/gui/core/panel.c

646 lines
16 KiB
C
Raw Normal View History

2012-03-15 00:04:12 +04:00
/* vim: tabstop=4 shiftwidth=4 noexpandtab
*
2014-05-06 08:47:45 +04:00
* Yutani Panel
*
* Provides a window list and clock as well some simple session management.
*
* Future goals:
* - Applications menu
* - More session management
* - Pluggable indicators
*
2012-03-15 00:04:12 +04:00
*/
2012-03-08 09:44:02 +04:00
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <time.h>
2013-05-03 10:33:57 +04:00
#include <unistd.h>
2014-05-06 08:47:45 +04:00
#include <signal.h>
#include <sys/time.h>
#include <sys/utsname.h>
#include <sys/wait.h>
2012-03-08 09:44:02 +04:00
2014-05-06 08:47:45 +04:00
/* TODO: Move all of the configurable rendering
* parameters up here */
#define PANEL_HEIGHT 28
2014-05-06 08:47:45 +04:00
#define FONT_SIZE 14
#define TIME_LEFT 108
#define DATE_WIDTH 70
2014-04-16 06:45:56 +04:00
#include "lib/pthread.h"
#include "lib/yutani.h"
2012-03-08 09:44:02 +04:00
#include "lib/graphics.h"
#include "lib/shmemfonts.h"
2014-05-04 00:20:16 +04:00
#include "lib/hashmap.h"
2014-05-06 08:47:45 +04:00
#include "lib/spinlock.h"
2012-03-08 09:44:02 +04:00
2014-05-06 08:47:45 +04:00
static gfx_context_t * ctx;
static yutani_t * yctx;
static yutani_window_t * panel;
static gfx_context_t * actx;
static yutani_window_t * alttab;
2014-05-06 08:47:45 +04:00
static list_t * window_list = NULL;
static volatile int lock = 0;
static volatile int drawlock = 0;
2012-03-08 09:44:02 +04:00
2014-05-06 08:47:45 +04:00
static hashmap_t * icon_cache;
2014-05-06 08:47:45 +04:00
static size_t bg_size;
static char * bg_blob;
2014-05-04 00:20:16 +04:00
2014-05-06 08:47:45 +04:00
static int width;
static int height;
2014-04-16 06:45:56 +04:00
2014-05-06 08:47:45 +04:00
static sprite_t * sprite_panel;
static sprite_t * sprite_logout;
2012-03-08 09:44:02 +04:00
#define ALTTAB_WIDTH 250
#define ALTTAB_HEIGHT 70
#define ALTTAB_BACKGROUND premultiply(rgba(0,0,0,150))
#define ALTTAB_OFFSET 10
2014-05-06 08:47:45 +04:00
static int center_x(int x) {
2014-04-16 06:45:56 +04:00
return (width - x) / 2;
2012-03-08 09:44:02 +04:00
}
2014-05-06 08:47:45 +04:00
static int center_y(int y) {
2014-04-16 06:45:56 +04:00
return (height - y) / 2;
2012-03-08 09:44:02 +04:00
}
static int center_x_a(int x) {
return (ALTTAB_WIDTH - x) / 2;
}
static int center_y_a(int y) {
return (ALTTAB_HEIGHT - y) / 2;
}
2014-05-06 08:47:45 +04:00
static void redraw(void);
2012-09-20 08:16:21 +04:00
2014-05-06 08:47:45 +04:00
static volatile int _continue = 1;
2014-04-20 12:24:10 +04:00
2014-05-03 23:22:16 +04:00
struct window_ad {
2014-05-06 08:18:13 +04:00
yutani_wid_t wid;
2014-05-03 23:22:16 +04:00
uint32_t flags;
char * name;
char * icon;
char * strings;
};
/* XXX Stores some quick access information about the window list */
static int icon_lefts[20] = {0};
static int icon_wids[20] = {0};
static int focused_app = -1;
static int wids_by_z[20] = {0};
static int active_window = -1;
static int was_tabbing = 0;
static int new_focused = -1;
static struct window_ad * ads_by_z[20] = {NULL};
static sprite_t * icon_get(char * name);
2014-05-06 08:47:45 +04:00
/* Handle SIGINT by telling other threads (clock) to shut down */
static void sig_int(int sig) {
printf("Received shutdown signal in panel!\n");
_continue = 0;
}
2014-05-06 08:47:45 +04:00
/* Update the hover-focus window */
static void set_focused(int i) {
2014-04-20 12:24:10 +04:00
if (focused_app != i) {
focused_app = i;
redraw();
}
}
2014-05-06 08:47:45 +04:00
/* Callback for mouse events */
static void panel_check_click(struct yutani_msg_window_mouse_event * evt) {
if (evt->command == YUTANI_MOUSE_EVENT_CLICK) {
2014-05-06 08:47:45 +04:00
/* Up-down click */
2014-04-16 06:45:56 +04:00
if (evt->new_x >= width - 24 ) {
yutani_session_end(yctx);
2012-09-20 08:16:21 +04:00
_continue = 0;
2014-04-20 12:24:10 +04:00
} else {
for (int i = 0; i < 18; ++i) {
if (evt->new_x >= icon_lefts[i] && evt->new_x < icon_lefts[i+1]) {
if (icon_wids[i]) {
yutani_focus_window(yctx, icon_wids[i]);
}
break;
}
}
}
} else if (evt->command == YUTANI_MOUSE_EVENT_MOVE || evt->command == YUTANI_MOUSE_EVENT_ENTER) {
2014-05-06 08:47:45 +04:00
/* Movement, or mouse entered window */
2014-04-20 12:24:10 +04:00
if (evt->new_y < PANEL_HEIGHT) {
for (int i = 0; i < 18; ++i) {
if (icon_lefts[i] == 0) {
set_focused(-1);
break;
}
if (evt->new_x >= icon_lefts[i] && evt->new_x < icon_lefts[i+1]) {
set_focused(i);
break;
}
}
} else {
set_focused(-1);
2012-09-20 08:16:21 +04:00
}
} else if (evt->command == YUTANI_MOUSE_EVENT_LEAVE) {
2014-05-06 08:47:45 +04:00
/* Mouse left panel window */
set_focused(-1);
2012-09-20 08:16:21 +04:00
}
}
static void launch_application(char * app) {
if (!fork()) {
char * args[] = {app, NULL};
execvp(args[0], args);
exit(1);
}
}
static void redraw_alttab(void) {
/* Draw the background, right now just a dark semi-transparent box */
draw_fill(actx, ALTTAB_BACKGROUND);
if (ads_by_z[new_focused]) {
struct window_ad * ad = ads_by_z[new_focused];
sprite_t * icon = icon_get(ad->icon);
/* Draw it, scaled if necessary */
if (icon->width == 24) {
draw_sprite(actx, icon, center_x_a(24), ALTTAB_OFFSET);
} else {
draw_sprite_scaled(actx, icon, center_x_a(24), ALTTAB_OFFSET, 24, 24);
}
set_font_face(FONT_SANS_SERIF_BOLD);
set_font_size(14);
int t = draw_string_width(ad->name);
draw_string(actx, center_x_a(t), 24+ALTTAB_OFFSET+16, rgb(255,255,255), ad->name);
}
flip(actx);
yutani_flip(yctx, alttab);
}
static void handle_key_event(struct yutani_msg_key_event * ke) {
if ((ke->event.modifiers & KEY_MOD_LEFT_CTRL) &&
(ke->event.modifiers & KEY_MOD_LEFT_ALT) &&
(ke->event.keycode == 't') &&
(ke->event.action == KEY_ACTION_DOWN)) {
launch_application("terminal");
}
if ((was_tabbing) && (ke->event.keycode == 0) &&
(ke->event.modifiers == 0) && (ke->event.action == KEY_ACTION_UP)) {
fprintf(stderr, "[panel] Stopping focus new_focused = %d\n", new_focused);
yutani_focus_window(yctx, wids_by_z[new_focused]);
was_tabbing = 0;
new_focused = -1;
free(actx->backbuffer);
free(actx);
yutani_close(yctx, alttab);
return;
}
if ((ke->event.modifiers & KEY_MOD_LEFT_ALT) &&
(ke->event.keycode == '\t') &&
(ke->event.action == KEY_ACTION_DOWN)) {
int direction = (ke->event.modifiers & KEY_MOD_LEFT_SHIFT) ? 1 : -1;
if (was_tabbing) {
new_focused = new_focused + direction;
} else {
new_focused = active_window + direction;
/* Create tab window */
alttab = yutani_window_create(yctx, ALTTAB_WIDTH, ALTTAB_HEIGHT);
/* And move it to the top layer */
yutani_window_move(yctx, alttab, center_x(ALTTAB_WIDTH), center_y(ALTTAB_HEIGHT));
/* Initialize graphics context against the window */
actx = init_graphics_yutani_double_buffer(alttab);
}
if (new_focused < 0) {
new_focused = 0;
for (int i = 0; i < 18; i++) {
if (wids_by_z[i+1] == 0) {
new_focused = i;
break;
}
}
} else if (wids_by_z[new_focused] == 0) {
new_focused = 0;
}
was_tabbing = 1;
redraw_alttab();
}
}
2014-05-06 08:47:45 +04:00
/* Default search paths for icons, in order of preference */
static char * icon_directories[] = {
"/usr/share/icons/24",
"/usr/share/icons/48",
"/usr/share/icons",
NULL
};
2014-05-06 08:47:45 +04:00
/*
* Get an icon from the cache, or if it is not in the cache,
* load it - or cache the generic icon if we can not find an
* appropriate matching icon on the filesystem.
*/
static sprite_t * icon_get(char * name) {
if (!strcmp(name,"")) {
2014-05-06 08:47:45 +04:00
/* If a window doesn't have an icon set, return the generic icon */
return hashmap_get(icon_cache, "generic");
}
2014-05-06 08:47:45 +04:00
/* Check the icon cache */
sprite_t * icon = hashmap_get(icon_cache, name);
if (!icon) {
2014-05-06 08:47:45 +04:00
/* We don't have an icon cached for this identifier, try search */
int i = 0;
char path[100];
while (icon_directories[i]) {
2014-05-06 08:47:45 +04:00
/* Check each path... */
sprintf(path, "%s/%s.png", icon_directories[i], name);
fprintf(stderr, "Checking %s for icon\n", path);
if (access(path, R_OK) == 0) {
2014-05-06 08:47:45 +04:00
/* And if we find one, cache it */
icon = malloc(sizeof(sprite_t));
load_sprite_png(icon, path);
hashmap_set(icon_cache, name, icon);
return icon;
}
i++;
}
2014-05-06 08:47:45 +04:00
/* If we've exhausted our search paths, just return the generic icon */
icon = hashmap_get(icon_cache, "generic");
hashmap_set(icon_cache, name, icon);
}
2014-05-06 08:47:45 +04:00
/* We have an icon, return it */
return icon;
}
2014-05-06 08:47:45 +04:00
static void redraw(void) {
spin_lock(&drawlock);
struct timeval now;
int last = 0;
struct tm * timeinfo;
char buffer[80];
uint32_t txt_color = rgb(230,230,230);
int t = 0;
2014-05-06 08:47:45 +04:00
/* Redraw the background */
memcpy(ctx->backbuffer, bg_blob, bg_size);
2014-05-06 08:47:45 +04:00
/* Get the current time for the clock */
gettimeofday(&now, NULL);
last = now.tv_sec;
timeinfo = localtime((time_t *)&now.tv_sec);
2014-05-06 08:47:45 +04:00
/* Hours : Minutes : Seconds */
strftime(buffer, 80, "%H:%M:%S", timeinfo);
set_font_face(FONT_SANS_SERIF_BOLD);
set_font_size(16);
draw_string(ctx, width - TIME_LEFT, 19, txt_color, buffer);
2014-05-06 08:47:45 +04:00
/* Day-of-week */
strftime(buffer, 80, "%A", timeinfo);
set_font_face(FONT_SANS_SERIF);
set_font_size(9);
t = draw_string_width(buffer);
t = (DATE_WIDTH - t) / 2;
draw_string(ctx, width - TIME_LEFT - DATE_WIDTH + t, 11, txt_color, buffer);
2014-05-06 08:47:45 +04:00
/* Month Day */
strftime(buffer, 80, "%h %e", timeinfo);
set_font_face(FONT_SANS_SERIF_BOLD);
set_font_size(9);
t = draw_string_width(buffer);
t = (DATE_WIDTH - t) / 2;
draw_string(ctx, width - TIME_LEFT - DATE_WIDTH + t, 21, txt_color, buffer);
2014-05-06 08:47:45 +04:00
/* TODO: Future applications menu */
set_font_face(FONT_SANS_SERIF_BOLD);
set_font_size(14);
draw_string(ctx, 10, 18, txt_color, "Applications");
2014-05-06 08:47:45 +04:00
/* Now draw the window list */
2014-04-20 12:24:10 +04:00
int i = 0, j = 0;
spin_lock(&lock);
if (window_list) {
foreach(node, window_list) {
2014-05-03 23:22:16 +04:00
struct window_ad * ad = node->value;
char * s = ad->name;
set_font_face(FONT_SANS_SERIF);
2014-05-04 00:20:16 +04:00
set_font_size(13);
int w = 26 + draw_string_width(s) + 20;
2014-05-06 08:47:45 +04:00
/* Hilight the focused window */
2014-05-04 00:20:16 +04:00
if (ad->flags & 1) {
/* This is the focused window */
for (int y = 0; y < 24; ++y) {
for (int x = 135 + i; x < 135 + i + w - 10; ++x) {
GFX(ctx, x, y) = alpha_blend_rgba(GFX(ctx, x, y), premultiply(rgba(72, 167, 255, ((24-y)*160)/24)));
}
}
}
2014-05-06 08:47:45 +04:00
/* Get the icon for this window */
sprite_t * icon = icon_get(ad->icon);
2014-05-04 00:20:16 +04:00
2014-05-06 08:47:45 +04:00
/* Draw it, scaled if necessary */
if (icon->width == 24) {
draw_sprite(ctx, icon, 140 + i, 0);
} else {
draw_sprite_scaled(ctx, icon, 140 + i, 0, 24, 24);
}
2014-05-04 00:20:16 +04:00
2014-05-06 08:47:45 +04:00
/* Then draw the window title, with appropriate color */
2014-04-20 12:24:10 +04:00
if (j == focused_app) {
2014-05-06 08:47:45 +04:00
/* Current hilighted - title should be a light blue */
2014-05-04 00:20:16 +04:00
draw_string(ctx, 140 + i + 26, 18, rgb(142,216,255), s);
2014-04-20 12:24:10 +04:00
} else {
2014-05-03 23:22:16 +04:00
if (ad->flags & 1) {
2014-05-06 08:47:45 +04:00
/* Top window should be white */
2014-05-04 00:20:16 +04:00
draw_string(ctx, 140 + i + 26, 18, rgb(255,255,255), s);
2014-05-03 23:22:16 +04:00
} else {
2014-05-06 08:47:45 +04:00
/* Otherwise, off white */
2014-05-04 00:20:16 +04:00
draw_string(ctx, 140 + i + 26, 18, txt_color, s);
2014-05-03 23:22:16 +04:00
}
2014-04-20 12:24:10 +04:00
}
2014-05-06 08:47:45 +04:00
/* XXX This keeps track of how far left each window list item is
* so we can map clicks up in the mouse callback. */
2014-04-20 12:24:10 +04:00
if (j < 18) {
icon_lefts[j] = 140 + i;
j++;
}
2014-05-04 00:20:16 +04:00
i += w;
}
2014-04-20 12:24:10 +04:00
if (j < 19) {
icon_lefts[j] = 140 + i;
icon_lefts[j+1] = 0;
}
}
spin_unlock(&lock);
2014-05-06 08:47:45 +04:00
/* Draw the logout button; XXX This should probably have some sort of focus hilight */
draw_sprite(ctx, sprite_logout, width - 23, 1); /* Logout button */
2014-05-06 08:47:45 +04:00
/* Flip */
flip(ctx);
yutani_flip(yctx, panel);
spin_unlock(&drawlock);
}
2014-04-20 12:24:10 +04:00
2014-05-06 08:47:45 +04:00
static void update_window_list(void) {
yutani_query_windows(yctx);
list_t * new_window_list = list_create();
int i = 0;
while (1) {
2014-05-06 08:47:45 +04:00
/* We wait for a series of WINDOW_ADVERTISE messsages */
yutani_msg_t * m = yutani_wait_for(yctx, YUTANI_MSG_WINDOW_ADVERTISE);
struct yutani_msg_window_advertise * wa = (void*)m->data;
if (wa->size == 0) {
2014-05-06 08:47:45 +04:00
/* A sentinal at the end will have a size of 0 */
free(m);
break;
}
2014-05-06 08:47:45 +04:00
/* Store each window advertisement */
2014-05-03 23:22:16 +04:00
struct window_ad * ad = malloc(sizeof(struct window_ad));
2014-05-03 23:07:03 +04:00
char * s = malloc(wa->size);
memcpy(s, wa->strings, wa->size);
2014-05-03 23:22:16 +04:00
ad->name = &s[wa->offsets[0]];
ad->icon = &s[wa->offsets[1]];
ad->strings = s;
ad->flags = wa->flags;
2014-05-06 08:18:13 +04:00
ad->wid = wa->wid;
2014-05-03 23:07:03 +04:00
wids_by_z[i] = ad->wid;
ads_by_z[i] = ad;
i++;
wids_by_z[i] = 0;
ads_by_z[i] = NULL;
2014-05-06 08:18:13 +04:00
node_t * next = NULL;
2014-05-06 08:47:45 +04:00
/* And insert it, ordered by wid, into the window list */
2014-05-06 08:18:13 +04:00
foreach(node, new_window_list) {
struct window_ad * n = node->value;
if (n->wid > ad->wid) {
next = node;
break;
}
}
if (next) {
list_insert_before(new_window_list, next, ad);
} else {
list_insert(new_window_list, ad);
}
free(m);
2014-05-06 08:47:45 +04:00
}
active_window = i-1;
2014-04-20 12:24:10 +04:00
i = 0;
2014-05-06 08:47:45 +04:00
/*
* Update each of the wid entries in our array so we can map
* clicks to window focus events for each window
*/
foreach(node, new_window_list) {
struct window_ad * ad = node->value;
if (i < 19) {
icon_wids[i] = ad->wid;
icon_wids[i+1] = 0;
}
2014-04-20 12:24:10 +04:00
i++;
}
2014-05-06 08:47:45 +04:00
/* Then free up the old list and replace it with the new list */
spin_lock(&lock);
if (window_list) {
2014-05-03 23:22:16 +04:00
foreach(node, window_list) {
struct window_ad * ad = (void*)node->value;
free(ad->strings);
free(ad);
}
list_free(window_list);
free(window_list);
}
window_list = new_window_list;
spin_unlock(&lock);
2014-05-06 08:47:45 +04:00
/* And redraw the panel */
redraw();
}
2014-05-06 08:47:45 +04:00
static void * clock_thread(void * garbage) {
/*
* This thread just calls redraw every so often so the clock
* continues to tick. We really shouldn't need this,
* but our current environment doens't provide timeouts,
* so we can't just bail out of a yutani poll and redraw...
*/
while (_continue) {
waitpid(-1, NULL, WNOHANG);
redraw();
2013-05-03 10:33:57 +04:00
usleep(500000);
2012-03-08 09:44:02 +04:00
}
2014-04-16 06:45:56 +04:00
}
int main (int argc, char ** argv) {
2014-05-06 08:47:45 +04:00
/* Connect to window server */
2014-04-16 06:45:56 +04:00
yctx = yutani_init();
2014-05-06 08:47:45 +04:00
/* For convenience, store the display size */
2014-04-16 06:45:56 +04:00
width = yctx->display_width;
height = yctx->display_height;
2014-05-06 08:47:45 +04:00
/* Initialize fonts. */
2014-04-16 06:45:56 +04:00
init_shmemfonts();
set_font_size(14);
2014-05-06 08:47:45 +04:00
/* Create the panel window */
2014-04-16 06:45:56 +04:00
panel = yutani_window_create(yctx, width, PANEL_HEIGHT);
2014-05-06 08:47:45 +04:00
/* And move it to the top layer */
2014-04-16 06:45:56 +04:00
yutani_set_stack(yctx, panel, YUTANI_ZORDER_TOP);
2014-05-06 08:47:45 +04:00
/* Initialize graphics context against the window */
2014-04-16 06:45:56 +04:00
ctx = init_graphics_yutani_double_buffer(panel);
2014-05-06 08:47:45 +04:00
/* Clear it out (the compositor should initialize it cleared anyway */
2014-04-16 06:45:56 +04:00
draw_fill(ctx, rgba(0,0,0,0));
flip(ctx);
yutani_flip(yctx, panel);
2014-05-06 08:47:45 +04:00
/* Initialize hashmap for icon cache */
2014-05-04 00:20:16 +04:00
icon_cache = hashmap_create(10);
2014-05-06 08:47:45 +04:00
/* Preload some common icons */
{ /* Generic fallback icon */
2014-05-04 00:20:16 +04:00
sprite_t * app_icon = malloc(sizeof(sprite_t));
load_sprite_png(app_icon, "/usr/share/icons/24/applications-generic.png");
hashmap_set(icon_cache, "generic", app_icon);
}
2014-05-06 08:47:45 +04:00
{ /* Terminal */
2014-05-04 00:20:16 +04:00
sprite_t * app_icon = malloc(sizeof(sprite_t));
load_sprite_png(app_icon, "/usr/share/icons/24/utilities-terminal.png");
hashmap_set(icon_cache, "utilities-terminal", app_icon);
}
2014-05-06 08:47:45 +04:00
{ /* Draw! icon */
2014-05-04 00:20:16 +04:00
sprite_t * app_icon = malloc(sizeof(sprite_t));
load_sprite_png(app_icon, "/usr/share/icons/24/applications-painting.png");
hashmap_set(icon_cache, "applications-painting", app_icon);
}
2014-05-06 08:47:45 +04:00
/* Load textures for the background and logout button */
sprite_panel = malloc(sizeof(sprite_t));
sprite_logout = malloc(sizeof(sprite_t));
2014-04-16 06:45:56 +04:00
2014-05-06 08:47:45 +04:00
load_sprite_png(sprite_panel, "/usr/share/panel.png");
2014-05-06 09:03:57 +04:00
load_sprite_png(sprite_logout, "/usr/share/icons/panel-shutdown.png");
2014-04-16 06:45:56 +04:00
2014-05-06 08:47:45 +04:00
/* Draw the background */
for (uint32_t i = 0; i < width; i += sprite_panel->width) {
draw_sprite(ctx, sprite_panel, i, 0);
}
2014-05-06 08:47:45 +04:00
/* Copy the prerendered background so we can redraw it quickly */
bg_size = panel->width * panel->height * sizeof(uint32_t);
bg_blob = malloc(bg_size);
memcpy(bg_blob, ctx->backbuffer, bg_size);
2014-05-06 08:47:45 +04:00
/* Catch SIGINT */
signal(SIGINT, sig_int);
/* Start clock thread XXX need timeouts in yutani calls */
2014-04-16 06:45:56 +04:00
pthread_t _clock_thread;
pthread_create(&_clock_thread, NULL, clock_thread, NULL);
2014-05-06 08:47:45 +04:00
/* Subscribe to window updates */
yutani_subscribe_windows(yctx);
/* Ask compositor for window list */
update_window_list();
/* Key bindings */
/* Launch terminal */
yutani_key_bind(yctx, 't', KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_ALT, YUTANI_BIND_STEAL);
/* Alt+Tab */
yutani_key_bind(yctx, '\t', KEY_MOD_LEFT_ALT, YUTANI_BIND_STEAL);
yutani_key_bind(yctx, '\t', KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT, YUTANI_BIND_STEAL);
/* This lets us receive all just-modifier key releases */
yutani_key_bind(yctx, 0, 0, YUTANI_BIND_PASSTHROUGH);
2014-04-16 06:45:56 +04:00
while (_continue) {
2014-05-06 08:47:45 +04:00
/* Respond to Yutani events */
2014-04-16 06:45:56 +04:00
yutani_msg_t * m = yutani_poll(yctx);
if (m) {
switch (m->type) {
2014-05-06 08:47:45 +04:00
/* New window information is available */
case YUTANI_MSG_NOTIFY:
update_window_list();
break;
2014-05-06 08:47:45 +04:00
/* Mouse movement / click */
case YUTANI_MSG_WINDOW_MOUSE_EVENT:
panel_check_click((struct yutani_msg_window_mouse_event *)m->data);
break;
case YUTANI_MSG_KEY_EVENT:
handle_key_event((struct yutani_msg_key_event *)m->data);
break;
default:
break;
}
2014-04-16 06:45:56 +04:00
free(m);
}
}
2012-03-08 09:44:02 +04:00
2014-05-06 08:47:45 +04:00
/* Close the panel window */
2014-04-16 06:45:56 +04:00
yutani_close(yctx, panel);
2014-05-06 08:47:45 +04:00
/* Stop notifying us of window changes */
yutani_unsubscribe_windows(yctx);
2012-03-08 09:44:02 +04:00
return 0;
}