src/video/haiku/SDL_BWin.h
author Gerasim Troeglazov <3dEyes@gmail.com>
Mon, 11 Nov 2019 22:21:17 -0500
changeset 13235 7e53f9a1d5e4
parent 13233 9dfa95a693ba
child 13306 c51961094960
permissions -rw-r--r--
haiku: Add support for relative mouse mode.

Partially fixes Bugzilla #4442.
     1 /*
     2   Simple DirectMedia Layer
     3   Copyright (C) 1997-2019 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 
    22 #ifndef SDL_BWin_h_
    23 #define SDL_BWin_h_
    24 
    25 #ifdef __cplusplus
    26 extern "C" {
    27 #endif
    28 
    29 #include "../../SDL_internal.h"
    30 #include "SDL.h"
    31 #include "SDL_syswm.h"
    32 #include "SDL_bframebuffer.h"
    33 
    34 #ifdef __cplusplus
    35 }
    36 #endif
    37 
    38 #include <stdio.h>
    39 #include <AppKit.h>
    40 #include <InterfaceKit.h>
    41 #include <game/DirectWindow.h>
    42 #if SDL_VIDEO_OPENGL
    43 #include <opengl/GLView.h>
    44 #endif
    45 #include "SDL_events.h"
    46 #include "../../main/haiku/SDL_BApp.h"
    47 
    48 
    49 enum WinCommands {
    50     BWIN_MOVE_WINDOW,
    51     BWIN_RESIZE_WINDOW,
    52     BWIN_SHOW_WINDOW,
    53     BWIN_HIDE_WINDOW,
    54     BWIN_MAXIMIZE_WINDOW,
    55     BWIN_MINIMIZE_WINDOW,
    56     BWIN_RESTORE_WINDOW,
    57     BWIN_SET_TITLE,
    58     BWIN_SET_BORDERED,
    59     BWIN_SET_RESIZABLE,
    60     BWIN_FULLSCREEN
    61 };
    62 
    63 
    64 class SDL_BWin:public BDirectWindow
    65 {
    66   public:
    67     /* Constructor/Destructor */
    68     SDL_BWin(BRect bounds, window_look look, uint32 flags)
    69         : BDirectWindow(bounds, "Untitled", look, B_NORMAL_WINDOW_FEEL, flags)
    70     {
    71         _last_buttons = 0;
    72 
    73 #if SDL_VIDEO_OPENGL
    74         _SDL_GLView = NULL;
    75         _gl_type = 0;
    76 #endif
    77         _shown = false;
    78         _inhibit_resize = false;
    79         _mouse_focused = false;
    80         _prev_frame = NULL;
    81 
    82         /* Handle framebuffer stuff */
    83         _connected = _connection_disabled = false;
    84         _buffer_created = _buffer_dirty = false;
    85         _trash_window_buffer = false;
    86         _buffer_locker = new BLocker();
    87         _bitmap = NULL;
    88         _clips = NULL;
    89         _num_clips = 0;
    90 
    91 #ifdef DRAWTHREAD
    92         _draw_thread_id = spawn_thread(HAIKU_DrawThread, "drawing_thread",
    93                             B_NORMAL_PRIORITY, (void*) this);
    94         resume_thread(_draw_thread_id);
    95 #endif
    96     }
    97 
    98     virtual ~ SDL_BWin()
    99     {
   100         Lock();
   101         _connection_disabled = true;
   102         int32 result;
   103 
   104 #if SDL_VIDEO_OPENGL
   105         if (_SDL_GLView) {
   106             _SDL_GLView->UnlockGL();
   107             RemoveChild(_SDL_GLView);   /* Why was this outside the if
   108                                             statement before? */
   109         }
   110 
   111 #endif
   112         Unlock();
   113 #if SDL_VIDEO_OPENGL
   114         if (_SDL_GLView) {
   115             delete _SDL_GLView;
   116         }
   117 #endif
   118 
   119         delete _prev_frame;
   120 
   121         /* Clean up framebuffer stuff */
   122         _buffer_locker->Lock();
   123 #ifdef DRAWTHREAD
   124         wait_for_thread(_draw_thread_id, &result);
   125 #endif
   126         free(_clips);
   127         delete _buffer_locker;
   128     }
   129 
   130 
   131     /* * * * * OpenGL functionality * * * * */
   132 #if SDL_VIDEO_OPENGL
   133     virtual BGLView *CreateGLView(Uint32 gl_flags) {
   134         Lock();
   135         if (_SDL_GLView == NULL) {
   136             _SDL_GLView = new BGLView(Bounds(), "SDL GLView",
   137                                      B_FOLLOW_ALL_SIDES,
   138                                      (B_WILL_DRAW | B_FRAME_EVENTS),
   139                                      gl_flags);
   140             _gl_type = gl_flags;
   141         }
   142         AddChild(_SDL_GLView);
   143         _SDL_GLView->EnableDirectMode(true);
   144         _SDL_GLView->LockGL();  /* "New" GLViews are created */
   145         Unlock();
   146         return (_SDL_GLView);
   147     }
   148 
   149     virtual void RemoveGLView() {
   150         Lock();
   151         if(_SDL_GLView) {
   152             _SDL_GLView->UnlockGL();
   153             RemoveChild(_SDL_GLView);
   154         }
   155         Unlock();
   156     }
   157 
   158     virtual void SwapBuffers(void) {
   159         _SDL_GLView->UnlockGL();
   160         _SDL_GLView->LockGL();
   161         _SDL_GLView->SwapBuffers();
   162     }
   163 #endif
   164 
   165     /* * * * * Framebuffering* * * * */
   166     virtual void DirectConnected(direct_buffer_info *info) {
   167         if(!_connected && _connection_disabled) {
   168             return;
   169         }
   170 
   171         /* Determine if the pixel buffer is usable after this update */
   172         _trash_window_buffer =      _trash_window_buffer
   173                                 || ((info->buffer_state & B_BUFFER_RESIZED)
   174                                 || (info->buffer_state & B_BUFFER_RESET)
   175                                 || (info->driver_state == B_MODE_CHANGED));
   176         LockBuffer();
   177 
   178         switch(info->buffer_state & B_DIRECT_MODE_MASK) {
   179         case B_DIRECT_START:
   180             _connected = true;
   181 
   182         case B_DIRECT_MODIFY:
   183             if (info->clip_list_count > _num_clips)
   184             {
   185                 if(_clips) {
   186                     free(_clips);
   187                     _clips = NULL;
   188                 }
   189             }
   190 
   191             _num_clips = info->clip_list_count;
   192             if (_clips == NULL)
   193                 _clips = (clipping_rect *)malloc(_num_clips*sizeof(clipping_rect));
   194             if(_clips) {
   195                 memcpy(_clips, info->clip_list,
   196                     _num_clips*sizeof(clipping_rect));
   197 
   198                 _bits = (uint8*) info->bits;
   199                 _row_bytes = info->bytes_per_row;
   200                 _bounds = info->window_bounds;
   201                 _bytes_per_px = info->bits_per_pixel / 8;
   202                 _buffer_dirty = true;
   203             }
   204             break;
   205 
   206         case B_DIRECT_STOP:
   207             _connected = false;
   208             break;
   209         }
   210 #if SDL_VIDEO_OPENGL
   211         if(_SDL_GLView) {
   212             _SDL_GLView->DirectConnected(info);
   213         }
   214 #endif
   215 
   216 
   217         /* Call the base object directconnected */
   218         BDirectWindow::DirectConnected(info);
   219 
   220         UnlockBuffer();
   221 
   222     }
   223 
   224 
   225 
   226 
   227     /* * * * * Event sending * * * * */
   228     /* Hook functions */
   229     virtual void FrameMoved(BPoint origin) {
   230         /* Post a message to the BApp so that it can handle the window event */
   231         BMessage msg(BAPP_WINDOW_MOVED);
   232         msg.AddInt32("window-x", (int)origin.x);
   233         msg.AddInt32("window-y", (int)origin.y);
   234         _PostWindowEvent(msg);
   235 
   236         /* Perform normal hook operations */
   237         BDirectWindow::FrameMoved(origin);
   238     }
   239 
   240     virtual void FrameResized(float width, float height) {
   241         /* Post a message to the BApp so that it can handle the window event */
   242         BMessage msg(BAPP_WINDOW_RESIZED);
   243 
   244         msg.AddInt32("window-w", (int)width + 1);
   245         msg.AddInt32("window-h", (int)height + 1);
   246         _PostWindowEvent(msg);
   247 
   248         /* Perform normal hook operations */
   249         BDirectWindow::FrameResized(width, height);
   250     }
   251 
   252     virtual bool QuitRequested() {
   253         BMessage msg(BAPP_WINDOW_CLOSE_REQUESTED);
   254         _PostWindowEvent(msg);
   255 
   256         /* We won't allow a quit unless asked by DestroyWindow() */
   257         return false;
   258     }
   259 
   260     virtual void WindowActivated(bool active) {
   261         BMessage msg(BAPP_KEYBOARD_FOCUS);  /* Mouse focus sold separately */
   262         msg.AddBool("focusGained", active);
   263         _PostWindowEvent(msg);
   264     }
   265 
   266     virtual void Zoom(BPoint origin,
   267                 float width,
   268                 float height) {
   269         BMessage msg(BAPP_MAXIMIZE);    /* Closest thing to maximization Haiku has */
   270         _PostWindowEvent(msg);
   271 
   272         /* Before the window zooms, record its size */
   273         if( !_prev_frame )
   274             _prev_frame = new BRect(Frame());
   275 
   276         /* Perform normal hook operations */
   277         BDirectWindow::Zoom(origin, width, height);
   278     }
   279 
   280     /* Member functions */
   281     virtual void Show() {
   282         while(IsHidden()) {
   283             BDirectWindow::Show();
   284         }
   285         _shown = true;
   286 
   287         BMessage msg(BAPP_SHOW);
   288         _PostWindowEvent(msg);
   289     }
   290 
   291     virtual void Hide() {
   292         BDirectWindow::Hide();
   293         _shown = false;
   294 
   295         BMessage msg(BAPP_HIDE);
   296         _PostWindowEvent(msg);
   297     }
   298 
   299     virtual void Minimize(bool minimize) {
   300         BDirectWindow::Minimize(minimize);
   301         int32 minState = (minimize ? BAPP_MINIMIZE : BAPP_RESTORE);
   302 
   303         BMessage msg(minState);
   304         _PostWindowEvent(msg);
   305     }
   306 
   307 
   308     /* BView message interruption */
   309     virtual void DispatchMessage(BMessage * msg, BHandler * target)
   310     {
   311         BPoint where;   /* Used by mouse moved */
   312         int32 buttons;  /* Used for mouse button events */
   313         int32 key;      /* Used for key events */
   314 
   315         switch (msg->what) {
   316         case B_MOUSE_MOVED:
   317             int32 transit;
   318             if (msg->FindPoint("where", &where) == B_OK
   319                 && msg->FindInt32("be:transit", &transit) == B_OK) {
   320                 _MouseMotionEvent(where, transit);
   321             }
   322             break;
   323 
   324         case B_MOUSE_DOWN:
   325             if (msg->FindInt32("buttons", &buttons) == B_OK) {
   326                 _MouseButtonEvent(buttons, SDL_PRESSED);
   327             }
   328             break;
   329 
   330         case B_MOUSE_UP:
   331             if (msg->FindInt32("buttons", &buttons) == B_OK) {
   332                 _MouseButtonEvent(buttons, SDL_RELEASED);
   333             }
   334             break;
   335 
   336         case B_MOUSE_WHEEL_CHANGED:
   337             float x, y;
   338             if (msg->FindFloat("be:wheel_delta_x", &x) == B_OK
   339                 && msg->FindFloat("be:wheel_delta_y", &y) == B_OK) {
   340                     _MouseWheelEvent((int)x, (int)y);
   341             }
   342             break;
   343 
   344         case B_KEY_DOWN:
   345             {
   346                 int32 i = 0;
   347                 int8 byte;
   348                 int8 bytes[4] = { 0, 0, 0, 0 };
   349                 while (i < 4 && msg->FindInt8("byte", i, &byte) == B_OK) {
   350                     bytes[i] = byte;
   351                     i++;
   352                 }
   353                 if (msg->FindInt32("key", &key) == B_OK) {
   354                     _KeyEvent((SDL_Scancode)key, &bytes[0], i, SDL_PRESSED);
   355                 }
   356             }
   357             break;
   358             
   359         case B_UNMAPPED_KEY_DOWN:      /* modifier keys are unmapped */
   360             if (msg->FindInt32("key", &key) == B_OK) {
   361                 _KeyEvent((SDL_Scancode)key, NULL, 0, SDL_PRESSED);
   362             }
   363             break;
   364 
   365         case B_KEY_UP:
   366         case B_UNMAPPED_KEY_UP:        /* modifier keys are unmapped */
   367             if (msg->FindInt32("key", &key) == B_OK) {
   368                 _KeyEvent(key, NULL, 0, SDL_RELEASED);
   369             }
   370             break;
   371 
   372         default:
   373             /* move it after switch{} so it's always handled
   374                that way we keep Haiku features like:
   375                - CTRL+Q to close window (and other shortcuts)
   376                - PrintScreen to make screenshot into /boot/home
   377                - etc.. */
   378             /* BDirectWindow::DispatchMessage(msg, target); */
   379             break;
   380         }
   381 
   382         BDirectWindow::DispatchMessage(msg, target);
   383     }
   384 
   385     /* Handle command messages */
   386     virtual void MessageReceived(BMessage* message) {
   387         switch (message->what) {
   388             /* Handle commands from SDL */
   389             case BWIN_SET_TITLE:
   390                 _SetTitle(message);
   391                 break;
   392             case BWIN_MOVE_WINDOW:
   393                 _MoveTo(message);
   394                 break;
   395             case BWIN_RESIZE_WINDOW:
   396                 _ResizeTo(message);
   397                 break;
   398             case BWIN_SET_BORDERED:
   399                 _SetBordered(message);
   400                 break;
   401             case BWIN_SET_RESIZABLE:
   402                 _SetResizable(message);
   403                 break;
   404             case BWIN_SHOW_WINDOW:
   405                 Show();
   406                 break;
   407             case BWIN_HIDE_WINDOW:
   408                 Hide();
   409                 break;
   410             case BWIN_MAXIMIZE_WINDOW:
   411                 BWindow::Zoom();
   412                 break;
   413             case BWIN_MINIMIZE_WINDOW:
   414                 Minimize(true);
   415                 break;
   416             case BWIN_RESTORE_WINDOW:
   417                 _Restore();
   418                 break;
   419             case BWIN_FULLSCREEN:
   420                 _SetFullScreen(message);
   421                 break;
   422             default:
   423                 /* Perform normal message handling */
   424                 BDirectWindow::MessageReceived(message);
   425                 break;
   426         }
   427 
   428     }
   429 
   430 
   431 
   432     /* Accessor methods */
   433     bool IsShown() { return _shown; }
   434     int32 GetID() { return _id; }
   435     uint32 GetRowBytes() { return _row_bytes; }
   436     int32 GetFbX() { return _bounds.left; }
   437     int32 GetFbY() { return _bounds.top; }
   438     bool ConnectionEnabled() { return !_connection_disabled; }
   439     bool Connected() { return _connected; }
   440     clipping_rect *GetClips() { return _clips; }
   441     int32 GetNumClips() { return _num_clips; }
   442     uint8* GetBufferPx() { return _bits; }
   443     int32 GetBytesPerPx() { return _bytes_per_px; }
   444     bool CanTrashWindowBuffer() { return _trash_window_buffer; }
   445     bool BufferExists() { return _buffer_created; }
   446     bool BufferIsDirty() { return _buffer_dirty; }
   447     BBitmap *GetBitmap() { return _bitmap; }
   448 #if SDL_VIDEO_OPENGL
   449     BGLView *GetGLView() { return _SDL_GLView; }
   450     Uint32 GetGLType() { return _gl_type; }
   451 #endif
   452 
   453     /* Setter methods */
   454     void SetID(int32 id) { _id = id; }
   455     void SetBufferExists(bool bufferExists) { _buffer_created = bufferExists; }
   456     void LockBuffer() { _buffer_locker->Lock(); }
   457     void UnlockBuffer() { _buffer_locker->Unlock(); }
   458     void SetBufferDirty(bool bufferDirty) { _buffer_dirty = bufferDirty; }
   459     void SetTrashBuffer(bool trash) { _trash_window_buffer = trash;     }
   460     void SetBitmap(BBitmap *bitmap) { _bitmap = bitmap; }
   461 
   462 
   463 private:
   464     /* Event redirection */
   465     void _MouseMotionEvent(BPoint &where, int32 transit) {
   466         if(transit == B_EXITED_VIEW) {
   467             /* Change mouse focus */
   468             if(_mouse_focused) {
   469                 _MouseFocusEvent(false);
   470             }
   471         } else {
   472             /* Change mouse focus */
   473             if (!_mouse_focused) {
   474                 _MouseFocusEvent(true);
   475             }
   476             BMessage msg(BAPP_MOUSE_MOVED);
   477             msg.AddInt32("x", (int)where.x);
   478             msg.AddInt32("y", (int)where.y);
   479 
   480             _PostWindowEvent(msg);
   481         }
   482     }
   483 
   484     void _MouseFocusEvent(bool focusGained) {
   485         _mouse_focused = focusGained;
   486         BMessage msg(BAPP_MOUSE_FOCUS);
   487         msg.AddBool("focusGained", focusGained);
   488         _PostWindowEvent(msg);
   489 
   490 /* FIXME: Why were these here?
   491  if false: be_app->SetCursor(B_HAND_CURSOR);
   492  if true:  SDL_SetCursor(NULL); */
   493     }
   494 
   495     void _MouseButtonEvent(int32 buttons, Uint8 state) {
   496         int32 buttonStateChange = buttons ^ _last_buttons;
   497 
   498         if(buttonStateChange & B_PRIMARY_MOUSE_BUTTON) {
   499             _SendMouseButton(SDL_BUTTON_LEFT, state);
   500         }
   501         if(buttonStateChange & B_SECONDARY_MOUSE_BUTTON) {
   502             _SendMouseButton(SDL_BUTTON_RIGHT, state);
   503         }
   504         if(buttonStateChange & B_TERTIARY_MOUSE_BUTTON) {
   505             _SendMouseButton(SDL_BUTTON_MIDDLE, state);
   506         }
   507 
   508         _last_buttons = buttons;
   509     }
   510 
   511     void _SendMouseButton(int32 button, int32 state) {
   512         BMessage msg(BAPP_MOUSE_BUTTON);
   513         msg.AddInt32("button-id", button);
   514         msg.AddInt32("button-state", state);
   515         _PostWindowEvent(msg);
   516     }
   517 
   518     void _MouseWheelEvent(int32 x, int32 y) {
   519         /* Create a message to pass along to the BeApp thread */
   520         BMessage msg(BAPP_MOUSE_WHEEL);
   521         msg.AddInt32("xticks", x);
   522         msg.AddInt32("yticks", y);
   523         _PostWindowEvent(msg);
   524     }
   525 
   526     void _KeyEvent(int32 keyCode, const int8 *keyUtf8, const ssize_t & len, int32 keyState) {
   527         /* Create a message to pass along to the BeApp thread */
   528         BMessage msg(BAPP_KEY);
   529         msg.AddInt32("key-state", keyState);
   530         msg.AddInt32("key-scancode", keyCode);
   531         if (keyUtf8 != NULL) {
   532             msg.AddData("key-utf8", B_INT8_TYPE, (const void*)keyUtf8, len);
   533         }
   534         be_app->PostMessage(&msg);
   535     }
   536 
   537     void _RepaintEvent() {
   538         /* Force a repaint: Call the SDL exposed event */
   539         BMessage msg(BAPP_REPAINT);
   540         _PostWindowEvent(msg);
   541     }
   542     void _PostWindowEvent(BMessage &msg) {
   543         msg.AddInt32("window-id", _id);
   544         be_app->PostMessage(&msg);
   545     }
   546 
   547     /* Command methods (functions called upon by SDL) */
   548     void _SetTitle(BMessage *msg) {
   549         const char *title;
   550         if(
   551             msg->FindString("window-title", &title) != B_OK
   552         ) {
   553             return;
   554         }
   555         SetTitle(title);
   556     }
   557 
   558     void _MoveTo(BMessage *msg) {
   559         int32 x, y;
   560         if(
   561             msg->FindInt32("window-x", &x) != B_OK ||
   562             msg->FindInt32("window-y", &y) != B_OK
   563         ) {
   564             return;
   565         }
   566         MoveTo(x, y);
   567     }
   568 
   569     void _ResizeTo(BMessage *msg) {
   570         int32 w, h;
   571         if(
   572             msg->FindInt32("window-w", &w) != B_OK ||
   573             msg->FindInt32("window-h", &h) != B_OK
   574         ) {
   575             return;
   576         }
   577         ResizeTo(w, h);
   578     }
   579 
   580     void _SetBordered(BMessage *msg) {
   581         bool bEnabled;
   582         if(msg->FindBool("window-border", &bEnabled) != B_OK) {
   583             return;
   584         }
   585         SetLook(bEnabled ? B_TITLED_WINDOW_LOOK : B_NO_BORDER_WINDOW_LOOK);
   586     }
   587 
   588     void _SetResizable(BMessage *msg) {
   589         bool bEnabled;
   590         if(msg->FindBool("window-resizable", &bEnabled) != B_OK) {
   591             return;
   592         }
   593         if (bEnabled) {
   594             SetFlags(Flags() & ~(B_NOT_RESIZABLE | B_NOT_ZOOMABLE));
   595         } else {
   596             SetFlags(Flags() | (B_NOT_RESIZABLE | B_NOT_ZOOMABLE));
   597         }
   598     }
   599 
   600     void _Restore() {
   601         if(IsMinimized()) {
   602             Minimize(false);
   603         } else if(IsHidden()) {
   604             Show();
   605         } else if(_prev_frame != NULL) {    /* Zoomed */
   606             MoveTo(_prev_frame->left, _prev_frame->top);
   607             ResizeTo(_prev_frame->Width(), _prev_frame->Height());
   608         }
   609     }
   610 
   611     void _SetFullScreen(BMessage *msg) {
   612         bool fullscreen;
   613         if(
   614             msg->FindBool("fullscreen", &fullscreen) != B_OK
   615         ) {
   616             return;
   617         }
   618         SetFullScreen(fullscreen);
   619     }
   620 
   621     /* Members */
   622 #if SDL_VIDEO_OPENGL
   623     BGLView * _SDL_GLView;
   624     Uint32 _gl_type;
   625 #endif
   626 
   627     int32 _last_buttons;
   628     int32 _id;  /* Window id used by SDL_BApp */
   629     bool  _mouse_focused;       /* Does this window have mouse focus? */
   630     bool  _shown;
   631     bool  _inhibit_resize;
   632 
   633     BRect *_prev_frame; /* Previous position and size of the window */
   634 
   635     /* Framebuffer members */
   636     bool            _connected,
   637                     _connection_disabled,
   638                     _buffer_created,
   639                     _buffer_dirty,
   640                     _trash_window_buffer;
   641     uint8          *_bits;
   642     uint32          _row_bytes;
   643     clipping_rect   _bounds;
   644     BLocker        *_buffer_locker;
   645     clipping_rect  *_clips;
   646     uint32          _num_clips;
   647     int32           _bytes_per_px;
   648     thread_id       _draw_thread_id;
   649 
   650     BBitmap        *_bitmap;
   651 };
   652 
   653 
   654 /* FIXME:
   655  * An explanation of framebuffer flags.
   656  *
   657  * _connected -           Original variable used to let the drawing thread know
   658  *                         when changes are being made to the other framebuffer
   659  *                         members.
   660  * _connection_disabled - Used to signal to the drawing thread that the window
   661  *                         is closing, and the thread should exit.
   662  * _buffer_created -      True if the current buffer is valid
   663  * _buffer_dirty -        True if the window should be redrawn.
   664  * _trash_window_buffer - True if the window buffer needs to be trashed partway
   665  *                         through a draw cycle.  Occurs when the previous
   666  *                         buffer provided by DirectConnected() is invalidated.
   667  */
   668 #endif /* SDL_BWin_h_ */
   669 
   670 /* vi: set ts=4 sw=4 expandtab: */