Fix X11 area enter & leave bug

Signed-off-by: Tin <tin.svagelj@live.com>
This commit is contained in:
Tin 2023-11-09 13:04:32 +01:00 committed by Brenden Matthews
parent bf900545c0
commit a4ac632db7
No known key found for this signature in database
GPG Key ID: 137B7AC2BDFD8DF0
7 changed files with 361 additions and 53 deletions

View File

@ -169,7 +169,6 @@ if(BUILD_X11)
option(BUILD_XINERAMA "Build Xinerama support" true)
option(BUILD_XDBE "Build Xdbe (double-buffer) support" true)
option(BUILD_XFT "Build Xft (freetype fonts) support" true)
option(BUILD_IMLIB2 "Enable Imlib2 support" true)
option(BUILD_XSHAPE "Enable Xshape support" true)
else(BUILD_X11)
set(OWN_WINDOW false CACHE BOOL "Enable own_window support" FORCE)
@ -178,7 +177,6 @@ else(BUILD_X11)
set(BUILD_XINERAMA false CACHE BOOL "Build Xinerama support" FORCE)
set(BUILD_XDBE false CACHE BOOL "Build Xdbe (double-buffer) support" FORCE)
set(BUILD_XFT false CACHE BOOL "Build Xft (freetype fonts) support" FORCE)
set(BUILD_IMLIB2 false CACHE BOOL "Enable Imlib2 support" FORCE)
set(BUILD_XSHAPE false CACHE BOOL "Enable Xshape support" FORCE)
set(BUILD_NVIDIA false)
endif(BUILD_X11)
@ -192,9 +190,14 @@ if(BUILD_WAYLAND)
endif(BUILD_WAYLAND)
if(BUILD_GUI)
option(BUILD_IMLIB2 "Enable Imlib2 support" true)
option(BUILD_MOUSE_EVENTS "Enable mouse event support" true)
endif(BUILD_GUI)
if(BUILD_GUI AND BUILD_MOUSE_EVENTS)
option(BUILD_XINPUT "Build Xinput 2 support" true)
endif(BUILD_GUI AND BUILD_MOUSE_EVENTS)
if(OWN_WINDOW)
option(BUILD_ARGB "Build ARGB (real transparency) support" true)
else(OWN_WINDOW)

View File

@ -402,6 +402,29 @@ if(BUILD_X11)
set(conky_libs ${conky_libs} ${X11_Xfixes_LIB})
endif(BUILD_XFIXES)
# check for Xinput
if(BUILD_XINPUT)
if(NOT X11_Xinput_FOUND)
message(FATAL_ERROR "Unable to find Xinput library")
endif(NOT X11_Xinput_FOUND)
set(conky_libs ${conky_libs} ${X11_Xinput_LIB})
endif(BUILD_XINPUT)
if(X11_xcb_FOUND)
set(HAVE_XCB true)
set(conky_libs ${conky_libs} ${X11_xcb_LIB})
if(X11_xcb_errors_FOUND)
set(HAVE_XCB_ERRORS true)
set(conky_libs ${conky_libs} ${X11_xcb_LIB} ${X11_xcb_errors_LIB})
else(X11_xcb_errors_FOUND)
set(HAVE_XCB_ERRORS false)
endif(X11_xcb_errors_FOUND)
else(X11_xcb_FOUND)
set(HAVE_XCB false)
endif(X11_xcb_FOUND)
else(X11_FOUND)
message(FATAL_ERROR "Unable to find X11 library")
endif(X11_FOUND)

View File

@ -42,6 +42,9 @@
#cmakedefine HAVE_CLOCK_GETTIME 1
#cmakedefine HAVE_XCB 1
#cmakedefine HAVE_XCB_ERRORS 1
#cmakedefine BUILD_WAYLAND 1
#cmakedefine BUILD_X11 1
@ -60,6 +63,8 @@
#cmakedefine BUILD_XFIXES 1
#cmakedefine BUILD_XINPUT 1
#cmakedefine BUILD_ARGB 1
#cmakedefine BUILD_XDBE 1

View File

@ -24,6 +24,7 @@
*
*/
#include <X11/extensions/XI2.h>
#include <config.h>
#ifdef BUILD_X11
@ -44,6 +45,9 @@
#endif /* BUILD_IMLIB2 */
#ifdef BUILD_MOUSE_EVENTS
#include "mouse-events.h"
#ifdef BUILD_XINPUT
#include <X11/extensions/XInput2.h>
#endif /* BUILD_XINPUT */
#endif /* BUILD_MOUSE_EVENTS */
#endif /* BUILD_X11 */
@ -51,6 +55,7 @@
#include <sstream>
#include <unordered_map>
#include <cstdint>
#include <vector>
#include "colours.h"
#include "conky.h"
@ -229,27 +234,6 @@ bool display_output_x11::shutdown() {
return true;
}
inline void propagate_unconsumed_event(XEvent &ev, bool is_consumed) {
const uint32_t capture_mask = ButtonPressMask | ButtonReleaseMask | ButtonMotionMask;
// SAFETY: XEvent is a union and all event types this function gets called for
// share same alignment and accessed fields share same offsets.
if (!is_consumed) {
/* forward the event to the desktop window */
ev.xmotion.window = window.desktop;
ev.xmotion.x = ev.xmotion.x_root;
ev.xmotion.y = ev.xmotion.y_root;
XSendEvent(display, window.desktop, False, capture_mask, &ev);
if (false && ev.type == ButtonPress) {
XSetInputFocus(display, window.desktop, RevertToNone, ev.xmotion.time);
} else if (false && ev.type == MotionNotify) {
XSetInputFocus(display, window.window, RevertToParent, ev.xmotion.time);
}
} else {
XSetInputFocus(display, window.window, RevertToParent, ev.xmotion.time);
}
}
bool display_output_x11::main_loop_wait(double t) {
/* wait for X event or timeout */
if (!display || !window.gc) return true;
@ -397,6 +381,50 @@ bool display_output_x11::main_loop_wait(double t) {
bool consumed = false;
XNextEvent(display, &ev);
#ifdef BUILD_XINPUT
if (ev.type == GenericEvent && ev.xcookie.extension == window.xi_opcode) {
XGetEventData(display, &ev.xcookie);
if (ev.xcookie.evtype == XI_Motion) {
auto *data = reinterpret_cast<XIDeviceEvent*>(ev.xcookie.data);
// XQueryPointer returns wrong results because conky is a weird window
Window query_result = query_x11_window_at_pos(display, data->root_x, data->root_y);
//printf("over: %#x w:%#x d:%#x r:%#x\n", query_result, window.window, window.desktop, window.root);
static bool cursor_inside = false;
if ((query_result != 0 && query_result == window.window) ||
((query_result == window.desktop || query_result == window.root || query_result == 0) && data->root_x >= window.x && data->root_x < (window.x + window.width) &&
data->root_y >= window.y && data->root_y < (window.y + window.height))) {
//puts("over self");
if (!cursor_inside) {
llua_mouse_hook(mouse_crossing_event(
mouse_event_t::AREA_ENTER,
data->root_x - window.x, data->root_y - window.x,
data->root_x, data->root_y
));
}
cursor_inside = true;
} else if (cursor_inside) {
//puts("left");
llua_mouse_hook(mouse_crossing_event(
mouse_event_t::AREA_LEAVE,
data->root_x - window.x, data->root_y - window.x,
data->root_x, data->root_y
));
cursor_inside = false;
} else {
//printf("not over self\n");
}
}
XFreeEventData(display, &ev.xcookie);
continue;
}
#endif /* BUILD_XINPUT */
// Any of the remaining events apply to conky window
if (ev.xany.window != window.window) continue;
switch (ev.type) {
case Expose: {
XRectangle r;
@ -406,6 +434,10 @@ bool display_output_x11::main_loop_wait(double t) {
r.height = ev.xexpose.height;
XUnionRectWithRegion(&r, x11_stuff.region, x11_stuff.region);
XSync(display, False);
// modify for propagation
ev.xexpose.x += window.x;
ev.xexpose.y += window.y;
break;
}
@ -427,7 +459,7 @@ bool display_output_x11::main_loop_wait(double t) {
#ifdef USE_ARGB
}
#endif
break;
continue;
}
#ifdef OWN_WINDOW
@ -436,7 +468,7 @@ bool display_output_x11::main_loop_wait(double t) {
if (own_window.get(*state)) {
set_transparent_background(window.window);
}
break;
continue;
case ConfigureNotify:
if (own_window.get(*state)) {
@ -477,7 +509,7 @@ bool display_output_x11::main_loop_wait(double t) {
fixed_pos = 1;
} */
}
break;
continue;
case ButtonPress:
#ifdef BUILD_MOUSE_EVENTS
@ -519,7 +551,6 @@ bool display_output_x11::main_loop_wait(double t) {
/* allow conky to hold input focus. */
break;
}
propagate_unconsumed_event(ev, consumed);
}
break;
@ -553,7 +584,6 @@ bool display_output_x11::main_loop_wait(double t) {
/* allow conky to hold input focus. */
break;
}
propagate_unconsumed_event(ev, consumed);
}
break;
#ifdef BUILD_MOUSE_EVENTS
@ -565,30 +595,25 @@ bool display_output_x11::main_loop_wait(double t) {
consumed = llua_mouse_hook(mouse_move_event(
ev.xmotion.x, ev.xmotion.y, ev.xmotion.x_root, ev.xmotion.y_root, ev.xmotion.state
));
propagate_unconsumed_event(ev, consumed);
break;
case LeaveNotify:
XUngrabPointer(display, ev.xcrossing.time);
case EnterNotify:
{
if (window.xi_opcode == 0) {
bool not_over_conky = ev.xcrossing.x_root <= window.x || ev.xcrossing.y_root <= window.y ||
ev.xcrossing.x_root >= window.x + window.width || ev.xcrossing.y_root >= window.y + window.height;
static bool pointer_inside = false;
if ((not_over_conky && ev.xcrossing.type == LeaveNotify) ||
(!pointer_inside && ev.xcrossing.type == EnterNotify)) {
(!not_over_conky && ev.xcrossing.type == EnterNotify)) {
llua_mouse_hook(mouse_crossing_event(
ev.xcrossing.type == EnterNotify ? mouse_event_t::AREA_ENTER: mouse_event_t::AREA_LEAVE,
ev.xcrossing.type == EnterNotify ? mouse_event_t::AREA_ENTER : mouse_event_t::AREA_LEAVE,
ev.xcrossing.x, ev.xcrossing.y, ev.xcrossing.x_root, ev.xcrossing.y_root
));
pointer_inside = ev.xcrossing.type == EnterNotify;
// can't propagate these events in a way that makes sense
}
}
break;
// can't propagate these events in a way that makes sense for desktop
continue;
#endif /* BUILD_MOUSE_EVENTS */
#endif
#endif /* OWN_WINDOW */
default:
#ifdef BUILD_XDAMAGE
if (ev.type == x11_stuff.event_base + XDamageNotify) {
@ -597,10 +622,20 @@ bool display_output_x11::main_loop_wait(double t) {
XFixesSetRegion(display, x11_stuff.part, &dev->area, 1);
XFixesUnionRegion(display, x11_stuff.region2, x11_stuff.region2,
x11_stuff.part);
continue; // TODO: Propagate damage
}
#endif /* BUILD_XDAMAGE */
break;
}
if (!consumed) {
propagate_x11_event(ev);
} else {
InputEvent *i_ev = xev_as_input_event(ev);
if (i_ev != nullptr) {
XSetInputFocus(display, window.window, RevertToParent, i_ev->common.time);
}
}
}
#ifdef BUILD_XDAMAGE
@ -648,6 +683,7 @@ bool display_output_x11::main_loop_wait(double t) {
}
void display_output_x11::sigterm_cleanup() {
puts("sigtermed");
XDestroyRegion(x11_stuff.region);
x11_stuff.region = nullptr;
#ifdef BUILD_XDAMAGE
@ -660,6 +696,7 @@ void display_output_x11::sigterm_cleanup() {
}
void display_output_x11::cleanup() {
puts("normal cleanup");
if (window_created == 1) {
int border_total = get_border_total();

View File

@ -31,6 +31,7 @@
#define _LOGGING_H
#include <cstdio>
#include <cinttypes>
#include <stdexcept>
#include "config.h"
#include "i18n.h"
@ -81,6 +82,7 @@ void NORM_ERR(const char *format, Args &&...args) {
/* critical error with additional cleanup */
template <typename... Args>
__attribute__((noreturn))
inline void CRIT_ERR_FREE(void *memtofree1, void *memtofree2,
const char *format, Args &&...args) {
NORM_ERR(format, args...);
@ -92,6 +94,7 @@ inline void CRIT_ERR_FREE(void *memtofree1, void *memtofree2,
/* critical error */
template <typename... Args>
__attribute__((noreturn))
inline void CRIT_ERR(const char *format, Args &&...args) {
CRIT_ERR_FREE(nullptr, nullptr, format, args...);
}

View File

@ -28,10 +28,18 @@
*/
#include <X11/X.h>
#include <sys/types.h>
#include "common.h"
#include "config.h"
#include "conky.h"
#include "logging.h"
#include "x11.h"
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdlib>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wvariadic-macros"
@ -61,10 +69,24 @@
#ifdef BUILD_XFIXES
#include <X11/extensions/Xfixes.h>
#endif /* BUILD_XFIXES */
#ifdef BUILD_XINPUT
#include <vector>
#include <X11/extensions/XInput2.h>
#endif /* BUILD_XINPUT */
#ifdef HAVE_XCB_ERRORS
#include <xcb/xcb.h>
#include <xcb/xcb_errors.h>
#endif
/* some basic X11 stuff */
Display *display = nullptr;
#ifdef HAVE_XCB_ERRORS
xcb_connection_t *xcb_connection;
xcb_errors_context_t *xcb_errors_ctx;
#endif
/* Window stuff */
struct conky_x11_window window;
@ -215,20 +237,77 @@ imlib_cache_size_setting imlib_cache_size;
/******************** </SETTINGS> ************************/
/* WARNING, this type not in Xlib spec */
static int __attribute__((noreturn))
static int
x11_error_handler(Display *d, XErrorEvent *err) {
char *error_name = nullptr;
bool name_allocated = false;
char *code_description = nullptr;
bool code_allocated = false;
#ifdef HAVE_XCB_ERRORS
if (xcb_errors_ctx != nullptr) {
const char *extension;
const char *base_name = xcb_errors_get_name_for_error(xcb_errors_ctx, err->error_code, &extension);
if (extension != nullptr) {
error_name = new char(strlen(base_name) + strlen(extension) + 4);
sprintf(error_name, "%s (%s)", base_name, extension);
name_allocated = true;
} else {
error_name = const_cast<char*>(base_name);
}
const char *major = xcb_errors_get_name_for_major_code(xcb_errors_ctx, err->request_code);
const char *minor = xcb_errors_get_name_for_minor_code(xcb_errors_ctx, err->request_code, err->minor_code);
if (minor != nullptr) {
code_description = new char(strlen(base_name) + strlen(extension) + 4);
sprintf(code_description, "%s - %s", major, minor);
code_allocated = true;
} else {
code_description = const_cast<char*>(major);
}
}
#endif
if (error_name == nullptr) {
if (err->error_code > 0 && err->error_code < 17) {
static std::array<std::string, 17> NAMES = {
"request", "value", "window", "pixmap", "atom", "cursor", "font", "match",
"drawable", "access", "alloc", "colormap", "G context", "ID choice",
"name", "length", "implementation"
};
error_name = const_cast<char*>(NAMES[err->error_code].c_str());
} else {
static char code_name_buffer[3];
error_name = reinterpret_cast<char*>(&code_name_buffer);
sprintf(error_name, "%d", err->error_code);
}
}
if (code_description == nullptr) {
code_description = new char(30 + 6 + 1);
sprintf(code_description, "error code: [major: %i, minor: %i]", err->request_code, err->minor_code);
code_allocated = true;
}
NORM_ERR(
"X Error: type %i Display %lx XID %li serial %lu error_code %i "
"request_code %i minor_code %i other Display: %lx\n",
err->type, (long unsigned)err->display,
static_cast<long>(err->resourceid), err->serial, err->error_code,
err->request_code, err->minor_code, (long unsigned)d);
abort();
"X %s Error:\n"
"Display: %lx, XID: %li, Serial: %lu\n"
"%s",
error_name,
reinterpret_cast<uint64_t>(err->display),
static_cast<int64_t>(err->resourceid), err->serial,
code_description
);
if (name_allocated) free(error_name);
if (code_allocated) free(code_description);
return 0;
}
static int __attribute__((noreturn)) x11_ioerror_handler(Display *d) {
NORM_ERR("X Error: Display %lx\n", (long unsigned)d);
exit(1);
__attribute__((noreturn))
static int x11_ioerror_handler(Display *d) {
CRIT_ERR("X IO Error: Display %lx\n", reinterpret_cast<uint64_t>(d));
}
/* X11 initializer */
@ -267,6 +346,15 @@ static void init_x11() {
update_workarea();
#ifdef HAVE_XCB_ERRORS
auto connection = xcb_connect(NULL, NULL);
if (!xcb_connection_has_error(connection)) {
if (xcb_errors_context_new(connection, &xcb_errors_ctx) != Success) {
xcb_errors_ctx = nullptr;
}
}
#endif /* HAVE_XCB_ERRORS */
/* WARNING, this type not in Xlib spec */
XSetErrorHandler(&x11_error_handler);
XSetIOErrorHandler(&x11_ioerror_handler);
@ -854,7 +942,7 @@ void x11_init_window(lua::state &l __attribute__((unused)), bool own) {
XFlush(display);
long input_mask = ExposureMask | PropertyChangeMask;
int64_t input_mask = ExposureMask | PropertyChangeMask;
#ifdef OWN_WINDOW
if (own_window.get(l)) {
input_mask |= StructureNotifyMask | ButtonPressMask | ButtonReleaseMask;
@ -864,10 +952,40 @@ void x11_init_window(lua::state &l __attribute__((unused)), bool own) {
/* it's not recommended to add event masks to special windows in X; causes a
* crash */
if (own_window_type.get(l) != TYPE_DESKTOP) {
input_mask |= ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
EnterWindowMask | LeaveWindowMask;
input_mask |= ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
}
bool xinput_ok = false;
#ifdef BUILD_XINPUT
do { // not loop
int _ignored; // segfault if NULL
if (!XQueryExtension(display, "XInputExtension", &window.xi_opcode, &_ignored, &_ignored)) {
// events will still ~work but let the user know why they're buggy
NORM_ERR("XInput extension is not supported by X11!");
break;
}
int32_t major = 2, minor = 0;
uint32_t retval = XIQueryVersion(display, &major, &minor);
if (retval != Success) {
NORM_ERR("Error: XInput 2.0 is not supported!");
break;
}
unsigned char mask_bytes[(XI_LASTEVENT + 7) / 8] = {0}; /* must be zeroed! */
XISetMask(mask_bytes, XI_Motion);
XIEventMask ev_masks[1];
ev_masks[0].deviceid = XIAllDevices;
ev_masks[0].mask_len = sizeof(mask_bytes);
ev_masks[0].mask = mask_bytes;
XISelectEvents(display, window.root, ev_masks, 1);
xinput_ok = true;
} while (false);
#endif /* BUILD_XINPUT */
if (!xinput_ok && own_window_type.get(l) != TYPE_DESKTOP) {
input_mask |= EnterWindowMask | LeaveWindowMask;
}
#endif /* BUILD_MOUSE_EVENTS */
window.event_mask = input_mask;
XSelectInput(display, window.window, input_mask);
window_created = 1;
@ -1229,3 +1347,74 @@ void print_mouse_speed(struct text_object *obj, char *p,
XGetPointerControl(display, &acc_num, &acc_denom, &threshold);
snprintf(p, p_max_size, "%d%%", (110 - threshold));
}
InputEvent *xev_as_input_event(XEvent &ev) {
if (ev.type == KeyPress || ev.type == KeyRelease ||
ev.type == ButtonPress || ev.type == ButtonRelease || ev.type == MotionNotify ||
ev.type == EnterNotify || ev.type == LeaveNotify) {
return reinterpret_cast<InputEvent*>(&ev);
} else {
return nullptr;
}
}
void propagate_x11_event(XEvent &ev) {
InputEvent *i_ev = xev_as_input_event(ev);
/* forward the event to the desktop window */
if (i_ev != nullptr) {
i_ev->common.window = window.desktop;
i_ev->common.x = i_ev->common.x_root;
i_ev->common.y = i_ev->common.y_root;
}
XSendEvent(display, window.desktop, False, window.event_mask, &ev);
// FIXME (before-merge): Should we be setting input focus after forwarding
// events?
if (false && ev.type == ButtonPress) {
XSetInputFocus(display, window.desktop, RevertToNone, i_ev->common.time);
} else if (false && ev.type == MotionNotify) {
XSetInputFocus(display, window.window, RevertToParent, i_ev->common.time);
}
}
#ifdef BUILD_MOUSE_EVENTS
Window last_descendant(Display* display, Window parent) {
Window ignored, *children;
uint32_t count;
Window current = parent;
while (XQueryTree(display, current, &ignored, &ignored, &children, &count) && count != 0) {
current = children[count - 1];
XFree(children);
}
return current;
}
Window query_x11_window_at_pos(Display* display, int x, int y) {
Window root = DefaultRootWindow(display);
Window root_return, parent_return, *windows;
unsigned int count;
Window last = None;
XWindowAttributes attrs;
if (XQueryTree(display, root, &root_return, &parent_return, &windows, &count) != 0) {
for (unsigned int i = 0; i < count; i++) {
if (XGetWindowAttributes(display, windows[i], &attrs)) {
if (attrs.map_state == IsViewable && x >= attrs.x && x < (attrs.x + attrs.width) && y >= attrs.y && y < (attrs.y + attrs.height)) {
last = windows[i];
}
}
}
if (count != 0) {
XFree(windows);
}
}
return last_descendant(display, last);
}
#endif /* BUILD_MOUSE_EVENTS */

View File

@ -5,7 +5,7 @@
* Please see COPYING for details
*
* Copyright (c) 2005-2021 Brenden Matthews, Philip Kovacs, et. al.
* (see AUTHORS)
* (see AUTHORS)
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
@ -38,6 +38,7 @@
#endif
#include "setting.hh"
#include <cstdint>
#ifdef BUILD_ARGB
/* true if use_argb_visual=true and argb visual was found*/
@ -78,6 +79,9 @@ struct conky_x11_window {
Colormap colourmap;
GC gc;
// Mask containing all events captured by conky
int64_t event_mask;
#ifdef BUILD_XDBE
XdbeBackBuffer back_buffer;
#else /*BUILD_XDBE*/
@ -86,6 +90,9 @@ struct conky_x11_window {
#ifdef BUILD_XFT
XftDraw *xftdraw;
#endif /*BUILD_XFT*/
#ifdef BUILD_XINPUT
int32_t xi_opcode;
#endif /* BUILD_XINPUT */
int width;
int height;
@ -106,6 +113,47 @@ void set_struts(int);
void x11_init_window(lua::state &l, bool own);
void deinit_x11();
// Fields common to all X11 input events
struct InputEventCommon {
int type; /* event type */
uint64_t serial; /* # of last request processed by server */
Bool send_event; /* true if this came from a SendEvent request */
Display *display; /* Display the event was read from */
Window window; /* "event" window reported relative to */
Window root; /* root window that the event occurred on */
Window subwindow; /* child window */
Time time; /* milliseconds */
int32_t x, y; /* pointer x, y coordinates in event window */
int32_t x_root, y_root; /* coordinates relative to root */
uint32_t state; /* key or button mask */
};
union InputEvent {
int type; // event type
InputEventCommon common;
// Discrete interfaces
XAnyEvent xany; // common event interface
XKeyEvent xkey; // KeyPress & KeyRelease events
XButtonEvent xbutton; // ButtonPress & ButtonRelease events
XMotionEvent xmotion; // MotionNotify event
XCrossingEvent xcrossing; // EnterNotify & LeaveNotify events
// Ensures InputEvent matches memory layout of XEvent.
// Accessing base variant is as code smell.
XEvent base;
};
// Returns InputEvent pointer to provided XEvent is an input event; nullptr otherwise.
InputEvent *xev_as_input_event(XEvent &ev);
void propagate_x11_event(XEvent &ev);
#ifdef BUILD_MOUSE_EVENTS
Window query_x11_window_at_pos(Display* display, int x, int y);
#endif /* BUILD_MOUSE_EVENTS */
#ifdef BUILD_XDBE
void xdbe_swap_buffers(void);
#else