cscg22-gearboy

CSCG 2022 Challenge 'Gearboy'
git clone https://git.sinitax.com/sinitax/cscg22-gearboy
Log | Files | Refs | sfeed.txt

SDL_ibus.c (19660B)


      1/*
      2  Simple DirectMedia Layer
      3  Copyright (C) 1997-2014 Sam Lantinga <slouken@libsdl.org>
      4
      5  This software is provided 'as-is', without any express or implied
      6  warranty.  In no event will the authors be held liable for any damages
      7  arising from the use of this software.
      8
      9  Permission is granted to anyone to use this software for any purpose,
     10  including commercial applications, and to alter it and redistribute it
     11  freely, subject to the following restrictions:
     12
     13  1. The origin of this software must not be misrepresented; you must not
     14     claim that you wrote the original software. If you use this software
     15     in a product, an acknowledgment in the product documentation would be
     16     appreciated but is not required.
     17  2. Altered source versions must be plainly marked as such, and must not be
     18     misrepresented as being the original software.
     19  3. This notice may not be removed or altered from any source distribution.
     20*/
     21#include "../../SDL_internal.h"
     22
     23#ifdef HAVE_IBUS_IBUS_H
     24#include "SDL.h"
     25#include "SDL_syswm.h"
     26#include "SDL_ibus.h"
     27#include "SDL_dbus.h"
     28#include "../../video/SDL_sysvideo.h"
     29#include "../../events/SDL_keyboard_c.h"
     30
     31#if SDL_VIDEO_DRIVER_X11
     32    #include "../../video/x11/SDL_x11video.h"
     33#endif
     34
     35#include <sys/inotify.h>
     36#include <unistd.h>
     37#include <fcntl.h>
     38
     39static const char IBUS_SERVICE[]         = "org.freedesktop.IBus";
     40static const char IBUS_PATH[]            = "/org/freedesktop/IBus";
     41static const char IBUS_INTERFACE[]       = "org.freedesktop.IBus";
     42static const char IBUS_INPUT_INTERFACE[] = "org.freedesktop.IBus.InputContext";
     43
     44static char *input_ctx_path = NULL;
     45static SDL_Rect ibus_cursor_rect = {0};
     46static DBusConnection *ibus_conn = NULL;
     47static char *ibus_addr_file = NULL;
     48int inotify_fd = -1, inotify_wd = -1;
     49
     50static Uint32
     51IBus_ModState(void)
     52{
     53    Uint32 ibus_mods = 0;
     54    SDL_Keymod sdl_mods = SDL_GetModState();
     55    
     56    /* Not sure about MOD3, MOD4 and HYPER mappings */
     57    if (sdl_mods & KMOD_LSHIFT) ibus_mods |= IBUS_SHIFT_MASK;
     58    if (sdl_mods & KMOD_CAPS)   ibus_mods |= IBUS_LOCK_MASK;
     59    if (sdl_mods & KMOD_LCTRL)  ibus_mods |= IBUS_CONTROL_MASK;
     60    if (sdl_mods & KMOD_LALT)   ibus_mods |= IBUS_MOD1_MASK;
     61    if (sdl_mods & KMOD_NUM)    ibus_mods |= IBUS_MOD2_MASK;
     62    if (sdl_mods & KMOD_MODE)   ibus_mods |= IBUS_MOD5_MASK;
     63    if (sdl_mods & KMOD_LGUI)   ibus_mods |= IBUS_SUPER_MASK;
     64    if (sdl_mods & KMOD_RGUI)   ibus_mods |= IBUS_META_MASK;
     65
     66    return ibus_mods;
     67}
     68
     69static const char *
     70IBus_GetVariantText(DBusConnection *conn, DBusMessageIter *iter, SDL_DBusContext *dbus)
     71{
     72    /* The text we need is nested weirdly, use dbus-monitor to see the structure better */
     73    const char *text = NULL;
     74    const char *struct_id = NULL;
     75    DBusMessageIter sub1, sub2;
     76
     77    if (dbus->message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) {
     78        return NULL;
     79    }
     80    
     81    dbus->message_iter_recurse(iter, &sub1);
     82    
     83    if (dbus->message_iter_get_arg_type(&sub1) != DBUS_TYPE_STRUCT) {
     84        return NULL;
     85    }
     86    
     87    dbus->message_iter_recurse(&sub1, &sub2);
     88    
     89    if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
     90        return NULL;
     91    }
     92    
     93    dbus->message_iter_get_basic(&sub2, &struct_id);
     94    if (!struct_id || SDL_strncmp(struct_id, "IBusText", sizeof("IBusText")) != 0) {
     95        return NULL;
     96    }
     97    
     98    dbus->message_iter_next(&sub2);
     99    dbus->message_iter_next(&sub2);
    100    
    101    if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
    102        return NULL;
    103    }
    104    
    105    dbus->message_iter_get_basic(&sub2, &text);
    106    
    107    return text;
    108}
    109
    110static size_t 
    111IBus_utf8_strlen(const char *str)
    112{
    113    size_t utf8_len = 0;
    114    const char *p;
    115    
    116    for (p = str; *p; ++p) {
    117        if (!((*p & 0x80) && !(*p & 0x40))) {
    118            ++utf8_len;
    119        }
    120    }
    121    
    122    return utf8_len;
    123}
    124
    125static DBusHandlerResult
    126IBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *user_data)
    127{
    128    SDL_DBusContext *dbus = (SDL_DBusContext *)user_data;
    129        
    130    if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "CommitText")) {
    131        DBusMessageIter iter;
    132        const char *text;
    133
    134        dbus->message_iter_init(msg, &iter);
    135        
    136        text = IBus_GetVariantText(conn, &iter, dbus);
    137        if (text && *text) {
    138            char buf[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
    139            size_t text_bytes = SDL_strlen(text), i = 0;
    140            
    141            while (i < text_bytes) {
    142                size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
    143                SDL_SendKeyboardText(buf);
    144                
    145                i += sz;
    146            }
    147        }
    148        
    149        return DBUS_HANDLER_RESULT_HANDLED;
    150    }
    151    
    152    if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) {
    153        DBusMessageIter iter;
    154        const char *text;
    155
    156        dbus->message_iter_init(msg, &iter);
    157        text = IBus_GetVariantText(conn, &iter, dbus);
    158        
    159        if (text && *text) {
    160            char buf[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
    161            size_t text_bytes = SDL_strlen(text), i = 0;
    162            size_t cursor = 0;
    163            
    164            while (i < text_bytes) {
    165                size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
    166                size_t chars = IBus_utf8_strlen(buf);
    167                
    168                SDL_SendEditingText(buf, cursor, chars);
    169
    170                i += sz;
    171                cursor += chars;
    172            }
    173        }
    174        
    175        SDL_IBus_UpdateTextRect(NULL);
    176        
    177        return DBUS_HANDLER_RESULT_HANDLED;
    178    }
    179    
    180    if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "HidePreeditText")) {
    181        SDL_SendEditingText("", 0, 0);
    182        return DBUS_HANDLER_RESULT_HANDLED;
    183    }
    184    
    185    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    186}
    187
    188static char *
    189IBus_ReadAddressFromFile(const char *file_path)
    190{
    191    char addr_buf[1024];
    192    SDL_bool success = SDL_FALSE;
    193    FILE *addr_file;
    194
    195    addr_file = fopen(file_path, "r");
    196    if (!addr_file) {
    197        return NULL;
    198    }
    199
    200    while (fgets(addr_buf, sizeof(addr_buf), addr_file)) {
    201        if (SDL_strncmp(addr_buf, "IBUS_ADDRESS=", sizeof("IBUS_ADDRESS=")-1) == 0) {
    202            size_t sz = SDL_strlen(addr_buf);
    203            if (addr_buf[sz-1] == '\n') addr_buf[sz-1] = 0;
    204            if (addr_buf[sz-2] == '\r') addr_buf[sz-2] = 0;
    205            success = SDL_TRUE;
    206            break;
    207        }
    208    }
    209
    210    fclose(addr_file);
    211
    212    if (success) {
    213        return SDL_strdup(addr_buf + (sizeof("IBUS_ADDRESS=") - 1));
    214    } else {
    215        return NULL;
    216    }
    217}
    218
    219static char *
    220IBus_GetDBusAddressFilename(void)
    221{
    222    SDL_DBusContext *dbus;
    223    const char *disp_env;
    224    char config_dir[PATH_MAX];
    225    char *display = NULL;
    226    const char *addr;
    227    const char *conf_env;
    228    char *key;
    229    char file_path[PATH_MAX];
    230    const char *host;
    231    char *disp_num, *screen_num;
    232
    233    if (ibus_addr_file) {
    234        return SDL_strdup(ibus_addr_file);
    235    }
    236    
    237    dbus = SDL_DBus_GetContext();
    238    if (!dbus) {
    239        return NULL;
    240    }
    241    
    242    /* Use this environment variable if it exists. */
    243    addr = SDL_getenv("IBUS_ADDRESS");
    244    if (addr && *addr) {
    245        return SDL_strdup(addr);
    246    }
    247    
    248    /* Otherwise, we have to get the hostname, display, machine id, config dir
    249       and look up the address from a filepath using all those bits, eek. */
    250    disp_env = SDL_getenv("DISPLAY");
    251
    252    if (!disp_env || !*disp_env) {
    253        display = SDL_strdup(":0.0");
    254    } else {
    255        display = SDL_strdup(disp_env);
    256    }
    257    
    258    host = display;
    259    disp_num   = SDL_strrchr(display, ':');
    260    screen_num = SDL_strrchr(display, '.');
    261    
    262    if (!disp_num) {
    263        SDL_free(display);
    264        return NULL;
    265    }
    266    
    267    *disp_num = 0;
    268    disp_num++;
    269    
    270    if (screen_num) {
    271        *screen_num = 0;
    272    }
    273    
    274    if (!*host) {
    275        host = "unix";
    276    }
    277        
    278    SDL_memset(config_dir, 0, sizeof(config_dir));
    279    
    280    conf_env = SDL_getenv("XDG_CONFIG_HOME");
    281    if (conf_env && *conf_env) {
    282        SDL_strlcpy(config_dir, conf_env, sizeof(config_dir));
    283    } else {
    284        const char *home_env = SDL_getenv("HOME");
    285        if (!home_env || !*home_env) {
    286            SDL_free(display);
    287            return NULL;
    288        }
    289        SDL_snprintf(config_dir, sizeof(config_dir), "%s/.config", home_env);
    290    }
    291    
    292    key = dbus->get_local_machine_id();
    293
    294    SDL_memset(file_path, 0, sizeof(file_path));
    295    SDL_snprintf(file_path, sizeof(file_path), "%s/ibus/bus/%s-%s-%s", 
    296                                               config_dir, key, host, disp_num);
    297    dbus->free(key);
    298    SDL_free(display);
    299    
    300    return SDL_strdup(file_path);
    301}
    302
    303static SDL_bool IBus_CheckConnection(SDL_DBusContext *dbus);
    304
    305static void
    306IBus_SetCapabilities(void *data, const char *name, const char *old_val,
    307                                                   const char *internal_editing)
    308{
    309    SDL_DBusContext *dbus = SDL_DBus_GetContext();
    310    
    311    if (IBus_CheckConnection(dbus)) {
    312
    313        DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
    314                                                         input_ctx_path,
    315                                                         IBUS_INPUT_INTERFACE,
    316                                                         "SetCapabilities");
    317        if (msg) {
    318            Uint32 caps = IBUS_CAP_FOCUS;
    319            if (!(internal_editing && *internal_editing == '1')) {
    320                caps |= IBUS_CAP_PREEDIT_TEXT;
    321            }
    322            
    323            dbus->message_append_args(msg,
    324                                      DBUS_TYPE_UINT32, &caps,
    325                                      DBUS_TYPE_INVALID);
    326        }
    327        
    328        if (msg) {
    329            if (dbus->connection_send(ibus_conn, msg, NULL)) {
    330                dbus->connection_flush(ibus_conn);
    331            }
    332            dbus->message_unref(msg);
    333        }
    334    }
    335}
    336
    337
    338static SDL_bool
    339IBus_SetupConnection(SDL_DBusContext *dbus, const char* addr)
    340{
    341    const char *path = NULL;
    342    SDL_bool result = SDL_FALSE;
    343    DBusMessage *msg;
    344
    345    ibus_conn = dbus->connection_open_private(addr, NULL);
    346
    347    if (!ibus_conn) {
    348        return SDL_FALSE;
    349    }
    350
    351    dbus->connection_flush(ibus_conn);
    352    
    353    if (!dbus->bus_register(ibus_conn, NULL)) {
    354        ibus_conn = NULL;
    355        return SDL_FALSE;
    356    }
    357    
    358    dbus->connection_flush(ibus_conn);
    359
    360    msg = dbus->message_new_method_call(IBUS_SERVICE, IBUS_PATH, IBUS_INTERFACE, "CreateInputContext");
    361    if (msg) {
    362        const char *client_name = "SDL2_Application";
    363        dbus->message_append_args(msg,
    364                                  DBUS_TYPE_STRING, &client_name,
    365                                  DBUS_TYPE_INVALID);
    366    }
    367    
    368    if (msg) {
    369        DBusMessage *reply;
    370        
    371        reply = dbus->connection_send_with_reply_and_block(ibus_conn, msg, 1000, NULL);
    372        if (reply) {
    373            if (dbus->message_get_args(reply, NULL,
    374                                       DBUS_TYPE_OBJECT_PATH, &path,
    375                                       DBUS_TYPE_INVALID)) {
    376                if (input_ctx_path) {
    377                    SDL_free(input_ctx_path);
    378                }
    379                input_ctx_path = SDL_strdup(path);
    380                result = SDL_TRUE;                          
    381            }
    382            dbus->message_unref(reply);
    383        }
    384        dbus->message_unref(msg);
    385    }
    386
    387    if (result) {
    388        SDL_AddHintCallback(SDL_HINT_IME_INTERNAL_EDITING, &IBus_SetCapabilities, NULL);
    389        
    390        dbus->bus_add_match(ibus_conn, "type='signal',interface='org.freedesktop.IBus.InputContext'", NULL);
    391        dbus->connection_add_filter(ibus_conn, &IBus_MessageFilter, dbus, NULL);
    392        dbus->connection_flush(ibus_conn);
    393    }
    394
    395    SDL_IBus_SetFocus(SDL_GetKeyboardFocus() != NULL);
    396    SDL_IBus_UpdateTextRect(NULL);
    397    
    398    return result;
    399}
    400
    401static SDL_bool
    402IBus_CheckConnection(SDL_DBusContext *dbus)
    403{
    404    if (!dbus) return SDL_FALSE;
    405    
    406    if (ibus_conn && dbus->connection_get_is_connected(ibus_conn)) {
    407        return SDL_TRUE;
    408    }
    409    
    410    if (inotify_fd > 0 && inotify_wd > 0) {
    411        char buf[1024];
    412        ssize_t readsize = read(inotify_fd, buf, sizeof(buf));
    413        if (readsize > 0) {
    414        
    415            char *p;
    416            SDL_bool file_updated = SDL_FALSE;
    417            
    418            for (p = buf; p < buf + readsize; /**/) {
    419                struct inotify_event *event = (struct inotify_event*) p;
    420                if (event->len > 0) {
    421                    char *addr_file_no_path = SDL_strrchr(ibus_addr_file, '/');
    422                    if (!addr_file_no_path) return SDL_FALSE;
    423                 
    424                    if (SDL_strcmp(addr_file_no_path + 1, event->name) == 0) {
    425                        file_updated = SDL_TRUE;
    426                        break;
    427                    }
    428                }
    429                
    430                p += sizeof(struct inotify_event) + event->len;
    431            }
    432            
    433            if (file_updated) {
    434                char *addr = IBus_ReadAddressFromFile(ibus_addr_file);
    435                if (addr) {
    436                    SDL_bool result = IBus_SetupConnection(dbus, addr);
    437                    SDL_free(addr);
    438                    return result;
    439                }
    440            }
    441        }
    442    }
    443    
    444    return SDL_FALSE;
    445}
    446
    447SDL_bool
    448SDL_IBus_Init(void)
    449{
    450    SDL_bool result = SDL_FALSE;
    451    SDL_DBusContext *dbus = SDL_DBus_GetContext();
    452    
    453    if (dbus) {
    454        char *addr_file = IBus_GetDBusAddressFilename();
    455        char *addr;
    456        char *addr_file_dir;
    457
    458        if (!addr_file) {
    459            return SDL_FALSE;
    460        }
    461        
    462        ibus_addr_file = SDL_strdup(addr_file);
    463        
    464        addr = IBus_ReadAddressFromFile(addr_file);
    465        
    466        if (inotify_fd < 0) {
    467            inotify_fd = inotify_init();
    468            fcntl(inotify_fd, F_SETFL, O_NONBLOCK);
    469        }
    470        
    471        addr_file_dir = SDL_strrchr(addr_file, '/');
    472        if (addr_file_dir) {
    473            *addr_file_dir = 0;
    474        }
    475        
    476        inotify_wd = inotify_add_watch(inotify_fd, addr_file, IN_CREATE | IN_MODIFY);
    477        SDL_free(addr_file);
    478        
    479        result = IBus_SetupConnection(dbus, addr);
    480        SDL_free(addr);
    481    }
    482    
    483    return result;
    484}
    485
    486void
    487SDL_IBus_Quit(void)
    488{   
    489    SDL_DBusContext *dbus;
    490
    491    if (input_ctx_path) {
    492        SDL_free(input_ctx_path);
    493        input_ctx_path = NULL;
    494    }
    495    
    496    if (ibus_addr_file) {
    497        SDL_free(ibus_addr_file);
    498        ibus_addr_file = NULL;
    499    }
    500    
    501    dbus = SDL_DBus_GetContext();
    502    
    503    if (dbus && ibus_conn) {
    504        dbus->connection_close(ibus_conn);
    505        dbus->connection_unref(ibus_conn);
    506    }
    507    
    508    if (inotify_fd > 0 && inotify_wd > 0) {
    509        inotify_rm_watch(inotify_fd, inotify_wd);
    510        inotify_wd = -1;
    511    }
    512    
    513    SDL_DelHintCallback(SDL_HINT_IME_INTERNAL_EDITING, &IBus_SetCapabilities, NULL);
    514    
    515    SDL_memset(&ibus_cursor_rect, 0, sizeof(ibus_cursor_rect));
    516}
    517
    518static void
    519IBus_SimpleMessage(const char *method)
    520{   
    521    SDL_DBusContext *dbus = SDL_DBus_GetContext();
    522    
    523    if (IBus_CheckConnection(dbus)) {
    524        DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
    525                                                         input_ctx_path,
    526                                                         IBUS_INPUT_INTERFACE,
    527                                                         method);
    528        if (msg) {
    529            if (dbus->connection_send(ibus_conn, msg, NULL)) {
    530                dbus->connection_flush(ibus_conn);
    531            }
    532            dbus->message_unref(msg);
    533        }
    534    }
    535}
    536
    537void
    538SDL_IBus_SetFocus(SDL_bool focused)
    539{ 
    540    const char *method = focused ? "FocusIn" : "FocusOut";
    541    IBus_SimpleMessage(method);
    542}
    543
    544void
    545SDL_IBus_Reset(void)
    546{
    547    IBus_SimpleMessage("Reset");
    548}
    549
    550SDL_bool
    551SDL_IBus_ProcessKeyEvent(Uint32 keysym, Uint32 keycode)
    552{ 
    553    SDL_bool result = SDL_FALSE;   
    554    SDL_DBusContext *dbus = SDL_DBus_GetContext();
    555    
    556    if (IBus_CheckConnection(dbus)) {
    557        DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
    558                                                         input_ctx_path,
    559                                                         IBUS_INPUT_INTERFACE,
    560                                                         "ProcessKeyEvent");
    561        if (msg) {
    562            Uint32 mods = IBus_ModState();
    563            dbus->message_append_args(msg,
    564                                      DBUS_TYPE_UINT32, &keysym,
    565                                      DBUS_TYPE_UINT32, &keycode,
    566                                      DBUS_TYPE_UINT32, &mods,
    567                                      DBUS_TYPE_INVALID);
    568        }
    569        
    570        if (msg) {
    571            DBusMessage *reply;
    572            
    573            reply = dbus->connection_send_with_reply_and_block(ibus_conn, msg, 300, NULL);
    574            if (reply) {
    575                if (!dbus->message_get_args(reply, NULL,
    576                                           DBUS_TYPE_BOOLEAN, &result,
    577                                           DBUS_TYPE_INVALID)) {
    578                    result = SDL_FALSE;                         
    579                }
    580                dbus->message_unref(reply);
    581            }
    582            dbus->message_unref(msg);
    583        }
    584        
    585    }
    586    
    587    SDL_IBus_UpdateTextRect(NULL);
    588
    589    return result;
    590}
    591
    592void
    593SDL_IBus_UpdateTextRect(SDL_Rect *rect)
    594{
    595    SDL_Window *focused_win;
    596    SDL_SysWMinfo info;
    597    int x = 0, y = 0;
    598    SDL_DBusContext *dbus;
    599
    600    if (rect) {
    601        SDL_memcpy(&ibus_cursor_rect, rect, sizeof(ibus_cursor_rect));
    602    }
    603
    604    focused_win = SDL_GetKeyboardFocus();
    605    if (!focused_win) {
    606        return;
    607    }
    608
    609    SDL_VERSION(&info.version);
    610    if (!SDL_GetWindowWMInfo(focused_win, &info)) {
    611        return;
    612    }
    613
    614    SDL_GetWindowPosition(focused_win, &x, &y);
    615   
    616#if SDL_VIDEO_DRIVER_X11    
    617    if (info.subsystem == SDL_SYSWM_X11) {
    618        SDL_DisplayData *displaydata = (SDL_DisplayData *) SDL_GetDisplayForWindow(focused_win)->driverdata;
    619            
    620        Display *x_disp = info.info.x11.display;
    621        Window x_win = info.info.x11.window;
    622        int x_screen = displaydata->screen;
    623        Window unused;
    624            
    625        X11_XTranslateCoordinates(x_disp, x_win, RootWindow(x_disp, x_screen), 0, 0, &x, &y, &unused);
    626    }
    627#endif
    628
    629    x += ibus_cursor_rect.x;
    630    y += ibus_cursor_rect.y;
    631        
    632    dbus = SDL_DBus_GetContext();
    633    
    634    if (IBus_CheckConnection(dbus)) {
    635        DBusMessage *msg = dbus->message_new_method_call(IBUS_SERVICE,
    636                                                         input_ctx_path,
    637                                                         IBUS_INPUT_INTERFACE,
    638                                                         "SetCursorLocation");
    639        if (msg) {
    640            dbus->message_append_args(msg,
    641                                      DBUS_TYPE_INT32, &x,
    642                                      DBUS_TYPE_INT32, &y,
    643                                      DBUS_TYPE_INT32, &ibus_cursor_rect.w,
    644                                      DBUS_TYPE_INT32, &ibus_cursor_rect.h,
    645                                      DBUS_TYPE_INVALID);
    646        }
    647        
    648        if (msg) {
    649            if (dbus->connection_send(ibus_conn, msg, NULL)) {
    650                dbus->connection_flush(ibus_conn);
    651            }
    652            dbus->message_unref(msg);
    653        }
    654    }
    655}
    656
    657void
    658SDL_IBus_PumpEvents(void)
    659{
    660    SDL_DBusContext *dbus = SDL_DBus_GetContext();
    661    
    662    if (IBus_CheckConnection(dbus)) {
    663        dbus->connection_read_write(ibus_conn, 0);
    664    
    665        while (dbus->connection_dispatch(ibus_conn) == DBUS_DISPATCH_DATA_REMAINS) {
    666            /* Do nothing, actual work happens in IBus_MessageFilter */
    667        }
    668    }
    669}
    670
    671#endif