Subversion Repositories Games.Descent

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 pmbaty 1
/*
2
 * Portions of this file are copyright Rebirth contributors and licensed as
3
 * described in COPYING.txt.
4
 * Portions of this file are copyright Parallax Software and licensed
5
 * according to the Parallax license below.
6
 * See COPYING.txt for license details.
7
 
8
THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
9
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
10
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
11
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
12
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
13
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
14
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
15
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
16
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
17
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
18
*/
19
 
20
/*
21
 *
22
 * Routines for menus.
23
 *
24
 */
25
 
26
#include <stdio.h>
27
#include <cstdlib>
28
#include <string.h>
29
#include <stdarg.h>
30
#include <ctype.h>
31
#include <functional>
32
 
33
#include "pstypes.h"
34
#include "dxxerror.h"
35
#include "gr.h"
36
#include "grdef.h"
37
#include "window.h"
38
#include "songs.h"
39
#include "key.h"
40
#include "mouse.h"
41
#include "palette.h"
42
#include "game.h"
43
#include "text.h"
44
#include "menu.h"
45
#include "newmenu.h"
46
#include "gamefont.h"
47
#include "iff.h"
48
#include "pcx.h"
49
#include "u_mem.h"
50
#include "mouse.h"
51
#include "joy.h"
52
#include "digi.h"
53
#include "multi.h"
54
#include "endlevel.h"
55
#include "screens.h"
56
#include "config.h"
57
#include "player.h"
58
#include "state.h"
59
#include "newdemo.h"
60
#include "kconfig.h"
61
#include "strutil.h"
62
#include "vers_id.h"
63
#include "timer.h"
64
#include "playsave.h"
65
#include "automap.h"
66
#include "rbaudio.h"
67
#include "args.h"
68
#if defined(DXX_BUILD_DESCENT_II)
69
#include "args.h"
70
#include "gamepal.h"
71
#endif
72
 
73
#if DXX_USE_OGL
74
#include "ogl_init.h"
75
#endif
76
 
77
#include "compiler-range_for.h"
78
#include "partial_range.h"
79
 
80
#define MAXDISPLAYABLEITEMS 14
81
#define MAXDISPLAYABLEITEMSTINY 21
82
#define MESSAGEBOX_TEXT_SIZE 2176  // How many characters in messagebox
83
#define MAX_TEXT_WIDTH FSPACX(120) // How many pixels wide a input box can be
84
 
85
struct newmenu : embed_window_pointer_t
86
{
87
        int                             x,y,w,h;
88
        short                   swidth, sheight;
89
        // with these we check if resolution or fonts have changed so menu structure can be recreated
90
        font_x_scale_proportion fntscalex;
91
        font_y_scale_proportion fntscaley;
92
        const char                      *title;
93
        const char                      *subtitle;
94
        unsigned                nitems;
95
        int                             citem;
96
        newmenu_item    *items;
97
        int                             (*subfunction)(newmenu *menu,const d_event &event, void *userdata);
98
        const char                      *filename;
99
        int                             tiny_mode;
100
        int                     tabs_flag;
101
        int                             scroll_offset, max_displayable;
102
        int                             all_text;               //set true if all text items
103
        int                             is_scroll_box;   // Is this a scrolling box? Set to false at init
104
        int                             max_on_menu;
105
        int                             mouse_state, dblclick_flag;
106
        int                             *rval;                  // Pointer to return value (for polling newmenus)
107
        void                    *userdata;              // For whatever - like with window system
108
        partial_range_t<newmenu_item *> item_range()
109
        {
110
                return unchecked_partial_range(items, nitems);
111
        }
112
};
113
 
114
constexpr std::integral_constant<unsigned, NM_TYPE_INPUT> newmenu_item::input_specific_type::nm_type;
115
constexpr std::integral_constant<unsigned, NM_TYPE_RADIO> newmenu_item::radio_specific_type::nm_type;
116
constexpr std::integral_constant<unsigned, NM_TYPE_NUMBER> newmenu_item::number_specific_type::nm_type;
117
constexpr std::integral_constant<unsigned, NM_TYPE_INPUT_MENU> newmenu_item::imenu_specific_type::nm_type;
118
constexpr std::integral_constant<unsigned, NM_TYPE_SLIDER> newmenu_item::slider_specific_type::nm_type;
119
 
120
static grs_main_bitmap nm_background, nm_background1;
121
static grs_subbitmap_ptr nm_background_sub;
122
 
123
void newmenu_free_background()  {
124
        if (nm_background.bm_data)
125
        {
126
                nm_background_sub.reset();
127
        }
128
        nm_background.reset();
129
        nm_background1.reset();
130
}
131
 
132
namespace dsx {
133
 
134
#if defined(DXX_BUILD_DESCENT_I)
135
static const char *UP_ARROW_MARKER(const grs_font &, const grs_font &)
136
{
137
        return "+";  // 135
138
}
139
 
140
static const char *DOWN_ARROW_MARKER(const grs_font &, const grs_font &)
141
{
142
        return "+";  // 136
143
}
144
#elif defined(DXX_BUILD_DESCENT_II)
145
static const char *UP_ARROW_MARKER(const grs_font &cv_font, const grs_font &game_font)
146
{
147
        return &cv_font == &game_font ? "\202" : "\207";  // 135
148
}
149
 
150
static const char *DOWN_ARROW_MARKER(const grs_font &cv_font, const grs_font &game_font)
151
{
152
        return &cv_font == &game_font ? "\200" : "\210";  // 136
153
}
154
#endif
155
 
156
// Draws the custom menu background pcx, if available
157
static void nm_draw_background1(grs_canvas &canvas, const char * filename)
158
{
159
        if (filename != NULL)
160
        {
161
                if (nm_background1.bm_data == NULL)
162
                {
163
                        const auto pcx_error = pcx_read_bitmap(filename, nm_background1, gr_palette);
164
                        if (pcx_error != pcx_result::SUCCESS)
165
                                return;
166
                }
167
                gr_palette_load( gr_palette );
168
                show_fullscr(canvas, nm_background1);
169
        }
170
#if defined(DXX_BUILD_DESCENT_II)
171
        strcpy(last_palette_loaded,"");         //force palette load next time
172
#endif
173
}
174
}
175
 
176
#define MENU_BACKGROUND_BITMAP_HIRES (PHYSFSX_exists("scoresb.pcx",1)?"scoresb.pcx":"scores.pcx")
177
#define MENU_BACKGROUND_BITMAP_LORES (PHYSFSX_exists("scores.pcx",1)?"scores.pcx":"scoresb.pcx")
178
#define MENU_BACKGROUND_BITMAP (HIRESMODE?MENU_BACKGROUND_BITMAP_HIRES:MENU_BACKGROUND_BITMAP_LORES)
179
 
180
// Draws the frame background for menus
181
void nm_draw_background(grs_canvas &canvas, int x1, int y1, int x2, int y2)
182
{
183
        int w,h,init_sub=0;
184
        static float BGScaleX=1,BGScaleY=1;
185
        if (nm_background.bm_data == NULL)
186
        {
187
                palette_array_t background_palette;
188
                const auto pcx_error = pcx_read_bitmap(MENU_BACKGROUND_BITMAP, nm_background,background_palette);
189
                if (pcx_error != pcx_result::SUCCESS)
190
                        return;
191
                gr_remap_bitmap_good(nm_background, background_palette, -1, -1);
192
                BGScaleX=(static_cast<float>(SWIDTH)/nm_background.bm_w);
193
                BGScaleY=(static_cast<float>(SHEIGHT)/nm_background.bm_h);
194
                init_sub=1;
195
        }
196
 
197
        if ( x1 < 0 ) x1 = 0;
198
        if ( y1 < 0 ) y1 = 0;
199
        if ( x2 > SWIDTH - 1) x2 = SWIDTH - 1;
200
        if ( y2 > SHEIGHT - 1) y2 = SHEIGHT - 1;
201
 
202
        w = x2-x1;
203
        h = y2-y1;
204
 
205
        if (w > SWIDTH) w = SWIDTH;
206
        if (h > SHEIGHT) h = SHEIGHT;
207
 
208
        gr_palette_load( gr_palette );
209
        {
210
                const auto &&tmp = gr_create_sub_canvas(canvas, x1, y1, w, h);
211
                show_fullscr(*tmp, nm_background); // show so we load all necessary data for the sub-bitmap
212
        if (!init_sub && ((nm_background_sub->bm_w != w*((static_cast<float>(nm_background.bm_w))/SWIDTH)) || (nm_background_sub->bm_h != h*((static_cast<float>(nm_background.bm_h))/SHEIGHT))))
213
        {
214
                init_sub=1;
215
        }
216
        if (init_sub)
217
                nm_background_sub = gr_create_sub_bitmap(nm_background,0,0,w*((static_cast<float>(nm_background.bm_w))/SWIDTH),h*((static_cast<float>(nm_background.bm_h))/SHEIGHT));
218
        show_fullscr(*tmp, *nm_background_sub.get());
219
        }
220
 
221
        gr_settransblend(canvas, 14, gr_blend::normal);
222
        {
223
                const auto color = BM_XRGB(1, 1, 1);
224
        for (w=5*BGScaleX;w>0;w--)
225
                        gr_urect(canvas, x2 - w, y1 + w * (BGScaleY / BGScaleX), x2 - w, y2 - w * (BGScaleY / BGScaleX), color);//right edge
226
        }
227
        {
228
                const auto color = BM_XRGB(0, 0, 0);
229
        for (h=5*BGScaleY;h>0;h--)
230
                        gr_urect(canvas, x1 + h * (BGScaleX / BGScaleY), y2 - h, x2 - h * (BGScaleX / BGScaleY), y2 - h, color);//bottom edge
231
        }
232
        gr_settransblend(canvas, GR_FADE_OFF, gr_blend::normal);
233
}
234
 
235
// Draw a left justfied string
236
static void nm_string(grs_canvas &canvas, const int w1, int x, const int y, const char *const s, const int tabs_flag)
237
{
238
        if (!tabs_flag)
239
        {
240
                const char *s1 = s;
241
                const char *p = nullptr;
242
                RAIIdmem<char[]> s2;
243
                if (w1 > 0 && (p = strchr(s, '\t')))
244
                {
245
                        s2.reset(d_strdup(s));
246
                        s1 = s2.get();
247
                        *std::next(s2.get(), std::distance(s, p)) = '\0';
248
                }
249
                gr_string(canvas, *canvas.cv_font, x, y, s1);
250
                if (p)
251
                {
252
                        int w, h;
253
                        ++ p;
254
                        gr_get_string_size(*canvas.cv_font, p, &w, &h, nullptr);
255
                        gr_string(canvas, *canvas.cv_font, x + w1 - w, y, p, w, h);
256
                }
257
                return;
258
        }
259
        std::array<int, 6> XTabs = {{18, 90, 127, 165, 231, 256}};
260
        const auto &&fspacx = FSPACX();
261
        range_for (auto &i, XTabs)
262
        {
263
                i = fspacx(i) + x;
264
        }
265
        unsigned t = 0;
266
        char measure[2];
267
        measure[1] = 0;
268
        for (unsigned i = 0; const char c = s[i]; ++i)
269
        {
270
                if (c == '\t')
271
                {
272
                        x=XTabs[t];
273
                        t++;
274
                        continue;
275
                }
276
                measure[0] = c;
277
                int tx, th;
278
                gr_get_string_size(*canvas.cv_font, measure, &tx, &th, nullptr);
279
                gr_string(canvas, *canvas.cv_font, x, y, measure, tx, th);
280
                x+=tx;
281
        }
282
}
283
 
284
// Draw a slider and it's string
285
static void nm_string_slider(grs_canvas &canvas, const int w1, const int x, const int y, char *const s)
286
{
287
        char *p,*s1;
288
 
289
        s1=NULL;
290
 
291
        p = strchr( s, '\t' );
292
        if (p)  {
293
                *p = '\0';
294
                s1 = p+1;
295
        }
296
 
297
        gr_string(canvas, *canvas.cv_font, x, y, s);
298
 
299
        if (p)  {
300
                int w, h;
301
                gr_get_string_size(*canvas.cv_font, s1, &w, &h, nullptr);
302
                gr_string(canvas, *canvas.cv_font, x + w1 - w, y, s1, w, h);
303
 
304
                *p = '\t';
305
        }
306
}
307
 
308
 
309
// Draw a left justfied string with black background.
310
static void nm_string_black(grs_canvas &canvas, int w1, const int x, const int y, const char *const s)
311
{
312
        int w,h;
313
        gr_get_string_size(*canvas.cv_font, s, &w, &h, nullptr);
314
 
315
        if (w1 == 0) w1 = w;
316
 
317
        const auto &&fspacx = FSPACX();
318
        const auto &&fspacy = FSPACY();
319
        {
320
                const uint8_t color = BM_XRGB(5, 5, 5);
321
                gr_rect(canvas, x - fspacx(2), y - fspacy(1), x + w1, y + h, color);
322
        }
323
        {
324
                const uint8_t color = BM_XRGB(2, 2, 2);
325
                gr_rect(canvas, x - fspacx(2), y - fspacy(1), x, y + h, color);
326
        }
327
        {
328
                const uint8_t color = BM_XRGB(0, 0, 0);
329
                gr_rect(canvas, x - fspacx(1), y - fspacy(1), x + w1 - fspacx(1), y + h, color);
330
        }
331
        gr_string(canvas, *canvas.cv_font, x, y, s, w, h);
332
}
333
 
334
 
335
// Draw a right justfied string
336
static void nm_rstring(grs_canvas &canvas, int w1, int x, const int y, const char *const s)
337
{
338
        int w, h;
339
        gr_get_string_size(*canvas.cv_font, s, &w, &h, nullptr);
340
        x -= FSPACX(3);
341
 
342
        if (w1 == 0) w1 = w;
343
        gr_string(canvas, *canvas.cv_font, x - w, y, s, w, h);
344
}
345
 
346
static void nm_string_inputbox(grs_canvas &canvas, const int w, const int x, const int y, const char *text, const int current)
347
{
348
        int w1;
349
 
350
        // even with variable char widths and a box that goes over the whole screen, we maybe never get more than 75 chars on the line
351
        if (strlen(text)>75)
352
                text+=strlen(text)-75;
353
        while( *text )  {
354
                gr_get_string_size(*canvas.cv_font, text, &w1, nullptr, nullptr);
355
                if ( w1 > w-FSPACX(10) )
356
                        text++;
357
                else
358
                        break;
359
        }
360
        if ( *text == 0 )
361
                w1 = 0;
362
 
363
        nm_string_black(canvas, w, x, y, text);
364
 
365
        if ( current && timer_query() & 0x8000 )
366
                gr_string(canvas, *canvas.cv_font, x + w1, y, CURSOR_STRING);
367
}
368
 
369
static void draw_item(grs_canvas &canvas, newmenu_item *item, int is_current, int tiny, int tabs_flag, int scroll_offset)
370
{
371
        if (tiny)
372
        {
373
                int r, g, b;
374
                if (item->text[0]=='\t')
375
                        r = g = b = 63;
376
                else if (is_current)
377
                        r = 57, g = 49, b = 20;
378
                else
379
                        r = g = 29, b = 47;
380
                gr_set_fontcolor(canvas, gr_find_closest_color_current(r, g, b), -1);
381
        }
382
        else
383
        {
384
                gr_set_curfont(canvas, is_current?MEDIUM2_FONT:MEDIUM1_FONT);
385
        }
386
 
387
        const int line_spacing = static_cast<int>(LINE_SPACING(*canvas.cv_font, *GAME_FONT));
388
        switch( item->type )    {
389
                case NM_TYPE_SLIDER:
390
                {
391
                        int i;
392
                        auto &slider = item->slider();
393
                        if (item->value < slider.min_value)
394
                                item->value = slider.min_value;
395
                        if (item->value > slider.max_value)
396
                                item->value = slider.max_value;
397
                        i = snprintf(item->saved_text.data(), item->saved_text.size(), "%s\t%s", item->text, SLIDER_LEFT);
398
                        for (uint_fast32_t j = (slider.max_value - slider.min_value + 1); j--;)
399
                        {
400
                                i += snprintf(item->saved_text.data() + i, item->saved_text.size() - i, "%s", SLIDER_MIDDLE);
401
                        }
402
                        i += snprintf(item->saved_text.data() + i, item->saved_text.size() - i, "%s", SLIDER_RIGHT);
403
                        item->saved_text[item->value+1+strlen(item->text)+1] = SLIDER_MARKER[0];
404
                        nm_string_slider(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->saved_text.data());
405
                }
406
                        break;
407
                case NM_TYPE_INPUT_MENU:
408
                        if (item->imenu().group == 0)
409
                        {
410
                        case NM_TYPE_TEXT:
411
                        case NM_TYPE_MENU:
412
                                nm_string(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->text, tabs_flag);
413
                                break;
414
                        }
415
                        DXX_BOOST_FALLTHROUGH;
416
                case NM_TYPE_INPUT:
417
                        nm_string_inputbox(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->text, is_current);
418
                        break;
419
                case NM_TYPE_CHECK:
420
                        nm_string(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->text, tabs_flag);
421
                        nm_rstring(canvas, item->right_offset, item->x, item->y - (line_spacing * scroll_offset), item->value ? CHECKED_CHECK_BOX : NORMAL_CHECK_BOX);
422
                        break;
423
                case NM_TYPE_RADIO:
424
                        nm_string(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->text, tabs_flag);
425
                        nm_rstring(canvas, item->right_offset, item->x, item->y - (line_spacing * scroll_offset), item->value ? CHECKED_RADIO_BOX : NORMAL_RADIO_BOX);
426
                        break;
427
                case NM_TYPE_NUMBER:
428
                {
429
                        char text[sizeof("-2147483647")];
430
                        auto &number = item->number();
431
                        if (item->value < number.min_value)
432
                                item->value = number.min_value;
433
                        if (item->value > number.max_value)
434
                                item->value = number.max_value;
435
                        nm_string(canvas, item->w, item->x, item->y - (line_spacing * scroll_offset), item->text, tabs_flag);
436
                        snprintf(text, sizeof(text), "%d", item->value );
437
                        nm_rstring(canvas, item->right_offset, item->x, item->y - (line_spacing * scroll_offset), text);
438
                }
439
                        break;
440
        }
441
}
442
 
443
const char *Newmenu_allowed_chars=NULL;
444
 
445
//returns true if char is allowed
446
static bool char_disallowed(char c)
447
{
448
        const char *p = Newmenu_allowed_chars;
449
        if (!p)
450
                return false;
451
        for (uint8_t a, b; (a = p[0]) && (b = p[1]); p += 2)
452
        {
453
                if (likely(c >= a && c <= b))
454
                        return false;
455
        }
456
        return true;
457
}
458
 
459
static bool char_allowed(char c)
460
{
461
        return !char_disallowed(c);
462
}
463
 
464
static void strip_end_whitespace( char * text )
465
{
466
        char *ns = text;
467
        for (char c; (c = *text);)
468
        {
469
                ++ text;
470
                if (!isspace(static_cast<unsigned>(c)))
471
                        ns = text;
472
        }
473
        *ns = 0;
474
}
475
 
476
int newmenu_do2(const char *const title, const char *const subtitle, const uint_fast32_t nitems, newmenu_item *const item, const newmenu_subfunction subfunction, void *const userdata, const int citem, const char *const filename)
477
{
478
        newmenu *menu;
479
        bool exists = true;
480
        int rval = -1;
481
 
482
        menu = newmenu_do3( title, subtitle, nitems, item, subfunction, userdata, citem, filename );
483
 
484
        if (!menu)
485
                return -1;
486
        menu->rval = &rval;
487
 
488
        // Track to see when the window is freed
489
        // Doing this way in case another window is opened on top without its own polling loop
490
        menu->wind->track(&exists);
491
 
492
        // newmenu_do2 and simpler get their own event loop
493
        // This is so the caller doesn't have to provide a callback that responds to EVENT_NEWMENU_SELECTED
494
        while (exists)
495
                event_process();
496
 
497
        return rval;
498
}
499
 
500
static void swap_menu_item_entries(newmenu_item &a, newmenu_item &b)
501
{
502
        using std::swap;
503
        swap(a.text, b.text);
504
        swap(a.value, b.value);
505
}
506
 
507
template <typename T>
508
static void move_menu_item_entry(T &&t, newmenu_item *const items, uint_fast32_t citem, uint_fast32_t boundary)
509
{
510
        if (citem == boundary)
511
                return;
512
        auto a = &items[citem];
513
        auto selected = std::make_pair(a->text, a->value);
514
        for (; citem != boundary;)
515
        {
516
                citem = t(citem, 1);
517
                auto &b = items[citem];
518
                a->text = b.text;
519
                a->value = b.value;
520
                a = &b;
521
        }
522
        a->text = selected.first;
523
        a->value = selected.second;
524
}
525
 
526
static int newmenu_save_selection_key(newmenu *menu, const d_event &event)
527
{
528
        auto k = event_key_get(event);
529
        switch(k)
530
        {
531
                case KEY_SHIFTED+KEY_UP:
532
                        if (menu->citem > 0)
533
                        {
534
                                auto &a = menu->items[menu->citem];
535
                                auto &b = menu->items[-- menu->citem];
536
                                swap_menu_item_entries(a, b);
537
                        }
538
                        break;
539
                case KEY_SHIFTED+KEY_DOWN:
540
                        if (menu->citem < (menu->nitems - 1))
541
                        {
542
                                auto &a = menu->items[menu->citem];
543
                                auto &b = menu->items[++ menu->citem];
544
                                swap_menu_item_entries(a, b);
545
                        }
546
                        break;
547
                case KEY_PAGEUP + KEY_SHIFTED:
548
                        move_menu_item_entry(std::minus<uint_fast32_t>(), menu->items, menu->citem, 0);
549
                        break;
550
                case KEY_PAGEDOWN + KEY_SHIFTED:
551
                        move_menu_item_entry(std::plus<uint_fast32_t>(), menu->items, menu->citem, menu->nitems - 1);
552
                        break;
553
        }
554
        return 0;
555
}
556
 
557
static int newmenu_save_selection_handler(newmenu *menu, const d_event &event, const unused_newmenu_userdata_t *)
558
{
559
        switch(event.type)
560
        {
561
                case EVENT_KEY_COMMAND:
562
                        return newmenu_save_selection_key(menu, event);
563
                default:
564
                        break;
565
        }
566
        return 0;
567
}
568
 
569
// Basically the same as do2 but sets reorderitems flag for weapon priority menu a bit redundant to get lose of a global variable but oh well...
570
void newmenu_doreorder( const char * title, const char * subtitle, uint_fast32_t nitems, newmenu_item * item)
571
{
572
        newmenu_do2(title, subtitle, nitems, item, newmenu_save_selection_handler, unused_newmenu_userdata, 0, nullptr);
573
}
574
 
575
newmenu_item *newmenu_get_items(newmenu *menu)
576
{
577
        return menu->items;
578
}
579
 
580
int newmenu_get_nitems(newmenu *menu)
581
{
582
        return menu->nitems;
583
}
584
 
585
int newmenu_get_citem(newmenu *menu)
586
{
587
        return menu->citem;
588
}
589
 
590
namespace {
591
 
592
struct step_down
593
{
594
        template <typename T>
595
                T operator()(T &t) const
596
                {
597
                        return ++t;
598
                }
599
};
600
 
601
struct step_up
602
{
603
        template <typename T>
604
                T operator()(T &t) const
605
                {
606
                        return --t;
607
                }
608
};
609
 
610
}
611
 
612
template <typename S, typename O>
613
static void update_menu_position(newmenu &menu, newmenu_item *const stop, int_fast32_t amount, S step, O overflow)
614
{
615
        auto icitem = menu.citem;
616
        auto pcitem = &menu.items[icitem];
617
        do // count until we reached a non NM_TYPE_TEXT item and reached our amount
618
        {
619
                if (pcitem == stop)
620
                        break;
621
                step(icitem);
622
                step(pcitem);
623
                if (menu.is_scroll_box) // update scroll_offset as we go
624
                {
625
                        if (overflow(icitem))
626
                                step(menu.scroll_offset);
627
                }
628
        } while (-- amount > 0 || pcitem->type == NM_TYPE_TEXT);
629
        menu.citem = icitem;
630
}
631
 
632
static void newmenu_scroll(newmenu *const menu, const int amount)
633
{
634
        if (amount == 0) // nothing to do for us
635
                return;
636
 
637
        if (menu->all_text)
638
        {
639
                menu->scroll_offset += amount;
640
                if (menu->scroll_offset < 0)
641
                        menu->scroll_offset = 0;
642
                if (menu->max_on_menu+menu->scroll_offset > menu->nitems)
643
                        menu->scroll_offset = menu->nitems-menu->max_on_menu;
644
                return;
645
        }
646
        const auto &range = menu->item_range();
647
        const auto predicate = [](const newmenu_item &n) {
648
                return n.type != NM_TYPE_TEXT;
649
        };
650
        const auto first = std::find_if(range.begin(), range.end(), predicate);
651
        if (first == range.end())
652
                return;
653
        const auto rlast = std::find_if(range.rbegin(), std::reverse_iterator<newmenu_item *>(first), predicate).base();
654
        /* `first == rlast` should not happen, since that would mean that
655
         * there are no elements in `range` for which `predicate` is true.
656
         * If there are no such elements, then `first == range.end()` should
657
         * be true and the function would have returned above.
658
         */
659
        assert(first != rlast);
660
        if (first == rlast) // nothing to do for us
661
                return;
662
        const auto last = std::prev(rlast);
663
        /* Exactly one element that satisfies `predicate` exists in `range`.
664
         * Only elements that satisfy `predicate` can be selected, so the
665
         * selection cannot be changed.
666
         */
667
        if (first == last)
668
                return;
669
        auto citem = &menu->items[menu->citem];
670
        if (citem == last && amount == 1) // if citem == last item and we want to go down one step, go to first item
671
        {
672
                newmenu_scroll(menu, -menu->nitems);
673
                return;
674
        }
675
        if (citem == first && amount == -1) // if citem == first item and we want to go up one step, go to last item
676
        {
677
                newmenu_scroll(menu, menu->nitems);
678
                return;
679
        }
680
 
681
        if (amount > 0) // down the list
682
        {
683
                const auto overflow = [menu](int icitem) {
684
                        return icitem + 4 >= menu->max_on_menu + menu->scroll_offset && menu->scroll_offset < menu->nitems - menu->max_on_menu;
685
                };
686
                update_menu_position(*menu, last, amount, step_down(), overflow);
687
        }
688
        else if (amount < 0) // up the list
689
        {
690
                const auto overflow = [menu](int icitem) {
691
                        return icitem - 4 < menu->scroll_offset && menu->scroll_offset > 0;
692
                };
693
                update_menu_position(*menu, first, std::abs(amount), step_up(), overflow);
694
        }
695
}
696
 
697
static int nm_trigger_radio_button(newmenu &menu, newmenu_item &citem)
698
{
699
        citem.value = 1;
700
        const auto cg = citem.radio().group;
701
        range_for (auto &r, menu.item_range())
702
        {
703
                if (&r != &citem && r.type == NM_TYPE_RADIO && r.radio().group == cg)
704
                {
705
                        if (r.value)
706
                        {
707
                                r.value = 0;
708
                                return 1;
709
                        }
710
                }
711
        }
712
        return 0;
713
}
714
 
715
static window_event_result newmenu_mouse(window *wind,const d_event &event, newmenu *menu, int button)
716
{
717
        int old_choice, mx=0, my=0, mz=0, x1 = 0, x2, y1, y2, changed = 0;
718
        grs_canvas &menu_canvas = window_get_canvas(*wind);
719
        grs_canvas &save_canvas = *grd_curcanv;
720
 
721
        switch (button)
722
        {
723
                case MBTN_LEFT:
724
                {
725
                        gr_set_current_canvas(menu_canvas);
726
                        auto &canvas = *grd_curcanv;
727
 
728
                        old_choice = menu->citem;
729
 
730
                        const auto &&fspacx = FSPACX();
731
                        if ((event.type == EVENT_MOUSE_BUTTON_DOWN) && !menu->all_text)
732
                        {
733
                                mouse_get_pos(&mx, &my, &mz);
734
                                const int line_spacing = static_cast<int>(LINE_SPACING(*grd_curcanv->cv_font, *GAME_FONT));
735
                                for (int i = menu->scroll_offset; i < menu->max_on_menu + menu->scroll_offset; ++i)
736
                                {
737
                                        x1 = canvas.cv_bitmap.bm_x + menu->items[i].x - fspacx(13) /*- menu->items[i].right_offset - 6*/;
738
                                        x2 = x1 + menu->items[i].w + fspacx(13);
739
                                        y1 = canvas.cv_bitmap.bm_y + menu->items[i].y - (line_spacing * menu->scroll_offset);
740
                                        y2 = y1 + menu->items[i].h;
741
                                        if (((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2))) {
742
                                                menu->citem = i;
743
                                                auto &citem = menu->items[menu->citem];
744
                                                switch (citem.type)
745
                                                {
746
                                                        case NM_TYPE_CHECK:
747
                                                                citem.value = !citem.value;
748
                                                                changed = 1;
749
                                                                break;
750
                                                        case NM_TYPE_RADIO:
751
                                                                changed = nm_trigger_radio_button(*menu, citem);
752
                                                                break;
753
                                                        case NM_TYPE_TEXT:
754
                                                                menu->citem=old_choice;
755
                                                                menu->mouse_state=0;
756
                                                                break;
757
                                                        case NM_TYPE_MENU:
758
                                                        case NM_TYPE_INPUT:
759
                                                        case NM_TYPE_NUMBER:
760
                                                        case NM_TYPE_INPUT_MENU:
761
                                                        case NM_TYPE_SLIDER:
762
                                                                break;
763
                                                }
764
                                                break;
765
                                        }
766
                                }
767
                        }
768
 
769
                        if ( menu->mouse_state ) {
770
                                mouse_get_pos(&mx, &my, &mz);
771
 
772
                                // check possible scrollbar stuff first
773
                                const int line_spacing = static_cast<int>(LINE_SPACING(*grd_curcanv->cv_font, *GAME_FONT));
774
                                if (menu->is_scroll_box) {
775
                                        int ScrollAllow=0;
776
                                        static fix64 ScrollTime=0;
777
                                        if (ScrollTime + F1_0/5 < timer_query())
778
                                        {
779
                                                ScrollTime = timer_query();
780
                                                ScrollAllow = 1;
781
                                        }
782
 
783
                                        if (menu->scroll_offset != 0) {
784
                                                int arrow_width, arrow_height;
785
                                                gr_get_string_size(*canvas.cv_font, UP_ARROW_MARKER(*canvas.cv_font, *GAME_FONT), &arrow_width, &arrow_height, nullptr);
786
                                                x1 = canvas.cv_bitmap.bm_x + BORDERX - fspacx(12);
787
                                                y1 = canvas.cv_bitmap.bm_y + menu->items[menu->scroll_offset].y - (line_spacing * menu->scroll_offset);
788
                                                x2 = x1 + arrow_width;
789
                                                y2 = y1 + arrow_height;
790
                                                if (((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2)) && ScrollAllow) {
791
                                                        newmenu_scroll(menu, -1);
792
                                                }
793
                                        }
794
                                        if (menu->scroll_offset+menu->max_displayable<menu->nitems) {
795
                                                int arrow_width, arrow_height;
796
                                                gr_get_string_size(*canvas.cv_font, DOWN_ARROW_MARKER(*canvas.cv_font, *GAME_FONT), &arrow_width, &arrow_height, nullptr);
797
                                                x1 = canvas.cv_bitmap.bm_x + BORDERX - fspacx(12);
798
                                                y1 = canvas.cv_bitmap.bm_y + menu->items[menu->scroll_offset + menu->max_displayable - 1].y - (line_spacing * menu->scroll_offset);
799
                                                x2 = x1 + arrow_width;
800
                                                y2 = y1 + arrow_height;
801
                                                if (((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2)) && ScrollAllow) {
802
                                                        newmenu_scroll(menu, 1);
803
                                                }
804
                                        }
805
                                }
806
 
807
                                for (int i = menu->scroll_offset; i < menu->max_on_menu + menu->scroll_offset; ++i)
808
                                {
809
                                        x1 = canvas.cv_bitmap.bm_x + menu->items[i].x - fspacx(13);
810
                                        x2 = x1 + menu->items[i].w + fspacx(13);
811
                                        y1 = canvas.cv_bitmap.bm_y + menu->items[i].y - (line_spacing * menu->scroll_offset);
812
                                        y2 = y1 + menu->items[i].h;
813
 
814
                                        if (((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2)) && (menu->items[i].type != NM_TYPE_TEXT) ) {
815
                                                menu->citem = i;
816
                                                auto &citem = menu->items[menu->citem];
817
                                                if (citem.type == NM_TYPE_SLIDER)
818
                                                {
819
                                                        char slider_text[NM_MAX_TEXT_LEN+1], *p, *s1;
820
 
821
                                                        strcpy(slider_text, citem.saved_text);
822
                                                        p = strchr(slider_text, '\t');
823
                                                        if (p) {
824
                                                                *p = '\0';
825
                                                                s1 = p+1;
826
                                                        }
827
                                                        if (p) {
828
                                                                int slider_width, sleft_width, sright_width, smiddle_width;
829
                                                                gr_get_string_size(*canvas.cv_font, s1, &slider_width, nullptr, nullptr);
830
                                                                gr_get_string_size(*canvas.cv_font, SLIDER_LEFT, &sleft_width, nullptr, nullptr);
831
                                                                gr_get_string_size(*canvas.cv_font, SLIDER_RIGHT, &sright_width, nullptr, nullptr);
832
                                                                gr_get_string_size(*canvas.cv_font, SLIDER_MIDDLE, &smiddle_width, nullptr, nullptr);
833
 
834
                                                                x1 = canvas.cv_bitmap.bm_x + citem.x + citem.w - slider_width;
835
                                                                x2 = x1 + slider_width + sright_width;
836
                                                                int new_value;
837
                                                                auto &slider = citem.slider();
838
                                                                if (
839
                                                                        (mx > x1 && mx < x1 + sleft_width && (new_value = slider.min_value, true)) ||
840
                                                                        (mx < x2 && mx > x2 - sright_width && (new_value = slider.max_value, true)) ||
841
                                                                        (mx > x1 + sleft_width && mx < x2 - sright_width - sleft_width && (new_value = (mx - x1 - sleft_width) / ((slider_width - sleft_width - sright_width) / (slider.max_value - slider.min_value + 1)), true))
842
                                                                )
843
                                                                        if (citem.value != new_value)
844
                                                                        {
845
                                                                                citem.value = new_value;
846
                                                                                changed = 1;
847
                                                                        }
848
                                                                *p = '\t';
849
                                                        }
850
                                                }
851
                                                if (menu->citem == old_choice)
852
                                                        break;
853
                                                if (citem.type == NM_TYPE_INPUT && menu->citem != old_choice)
854
                                                        citem.value = -1;
855
                                                if ((old_choice>-1) && (menu->items[old_choice].type==NM_TYPE_INPUT_MENU) && (old_choice!=menu->citem)) {
856
                                                        menu->items[old_choice].imenu().group = 0;
857
                                                        strcpy(menu->items[old_choice].text, menu->items[old_choice].saved_text );
858
                                                        menu->items[old_choice].value = -1;
859
                                                }
860
                                                break;
861
                                        }
862
                                }
863
                        }
864
 
865
                        if ((event.type == EVENT_MOUSE_BUTTON_UP) && !menu->all_text && (menu->citem != -1) && (menu->items[menu->citem].type == NM_TYPE_MENU) )
866
                        {
867
                                mouse_get_pos(&mx, &my, &mz);
868
                                const int line_spacing = static_cast<int>(LINE_SPACING(*grd_curcanv->cv_font, *GAME_FONT));
869
                                for (int i = menu->scroll_offset; i < menu->max_on_menu + menu->scroll_offset; ++i)
870
                                {
871
                                        x1 = canvas.cv_bitmap.bm_x + menu->items[i].x - fspacx(13);
872
                                        x2 = x1 + menu->items[i].w + fspacx(13);
873
                                        y1 = canvas.cv_bitmap.bm_y + menu->items[i].y - (line_spacing * menu->scroll_offset);
874
                                        y2 = y1 + menu->items[i].h;
875
                                        if (((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2))) {
876
                                                        // Tell callback, allow staying in menu
877
                                                        const d_select_event selected{menu->citem};
878
                                                        if (menu->subfunction && (*menu->subfunction)(menu, selected, menu->userdata))
879
                                                                return window_event_result::handled;
880
 
881
                                                        if (menu->rval)
882
                                                                *menu->rval = menu->citem;
883
                                                        gr_set_current_canvas(save_canvas);
884
                                                        return window_event_result::close;
885
                                        }
886
                                }
887
                        }
888
 
889
                        if (event.type == EVENT_MOUSE_BUTTON_UP && menu->citem > -1)
890
                        {
891
                                auto &citem = menu->items[menu->citem];
892
                                if (citem.type == NM_TYPE_INPUT_MENU && citem.imenu().group == 0)
893
                                {
894
                                        citem.imenu().group = 1;
895
                                        if (!d_stricmp(citem.saved_text, TXT_EMPTY))
896
                                        {
897
                                                citem.text[0] = 0;
898
                                                citem.value = -1;
899
                                        } else {
900
                                                strip_end_whitespace(citem.text);
901
                                        }
902
                                }
903
                        }
904
 
905
                        gr_set_current_canvas(save_canvas);
906
 
907
                        if (changed && menu->subfunction)
908
                        {
909
                                (*menu->subfunction)(menu, d_change_event{menu->citem}, menu->userdata);
910
                        }
911
                        break;
912
                }
913
                case MBTN_RIGHT:
914
                        if (menu->mouse_state)
915
                        {
916
                                if (!(menu->citem > -1))
917
                                        return window_event_result::close;
918
                                auto &citem = menu->items[menu->citem];
919
                                if (citem.type == NM_TYPE_INPUT_MENU && citem.imenu().group == 1)
920
                                {
921
                                        citem.imenu().group = 0;
922
                                        strcpy(citem.text, citem.saved_text);
923
                                        citem.value = -1;
924
                                } else {
925
                                        return window_event_result::close;
926
                                }
927
                        }
928
                        break;
929
                case MBTN_Z_UP:
930
                        if (menu->mouse_state)
931
                                newmenu_scroll(menu, -1);
932
                        break;
933
                case MBTN_Z_DOWN:
934
                        if (menu->mouse_state)
935
                                newmenu_scroll(menu, 1);
936
                        break;
937
        }
938
 
939
        return window_event_result::ignored;
940
}
941
 
942
static window_event_result newmenu_key_command(window *, const d_event &event, newmenu *menu)
943
{
944
        int k = event_key_get(event);
945
        int old_choice;
946
        int changed = 0;
947
        window_event_result rval = window_event_result::handled;
948
 
949
        if (keyd_pressed[KEY_NUMLOCK])
950
        {
951
                switch( k )
952
                {
953
                        case KEY_PAD0: k = KEY_0;  break;
954
                        case KEY_PAD1: k = KEY_1;  break;
955
                        case KEY_PAD2: k = KEY_2;  break;
956
                        case KEY_PAD3: k = KEY_3;  break;
957
                        case KEY_PAD4: k = KEY_4;  break;
958
                        case KEY_PAD5: k = KEY_5;  break;
959
                        case KEY_PAD6: k = KEY_6;  break;
960
                        case KEY_PAD7: k = KEY_7;  break;
961
                        case KEY_PAD8: k = KEY_8;  break;
962
                        case KEY_PAD9: k = KEY_9;  break;
963
                        case KEY_PADPERIOD: k = KEY_PERIOD; break;
964
                }
965
        }
966
 
967
        old_choice = menu->citem;
968
        auto &citem = menu->items[menu->citem];
969
 
970
        switch( k )     {
971
                case KEY_HOME:
972
                case KEY_PAD7:
973
                        newmenu_scroll(menu, -menu->nitems);
974
                        break;
975
                case KEY_END:
976
                case KEY_PAD1:
977
                        newmenu_scroll(menu, menu->nitems);
978
                        break;
979
                case KEY_TAB + KEY_SHIFTED:
980
                case KEY_UP:
981
                case KEY_PAGEUP:
982
                case KEY_PAD8:
983
                        newmenu_scroll(menu, k == KEY_PAGEUP ? -10 : -1);
984
                        if (citem.type == NM_TYPE_INPUT && menu->citem != old_choice)
985
                                citem.value = -1;
986
                        if ((old_choice>-1) && (menu->items[old_choice].type==NM_TYPE_INPUT_MENU) && (old_choice!=menu->citem)) {
987
                                menu->items[old_choice].imenu().group=0;
988
                                strcpy(menu->items[old_choice].text, menu->items[old_choice].saved_text );
989
                                menu->items[old_choice].value = -1;
990
                        }
991
                        break;
992
                case KEY_TAB:
993
                case KEY_DOWN:
994
                case KEY_PAGEDOWN:
995
                case KEY_PAD2:
996
                        newmenu_scroll(menu, k == KEY_PAGEDOWN ? 10 : 1);
997
                        if (citem.type == NM_TYPE_INPUT && menu->citem != old_choice)
998
                                citem.value = -1;
999
                        if ( (old_choice>-1) && (menu->items[old_choice].type==NM_TYPE_INPUT_MENU) && (old_choice!=menu->citem))        {
1000
                                menu->items[old_choice].imenu().group=0;
1001
                                strcpy(menu->items[old_choice].text, menu->items[old_choice].saved_text );
1002
                                menu->items[old_choice].value = -1;
1003
                        }
1004
                        break;
1005
                case KEY_SPACEBAR:
1006
                        if ( menu->citem > -1 ) {
1007
                                switch (citem.type)
1008
                                {
1009
                                        case NM_TYPE_TEXT:
1010
                                        case NM_TYPE_NUMBER:
1011
                                        case NM_TYPE_SLIDER:
1012
                                        case NM_TYPE_MENU:
1013
                                        case NM_TYPE_INPUT:
1014
                                        case NM_TYPE_INPUT_MENU:
1015
                                                break;
1016
                                        case NM_TYPE_CHECK:
1017
                                                citem.value = !citem.value;
1018
                                                changed = 1;
1019
                                                break;
1020
                                        case NM_TYPE_RADIO:
1021
                                                changed = nm_trigger_radio_button(*menu, citem);
1022
                                                break;
1023
                                }
1024
                        }
1025
                        break;
1026
 
1027
                case KEY_ENTER:
1028
                case KEY_PADENTER:
1029
                        if (menu->citem > -1 && citem.type == NM_TYPE_INPUT_MENU && citem.imenu().group == 0)
1030
                        {
1031
                                citem.imenu().group = 1;
1032
                                if (!d_stricmp(citem.saved_text, TXT_EMPTY))
1033
                                {
1034
                                        citem.text[0] = 0;
1035
                                        citem.value = -1;
1036
                                } else {
1037
                                        strip_end_whitespace(citem.text);
1038
                                }
1039
                        } else
1040
                        {
1041
                                if (citem.type == NM_TYPE_INPUT_MENU)
1042
                                        citem.imenu().group = 0;        // go out of editing mode
1043
 
1044
                                // Tell callback, allow staying in menu
1045
                                const d_select_event selected{menu->citem};
1046
                                if (menu->subfunction && (*menu->subfunction)(menu, selected, menu->userdata))
1047
                                        return window_event_result::handled;
1048
 
1049
                                if (menu->rval)
1050
                                        *menu->rval = menu->citem;
1051
                                return window_event_result::close;
1052
                        }
1053
                        break;
1054
 
1055
                case KEY_ESC:
1056
                        if (menu->citem > -1 && citem.type == NM_TYPE_INPUT_MENU && citem.imenu().group == 1)
1057
                        {
1058
                                citem.imenu().group = 0;
1059
                                strcpy(citem.text, citem.saved_text);
1060
                                citem.value = -1;
1061
                        } else {
1062
                                return window_event_result::close;
1063
                        }
1064
                        break;
1065
                default:
1066
                        rval = window_event_result::ignored;
1067
                        break;
1068
        }
1069
 
1070
        if ( menu->citem > -1 ) {
1071
                // Alerting callback of every keypress for NM_TYPE_INPUT. Alternatively, just respond to EVENT_NEWMENU_SELECTED
1072
                if ((citem.type == NM_TYPE_INPUT || (citem.type == NM_TYPE_INPUT_MENU && citem.imenu().group == 1)) && (old_choice == menu->citem) )    {
1073
                        if ( k==KEY_LEFT || k==KEY_BACKSP || k==KEY_PAD4 )      {
1074
                                if (citem.value == -1) citem.value = strlen(citem.text);
1075
                                if (citem.value > 0)
1076
                                        citem.value--;
1077
                                citem.text[citem.value] = 0;
1078
 
1079
                                if (citem.type == NM_TYPE_INPUT)
1080
                                        changed = 1;
1081
                                rval = window_event_result::handled;
1082
                        }
1083
                        else if (citem.value < citem.input_or_menu()->text_len)
1084
                        {
1085
                                auto ascii = key_ascii();
1086
                                if (ascii < 255)
1087
                                {
1088
                                        if (citem.value == -1) {
1089
                                                citem.value = 0;
1090
                                        }
1091
                                        if (char_allowed(ascii) || (ascii == ' ' && char_allowed(ascii = '_')))
1092
                                        {
1093
                                                citem.text[citem.value++] = ascii;
1094
                                                citem.text[citem.value] = 0;
1095
 
1096
                                                if (citem.type == NM_TYPE_INPUT)
1097
                                                        changed = 1;
1098
                                        }
1099
                                }
1100
                        }
1101
                }
1102
                else if ((citem.type != NM_TYPE_INPUT) && (citem.type != NM_TYPE_INPUT_MENU) )
1103
                {
1104
                        auto ascii = key_ascii();
1105
                        if (ascii < 255 ) {
1106
                                int choice1 = menu->citem;
1107
                                ascii = toupper(ascii);
1108
                                do {
1109
                                        int i,ch;
1110
                                        choice1++;
1111
                                        if (choice1 >= menu->nitems )
1112
                                                choice1=0;
1113
 
1114
                                        for (i=0;(ch=menu->items[choice1].text[i])!=0 && ch==' ';i++);
1115
 
1116
                                        if ( ( (menu->items[choice1].type==NM_TYPE_MENU) ||
1117
                                                  (menu->items[choice1].type==NM_TYPE_CHECK) ||
1118
                                                  (menu->items[choice1].type==NM_TYPE_RADIO) ||
1119
                                                  (menu->items[choice1].type==NM_TYPE_NUMBER) ||
1120
                                                  (menu->items[choice1].type==NM_TYPE_SLIDER) )
1121
                                                && (ascii==toupper(ch)) )
1122
                                        {
1123
                                                k = 0;
1124
                                                menu->citem = choice1;
1125
                                        }
1126
 
1127
                                        while (menu->citem+4>=menu->max_on_menu+menu->scroll_offset && menu->scroll_offset < menu->nitems-menu->max_on_menu)
1128
                                                menu->scroll_offset++;
1129
                                        while (menu->citem-4<menu->scroll_offset && menu->scroll_offset > 0)
1130
                                                menu->scroll_offset--;
1131
 
1132
                                } while (choice1 != menu->citem );
1133
                        }
1134
                }
1135
 
1136
                if (const auto ns = citem.number_or_slider())
1137
                {
1138
                        switch( k ) {
1139
                                case KEY_LEFT:
1140
                                case KEY_PAD4:
1141
                                        citem.value -= 1;
1142
                                        changed = 1;
1143
                                        rval = window_event_result::handled;
1144
                                        break;
1145
                                case KEY_RIGHT:
1146
                                case KEY_PAD6:
1147
                                        citem.value++;
1148
                                        changed = 1;
1149
                                        rval = window_event_result::handled;
1150
                                        break;
1151
                                case KEY_SPACEBAR:
1152
                                        citem.value += 10;
1153
                                        changed = 1;
1154
                                        rval = window_event_result::handled;
1155
                                        break;
1156
                                case KEY_BACKSP:
1157
                                        citem.value -= 10;
1158
                                        changed = 1;
1159
                                        rval = window_event_result::handled;
1160
                                        break;
1161
                        }
1162
 
1163
                        auto &min_value = ns->min_value;
1164
                        if (citem.value < min_value)
1165
                                citem.value = min_value;
1166
                        auto &max_value = ns->max_value;
1167
                        if (citem.value > max_value)
1168
                                citem.value = max_value;
1169
                }
1170
 
1171
        }
1172
 
1173
        if (changed && menu->subfunction)
1174
        {
1175
                (*menu->subfunction)(menu, d_change_event{menu->citem}, menu->userdata);
1176
        }
1177
 
1178
        return rval;
1179
}
1180
 
1181
namespace dsx {
1182
static void newmenu_create_structure( newmenu *menu )
1183
{
1184
        int aw, tw, th, twidth,right_offset;
1185
        int nmenus, nothers;
1186
        grs_canvas &save_canvas = *grd_curcanv;
1187
        gr_set_default_canvas();
1188
        auto &canvas = *grd_curcanv;
1189
 
1190
        tw = th = 0;
1191
 
1192
        if ( menu->title )      {
1193
                int string_width, string_height;
1194
                auto &huge_font = *HUGE_FONT;
1195
                gr_get_string_size(huge_font, menu->title, &string_width, &string_height, nullptr);
1196
                tw = string_width;
1197
                th = string_height;
1198
        }
1199
        if ( menu->subtitle )   {
1200
                int string_width, string_height;
1201
                auto &medium3_font = *MEDIUM3_FONT;
1202
                gr_get_string_size(medium3_font, menu->subtitle, &string_width, &string_height, nullptr);
1203
                if (string_width > tw )
1204
                        tw = string_width;
1205
                th += string_height;
1206
        }
1207
 
1208
        th += FSPACY(5);                //put some space between titles & body
1209
 
1210
        auto &cv_font = *(menu->tiny_mode ? GAME_FONT : MEDIUM1_FONT).get();
1211
 
1212
        menu->w = aw = 0;
1213
        menu->h = th;
1214
        nmenus = nothers = 0;
1215
 
1216
        const auto &&fspacx = FSPACX();
1217
        const auto &&fspacy = FSPACY();
1218
        // Find menu height & width (store in w,h)
1219
        range_for (auto &i, menu->item_range())
1220
        {
1221
                i.y = menu->h;
1222
                int string_width, string_height, average_width;
1223
                gr_get_string_size(cv_font, i.text, &string_width, &string_height, &average_width);
1224
                i.right_offset = 0;
1225
 
1226
                i.saved_text[0] = '\0';
1227
 
1228
                if (i.type == NM_TYPE_SLIDER)
1229
                {
1230
                        int index,w1;
1231
                        nothers++;
1232
                        index = snprintf (i.saved_text.data(), i.saved_text.size(), "%s", SLIDER_LEFT);
1233
                        auto &slider = i.slider();
1234
                        for (uint_fast32_t j = (slider.max_value - slider.min_value + 1); j--;)
1235
                        {
1236
                                index += snprintf(i.saved_text.data() + index, i.saved_text.size() - index, "%s", SLIDER_MIDDLE);
1237
                        }
1238
                        index += snprintf(i.saved_text.data() + index, i.saved_text.size() - index, "%s", SLIDER_RIGHT);
1239
                        gr_get_string_size(cv_font, i.saved_text.data(), &w1, nullptr, nullptr);
1240
                        string_width += w1 + aw;
1241
                }
1242
 
1243
                if (i.type == NM_TYPE_MENU)
1244
                {
1245
                        nmenus++;
1246
                }
1247
 
1248
                if (i.type == NM_TYPE_CHECK)
1249
                {
1250
                        int w1;
1251
                        nothers++;
1252
                        gr_get_string_size(cv_font, NORMAL_CHECK_BOX, &w1, nullptr, nullptr);
1253
                        i.right_offset = w1;
1254
                        gr_get_string_size(cv_font, CHECKED_CHECK_BOX, &w1, nullptr, nullptr);
1255
                        if (w1 > i.right_offset)
1256
                                i.right_offset = w1;
1257
                }
1258
 
1259
                if (i.type == NM_TYPE_RADIO)
1260
                {
1261
                        int w1;
1262
                        nothers++;
1263
                        gr_get_string_size(cv_font, NORMAL_RADIO_BOX, &w1, nullptr, nullptr);
1264
                        i.right_offset = w1;
1265
                        gr_get_string_size(cv_font, CHECKED_RADIO_BOX, &w1, nullptr, nullptr);
1266
                        if (w1 > i.right_offset)
1267
                                i.right_offset = w1;
1268
                }
1269
 
1270
                if (i.type == NM_TYPE_NUMBER)
1271
                {
1272
                        int w1;
1273
                        char test_text[20];
1274
                        nothers++;
1275
                        auto &number = i.number();
1276
                        snprintf(test_text, sizeof(test_text), "%d", number.max_value);
1277
                        gr_get_string_size(cv_font, test_text, &w1, nullptr, nullptr);
1278
                        i.right_offset = w1;
1279
                        snprintf(test_text, sizeof(test_text), "%d", number.min_value);
1280
                        gr_get_string_size(cv_font, test_text, &w1, nullptr, nullptr);
1281
                        if (w1 > i.right_offset)
1282
                                i.right_offset = w1;
1283
                }
1284
 
1285
                if (const auto input_or_menu = i.input_or_menu())
1286
                {
1287
                        i.saved_text.copy_if(i.text);
1288
                        const auto text_len = input_or_menu->text_len;
1289
                        string_width = text_len * fspacx(8) + text_len;
1290
                        if (i.type == NM_TYPE_INPUT && string_width > MAX_TEXT_WIDTH)
1291
                                string_width = MAX_TEXT_WIDTH;
1292
 
1293
                        i.value = -1;
1294
                        if (i.type == NM_TYPE_INPUT_MENU)
1295
                        {
1296
                                i.imenu().group = 0;
1297
                                nmenus++;
1298
                        }
1299
                        else
1300
                        {
1301
                                nothers++;
1302
                        }
1303
                }
1304
 
1305
                i.w = string_width;
1306
                i.h = string_height;
1307
 
1308
                if ( string_width > menu->w )
1309
                        menu->w = string_width;         // Save maximum width
1310
                if ( average_width > aw )
1311
                        aw = average_width;
1312
                menu->h += string_height + fspacy(1);           // Find the height of all strings
1313
        }
1314
 
1315
        if (menu->nitems > menu->max_on_menu)
1316
        {
1317
                menu->is_scroll_box=1;
1318
                menu->h = th + (LINE_SPACING(cv_font, *GAME_FONT) * menu->max_on_menu);
1319
                menu->max_displayable=menu->max_on_menu;
1320
 
1321
                // if our last citem was > menu->max_on_menu, make sure we re-scroll when we call this menu again
1322
                if (menu->citem > menu->max_on_menu-4)
1323
                {
1324
                        menu->scroll_offset = menu->citem - (menu->max_on_menu-4);
1325
                        if (menu->scroll_offset + menu->max_on_menu > menu->nitems)
1326
                                menu->scroll_offset = menu->nitems - menu->max_on_menu;
1327
                }
1328
        }
1329
        else
1330
        {
1331
                menu->is_scroll_box=0;
1332
                menu->max_on_menu = menu->nitems;
1333
        }
1334
 
1335
        right_offset=0;
1336
 
1337
        range_for (auto &i, menu->item_range())
1338
        {
1339
                i.w = menu->w;
1340
                if (right_offset < i.right_offset)
1341
                        right_offset = i.right_offset;
1342
        }
1343
 
1344
        menu->w += right_offset;
1345
 
1346
        twidth = 0;
1347
        if ( tw > menu->w )     {
1348
                twidth = ( tw - menu->w )/2;
1349
                menu->w = tw;
1350
        }
1351
 
1352
        // Find min point of menu border
1353
        menu->w += BORDERX*2;
1354
        menu->h += BORDERY*2;
1355
 
1356
        menu->x = (canvas.cv_bitmap.bm_w - menu->w) / 2;
1357
        menu->y = (canvas.cv_bitmap.bm_h - menu->h) / 2;
1358
 
1359
        if ( menu->x < 0 ) menu->x = 0;
1360
        if ( menu->y < 0 ) menu->y = 0;
1361
 
1362
        nm_draw_background1(canvas, menu->filename);
1363
 
1364
        // Update all item's x & y values.
1365
        range_for (auto &i, menu->item_range())
1366
        {
1367
                i.x = BORDERX + twidth + right_offset;
1368
                i.y += BORDERY;
1369
                if (i.type == NM_TYPE_RADIO) {
1370
                        // find first marked one
1371
                        newmenu_item *fm = nullptr;
1372
                        range_for (auto &j, menu->item_range())
1373
                        {
1374
                                if (j.type == NM_TYPE_RADIO && j.radio().group == i.radio().group) {
1375
                                        if (!fm && j.value)
1376
                                                fm = &j;
1377
                                        j.value = 0;
1378
                                }
1379
                        }
1380
                        (fm ? *fm : i).value = 1;
1381
                }
1382
        }
1383
 
1384
        if (menu->citem != -1)
1385
        {
1386
                if (menu->citem < 0 ) menu->citem = 0;
1387
                if (menu->citem > menu->nitems-1 ) menu->citem = menu->nitems-1;
1388
 
1389
                menu->dblclick_flag = 1;
1390
                uint_fast32_t i = 0;
1391
                while ( menu->items[menu->citem].type==NM_TYPE_TEXT )   {
1392
                        menu->citem++;
1393
                        i++;
1394
                        if (menu->citem >= menu->nitems ) {
1395
                                menu->citem=0;
1396
                        }
1397
                        if (i > menu->nitems ) {
1398
                                menu->citem=0;
1399
                                menu->all_text=1;
1400
                                break;
1401
                        }
1402
                }
1403
        }
1404
 
1405
        menu->mouse_state = 0;
1406
        menu->swidth = SWIDTH;
1407
        menu->sheight = SHEIGHT;
1408
        menu->fntscalex = FNTScaleX;
1409
        menu->fntscaley = FNTScaleY;
1410
        gr_set_current_canvas(save_canvas);
1411
}
1412
 
1413
static window_event_result newmenu_draw(window *wind, newmenu *menu)
1414
{
1415
        grs_canvas &menu_canvas = window_get_canvas(*wind);
1416
        grs_canvas &save_canvas = *grd_curcanv;
1417
        int th = 0, ty, sx, sy;
1418
        int i;
1419
 
1420
        if (menu->swidth != SWIDTH || menu->sheight != SHEIGHT || menu->fntscalex != FNTScaleX || menu->fntscaley != FNTScaleY)
1421
        {
1422
                newmenu_create_structure ( menu );
1423
                {
1424
                        gr_init_sub_canvas(menu_canvas, grd_curscreen->sc_canvas, menu->x, menu->y, menu->w, menu->h);
1425
                }
1426
        }
1427
 
1428
        gr_set_default_canvas();
1429
        nm_draw_background1(*grd_curcanv, menu->filename);
1430
        if (menu->filename == NULL)
1431
        {
1432
                const auto mx = menu->x;
1433
                const auto my = menu->y;
1434
                auto ex = mx;
1435
                if (menu->is_scroll_box)
1436
                        ex -= FSPACX(5);
1437
                nm_draw_background(*grd_curcanv, ex, my, mx + menu->w, my + menu->h);
1438
        }
1439
 
1440
        gr_set_current_canvas( menu_canvas );
1441
 
1442
        ty = BORDERY;
1443
 
1444
        if ( menu->title )      {
1445
                gr_set_fontcolor(*grd_curcanv, BM_XRGB(31, 31, 31), -1);
1446
                int string_width, string_height;
1447
                auto &huge_font = *HUGE_FONT;
1448
                gr_get_string_size(huge_font, menu->title, &string_width, &string_height, nullptr);
1449
                th = string_height;
1450
                gr_string(*grd_curcanv, huge_font, 0x8000, ty, menu->title, string_width, string_height);
1451
        }
1452
 
1453
        if ( menu->subtitle )   {
1454
                gr_set_fontcolor(*grd_curcanv, BM_XRGB(21, 21, 21), -1);
1455
                auto &medium3_font = *MEDIUM3_FONT;
1456
                gr_string(*grd_curcanv, medium3_font, 0x8000, ty + th, menu->subtitle);
1457
        }
1458
 
1459
        gr_set_curfont(*grd_curcanv, menu->tiny_mode?GAME_FONT:MEDIUM1_FONT);
1460
 
1461
        // Redraw everything...
1462
        for (i=menu->scroll_offset; i<menu->max_displayable+menu->scroll_offset; i++ )
1463
        {
1464
                draw_item(*grd_curcanv, &menu->items[i], (i==menu->citem && !menu->all_text),menu->tiny_mode, menu->tabs_flag, menu->scroll_offset);
1465
        }
1466
 
1467
        if (menu->is_scroll_box)
1468
        {
1469
                auto &cv_font = *(menu->tiny_mode ? GAME_FONT : MEDIUM2_FONT);
1470
 
1471
                const int line_spacing = static_cast<int>(LINE_SPACING(cv_font, *GAME_FONT));
1472
                sy = menu->items[menu->scroll_offset].y - (line_spacing * menu->scroll_offset);
1473
                const auto &&fspacx = FSPACX();
1474
                sx = BORDERX - fspacx(12);
1475
 
1476
                gr_string(*grd_curcanv, cv_font, sx, sy, menu->scroll_offset ? UP_ARROW_MARKER(cv_font, *GAME_FONT) : "  ");
1477
 
1478
                sy = menu->items[menu->scroll_offset + menu->max_displayable - 1].y - (line_spacing * menu->scroll_offset);
1479
                sx = BORDERX - fspacx(12);
1480
 
1481
                gr_string(*grd_curcanv, cv_font, sx, sy, (menu->scroll_offset + menu->max_displayable < menu->nitems) ? DOWN_ARROW_MARKER(*grd_curcanv->cv_font, *GAME_FONT) : "  ");
1482
        }
1483
 
1484
                if (menu->subfunction)
1485
                        (*menu->subfunction)(menu, d_event{EVENT_NEWMENU_DRAW}, menu->userdata);
1486
 
1487
        gr_set_current_canvas(save_canvas);
1488
 
1489
        return window_event_result::handled;
1490
}
1491
 
1492
static window_event_result newmenu_handler(window *wind,const d_event &event, newmenu *menu)
1493
{
1494
#if DXX_MAX_JOYSTICKS // Pierre-Marie Baty -- missing preprocessor condition
1495
        if (joy_translate_menu_key(event))
1496
                return window_event_result::handled;
1497
#endif
1498
 
1499
        if (menu->subfunction)
1500
        {
1501
                int rval = (*menu->subfunction)(menu, event, menu->userdata);
1502
 
1503
#if 0   // No current instances of the subfunction closing the window itself (which is preferred)
1504
                // Enable when all subfunctions return a window_event_result
1505
                if (rval == window_event_result::deleted)
1506
                        return rval;    // some subfunction closed the window: bail!
1507
#endif
1508
 
1509
                if (rval)
1510
                {
1511
                        if (rval < -1)
1512
                        {
1513
                                if (menu->rval)
1514
                                        *menu->rval = rval;
1515
                                return window_event_result::close;
1516
                        }
1517
 
1518
                        return window_event_result::handled;            // event handled
1519
                }
1520
        }
1521
 
1522
        switch (event.type)
1523
        {
1524
                case EVENT_WINDOW_ACTIVATED:
1525
                        game_flush_inputs();
1526
                        event_toggle_focus(0);
1527
                        key_toggle_repeat(1);
1528
                        break;
1529
 
1530
                case EVENT_WINDOW_DEACTIVATED:
1531
                        //event_toggle_focus(1);        // No cursor recentering
1532
                        key_toggle_repeat(1);
1533
                        menu->mouse_state = 0;
1534
                        break;
1535
 
1536
                case EVENT_MOUSE_BUTTON_DOWN:
1537
                case EVENT_MOUSE_BUTTON_UP:
1538
                {
1539
                        int button = event_mouse_get_button(event);
1540
                        menu->mouse_state = event.type == EVENT_MOUSE_BUTTON_DOWN;
1541
                        return newmenu_mouse(wind, event, menu, button);
1542
                }
1543
 
1544
                case EVENT_KEY_COMMAND:
1545
                        return newmenu_key_command(wind, event, menu);
1546
                case EVENT_IDLE:
1547
                        if (!(Game_mode & GM_MULTI && Game_wind))
1548
                                timer_delay2(CGameArg.SysMaxFPS);
1549
                        break;
1550
                case EVENT_WINDOW_DRAW:
1551
                        return newmenu_draw(wind, menu);
1552
                case EVENT_WINDOW_CLOSE:
1553
                        delete menu;
1554
                        break;
1555
 
1556
                default:
1557
                        break;
1558
        }
1559
        return window_event_result::ignored;
1560
}
1561
 
1562
newmenu *newmenu_do4( const char * title, const char * subtitle, uint_fast32_t nitems, newmenu_item * item, newmenu_subfunction subfunction, void *userdata, int citem, const char * filename, int TinyMode, int TabsFlag )
1563
{
1564
        newmenu *menu = new newmenu{};
1565
        menu->citem = citem;
1566
        menu->scroll_offset = 0;
1567
        menu->all_text = 0;
1568
        menu->is_scroll_box = 0;
1569
        menu->max_on_menu = TinyMode?MAXDISPLAYABLEITEMSTINY:MAXDISPLAYABLEITEMS;
1570
        menu->dblclick_flag = 0;
1571
        menu->title = title;
1572
        menu->subtitle = subtitle;
1573
        menu->nitems = nitems;
1574
        menu->subfunction = subfunction;
1575
        menu->items = item;
1576
        menu->filename = filename;
1577
        menu->tiny_mode = TinyMode;
1578
        menu->tabs_flag = TabsFlag;
1579
        menu->rval = NULL;              // Default to not returning a value - respond to EVENT_NEWMENU_SELECTED instead
1580
        menu->userdata = userdata;
1581
 
1582
        newmenu_free_background();
1583
 
1584
        if (nitems < 1 )
1585
        {
1586
                delete menu;
1587
                return NULL;
1588
        }
1589
 
1590
        menu->max_displayable=nitems;
1591
 
1592
        //set_screen_mode(SCREEN_MENU); //hafta set the screen mode here or fonts might get changed/freed up if screen res changes
1593
 
1594
        newmenu_create_structure(menu);
1595
 
1596
        // Create the basic window
1597
        const auto wind = window_create(grd_curscreen->sc_canvas, menu->x, menu->y, menu->w, menu->h, newmenu_handler, menu);
1598
 
1599
        if (!wind)
1600
        {
1601
                delete menu;
1602
                return NULL;
1603
        }
1604
        return menu;
1605
}
1606
}
1607
 
1608
int (vnm_messagebox_aN)(const char *title, const nm_messagebox_tie &tie, const char *format, ...)
1609
{
1610
        va_list args;
1611
        char nm_text[MESSAGEBOX_TEXT_SIZE];
1612
        va_start(args, format);
1613
        vsnprintf(nm_text,sizeof(nm_text),format,args);
1614
        va_end(args);
1615
        return nm_messagebox_str(title, tie, nm_text);
1616
}
1617
 
1618
int nm_messagebox_str(const char *title, const nm_messagebox_tie &tie, const char *str)
1619
{
1620
        newmenu_item items[nm_messagebox_tie::maximum_arity];
1621
        for (unsigned i=0; i < tie.count(); ++i) {
1622
                const char *s = tie.string(i);
1623
                nm_set_item_menu(items[i], s);
1624
        }
1625
        return newmenu_do( title, str, tie.count(), items, unused_newmenu_subfunction, unused_newmenu_userdata );
1626
}
1627
 
1628
// Example listbox callback function...
1629
// int lb_callback( int * citem, int *nitems, char * items[], int *keypress )
1630
// {
1631
//      int i;
1632
//
1633
//      if ( *keypress = KEY_CTRLED+KEY_D )     {
1634
//              if ( *nitems > 1 )      {
1635
//                      PHYSFS_delete(items[*citem]);     // Delete the file
1636
//                      for (i=*citem; i<*nitems-1; i++ )       {
1637
//                              items[i] = items[i+1];
1638
//                      }
1639
//                      *nitems = *nitems - 1;
1640
//                      d_free( items[*nitems] );
1641
//                      items[*nitems] = NULL;
1642
//                      return 1;       // redraw;
1643
//              }
1644
//                      *keypress = 0;
1645
//      }
1646
//      return 0;
1647
// }
1648
 
1649
#define LB_ITEMS_ON_SCREEN 8
1650
 
1651
struct listbox : embed_window_pointer_t
1652
{
1653
        struct marquee
1654
        {
1655
                class deleter : std::default_delete<fix64[]>
1656
                {
1657
                public:
1658
                        void operator()(marquee *const m) const
1659
                        {
1660
                                static_assert(std::is_trivially_destructible<marquee>::value, "marquee destructor not called");
1661
                                std::default_delete<fix64[]>::operator()(reinterpret_cast<fix64 *>(m));
1662
                        }
1663
                };
1664
                using ptr = std::unique_ptr<marquee, deleter>;
1665
                static ptr allocate(const unsigned maxchars)
1666
                {
1667
                        const unsigned max_bytes = maxchars + 1 + sizeof(marquee);
1668
                        auto pf = std::make_unique<fix64[]>(1 + (max_bytes / sizeof(fix64)));
1669
                        auto pm = ptr(new(pf.get()) marquee(maxchars));
1670
                        pf.release();
1671
                        return pm;
1672
                }
1673
                marquee(const unsigned mc) :
1674
                        maxchars(mc)
1675
                {
1676
                }
1677
                fix64 lasttime; // to scroll text if string does not fit in box
1678
                const unsigned maxchars;
1679
                int pos = 0, scrollback = 0;
1680
                char text[0];   /* must be last */
1681
        };
1682
        const char *title;
1683
        const char **item;
1684
        int allow_abort_flag;
1685
        listbox_subfunction_t<void> listbox_callback;
1686
        unsigned nitems;
1687
        unsigned items_on_screen;
1688
        int citem, first_item;
1689
        int box_w, height, box_x, box_y, title_height;
1690
        short swidth, sheight;
1691
        // with these we check if resolution or fonts have changed so listbox structure can be recreated
1692
        font_x_scale_proportion fntscalex;
1693
        font_y_scale_proportion fntscaley;
1694
        int mouse_state;
1695
        marquee::ptr marquee;
1696
        void *userdata;
1697
};
1698
 
1699
window *listbox_get_window(listbox *const lb)
1700
{
1701
        return lb->wind;
1702
}
1703
 
1704
const char **listbox_get_items(listbox *lb)
1705
{
1706
        return lb->item;
1707
}
1708
 
1709
int listbox_get_citem(listbox *lb)
1710
{
1711
        return lb->citem;
1712
}
1713
 
1714
void listbox_delete_item(listbox *lb, int item)
1715
{
1716
        Assert(item >= 0);
1717
 
1718
        const auto nitems = lb->nitems;
1719
        if (nitems <= 0)
1720
                return;
1721
        if (item < nitems - 1)
1722
        {
1723
                auto &items = lb->item;
1724
                std::rotate(&items[item], &items[item + 1], &items[nitems]);
1725
        }
1726
        -- lb->nitems;
1727
        if (lb->citem >= lb->nitems)
1728
                lb->citem = lb->nitems ? lb->nitems - 1 : 0;
1729
}
1730
 
1731
static void update_scroll_position(listbox *lb)
1732
{
1733
        if (lb->citem<0)
1734
                lb->citem = 0;
1735
 
1736
        if (lb->citem>=lb->nitems)
1737
                lb->citem = lb->nitems-1;
1738
 
1739
        if (lb->citem< lb->first_item)
1740
                lb->first_item = lb->citem;
1741
 
1742
        if (lb->citem >= lb->items_on_screen)
1743
        {
1744
                if (lb->first_item <= lb->citem - lb->items_on_screen)
1745
                        lb->first_item = lb->citem - lb->items_on_screen + 1;
1746
        }
1747
 
1748
        if (lb->nitems <= lb->items_on_screen)
1749
                lb->first_item = 0;
1750
 
1751
        if (lb->nitems >= lb->items_on_screen)
1752
        {
1753
                if (lb->first_item > lb->nitems - lb->items_on_screen)
1754
                        lb->first_item = lb->nitems - lb->items_on_screen;
1755
        }
1756
        if (lb->first_item < 0 ) lb->first_item = 0;
1757
}
1758
 
1759
static window_event_result listbox_mouse(window *, const d_event &event, listbox *lb, int button)
1760
{
1761
        switch (button)
1762
        {
1763
                case MBTN_LEFT:
1764
                {
1765
                        if (lb->mouse_state)
1766
                        {
1767
                                int mx, my, mz;
1768
                                mouse_get_pos(&mx, &my, &mz);
1769
                                const int x1 = lb->box_x;
1770
                                if (mx <= x1)
1771
                                        break;
1772
                                const int x2 = x1 + lb->box_w;
1773
                                if (mx >= x2)
1774
                                        break;
1775
                                const int by = lb->box_y;
1776
                                if (my <= by)
1777
                                        break;
1778
                                const int my_relative_by = my - by;
1779
                                const auto &&line_spacing = LINE_SPACING(*MEDIUM3_FONT, *GAME_FONT);
1780
                                if (line_spacing < 1)
1781
                                        break;
1782
                                const int idx_relative_first_visible_item = my_relative_by / line_spacing;
1783
                                const auto first_visible_item = lb->first_item;
1784
                                const auto nitems = lb->nitems;
1785
                                const auto last_visible_item = std::min(first_visible_item + lb->items_on_screen, nitems);
1786
                                if (idx_relative_first_visible_item >= last_visible_item)
1787
                                        break;
1788
                                const int px_within_item = my_relative_by % static_cast<int>(line_spacing);
1789
                                const int h = gr_get_string_height(*MEDIUM3_FONT, 1);
1790
                                if (px_within_item >= h)
1791
                                        break;
1792
                                const int idx_absolute_item = idx_relative_first_visible_item + first_visible_item;
1793
                                if (idx_absolute_item >= nitems)
1794
                                        break;
1795
                                lb->citem = idx_absolute_item;
1796
                        }
1797
                        else if (event.type == EVENT_MOUSE_BUTTON_UP)
1798
                        {
1799
                                int h;
1800
 
1801
                                if (lb->citem < 0)
1802
                                        return window_event_result::ignored;
1803
 
1804
                                int mx, my, mz;
1805
                                mouse_get_pos(&mx, &my, &mz);
1806
                                gr_get_string_size(*MEDIUM3_FONT, lb->item[lb->citem], nullptr, &h, nullptr);
1807
                                const int x1 = lb->box_x;
1808
                                const int x2 = lb->box_x + lb->box_w;
1809
                                const int y1 = (lb->citem - lb->first_item) * LINE_SPACING(*MEDIUM3_FONT, *GAME_FONT) + lb->box_y;
1810
                                const int y2 = y1 + h;
1811
                                if ( ((mx > x1) && (mx < x2)) && ((my > y1) && (my < y2)) )
1812
                                {
1813
                                        // Tell callback, if it wants to close it will return window_event_result::close
1814
                                        const d_select_event selected{lb->citem};
1815
                                        if (lb->listbox_callback)
1816
                                                return (*lb->listbox_callback)(lb, selected, lb->userdata);
1817
                                        return window_event_result::close;
1818
                                }
1819
                        }
1820
                        break;
1821
                }
1822
                case MBTN_RIGHT:
1823
                {
1824
                        if (lb->allow_abort_flag && lb->mouse_state) {
1825
                                lb->citem = -1;
1826
                                return window_event_result::close;
1827
                        }
1828
                        break;
1829
                }
1830
                case MBTN_Z_UP:
1831
                {
1832
                        if (lb->mouse_state)
1833
                        {
1834
                                lb->citem--;
1835
                                update_scroll_position(lb);
1836
                        }
1837
                        break;
1838
                }
1839
                case MBTN_Z_DOWN:
1840
                {
1841
                        if (lb->mouse_state)
1842
                        {
1843
                                lb->citem++;
1844
                                update_scroll_position(lb);
1845
                        }
1846
                        break;
1847
                }
1848
                default:
1849
                        break;
1850
        }
1851
 
1852
        return window_event_result::ignored;
1853
}
1854
 
1855
static window_event_result listbox_key_command(window *, const d_event &event, listbox *lb)
1856
{
1857
        int key = event_key_get(event);
1858
        window_event_result rval = window_event_result::handled;
1859
 
1860
        switch(key)     {
1861
                case KEY_HOME:
1862
                case KEY_PAD7:
1863
                        lb->citem = 0;
1864
                        break;
1865
                case KEY_END:
1866
                case KEY_PAD1:
1867
                        lb->citem = lb->nitems-1;
1868
                        break;
1869
                case KEY_UP:
1870
                case KEY_PAD8:
1871
                        lb->citem--;
1872
                        break;
1873
                case KEY_DOWN:
1874
                case KEY_PAD2:
1875
                        lb->citem++;
1876
                        break;
1877
                case KEY_PAGEDOWN:
1878
                case KEY_PAD3:
1879
                        lb->citem += lb->items_on_screen;
1880
                        break;
1881
                case KEY_PAGEUP:
1882
                case KEY_PAD9:
1883
                        lb->citem -= lb->items_on_screen;
1884
                        break;
1885
                case KEY_ESC:
1886
                        if (lb->allow_abort_flag) {
1887
                                lb->citem = -1;
1888
                                return window_event_result::close;
1889
                        }
1890
                        break;
1891
                case KEY_ENTER:
1892
                case KEY_PADENTER:
1893
                        // Tell callback, if it wants to close it will return window_event_result::close
1894
                        {
1895
                                const d_select_event selected{lb->citem};
1896
                                if (lb->listbox_callback)
1897
                                        return (*lb->listbox_callback)(lb, selected, lb->userdata);
1898
                        }
1899
                        return window_event_result::close;
1900
                default:
1901
                {
1902
                        const unsigned ascii = key_ascii();
1903
                        if ( ascii < 255 )      {
1904
                                const unsigned upper_ascii = toupper(ascii);
1905
                                const auto nitems = lb->nitems;
1906
                                for (unsigned cc = lb->citem + 1, steps = 0; steps < nitems; ++steps, ++cc)
1907
                                {
1908
                                        if (cc >= nitems)
1909
                                                cc = 0;
1910
                                        if (toupper(static_cast<unsigned>(lb->item[cc][0])) == upper_ascii)
1911
                                        {
1912
                                                lb->citem = cc;
1913
                                                break;
1914
                                        }
1915
                                }
1916
                        }
1917
                        rval = window_event_result::ignored;
1918
                }
1919
        }
1920
        update_scroll_position(lb);
1921
        return rval;
1922
}
1923
 
1924
static void listbox_create_structure( listbox *lb)
1925
{
1926
        gr_set_default_canvas();
1927
        auto &canvas = *grd_curcanv;
1928
 
1929
        auto &medium3_font = *MEDIUM3_FONT;
1930
 
1931
        lb->box_w = 0;
1932
        const auto &&fspacx = FSPACX();
1933
        const auto &&fspacx10 = fspacx(10);
1934
        const unsigned max_box_width = SWIDTH - (BORDERX * 2);
1935
        unsigned marquee_maxchars = UINT_MAX;
1936
        range_for (const auto i, unchecked_partial_range(lb->item, lb->nitems))
1937
        {
1938
                int w;
1939
                gr_get_string_size(medium3_font, i, &w, nullptr, nullptr);
1940
                w += fspacx10;
1941
                if (w > max_box_width)
1942
                {
1943
                        unsigned mmc = 1;
1944
                        for (;; ++mmc)
1945
                        {
1946
                                int w2;
1947
                                gr_get_string_size(medium3_font, i, &w2, nullptr, nullptr, mmc);
1948
                                if (w2 > max_box_width - fspacx10 || mmc > 128)
1949
                                        break;
1950
                        }
1951
                        /* mmc is now the shortest initial subsequence that is wider
1952
                         * than max_box_width.
1953
                         *
1954
                         * Next, search for whether any internal subsequences of
1955
                         * lesser length are also too wide.  This can happen if all
1956
                         * the initial characters are narrow, then characters
1957
                         * outside the initial subsequence are wide.
1958
                         */
1959
                        for (auto j = i;;)
1960
                        {
1961
                                int w2;
1962
                                gr_get_string_size(medium3_font, j, &w2, nullptr, nullptr, mmc);
1963
                                if (w2 > max_box_width - fspacx10)
1964
                                {
1965
                                        /* This subsequence is too long.  Reduce the length
1966
                                         * and retry.
1967
                                         */
1968
                                        if (!--mmc)
1969
                                                break;
1970
                                }
1971
                                else
1972
                                {
1973
                                        /* This subsequence fits.  Move to the next
1974
                                         * character.
1975
                                         */
1976
                                        if (!*++j)
1977
                                                break;
1978
                                }
1979
                        }
1980
                        w = max_box_width;
1981
                        if (marquee_maxchars > mmc)
1982
                                marquee_maxchars = mmc;
1983
                }
1984
                if (lb->box_w < w)
1985
                        lb->box_w = w;
1986
        }
1987
 
1988
        {
1989
                int w, h;
1990
                gr_get_string_size(medium3_font, lb->title, &w, &h, nullptr);
1991
                if ( w > lb->box_w )
1992
                        lb->box_w = w;
1993
                lb->title_height = h+FSPACY(5);
1994
        }
1995
 
1996
        // The box is bigger than we can fit on the screen since at least one string is too long. Check how many chars we can fit on the screen (at least only - MEDIUM*_FONT is variable font!) so we can make a marquee-like effect.
1997
        if (marquee_maxchars != UINT_MAX)
1998
        {
1999
                lb->box_w = max_box_width;
2000
                lb->marquee = listbox::marquee::allocate(marquee_maxchars);
2001
                lb->marquee->lasttime = timer_query();
2002
        }
2003
 
2004
        const auto &&line_spacing = LINE_SPACING(medium3_font, *GAME_FONT);
2005
        const unsigned bordery2 = BORDERY * 2;
2006
        const auto items_on_screen = std::max<unsigned>(
2007
                std::min<unsigned>(((canvas.cv_bitmap.bm_h - bordery2 - lb->title_height) / line_spacing) - 2, lb->nitems),
2008
                LB_ITEMS_ON_SCREEN);
2009
        lb->items_on_screen = items_on_screen;
2010
        lb->height = line_spacing * items_on_screen;
2011
        lb->box_x = (canvas.cv_bitmap.bm_w - lb->box_w) / 2;
2012
        lb->box_y = (canvas.cv_bitmap.bm_h - (lb->height + lb->title_height)) / 2 + lb->title_height;
2013
        if (lb->box_y < bordery2)
2014
                lb->box_y = bordery2;
2015
 
2016
        if ( lb->citem < 0 ) lb->citem = 0;
2017
        if ( lb->citem >= lb->nitems ) lb->citem = 0;
2018
 
2019
        lb->first_item = 0;
2020
        update_scroll_position(lb);
2021
 
2022
        lb->mouse_state = 0;    //dblclick_flag = 0;
2023
        lb->swidth = SWIDTH;
2024
        lb->sheight = SHEIGHT;
2025
        lb->fntscalex = FNTScaleX;
2026
        lb->fntscaley = FNTScaleY;
2027
}
2028
 
2029
static window_event_result listbox_draw(window *, listbox *lb)
2030
{
2031
        if (lb->swidth != SWIDTH || lb->sheight != SHEIGHT || lb->fntscalex != FNTScaleX || lb->fntscaley != FNTScaleY)
2032
                listbox_create_structure ( lb );
2033
 
2034
        gr_set_default_canvas();
2035
        auto &canvas = *grd_curcanv;
2036
        nm_draw_background(canvas, lb->box_x - BORDERX, lb->box_y - lb->title_height - BORDERY,lb->box_x + lb->box_w + BORDERX, lb->box_y + lb->height + BORDERY);
2037
        auto &medium3_font = *MEDIUM3_FONT;
2038
        gr_string(canvas, medium3_font, 0x8000, lb->box_y - lb->title_height, lb->title);
2039
 
2040
        const auto &&line_spacing = LINE_SPACING(medium3_font, *GAME_FONT);
2041
        for (int i = lb->first_item; i < lb->first_item + lb->items_on_screen; ++i)
2042
        {
2043
                int y = (i - lb->first_item) * line_spacing + lb->box_y;
2044
                const auto &&fspacx = FSPACX();
2045
                const auto &&fspacy = FSPACY();
2046
                const uint8_t color5 = BM_XRGB(5, 5, 5);
2047
                const uint8_t color2 = BM_XRGB(2, 2, 2);
2048
                const uint8_t color0 = BM_XRGB(0, 0, 0);
2049
                if ( i >= lb->nitems )  {
2050
                        gr_rect(canvas, lb->box_x + lb->box_w - fspacx(1), y - fspacy(1), lb->box_x + lb->box_w, y + line_spacing, color5);
2051
                        gr_rect(canvas, lb->box_x - fspacx(1), y - fspacy(1), lb->box_x, y + line_spacing, color2);
2052
                        gr_rect(canvas, lb->box_x, y - fspacy(1), lb->box_x + lb->box_w - fspacx(1), y + line_spacing, color0);
2053
                } else {
2054
                        auto &mediumX_font = *(i == lb->citem ? MEDIUM2_FONT : MEDIUM1_FONT);
2055
                        gr_rect(canvas, lb->box_x + lb->box_w - fspacx(1), y - fspacy(1), lb->box_x + lb->box_w, y + line_spacing, color5);
2056
                        gr_rect(canvas, lb->box_x - fspacx(1), y - fspacy(1), lb->box_x, y + line_spacing, color2);
2057
                        gr_rect(canvas, lb->box_x, y - fspacy(1), lb->box_x + lb->box_w - fspacx(1), y + line_spacing, color0);
2058
 
2059
                        const char *showstr;
2060
                        std::size_t item_len;
2061
                        if (lb->marquee && (item_len = strlen(lb->item[i])) > lb->marquee->maxchars)
2062
                        {
2063
                                showstr = lb->marquee->text;
2064
                                static int prev_citem = -1;
2065
 
2066
                                if (prev_citem != lb->citem)
2067
                                {
2068
                                        lb->marquee->pos = lb->marquee->scrollback = 0;
2069
                                        lb->marquee->lasttime = timer_query();
2070
                                        prev_citem = lb->citem;
2071
                                }
2072
 
2073
                                unsigned srcoffset = 0;
2074
                                if (i == lb->citem)
2075
                                {
2076
                                        const auto tq = timer_query();
2077
                                        if (lb->marquee->lasttime + (F1_0 / 3) < tq)
2078
                                        {
2079
                                                lb->marquee->pos += lb->marquee->scrollback ? -1 : 1;
2080
                                                lb->marquee->lasttime = tq;
2081
                                        }
2082
                                        if (lb->marquee->pos < 0) // reached beginning of string -> scroll forward
2083
                                        {
2084
                                                lb->marquee->pos = 0;
2085
                                                lb->marquee->scrollback = 0;
2086
                                        }
2087
                                        if (lb->marquee->pos + lb->marquee->maxchars - 1 > item_len) // reached end of string -> scroll backward
2088
                                        {
2089
                                                lb->marquee->pos = item_len - lb->marquee->maxchars + 1;
2090
                                                lb->marquee->scrollback = 1;
2091
                                        }
2092
                                        srcoffset = lb->marquee->pos;
2093
                                }
2094
                                snprintf(lb->marquee->text, lb->marquee->maxchars, "%s", lb->item[i] + srcoffset);
2095
                        }
2096
                        else
2097
                        {
2098
                                showstr = lb->item[i];
2099
                        }
2100
                        gr_string(canvas, mediumX_font, lb->box_x + fspacx(5), y, showstr);
2101
                }
2102
        }
2103
 
2104
                if ( lb->listbox_callback )
2105
                        return (*lb->listbox_callback)(lb, d_event{EVENT_NEWMENU_DRAW}, lb->userdata);
2106
        return window_event_result::handled;
2107
}
2108
 
2109
static window_event_result listbox_handler(window *wind,const d_event &event, listbox *lb)
2110
{
2111
        if (lb->listbox_callback)
2112
        {
2113
                auto rval = (*lb->listbox_callback)(lb, event, lb->userdata);
2114
                if (rval != window_event_result::ignored)
2115
                        return rval;            // event handled
2116
        }
2117
 
2118
#if DXX_MAX_JOYSTICKS // Pierre-Marie Baty -- missing preprocessor condition
2119
        if (joy_translate_menu_key(event))
2120
                return window_event_result::handled;
2121
#endif
2122
 
2123
        switch (event.type)
2124
        {
2125
                case EVENT_WINDOW_ACTIVATED:
2126
                        game_flush_inputs();
2127
                        event_toggle_focus(0);
2128
                        key_toggle_repeat(1);
2129
                        break;
2130
 
2131
                case EVENT_WINDOW_DEACTIVATED:
2132
                        //event_toggle_focus(1);        // No cursor recentering
2133
                        key_toggle_repeat(0);
2134
                        break;
2135
 
2136
                case EVENT_MOUSE_BUTTON_DOWN:
2137
                case EVENT_MOUSE_BUTTON_UP:
2138
                        lb->mouse_state = event.type == EVENT_MOUSE_BUTTON_DOWN;
2139
                        return listbox_mouse(wind, event, lb, event_mouse_get_button(event));
2140
                case EVENT_KEY_COMMAND:
2141
                        return listbox_key_command(wind, event, lb);
2142
                case EVENT_IDLE:
2143
                        if (!(Game_mode & GM_MULTI && Game_wind))
2144
                                timer_delay2(CGameArg.SysMaxFPS);
2145
                        return listbox_mouse(wind, event, lb, -1);
2146
                case EVENT_WINDOW_DRAW:
2147
                        return listbox_draw(wind, lb);
2148
                case EVENT_WINDOW_CLOSE:
2149
                        std::default_delete<listbox>()(lb);
2150
                        break;
2151
                default:
2152
                        break;
2153
        }
2154
        return window_event_result::ignored;
2155
}
2156
 
2157
listbox *newmenu_listbox1(const char *const title, const uint_fast32_t nitems, const char *items[], const int allow_abort_flag, const int default_item, const listbox_subfunction_t<void> listbox_callback, void *const userdata)
2158
{
2159
        window *wind;
2160
        newmenu_free_background();
2161
 
2162
        auto lb = std::make_unique<listbox>();
2163
        *lb = {};
2164
        lb->title = title;
2165
        lb->nitems = nitems;
2166
        lb->item = items;
2167
        lb->citem = default_item;
2168
        lb->allow_abort_flag = allow_abort_flag;
2169
        lb->listbox_callback = listbox_callback;
2170
        lb->userdata = userdata;
2171
 
2172
        set_screen_mode(SCREEN_MENU);   //hafta set the screen mode here or fonts might get changed/freed up if screen res changes
2173
 
2174
        listbox_create_structure(lb.get());
2175
 
2176
        wind = window_create(grd_curscreen->sc_canvas, lb->box_x-BORDERX, lb->box_y-lb->title_height-BORDERY, lb->box_w+2*BORDERX, lb->height+2*BORDERY, listbox_handler, lb.get());
2177
        if (!wind)
2178
        {
2179
                lb.reset();
2180
        }
2181
        return lb.release();
2182
}