1 /**
2 ** A GTK widget showing the list of NPC's.
3 **
4 ** Written: 7/6/2005 - JSF
5 **/
6
7 /*
8 Copyright (C) 2005-2020 The Exult Team
9
10 This program is free software; you can redistribute it and/or
11 modify it under the terms of the GNU General Public License
12 as published by the Free Software Foundation; either version 2
13 of the License, or (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23 */
24
25 #ifdef HAVE_CONFIG_H
26 # include <config.h>
27 #endif
28
29 #include "npclst.h"
30
31 #include "fontgen.h"
32 #include "ibuf8.h"
33 #include "pngio.h"
34 #include "shapefile.h"
35 #include "shapegroup.h"
36 #include "shapevga.h"
37 #include "u7drag.h"
38 #include "utils.h"
39
40 #include <glib.h>
41 #include <sys/stat.h>
42 #include <unistd.h>
43
44 #include <cmath>
45 #include <cstdlib>
46
47 using EStudio::Add_menu_item;
48 using EStudio::Alert;
49 using std::cout;
50 using std::cerr;
51 using std::endl;
52 using std::string;
53 using std::vector;
54
55 /*
56 * Blit onto screen.
57 */
58
show(int x,int y,int w,int h)59 void Npc_chooser::show(
60 int x, int y, int w, int h // Area to blit.
61 ) {
62 Shape_draw::show(x, y, w, h);
63 if ((selected >= 0) && (drawgc != nullptr)) { // Show selected.
64 TileRect b = info[selected].box;
65 // Draw yellow box.
66 cairo_set_line_width(drawgc, 1.0);
67 cairo_set_source_rgb(drawgc,
68 ((drawfg >> 16) & 255) / 255.0,
69 ((drawfg >> 8) & 255) / 255.0,
70 (drawfg & 255) / 255.0);
71 cairo_rectangle(drawgc, b.x, b.y - voffset, b.w, b.h);
72 cairo_stroke(drawgc);
73 }
74 }
75
76 /*
77 * Select an entry. This should be called after rendering
78 * the shape.
79 */
80
select(int new_sel)81 void Npc_chooser::select(
82 int new_sel
83 ) {
84 selected = new_sel;
85 update_statusbar();
86 }
87
88 const int border = 4; // Border at bottom, sides.
89 /*
90 * Render as many shapes as fit in the shape chooser window.
91 */
92
render()93 void Npc_chooser::render(
94 ) {
95 vector<Estudio_npc> &npcs = get_npcs();
96 // Get drawing area dimensions.
97 GtkAllocation alloc = {0, 0, 0, 0};
98 gtk_widget_get_allocation(draw, &alloc);
99 gint winh = alloc.height;
100 // Clear window first.
101 iwin->fill8(255); // Set to background_color.
102 int curr_y = -row0_voffset;
103 //int total_cnt = get_count();
104 // filter (group).
105 for (unsigned rownum = row0; curr_y < winh && rownum < rows.size();
106 ++rownum) {
107 Npc_row &row = rows[rownum];
108 unsigned cols = get_num_cols(rownum);
109 for (unsigned index = row.index0; cols; --cols, ++index) {
110 int npcnum = info[index].npcnum;
111 int shapenum = npcs[npcnum].shapenum;
112 int framenum = info[index].framenum;
113 Shape_frame *shape = ifile->get_shape(shapenum,
114 framenum);
115 if (shape) {
116 int sx = info[index].box.x - hoffset;
117 int sy = info[index].box.y - voffset;
118 shape->paint(iwin, sx + shape->get_xleft(),
119 sy + shape->get_yabove());
120 if (npcs[npcnum].unused)
121 shape->paint_rle_outline(iwin,
122 sx + shape->get_xleft(),
123 sy + shape->get_yabove(), red);
124 last_npc = npcnum;
125 }
126 }
127 curr_y += rows[rownum].height;
128 }
129 gtk_widget_queue_draw(draw);
130 }
131
132 /*
133 * Get maximum shape height for all its frames.
134 */
135
Get_max_height(Shape * shape)136 static int Get_max_height(
137 Shape *shape
138 ) {
139 int cnt = shape->get_num_frames();
140 int maxh = 0;
141 for (int i = 0; i < cnt; i++) {
142 Shape_frame *frame = shape->get_frame(i);
143 if (!frame) {
144 continue;
145 }
146 int ht = frame->get_height();
147 if (ht > maxh)
148 maxh = ht;
149 }
150 return maxh;
151 }
152
153 /*
154 * Get the x-offset in pixels where a frame will be drawn.
155 *
156 * Output: Offset from left edge of (virtual) drawing area.
157 */
158
Get_x_offset(Shape * shape,int framenum)159 static int Get_x_offset(
160 Shape *shape,
161 int framenum
162 ) {
163 if (!shape)
164 return 0;
165 int nframes = shape->get_num_frames();
166 if (framenum >= nframes)
167 framenum = nframes - 1;
168 int xoff = 0;
169 for (int i = 0; i < framenum; i++) {
170 Shape_frame *fr = shape->get_frame(i);
171 if (fr) {
172 xoff += fr->get_width() + border;
173 }
174 }
175 return xoff;
176 }
177
178 /*
179 * Find where everything goes.
180 */
181
setup_info(bool savepos)182 void Npc_chooser::setup_info(
183 bool savepos // Try to keep current position.
184 ) {
185 unsigned oldind = rows[row0].index0;
186 info.resize(0);
187 rows.resize(0);
188 row0 = row0_voffset = 0;
189 last_npc = 0;
190 /* +++++NOTE: index0 is always 0 for the NPC browse. It should
191 probably be removed from the base Obj_browser class */
192 index0 = 0;
193 voffset = 0;
194 total_height = 0;
195 if (frames_mode)
196 setup_frames_info();
197 else
198 setup_shapes_info();
199 setup_vscrollbar();
200 if (savepos)
201 goto_index(oldind);
202 }
203
204 /*
205 * Setup info when not in 'frames' mode.
206 */
207
setup_shapes_info()208 void Npc_chooser::setup_shapes_info(
209 ) {
210 vector<Estudio_npc> &npcs = get_npcs();
211 if (npcs.empty()) // No NPC's? Try to get them.
212 static_cast<Npcs_file_info *>(file_info)->setup();
213 int selnpc = -1;
214 int selframe = -1;
215 if (selected >= 0) { // Save selection info.
216 selnpc = info[selected].npcnum;
217 selframe = info[selected].framenum;
218 }
219 // Get drawing area dimensions.
220 GtkAllocation alloc = {0, 0, 0, 0};
221 gtk_widget_get_allocation(draw, &alloc);
222 gint winw = alloc.width;
223 int x = 0;
224 int curr_y = 0;
225 int row_h = 0;
226 int total_cnt = get_count();
227 int num_shapes = ifile->get_num_shapes();
228 // filter (group).
229 rows.resize(1); // Start 1st row.
230 rows[0].index0 = 0;
231 rows[0].y = 0;
232 for (int index = 0; index < total_cnt; index++) {
233 int npcnum = group ? (*group)[index] : index;
234 if (npcnum >= 356 && npcnum <= 359)
235 continue;
236 int shapenum = npcs[npcnum].shapenum;
237 int framenum = npcnum == selnpc ? selframe : framenum0;
238 if (shapenum < 0 || shapenum >= num_shapes)
239 continue;
240 Shape_frame *shape = ifile->get_shape(shapenum, framenum);
241 if (!shape)
242 continue;
243 int sh = shape->get_height();
244 int sw = shape->get_width();
245 // Check if we've exceeded max width
246 if (x + sw > winw && x) { // But don't leave row empty.
247 // Next line.
248 rows.back().height = row_h + border;
249 curr_y += row_h + border;
250 row_h = 0;
251 x = 0;
252 rows.emplace_back();
253 rows.back().index0 = info.size();
254 rows.back().y = curr_y;
255 }
256 if (sh > row_h)
257 row_h = sh;
258 int sy = curr_y + border; // Get top y-coord.
259 // Store info. about where drawn.
260 info.emplace_back();
261 info.back().set(npcnum, framenum, x, sy, sw, sh);
262 x += sw + border;
263 }
264 rows.back().height = row_h + border;
265 total_height = curr_y + rows.back().height + border;
266 }
267
268 /*
269 * Setup one NPC per row, showing its frames from left to right.
270 */
271
setup_frames_info()272 void Npc_chooser::setup_frames_info(
273 ) {
274 vector<Estudio_npc> &npcs = get_npcs();
275 if (npcs.empty()) // No NPC's? Try to get them.
276 static_cast<Npcs_file_info *>(file_info)->setup();
277 // Get drawing area dimensions.
278 int curr_y = 0;
279 int maxw = 0;
280 unsigned total_cnt = get_count();
281 int num_shapes = ifile->get_num_shapes();
282 // filter (group).
283 for (unsigned index = index0; index < total_cnt; index++) {
284 int npcnum = group ? (*group)[index] : index;
285 if (npcnum >= 356 && npcnum <= 359)
286 continue;
287 int shapenum = npcs[npcnum].shapenum;
288 if (shapenum < 0 || shapenum >= num_shapes)
289 continue;
290 // Get all frames.
291 Shape *shape = ifile->extract_shape(shapenum);
292 int nframes = shape ? shape->get_num_frames() : 0;
293 if (!nframes)
294 continue;
295 int row_h = Get_max_height(shape);
296 rows.emplace_back();
297 rows.back().index0 = info.size();
298 rows.back().y = curr_y;
299 rows.back().height = row_h + border;
300 int x = 0;
301 int sw;
302 int sh;
303 for (int framenum = 0; framenum < nframes; framenum++,
304 x += sw + border) {
305 Shape_frame *frame = shape->get_frame(framenum);
306 if (!frame) {
307 sw = 0;
308 continue;
309 }
310 sh = frame->get_height();
311 sw = frame->get_width();
312 int sy = curr_y + border; // Get top y-coord.
313 // Store info. about where drawn.
314 info.emplace_back();
315 info.back().set(npcnum, framenum, x, sy, sw, sh);
316 }
317 if (x > maxw)
318 maxw = x;
319 // Next line.
320 curr_y += row_h + border;
321 }
322 total_height = curr_y + border;
323 setup_hscrollbar(maxw);
324 }
325
326 /*
327 * Horizontally scroll so that the selected frame is visible (in frames
328 * mode).
329 */
330
scroll_to_frame()331 void Npc_chooser::scroll_to_frame(
332 ) {
333 if (selected >= 0) { // Save selection info.
334 vector<Estudio_npc> &npcs = get_npcs();
335 int selnpc = info[selected].npcnum;
336 int selframe = info[selected].framenum;
337 Shape *shape = ifile->extract_shape(npcs[selnpc].shapenum);
338 int xoff = Get_x_offset(shape, selframe);
339 if (xoff < hoffset) // Left of visual area?
340 hoffset = xoff > border ? xoff - border : 0;
341 else {
342 GtkAllocation alloc = {0, 0, 0, 0};
343 gtk_widget_get_allocation(draw, &alloc);
344 gint winw = alloc.width;
345 Shape_frame *fr = shape->get_frame(selframe);
346 if (fr) {
347 int sw = fr->get_width();
348 if (xoff + sw + border - hoffset > winw)
349 hoffset = xoff + sw + border - winw;
350 }
351 }
352 GtkAdjustment *adj = gtk_range_get_adjustment(
353 GTK_RANGE(hscroll));
354 gtk_adjustment_set_value(adj, hoffset);
355 }
356 }
357
358 /*
359 * Scroll so a desired index is in view.
360 */
361
goto_index(unsigned index)362 void Npc_chooser::goto_index(
363 unsigned index // Desired index in 'info'.
364 ) {
365 if (index >= info.size())
366 return; // Illegal index or empty chooser.
367 Npc_entry &inf = info[index]; // Already in view?
368 int midx = inf.box.x + inf.box.w / 2;
369 int midy = inf.box.y + inf.box.h / 2;
370 TileRect winrect(0, voffset, config_width, config_height);
371 if (winrect.has_point(midx, midy))
372 return;
373 unsigned start = 0;
374 unsigned count = rows.size();
375 while (count > 1) { // Binary search.
376 unsigned mid = start + count / 2;
377 if (index < rows[mid].index0)
378 count = mid - start;
379 else {
380 count = (start + count) - mid;
381 start = mid;
382 }
383 }
384 if (start < rows.size()) {
385 // Get to right spot again!
386 GtkAdjustment *adj = gtk_range_get_adjustment(
387 GTK_RANGE(vscroll));
388 gtk_adjustment_set_value(adj, rows[start].y);
389 }
390 }
391
392 /*
393 * Find index for a given NPC #.
394 */
395
find_npc(int npcnum)396 int Npc_chooser::find_npc(
397 int npcnum
398 ) {
399 if (group) { // They're not ordered.
400 int cnt = info.size();
401 for (int i = 0; i < cnt; ++i)
402 if (info[i].npcnum == npcnum)
403 return i;
404 return -1;
405 }
406 unsigned start = 0;
407 unsigned count = info.size();
408 while (count > 1) { // Binary search.
409 unsigned mid = start + count / 2;
410 if (npcnum < info[mid].npcnum)
411 count = mid - start;
412 else {
413 count = (start + count) - mid;
414 start = mid;
415 }
416 }
417 if (start < info.size())
418 return start;
419 else
420 return -1;
421 }
422
423 /*
424 * Configure the viewing window.
425 */
426
Configure_chooser(GtkWidget * widget,GdkEventConfigure * event,gpointer data)427 static gint Configure_chooser(
428 GtkWidget *widget, // The drawing area.
429 GdkEventConfigure *event,
430 gpointer data // ->Npc_chooser
431 ) {
432 ignore_unused_variable_warning(widget);
433 auto *chooser = static_cast<Npc_chooser *>(data);
434 return chooser->configure(event);
435 }
configure(GdkEventConfigure * event)436 gint Npc_chooser::configure(
437 GdkEventConfigure *event
438 ) {
439 Shape_draw::configure();
440 // Did the size change?
441 if (event->width != config_width || event->height != config_height) {
442 config_width = event->width;
443 config_height = event->height;
444 setup_info(true);
445 render();
446 update_statusbar();
447 } else
448 render(); // Same size? Just render it.
449 if (group) // Filtering?
450 enable_drop(); // Can drop NPCs here.
451 return TRUE;
452 }
453
454 /*
455 * Handle an expose event.
456 */
457
expose(GtkWidget * widget,cairo_t * cairo,gpointer data)458 gint Npc_chooser::expose(
459 GtkWidget *widget, // The view window.
460 cairo_t *cairo,
461 gpointer data // ->Npc_chooser.
462 ) {
463 ignore_unused_variable_warning(widget);
464 auto *chooser = static_cast<Npc_chooser *>(data);
465 chooser->set_graphic_context(cairo);
466 GdkRectangle area = { 0, 0, 0, 0 };
467 gdk_cairo_get_clip_rectangle(cairo, &area);
468 chooser->show(area.x, area.y, area.width, area.height);
469 chooser->set_graphic_context(nullptr);
470 return TRUE;
471 }
472
473 /*
474 * Handle a mouse drag event.
475 */
476
drag_motion(GtkWidget * widget,GdkEventMotion * event,gpointer data)477 gint Npc_chooser::drag_motion(
478 GtkWidget *widget, // The view window.
479 GdkEventMotion *event,
480 gpointer data // ->Npc_chooser.
481 ) {
482 ignore_unused_variable_warning(widget);
483 auto *chooser = static_cast<Npc_chooser *>(data);
484 if (!chooser->dragging && chooser->selected >= 0)
485 chooser->start_drag(U7_TARGET_NPCID_NAME,
486 U7_TARGET_NPCID, reinterpret_cast<GdkEvent *>(event));
487 return true;
488 }
489
490 /*
491 * Handle a mouse button-press event.
492 */
mouse_press(GtkWidget * widget,GdkEventButton * event)493 gint Npc_chooser::mouse_press(
494 GtkWidget *widget, // The view window.
495 GdkEventButton *event
496 ) {
497 gtk_widget_grab_focus(widget);
498
499 if (event->button == 4) {
500 if (row0 > 0)
501 scroll_row_vertical(row0 - 1);
502 return TRUE;
503 } else if (event->button == 5) {
504 scroll_row_vertical(row0 + 1);
505 return TRUE;
506 }
507 int old_selected = selected;
508 int new_selected = -1;
509 unsigned i; // Search through entries.
510 unsigned infosz = info.size();
511 int absx = static_cast<int>(event->x);
512 int absy = static_cast<int>(event->y) + voffset;
513 for (i = rows[row0].index0; i < infosz; i++) {
514 if (info[i].box.has_point(absx, absy)) {
515 // Found the box?
516 // Indicate we can drag.
517 new_selected = i;
518 break;
519 } else if (info[i].box.y - voffset >= config_height)
520 break; // Past bottom of screen.
521 }
522 if (new_selected >= 0) {
523 select(new_selected);
524 render();
525 if (sel_changed) // Tell client.
526 (*sel_changed)();
527 }
528 if (new_selected < 0 && event->button == 1)
529 unselect(true); // No selection.
530 else if (selected == old_selected && old_selected >= 0) {
531 // Same square. Check for dbl-click.
532 if (reinterpret_cast<GdkEvent *>(event)->type == GDK_2BUTTON_PRESS)
533 edit_npc();
534 }
535 if (event->button == 3)
536 gtk_menu_popup_at_pointer(GTK_MENU(create_popup()),
537 reinterpret_cast<GdkEvent *>(event));
538 return TRUE;
539 }
540
541 /*
542 * Handle mouse button press/release events.
543 */
Mouse_press(GtkWidget * widget,GdkEventButton * event,gpointer data)544 static gint Mouse_press(
545 GtkWidget *widget, // The view window.
546 GdkEventButton *event,
547 gpointer data // ->Npc_chooser.
548 ) {
549 auto *chooser = static_cast<Npc_chooser *>(data);
550 return chooser->mouse_press(widget, event);
551 }
Mouse_release(GtkWidget * widget,GdkEventButton * event,gpointer data)552 static gint Mouse_release(
553 GtkWidget *widget, // The view window.
554 GdkEventButton *event,
555 gpointer data // ->Npc_chooser.
556 ) {
557 ignore_unused_variable_warning(widget, event);
558 auto *chooser = static_cast<Npc_chooser *>(data);
559 chooser->mouse_up();
560 return true;
561 }
562
563 /*
564 * Keystroke in draw-area.
565 */
566 C_EXPORT gboolean
on_npc_draw_key_press(GtkEntry * entry,GdkEventKey * event,gpointer user_data)567 on_npc_draw_key_press(GtkEntry *entry,
568 GdkEventKey *event,
569 gpointer user_data) {
570 ignore_unused_variable_warning(entry, event, user_data);
571 //Npc_chooser *chooser = static_cast<Npc_chooser *>(user_data);
572 return FALSE; // Let parent handle it.
573 }
574
575 /*
576 * Bring up the NPC editor.
577 */
578
edit_npc()579 void Npc_chooser::edit_npc(
580 ) {
581 ExultStudio *studio = ExultStudio::get_instance();
582 int npcnum = info[selected].npcnum;
583 //Estudio_npc &npc = get_npcs()[npcnum];
584 unsigned char buf[Exult_server::maxlength];
585 unsigned char *ptr;
586 ptr = &buf[0];
587 Write2(ptr, npcnum);
588 if (!studio->send_to_server(Exult_server::edit_npc, buf, ptr - buf))
589 cerr << "Error sending data to server." << endl;
590 const gchar *const *locales = g_get_language_names();
591 if (!locales) {
592 cerr << "No locales!" << endl;
593 return;
594 }
595 while (*locales) {
596 cerr << "\"" << *locales << "\"" << endl;
597 locales++;
598 }
599 }
600
601 /*
602 * Update NPC information.
603 */
604
update_npc(int num)605 void Npc_chooser::update_npc(
606 int num
607 ) {
608 static_cast<Npcs_file_info *>(file_info)->read_npc(num);
609 render();
610 update_statusbar();
611 }
612
613 /*
614 * Someone wants the dragged shape.
615 */
616
drag_data_get(GtkWidget * widget,GdkDragContext * context,GtkSelectionData * seldata,guint info,guint time,gpointer data)617 void Npc_chooser::drag_data_get(
618 GtkWidget *widget, // The view window.
619 GdkDragContext *context,
620 GtkSelectionData *seldata, // Fill this in.
621 guint info,
622 guint time,
623 gpointer data // ->Npc_chooser.
624 ) {
625 ignore_unused_variable_warning(widget, context, time);
626 cout << "In DRAG_DATA_GET of Npc for '"
627 << gdk_atom_name(gtk_selection_data_get_target(seldata))
628 << "'" << endl;
629 auto *chooser = static_cast<Npc_chooser *>(data);
630 if (chooser->selected < 0 || info != U7_TARGET_NPCID)
631 return; // Not sure about this.
632 guchar buf[U7DND_DATA_LENGTH(1)];
633 int npcnum = chooser->info[chooser->selected].npcnum;
634 int len = Store_u7_npcid(buf, npcnum);
635 cout << "Setting selection data (" << npcnum << ')' << endl;
636 // Set data.
637 gtk_selection_data_set(seldata,
638 gtk_selection_data_get_target(seldata),
639 8, buf, len);
640 }
641
642 /*
643 * Beginning of a drag.
644 */
645
drag_begin(GtkWidget * widget,GdkDragContext * context,gpointer data)646 gint Npc_chooser::drag_begin(
647 GtkWidget *widget, // The view window.
648 GdkDragContext *context,
649 gpointer data // ->Npc_chooser.
650 ) {
651 ignore_unused_variable_warning(widget);
652 cout << "In DRAG_BEGIN of Npc" << endl;
653 auto *chooser = static_cast<Npc_chooser *>(data);
654 if (chooser->selected < 0)
655 return FALSE; // ++++Display a halt bitmap.
656 // Get ->npc.
657 int npcnum = chooser->info[chooser->selected].npcnum;
658 Estudio_npc &npc = chooser->get_npcs()[npcnum];
659 Shape_frame *shape = chooser->ifile->get_shape(npc.shapenum, 0);
660 if (!shape)
661 return FALSE;
662 chooser->set_drag_icon(context, shape); // Set icon for dragging.
663 return TRUE;
664 }
665
666 /*
667 * Chunk was dropped here.
668 */
669
drag_data_received(GtkWidget * widget,GdkDragContext * context,gint x,gint y,GtkSelectionData * seldata,guint info,guint time,gpointer udata)670 void Npc_chooser::drag_data_received(
671 GtkWidget *widget,
672 GdkDragContext *context,
673 gint x,
674 gint y,
675 GtkSelectionData *seldata,
676 guint info,
677 guint time,
678 gpointer udata // -> Npc_chooser.
679 ) {
680 ignore_unused_variable_warning(widget, context, x, y, info, time);
681 auto *chooser = static_cast<Npc_chooser *>(udata);
682 cout << "In DRAG_DATA_RECEIVED of Npc for '"
683 << gdk_atom_name(gtk_selection_data_get_data_type(seldata))
684 << "'" << endl;
685 if ((gtk_selection_data_get_data_type(seldata) == gdk_atom_intern(U7_TARGET_NPCID_NAME, 0) ||
686 gtk_selection_data_get_data_type(seldata) == gdk_atom_intern(U7_TARGET_DROPFILE_NAME_MIME, 0) ||
687 gtk_selection_data_get_data_type(seldata) == gdk_atom_intern(U7_TARGET_DROPFILE_NAME_MACOSX, 0)) &&
688 Is_u7_npcid(gtk_selection_data_get_data(seldata)) &&
689 gtk_selection_data_get_format(seldata) == 8 &&
690 gtk_selection_data_get_length(seldata) > 0) {
691 int npcnum;
692 Get_u7_npcid(gtk_selection_data_get_data(seldata), npcnum);
693 chooser->group->add(npcnum);
694 chooser->setup_info(true);
695 chooser->render();
696 }
697 }
698
699 /*
700 * Set to accept drops from drag-n-drop of a chunk.
701 */
702
enable_drop()703 void Npc_chooser::enable_drop(
704 ) {
705 if (drop_enabled) // More than once causes warning.
706 return;
707 drop_enabled = true;
708 gtk_widget_realize(draw);//???????
709 GtkTargetEntry tents[3];
710 tents[0].target = const_cast<char *>(U7_TARGET_NPCID_NAME);
711 tents[1].target = const_cast<char *>(U7_TARGET_DROPFILE_NAME_MIME);
712 tents[2].target = const_cast<char *>(U7_TARGET_DROPFILE_NAME_MACOSX);
713 tents[0].flags = 0;
714 tents[1].flags = 0;
715 tents[2].flags = 0;
716 tents[0].info = U7_TARGET_NPCID;
717 tents[1].info = U7_TARGET_NPCID;
718 tents[2].info = U7_TARGET_NPCID;
719 gtk_drag_dest_set(draw, GTK_DEST_DEFAULT_ALL, tents, 3,
720 static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE));
721
722 g_signal_connect(G_OBJECT(draw), "drag-data-received",
723 G_CALLBACK(drag_data_received), this);
724 }
725
726 /*
727 * Scroll to a new shape/frame.
728 */
729
scroll_row_vertical(unsigned newrow)730 void Npc_chooser::scroll_row_vertical(
731 unsigned newrow // Abs. index of row to show.
732 ) {
733 if (newrow >= rows.size())
734 return;
735 row0 = newrow;
736 row0_voffset = 0;
737 render();
738 }
739
740 /*
741 * Scroll to new pixel offset.
742 */
743
scroll_vertical(int newoffset)744 void Npc_chooser::scroll_vertical(
745 int newoffset
746 ) {
747 int delta = newoffset - voffset;
748 while (delta > 0 && row0 < rows.size() - 1) {
749 // Going down.
750 int rowh = rows[row0].height - row0_voffset;
751 if (delta < rowh) {
752 // Part of current row.
753 voffset += delta;
754 row0_voffset += delta;
755 delta = 0;
756 } else {
757 // Go down to next row.
758 voffset += rowh;
759 delta -= rowh;
760 ++row0;
761 row0_voffset = 0;
762 }
763 }
764 while (delta < 0) {
765 if (-delta <= row0_voffset) {
766 voffset += delta;
767 row0_voffset += delta;
768 delta = 0;
769 } else if (row0_voffset) {
770 voffset -= row0_voffset;
771 delta += row0_voffset;
772 row0_voffset = 0;
773 } else {
774 if (row0 == 0)
775 break;
776 --row0;
777 row0_voffset = 0;
778 voffset -= rows[row0].height;
779 delta += rows[row0].height;
780 if (delta > 0) {
781 row0_voffset = delta;
782 voffset += delta;
783 delta = 0;
784 }
785 }
786 }
787 render();
788 update_statusbar();
789 }
790
791 /*
792 * Adjust vertical scroll amounts after laying out shapes.
793 */
794
setup_vscrollbar()795 void Npc_chooser::setup_vscrollbar(
796 ) {
797 GtkAdjustment *adj = gtk_range_get_adjustment(
798 GTK_RANGE(vscroll));
799 gtk_adjustment_set_value(adj, 0);
800 gtk_adjustment_set_lower(adj, 0);
801 gtk_adjustment_set_upper(adj, total_height);
802 gtk_adjustment_set_step_increment(adj, 16); // +++++FOR NOW.
803 gtk_adjustment_set_page_increment(adj, config_height);
804 gtk_adjustment_set_page_size(adj, config_height);
805 g_signal_emit_by_name(G_OBJECT(adj), "changed");
806 }
807
808 /*
809 * Adjust horizontal scroll amounts.
810 */
811
setup_hscrollbar(int newmax)812 void Npc_chooser::setup_hscrollbar(
813 int newmax // New max., or -1 to leave alone.
814 ) {
815 GtkAdjustment *adj = gtk_range_get_adjustment(
816 GTK_RANGE(hscroll));
817 if (newmax > 0)
818 gtk_adjustment_set_upper(adj, newmax);
819 GtkAllocation alloc = {0, 0, 0, 0};
820 gtk_widget_get_allocation(draw, &alloc);
821 gtk_adjustment_set_page_increment(adj, alloc.width);
822 gtk_adjustment_set_page_size(adj, alloc.width);
823 if (gtk_adjustment_get_page_size(adj) > gtk_adjustment_get_upper(adj))
824 gtk_adjustment_set_upper(adj, gtk_adjustment_get_page_size(adj));
825 g_signal_emit_by_name(G_OBJECT(adj), "changed");
826 }
827
828 /*
829 * Handle a scrollbar event.
830 */
831
vscrolled(GtkAdjustment * adj,gpointer data)832 void Npc_chooser::vscrolled( // For vertical scrollbar.
833 GtkAdjustment *adj, // The adjustment.
834 gpointer data // ->Npc_chooser.
835 ) {
836 auto *chooser = static_cast<Npc_chooser *>(data);
837 cout << "Scrolled to " << gtk_adjustment_get_value(adj) << '\n';
838 gint newindex = static_cast<gint>(gtk_adjustment_get_value(adj));
839 chooser->scroll_vertical(newindex);
840 }
hscrolled(GtkAdjustment * adj,gpointer data)841 void Npc_chooser::hscrolled( // For horizontal scrollbar.
842 GtkAdjustment *adj, // The adjustment.
843 gpointer data // ->Npc_chooser.
844 ) {
845 auto *chooser = static_cast<Npc_chooser *>(data);
846 chooser->hoffset = static_cast<gint>(gtk_adjustment_get_value(adj));
847 chooser->render();
848 }
849
850 /*
851 * Handle a change to the 'frame' spin button.
852 */
853
frame_changed(GtkAdjustment * adj,gpointer data)854 void Npc_chooser::frame_changed(
855 GtkAdjustment *adj, // The adjustment.
856 gpointer data // ->Npc_chooser.
857 ) {
858 auto *chooser = static_cast<Npc_chooser *>(data);
859 gint newframe = static_cast<gint>(gtk_adjustment_get_value(adj));
860 if (chooser->selected >= 0) {
861 Npc_entry &npcinfo = chooser->info[chooser->selected];
862 vector<Estudio_npc> &npcs = chooser->get_npcs();
863 int nframes = chooser->ifile->get_num_frames(npcs[npcinfo.npcnum].shapenum);
864 if (newframe >= nframes) // Just checking
865 return;
866 npcinfo.framenum = newframe;
867 if (chooser->frames_mode) // Get sel. frame in view.
868 chooser->scroll_to_frame();
869 chooser->render();
870 chooser->update_statusbar();
871 }
872 }
873
874 /*
875 * 'All frames' toggled.
876 */
877
all_frames_toggled(GtkToggleButton * btn,gpointer data)878 void Npc_chooser::all_frames_toggled(
879 GtkToggleButton *btn,
880 gpointer data
881 ) {
882 auto *chooser = static_cast<Npc_chooser *>(data);
883 if (chooser->info.empty()) return;
884 bool on = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(btn));
885 chooser->frames_mode = on;
886 if (on) // Frame => show horiz. scrollbar.
887 gtk_widget_show(chooser->hscroll);
888 else
889 gtk_widget_hide(chooser->hscroll);
890 // The old index is no longer valid, so we need to remember the shape.
891 int indx = chooser->selected >= 0 ? chooser->selected
892 : static_cast<int>(chooser->rows[chooser->row0].index0);
893 int npcnum = chooser->info[indx].npcnum;
894 chooser->selected = -1;
895 chooser->setup_info();
896 indx = chooser->find_npc(npcnum);
897 if (indx >= 0) // Get back to given shape.
898 chooser->goto_index(indx);
899 }
900
901 /*
902 * Get # shapes we can display.
903 */
904
get_count()905 int Npc_chooser::get_count(
906 ) {
907 return group ? group->size() : get_npcs().size();
908 }
909
910 /*
911 * Get NPC list.
912 */
913
get_npcs()914 vector<Estudio_npc> &Npc_chooser::get_npcs(
915 ) {
916 return static_cast<Npcs_file_info *>(file_info)->get_npcs();
917 }
918
919 /*
920 * Search for an entry.
921 */
922
search(const char * srch,int dir)923 void Npc_chooser::search(
924 const char *srch, // What to search for.
925 int dir // 1 or -1.
926 ) {
927 int total = get_count();
928 if (!total)
929 return; // Empty.
930 vector<Estudio_npc> &npcs = get_npcs();
931 // Start with selection, or top.
932 int start = selected >= 0 ? selected : static_cast<int>(rows[row0].index0);
933 int i;
934 start += dir;
935 int stop = dir == -1 ? -1 : static_cast<int>(info.size());
936 for (i = start; i != stop; i += dir) {
937 unsigned npcnum = info[i].npcnum;
938 const char *nm = npcnum < npcs.size() ?
939 npcs[npcnum].name.c_str() : nullptr;
940 if (nm && search_name(nm, srch))
941 break; // Found it.
942 }
943 if (i == stop)
944 return; // Not found.
945 goto_index(i);
946 select(i);
947 render();
948 }
949
950 /*
951 * Locate NPC on game map.
952 */
953
locate(bool upwards)954 void Npc_chooser::locate(
955 bool upwards
956 ) {
957 ignore_unused_variable_warning(upwards);
958 if (selected < 0)
959 return; // Shouldn't happen.
960 int npcnum = info[selected].npcnum;
961 if (get_npcs()[npcnum].unused) {
962 EStudio::Alert("Npc %d is unused.", npcnum);
963 return;
964 }
965 unsigned char data[Exult_server::maxlength];
966 unsigned char *ptr = &data[0];
967 Write2(ptr, npcnum);
968 ExultStudio *studio = ExultStudio::get_instance();
969 studio->send_to_server(
970 Exult_server::locate_npc, data, ptr - data);
971 }
972
973 /*
974 * Handle popup menu items.
975 */
976
on_npc_popup_edit_activate(GtkMenuItem * item,gpointer udata)977 void on_npc_popup_edit_activate(
978 GtkMenuItem *item,
979 gpointer udata
980 ) {
981 ignore_unused_variable_warning(item);
982 static_cast<Npc_chooser *>(udata)->edit_npc();
983 }
984
985 /*
986 * Set up popup menu for shape browser.
987 */
988
create_popup()989 GtkWidget *Npc_chooser::create_popup(
990 ) {
991 // Create popup with groups, but not files.
992 create_popup_internal(false);
993 if (selected >= 0) { // Add editing choices.
994 Add_menu_item(popup, "Edit...",
995 G_CALLBACK(on_npc_popup_edit_activate),
996 this);
997 }
998 return popup;
999 }
1000
1001 /*
1002 * Create the list.
1003 */
1004
Npc_chooser(Vga_file * i,unsigned char * palbuf,int w,int h,Shape_group * g,Shape_file_info * fi)1005 Npc_chooser::Npc_chooser(
1006 Vga_file *i, // Where they're kept.
1007 unsigned char *palbuf, // Palette, 3*256 bytes (rgb triples).
1008 int w, int h, // Dimensions.
1009 Shape_group *g,
1010 Shape_file_info *fi
1011 ) : Object_browser(g, fi),
1012 Shape_draw(i, palbuf, gtk_drawing_area_new()),
1013 framenum0(0),
1014 info(0), rows(0), row0(0),
1015 row0_voffset(0), total_height(0),
1016 frames_mode(false), hoffset(0),
1017 voffset(0), status_id(-1), drop_enabled(false), sel_changed(nullptr) {
1018 rows.reserve(40);
1019
1020 // Put things in a vert. box.
1021 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1022 gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE);
1023 set_widget(vbox); // This is our "widget"
1024 gtk_widget_show(vbox);
1025
1026 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1027 gtk_box_set_homogeneous(GTK_BOX(hbox), FALSE);
1028 gtk_widget_show(hbox);
1029 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
1030
1031 // A frame looks nice.
1032 GtkWidget *frame = gtk_frame_new(nullptr);
1033 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
1034 widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1035 gtk_widget_show(frame);
1036 gtk_box_pack_start(GTK_BOX(hbox), frame, TRUE, TRUE, 0);
1037
1038 // NOTE: draw is in Shape_draw.
1039 // Indicate the events we want.
1040 gtk_widget_set_events(draw, GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK
1041 | GDK_BUTTON_RELEASE_MASK
1042 | GDK_BUTTON1_MOTION_MASK | GDK_KEY_PRESS_MASK);
1043 // Set "configure" handler.
1044 g_signal_connect(G_OBJECT(draw), "configure-event",
1045 G_CALLBACK(Configure_chooser), this);
1046 // Set "expose-event" - "draw" handler.
1047 g_signal_connect(G_OBJECT(draw), "draw",
1048 G_CALLBACK(expose), this);
1049 // Keystroke.
1050 g_signal_connect(G_OBJECT(draw), "key-press-event",
1051 G_CALLBACK(on_npc_draw_key_press),
1052 this);
1053 gtk_widget_set_can_focus(GTK_WIDGET(draw), TRUE);
1054 // Set mouse click handler.
1055 g_signal_connect(G_OBJECT(draw), "button-press-event",
1056 G_CALLBACK(Mouse_press), this);
1057 g_signal_connect(G_OBJECT(draw), "button-release-event",
1058 G_CALLBACK(Mouse_release), this);
1059 // Mouse motion.
1060 g_signal_connect(G_OBJECT(draw), "drag-begin",
1061 G_CALLBACK(drag_begin), this);
1062 g_signal_connect(G_OBJECT(draw), "motion-notify-event",
1063 G_CALLBACK(drag_motion), this);
1064 g_signal_connect(G_OBJECT(draw), "drag-data-get",
1065 G_CALLBACK(drag_data_get), this);
1066 gtk_container_add(GTK_CONTAINER(frame), draw);
1067 widget_set_margins(draw, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1068 gtk_widget_set_size_request(draw, w, h);
1069 gtk_widget_show(draw);
1070 // Want vert. scrollbar for the shapes.
1071 GtkAdjustment *shape_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0,
1072 std::ceil(get_count() / 4.0), 1, 1, 1));
1073 vscroll = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, GTK_ADJUSTMENT(shape_adj));
1074 gtk_box_pack_start(GTK_BOX(hbox), vscroll, FALSE, TRUE, 0);
1075 // Set scrollbar handler.
1076 g_signal_connect(G_OBJECT(shape_adj), "value-changed",
1077 G_CALLBACK(vscrolled), this);
1078 gtk_widget_show(vscroll);
1079 // Horizontal scrollbar.
1080 shape_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, 1600, 8, 16, 16));
1081 hscroll = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, GTK_ADJUSTMENT(shape_adj));
1082 gtk_box_pack_start(GTK_BOX(vbox), hscroll, FALSE, TRUE, 0);
1083 // Set scrollbar handler.
1084 g_signal_connect(G_OBJECT(shape_adj), "value-changed",
1085 G_CALLBACK(hscrolled), this);
1086 //++++ gtk_widget_hide(hscroll); // Only shown in 'frames' mode.
1087
1088 // At the bottom, status bar & frame:
1089 GtkWidget *hbox1 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1090 gtk_box_set_homogeneous(GTK_BOX(hbox1), FALSE);
1091 gtk_box_pack_start(GTK_BOX(vbox), hbox1, FALSE, FALSE, 0);
1092 gtk_widget_show(hbox1);
1093 // At left, a status bar.
1094 sbar = gtk_statusbar_new();
1095 sbar_sel = gtk_statusbar_get_context_id(GTK_STATUSBAR(sbar),
1096 "selection");
1097 gtk_box_pack_start(GTK_BOX(hbox1), sbar, TRUE, TRUE, 0);
1098 widget_set_margins(sbar, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1099 gtk_widget_show(sbar);
1100 GtkWidget *label = gtk_label_new("Frame:");
1101 gtk_box_pack_start(GTK_BOX(hbox1), label, FALSE, FALSE, 0);
1102 widget_set_margins(label, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1103 gtk_widget_show(label);
1104 // A spin button for frame#.
1105 frame_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0,
1106 16, 1,
1107 0, 0.0));
1108 fspin = gtk_spin_button_new(GTK_ADJUSTMENT(frame_adj),
1109 1, 0);
1110 g_signal_connect(G_OBJECT(frame_adj), "value-changed",
1111 G_CALLBACK(frame_changed), this);
1112 gtk_box_pack_start(GTK_BOX(hbox1), fspin, FALSE, FALSE, 0);
1113 widget_set_margins(fspin, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1114 gtk_widget_show(fspin);
1115 // A toggle for 'All Frames'.
1116 GtkWidget *allframes = gtk_toggle_button_new_with_label("Frames");
1117 gtk_box_pack_start(GTK_BOX(hbox1), allframes, FALSE, FALSE, 0);
1118 widget_set_margins(allframes, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
1119 gtk_widget_show(allframes);
1120 g_signal_connect(G_OBJECT(allframes), "toggled",
1121 G_CALLBACK(all_frames_toggled), this);
1122 // Add search controls to bottom.
1123 gtk_box_pack_start(GTK_BOX(vbox),
1124 create_controls(find_controls | locate_controls),
1125 FALSE, FALSE, 0);
1126 red = ExultStudio::get_instance()->find_palette_color(63, 5, 5);
1127 }
1128
1129 /*
1130 * Delete.
1131 */
1132
~Npc_chooser()1133 Npc_chooser::~Npc_chooser(
1134 ) {
1135 gtk_widget_destroy(get_widget());
1136 }
1137
1138 /*
1139 * Unselect.
1140 */
1141
unselect(bool need_render)1142 void Npc_chooser::unselect(
1143 bool need_render // 1 to render and show.
1144 ) {
1145 if (selected >= 0) {
1146 selected = -1;
1147 if (need_render) {
1148 render();
1149 }
1150 if (sel_changed) // Tell client.
1151 (*sel_changed)();
1152 }
1153 update_statusbar();
1154 }
1155
1156 /*
1157 * Show selection or range in window.
1158 */
1159
update_statusbar()1160 void Npc_chooser::update_statusbar(
1161 ) {
1162 char buf[150];
1163 if (status_id >= 0) // Remove prev. selection msg.
1164 gtk_statusbar_remove(GTK_STATUSBAR(sbar), sbar_sel, status_id);
1165 if (selected >= 0) {
1166 int npcnum = info[selected].npcnum;
1167 Estudio_npc &npc = get_npcs()[npcnum];
1168 g_snprintf(buf, sizeof(buf), "Npc %d: '%s'%s",
1169 npcnum, npc.name.c_str(),
1170 npc.unused ? " (unused)" : "");
1171 status_id = gtk_statusbar_push(GTK_STATUSBAR(sbar),
1172 sbar_sel, buf);
1173 } else if (!info.empty() && !group) {
1174 g_snprintf(buf, sizeof(buf), "NPCs %d to %d",
1175 info[rows[row0].index0].npcnum, last_npc);
1176 status_id = gtk_statusbar_push(GTK_STATUSBAR(sbar),
1177 sbar_sel, buf);
1178 } else
1179 status_id = -1;
1180 }
1181
1182
1183