1 /*
2 ** A GTK widget showing a list of shapes from an image file.
3 **
4 ** Written: 7/25/99 - JSF
5 **/
6
7 /*
8 Copyright (C) 1999-2020 Jeffrey S. Freedman
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 "shapelst.h"
30
31 #include "databuf.h"
32 #include "fontgen.h"
33 #include "frnameinf.h"
34 #include "ibuf8.h"
35 #include "items.h"
36 #include "pngio.h"
37 #include "shapefile.h"
38 #include "shapegroup.h"
39 #include "shapevga.h"
40 #include "studio.h"
41 #include "u7drag.h"
42 #include "utils.h"
43
44 #include <glib.h>
45 #include <sys/stat.h>
46 #include <unistd.h>
47
48 #include <cmath>
49 #include <cstdlib>
50 #include <iostream>
51 #include <memory>
52
53 #ifdef _WIN32
54 #include <windows.h>
55 #endif
56
57 using EStudio::Add_menu_item;
58 using EStudio::Alert;
59 using EStudio::Prompt;
60 using std::cout;
61 using std::cerr;
62 using std::endl;
63 using std::ifstream;
64 using std::make_unique;
65 using std::string;
66 using std::strlen;
67 using std::unique_ptr;
68
69 std::vector<std::unique_ptr<Editing_file>> Shape_chooser::editing_files;
70 int Shape_chooser::check_editing_timer = -1;
71
72 #define IS_FLAT(shnum) ((shnum) < c_first_obj_shape)
73
74 /*
75 * Here's a description of a file being edited by an external program
76 * like Gimp or Photoshop.
77 */
78 class Editing_file {
79 string vga_basename; // Name of image file this comes from.
80 string pathname; // Full path to file.
81 time_t mtime; // Last modification time.
82 int shapenum, framenum; // Shape/frame.
83 int tiles; // If > 0, #8x8 tiles per row or col.
84 bool bycolumns; // If true tile by column first.
85 public:
86 friend class Shape_chooser;
87 // Create for single frame:
Editing_file(const char * vganm,const char * pnm,time_t m,int sh,int fr)88 Editing_file(const char *vganm, const char *pnm, time_t m,
89 int sh, int fr)
90 : vga_basename(vganm), pathname(pnm),
91 mtime(m), shapenum(sh), framenum(fr), tiles(0)
92 { }
93 // Create tiled:
Editing_file(const char * vganm,const char * pnm,time_t m,int sh,int ts,bool bycol)94 Editing_file(const char *vganm, const char *pnm, time_t m, int sh,
95 int ts, bool bycol)
96 : vga_basename(vganm), pathname(pnm), mtime(m), shapenum(sh),
97 framenum(0), tiles(ts), bycolumns(bycol)
98 { }
99 };
100
101 /*
102 * Callback for when a shape is dropped on our draw area.
103 */
104
Shape_dropped_here(int file,int shape,int frame,void * udata)105 static void Shape_dropped_here(
106 int file, // U7_SHAPE_SHAPES.
107 int shape,
108 int frame,
109 void *udata
110 ) {
111 static_cast<Shape_chooser *>(udata)->shape_dropped_here(file, shape, frame);
112 }
113
114 /*
115 * Blit onto screen.
116 */
117
show(int x,int y,int w,int h)118 void Shape_chooser::show(
119 int x, int y, int w, int h // Area to blit.
120 ) {
121 Shape_draw::show(x, y, w, h);
122 if ((selected >= 0) && (drawgc != nullptr)) { // Show selected.
123 TileRect b = info[selected].box;
124 // Draw yellow box.
125 cairo_set_line_width(drawgc, 1.0);
126 cairo_set_source_rgb(drawgc,
127 ((drawfg >> 16) & 255) / 255.0,
128 ((drawfg >> 8) & 255) / 255.0,
129 (drawfg & 255) / 255.0);
130 cairo_rectangle(drawgc, b.x - hoffset, b.y - voffset, b.w, b.h);
131 cairo_stroke(drawgc);
132 }
133 }
134
135 /*
136 * Send selected shape/frame to Exult.
137 */
138
tell_server_shape()139 void Shape_chooser::tell_server_shape(
140 ) {
141 int shnum = -1;
142 int frnum = 0;
143 if (selected >= 0) {
144 shnum = info[selected].shapenum;
145 frnum = info[selected].framenum;
146 }
147 unsigned char buf[Exult_server::maxlength];
148 unsigned char *ptr = &buf[0];
149 Write2(ptr, shnum);
150 Write2(ptr, frnum);
151 ExultStudio *studio = ExultStudio::get_instance();
152 studio->send_to_server(Exult_server::set_edit_shape, buf, ptr - buf);
153 }
154
155 /*
156 * Select an entry. This should be called after rendering
157 * the shape.
158 */
159
select(int new_sel)160 void Shape_chooser::select(
161 int new_sel
162 ) {
163 selected = new_sel;
164 tell_server_shape(); // Tell Exult.
165 int shapenum = info[selected].shapenum;
166 // Update spin-button value, range.
167 gtk_widget_set_sensitive(fspin, true);
168 int nframes = ifile->get_num_frames(shapenum);
169 gtk_adjustment_set_upper(frame_adj, nframes - 1);
170 gtk_adjustment_set_value(frame_adj, info[selected].framenum);
171 gtk_widget_set_sensitive(fspin, true);
172 update_statusbar();
173 }
174
175 const int border = 4; // Border at bottom, sides.
176 /*
177 * Render as many shapes as fit in the shape chooser window.
178 */
179
render()180 void Shape_chooser::render(
181 ) {
182 // Get drawing area dimensions.
183 GtkAllocation alloc = {0, 0, 0, 0};
184 gtk_widget_get_allocation(draw, &alloc);
185 gint winh = alloc.height;
186 // Clear window first.
187 iwin->fill8(255); // Set to background_color.
188 int curr_y = -row0_voffset;
189 //int total_cnt = get_count();
190 // filter (group).
191 for (unsigned rownum = row0; curr_y < winh && rownum < rows.size();
192 ++rownum) {
193 Shape_row &row = rows[rownum];
194 unsigned cols = get_num_cols(rownum);
195 for (unsigned index = row.index0; cols; --cols, ++index) {
196 int shapenum = info[index].shapenum;
197 int framenum = info[index].framenum;
198 Shape_frame *shape = ifile->get_shape(shapenum,
199 framenum);
200 if (shape) {
201 int sx = info[index].box.x - hoffset;
202 int sy = info[index].box.y - voffset;
203 shape->paint(iwin, sx + shape->get_xleft(),
204 sy + shape->get_yabove());
205 last_shape = shapenum;
206 }
207 }
208 curr_y += rows[rownum].height;
209 }
210 gtk_widget_queue_draw(draw);
211 }
212
213 /*
214 * Get maximum shape height for all its frames.
215 */
216
Get_max_height(Shape * shape)217 static int Get_max_height(
218 Shape *shape
219 ) {
220 int cnt = shape->get_num_frames();
221 int maxh = 0;
222 for (int i = 0; i < cnt; i++) {
223 Shape_frame *frame = shape->get_frame(i);
224 if (!frame) {
225 continue;
226 }
227 int ht = frame->get_height();
228 if (ht > maxh)
229 maxh = ht;
230 }
231 return maxh;
232 }
233
234 /*
235 * Get the x-offset in pixels where a frame will be drawn.
236 *
237 * Output: Offset from left edge of (virtual) drawing area.
238 */
239
Get_x_offset(Shape * shape,int framenum)240 static int Get_x_offset(
241 Shape *shape,
242 int framenum
243 ) {
244 if (!shape)
245 return 0;
246 int nframes = shape->get_num_frames();
247 if (framenum >= nframes)
248 framenum = nframes - 1;
249 int xoff = 0;
250 for (int i = 0; i < framenum; i++) {
251 Shape_frame *fr = shape->get_frame(i);
252 if (fr) {
253 xoff += fr->get_width() + border;
254 }
255 }
256 return xoff;
257 }
258
259 /*
260 * Find where everything goes.
261 */
262
setup_info(bool savepos)263 void Shape_chooser::setup_info(
264 bool savepos // Try to keep current position.
265 ) {
266 unsigned oldind = rows[row0].index0;
267 info.resize(0);
268 rows.resize(0);
269 row0 = row0_voffset = 0;
270 last_shape = 0;
271 /* +++++NOTE: index0 is always 0 for the shape browse. It should
272 probably be removed from the base Obj_browser class */
273 index0 = 0;
274 voffset = 0;
275 total_height = 0;
276 if (frames_mode)
277 setup_frames_info();
278 else
279 setup_shapes_info();
280 setup_vscrollbar();
281 if (savepos)
282 goto_index(oldind);
283 }
284
285 /*
286 * Setup info when not in 'frames' mode.
287 */
288
setup_shapes_info()289 void Shape_chooser::setup_shapes_info(
290 ) {
291 int selshape = -1;
292 int selframe = -1;
293 if (selected >= 0) { // Save selection info.
294 selshape = info[selected].shapenum;
295 selframe = info[selected].framenum;
296 }
297 // Get drawing area dimensions.
298 GtkAllocation alloc = {0, 0, 0, 0};
299 gtk_widget_get_allocation(draw, &alloc);
300 gint winw = alloc.width;
301 int x = 0;
302 int curr_y = 0;
303 int row_h = 0;
304 int total_cnt = get_count();
305 // filter (group).
306 rows.resize(1); // Start 1st row.
307 rows[0].index0 = 0;
308 rows[0].y = 0;
309 for (int index = 0; index < total_cnt; index++) {
310 int shapenum = group ? (*group)[index] : index;
311 int framenum = shapenum == selshape ? selframe : framenum0;
312 Shape_frame *shape = ifile->get_shape(shapenum, framenum);
313 if (!shape)
314 continue;
315 int sh = shape->get_height();
316 int sw = shape->get_width();
317 // Check if we've exceeded max width
318 if (x + sw > winw && x) { // But don't leave row empty.
319 // Next line.
320 rows.back().height = row_h + border;
321 curr_y += row_h + border;
322 row_h = 0;
323 x = 0;
324 rows.emplace_back();
325 rows.back().index0 = info.size();
326 rows.back().y = curr_y;
327 }
328 if (sh > row_h)
329 row_h = sh;
330 int sy = curr_y + border; // Get top y-coord.
331 // Store info. about where drawn.
332 info.emplace_back();
333 info.back().set(shapenum, framenum, x, sy, sw, sh);
334 x += sw + border;
335 }
336 rows.back().height = row_h + border;
337 total_height = curr_y + rows.back().height + border;
338 }
339
340 /*
341 * Setup one shape per row, showing its frames from left to right.
342 */
343
setup_frames_info()344 void Shape_chooser::setup_frames_info(
345 ) {
346 // Get drawing area dimensions.
347 int curr_y = 0;
348 int maxw = 0;
349 unsigned total_cnt = get_count();
350 // filter (group).
351 for (unsigned index = index0; index < total_cnt; index++) {
352 int shapenum = group ? (*group)[index] : index;
353 // Get all frames.
354 Shape *shape = ifile->extract_shape(shapenum);
355 int nframes = shape ? shape->get_num_frames() : 0;
356 if (!nframes)
357 continue;
358 int row_h = Get_max_height(shape);
359 rows.emplace_back();
360 rows.back().index0 = info.size();
361 rows.back().y = curr_y;
362 rows.back().height = row_h + border;
363 int x = 0;
364 int sw;
365 int sh;
366 for (int framenum = 0; framenum < nframes; framenum++,
367 x += sw + border) {
368 Shape_frame *frame = shape->get_frame(framenum);
369 if (!frame) {
370 sw = 0;
371 continue;
372 }
373 sh = frame->get_height();
374 sw = frame->get_width();
375 int sy = curr_y + border; // Get top y-coord.
376 // Store info. about where drawn.
377 info.emplace_back();
378 info.back().set(shapenum, framenum, x, sy, sw, sh);
379 }
380 if (x > maxw)
381 maxw = x;
382 // Next line.
383 curr_y += row_h + border;
384 }
385 total_height = curr_y + border;
386 setup_hscrollbar(maxw);
387 }
388
389 /*
390 * Horizontally scroll so that the selected frame is visible (in frames
391 * mode).
392 */
393
scroll_to_frame()394 void Shape_chooser::scroll_to_frame(
395 ) {
396 if (selected >= 0) { // Save selection info.
397 int selshape = info[selected].shapenum;
398 int selframe = info[selected].framenum;
399 Shape *shape = ifile->extract_shape(selshape);
400 int xoff = Get_x_offset(shape, selframe);
401 if (xoff < hoffset) // Left of visual area?
402 hoffset = xoff > border ? xoff - border : 0;
403 else {
404 GtkAllocation alloc = {0, 0, 0, 0};
405 gtk_widget_get_allocation(draw, &alloc);
406 gint winw = alloc.width;
407 Shape_frame *fr = shape->get_frame(selframe);
408 if (fr) {
409 int sw = fr->get_width();
410 if (xoff + sw + border - hoffset > winw)
411 hoffset = xoff + sw + border - winw;
412 }
413 }
414 GtkAdjustment *adj = gtk_range_get_adjustment(
415 GTK_RANGE(hscroll));
416 gtk_adjustment_set_value(adj, hoffset);
417 }
418 }
419
420 /*
421 * Scroll so a desired index is in view.
422 */
423
goto_index(unsigned index)424 void Shape_chooser::goto_index(
425 unsigned index // Desired index in 'info'.
426 ) {
427 if (index >= info.size())
428 return; // Illegal index or empty chooser.
429 Shape_entry &inf = info[index]; // Already in view?
430 unsigned midx = inf.box.x + inf.box.w / 2;
431 unsigned midy = inf.box.y + inf.box.h / 2;
432 TileRect winrect(hoffset, voffset, config_width, config_height);
433 if (winrect.has_point(midx, midy))
434 return;
435 unsigned start = 0;
436 unsigned count = rows.size();
437 while (count > 1) { // Binary search.
438 unsigned mid = start + count / 2;
439 if (index < rows[mid].index0)
440 count = mid - start;
441 else {
442 count = (start + count) - mid;
443 start = mid;
444 }
445 }
446 if (start < rows.size()) {
447 // Get to right spot again!
448 GtkAdjustment *adj = gtk_range_get_adjustment(
449 GTK_RANGE(vscroll));
450 gtk_adjustment_set_value(adj, rows[start].y);
451 }
452 }
453
454 /*
455 * Find an index (not necessarily the 1st) for a given shape #.
456 */
457
find_shape(int shnum)458 int Shape_chooser::find_shape(
459 int shnum
460 ) {
461 if (group) { // They're not ordered.
462 unsigned cnt = info.size();
463 for (unsigned i = 0; i < cnt; ++i)
464 if (info[i].shapenum == shnum)
465 return i;
466 return -1;
467 }
468 unsigned start = 0;
469 unsigned count = info.size();
470 while (count > 1) { // Binary search.
471 unsigned mid = start + count / 2;
472 if (shnum < info[mid].shapenum)
473 count = mid - start;
474 else {
475 count = (start + count) - mid;
476 start = mid;
477 }
478 }
479 if (start < info.size())
480 return start;
481 else
482 return -1;
483 }
484
485 /*
486 * Configure the viewing window.
487 */
488
Configure_chooser(GtkWidget * widget,GdkEventConfigure * event,gpointer data)489 static gint Configure_chooser(
490 GtkWidget *widget, // The drawing area.
491 GdkEventConfigure *event,
492 gpointer data // ->Shape_chooser
493 ) {
494 ignore_unused_variable_warning(widget);
495 auto *chooser = static_cast<Shape_chooser *>(data);
496 return chooser->configure(event);
497 }
configure(GdkEventConfigure * event)498 gint Shape_chooser::configure(
499 GdkEventConfigure *event
500 ) {
501 Shape_draw::configure();
502 // Did the size change?
503 if (event->width != config_width || event->height != config_height) {
504 config_width = event->width;
505 config_height = event->height;
506 setup_info(true);
507 render();
508 update_statusbar();
509 } else
510 render(); // Same size? Just render it.
511 // Set handler for shape dropped here,
512 // BUT not more than once.
513 if (drop_callback != Shape_dropped_here)
514 enable_drop(Shape_dropped_here, this);
515 return FALSE;
516 }
517
518 /*
519 * Handle an expose event.
520 */
521
expose(GtkWidget * widget,cairo_t * cairo,gpointer data)522 gint Shape_chooser::expose(
523 GtkWidget *widget, // The view window.
524 cairo_t *cairo,
525 gpointer data // ->Shape_chooser.
526 ) {
527 ignore_unused_variable_warning(widget);
528 auto *chooser = static_cast<Shape_chooser *>(data);
529 chooser->set_graphic_context(cairo);
530 GdkRectangle area = { 0, 0, 0, 0 };
531 gdk_cairo_get_clip_rectangle(cairo, &area);
532 chooser->show(area.x, area.y, area.width, area.height);
533 chooser->set_graphic_context(nullptr);
534 return TRUE;
535 }
536
537 /*
538 * Handle a mouse drag event.
539 */
540
drag_motion(GtkWidget * widget,GdkEventMotion * event,gpointer data)541 gint Shape_chooser::drag_motion(
542 GtkWidget *widget, // The view window.
543 GdkEventMotion *event,
544 gpointer data // ->Shape_chooser.
545 ) {
546 ignore_unused_variable_warning(widget);
547 auto *chooser = static_cast<Shape_chooser *>(data);
548 if (!chooser->dragging && chooser->selected >= 0)
549 chooser->start_drag(U7_TARGET_SHAPEID_NAME,
550 U7_TARGET_SHAPEID, reinterpret_cast<GdkEvent *>(event));
551 return true;
552 }
553
554 /*
555 * Handle a mouse button-press event.
556 */
mouse_press(GtkWidget * widget,GdkEventButton * event)557 gint Shape_chooser::mouse_press(
558 GtkWidget *widget, // The view window.
559 GdkEventButton *event
560 ) {
561 gtk_widget_grab_focus(widget);
562
563 if (event->button == 4) {
564 if (row0 > 0)
565 scroll_row_vertical(row0 - 1);
566 return TRUE;
567 } else if (event->button == 5) {
568 scroll_row_vertical(row0 + 1);
569 return TRUE;
570 }
571 int old_selected = selected;
572 int new_selected = -1;
573 unsigned i; // Search through entries.
574 unsigned infosz = info.size();
575 int absx = static_cast<int>(event->x) + hoffset;
576 int absy = static_cast<int>(event->y) + voffset;
577 for (i = rows[row0].index0; i < infosz; i++) {
578 if (info[i].box.distance(absx, absy) <= 2) {
579 // Found the box?
580 // Indicate we can drag.
581 new_selected = i;
582 break;
583 } else if (info[i].box.y - voffset >= config_height)
584 break; // Past bottom of screen.
585 }
586 if (new_selected >= 0) {
587 select(new_selected);
588 render();
589 if (sel_changed) // Tell client.
590 (*sel_changed)();
591 }
592 if (new_selected < 0 && event->button == 1)
593 unselect(true); // No selection.
594 else if (selected == old_selected && old_selected >= 0) {
595 // Same square. Check for dbl-click.
596 if (reinterpret_cast<GdkEvent *>(event)->type == GDK_2BUTTON_PRESS)
597 edit_shape_info();
598 }
599 if (event->button == 3)
600 gtk_menu_popup_at_pointer(GTK_MENU(create_popup()),
601 reinterpret_cast<GdkEvent *>(event));
602 return TRUE;
603 }
604
605 /*
606 * Handle mouse button press/release events.
607 */
Mouse_press(GtkWidget * widget,GdkEventButton * event,gpointer data)608 static gint Mouse_press(
609 GtkWidget *widget, // The view window.
610 GdkEventButton *event,
611 gpointer data // ->Shape_chooser.
612 ) {
613 auto *chooser = static_cast<Shape_chooser *>(data);
614 return chooser->mouse_press(widget, event);
615 }
Mouse_release(GtkWidget * widget,GdkEventButton * event,gpointer data)616 static gint Mouse_release(
617 GtkWidget *widget, // The view window.
618 GdkEventButton *event,
619 gpointer data // ->Shape_chooser.
620 ) {
621 ignore_unused_variable_warning(widget, event);
622 auto *chooser = static_cast<Shape_chooser *>(data);
623 chooser->mouse_up();
624 return true;
625 }
626
627 /*
628 * Keystroke in draw-area.
629 */
630 C_EXPORT gboolean
on_draw_key_press(GtkEntry * entry,GdkEventKey * event,gpointer user_data)631 on_draw_key_press(GtkEntry *entry,
632 GdkEventKey *event,
633 gpointer user_data) {
634 ignore_unused_variable_warning(entry);
635 auto *chooser = static_cast<Shape_chooser *>(user_data);
636 switch (event->keyval) {
637 case GDK_KEY_Delete:
638 chooser->del_frame();
639 return TRUE;
640 case GDK_KEY_Insert:
641 chooser->new_frame();
642 return TRUE;
643 }
644 return FALSE; // Let parent handle it.
645 }
646
647 const unsigned char transp = 255;
648
649 /*
650 * Export the currently selected frame as a .png file.
651 *
652 * Output: Modification time of file written, or 0 if error.
653 */
654
export_png(const char * fname)655 time_t Shape_chooser::export_png(
656 const char *fname // File to write out.
657 ) {
658 int shnum = info[selected].shapenum;
659 int frnum = info[selected].framenum;
660 Shape_frame *frame = ifile->get_shape(shnum, frnum);
661 int w = frame->get_width();
662 int h = frame->get_height();
663 Image_buffer8 img(w, h); // Render into a buffer.
664 img.fill8(transp); // Fill with transparent pixel.
665 frame->paint(&img, frame->get_xleft(), frame->get_yabove());
666 int xoff = 0;
667 int yoff = 0;
668 if (frame->is_rle()) {
669 xoff = -frame->get_xright();
670 yoff = -frame->get_ybelow();
671 }
672 return export_png(fname, img, xoff, yoff);
673 }
674
675 /*
676 * Convert a GDK color map to a 3*256 byte RGB palette.
677 */
678
Get_rgb_palette(ExultRgbCmap * palette,unsigned char * buf)679 static void Get_rgb_palette(
680 ExultRgbCmap *palette,
681 unsigned char *buf // 768 bytes (3*256).
682 ) {
683 for (int i = 0; i < 256; i++) {
684 buf[3 * i] = (palette->colors[i] >> 16) & 0xff;
685 buf[3 * i + 1] = (palette->colors[i] >> 8) & 0xff;
686 buf[3 * i + 2] = palette->colors[i] & 0xff;
687 }
688 }
689
690 /*
691 * Export an image as a .png file.
692 *
693 * Output: Modification time of file written, or 0 if error.
694 */
695
export_png(const char * fname,Image_buffer8 & img,int xoff,int yoff)696 time_t Shape_chooser::export_png(
697 const char *fname, // File to write out.
698 Image_buffer8 &img, // Image.
699 int xoff, int yoff // Offset (from bottom-right).
700 ) {
701 unsigned char pal[3 * 256]; // Set up palette.
702 Get_rgb_palette(palette, pal);
703 int w = img.get_width();
704 int h = img.get_height();
705 struct stat fs; // Write out to the .png.
706 // (Rotate transp. pixel to 0 for the
707 // Gimp's sake.)
708 if (!Export_png8(fname, transp, w, h, w, xoff, yoff, img.get_bits(),
709 &pal[0], 256, true) ||
710 stat(fname, &fs) != 0) {
711 Alert("Error creating '%s'", fname);
712 return 0;
713 }
714 return fs.st_mtime;
715 }
716
717 /*
718 * Export the current shape (which better be 8x8 flat) as a tiled PNG.
719 *
720 * Output: modification time of file, or 0 if error.
721 */
722
export_tiled_png(const char * fname,int tiles,bool bycols)723 time_t Shape_chooser::export_tiled_png(
724 const char *fname, // File to write out.
725 int tiles, // If #0, write all frames as tiles,
726 // this many in each row (col).
727 bool bycols // Write tiles columns-first.
728 ) {
729 assert(selected >= 0);
730 int shnum = info[selected].shapenum;
731 ExultStudio *studio = ExultStudio::get_instance();
732 // Low shape in 'shapes.vga'?
733 assert(IS_FLAT(shnum) && file_info == studio->get_vgafile());
734 Shape *shape = ifile->extract_shape(shnum);
735 assert(shape != nullptr);
736 cout << "Writing " << fname << " tiled"
737 << (bycols ? ", by cols" : ", by rows") << " first" << endl;
738 int nframes = shape->get_num_frames();
739 // Figure #tiles in other dim.
740 int dim1_cnt = (nframes + tiles - 1) / tiles;
741 int w;
742 int h;
743 if (bycols) {
744 h = tiles * c_tilesize;
745 w = dim1_cnt * c_tilesize;
746 } else {
747 w = tiles * c_tilesize;
748 h = dim1_cnt * c_tilesize;
749 }
750 Image_buffer8 img(w, h);
751 img.fill8(transp); // Fill with transparent pixel.
752 for (int f = 0; f < nframes; f++) {
753 Shape_frame *frame = shape->get_frame(f);
754 if (!frame)
755 continue; // We'll just leave empty ones blank.
756 if (frame->is_rle() || frame->get_width() != c_tilesize ||
757 frame->get_height() != c_tilesize) {
758 char buf[250];
759 snprintf(buf, sizeof(buf), "Can only tile %dx%d flat shapes",
760 c_tilesize, c_tilesize);
761 Alert("%s", buf);
762 return 0;
763 }
764 int x;
765 int y;
766 if (bycols) {
767 y = f % tiles;
768 x = f / tiles;
769 } else {
770 x = f % tiles;
771 y = f / tiles;
772 }
773 frame->paint(&img, x * c_tilesize + frame->get_xleft(),
774 y * c_tilesize + frame->get_yabove());
775 }
776 // Write out to the .png.
777 return export_png(fname, img, 0, 0);
778 }
779
780 /*
781 * Bring up the shape-info editor for the selected shape.
782 */
783
edit_shape_info()784 void Shape_chooser::edit_shape_info(
785 ) {
786 ExultStudio *studio = ExultStudio::get_instance();
787 int shnum = info[selected].shapenum;
788 int frnum = info[selected].framenum;
789 Shape_info *info = nullptr;
790 const char *name = nullptr;
791 if (shapes_file) {
792 // Read info. the first time.
793 if (shapes_file->read_info(studio->get_game_type(), true))
794 studio->set_shapeinfo_modified();
795 if (!shapes_file->has_info(shnum))
796 shapes_file->set_info(shnum, Shape_info());
797 info = &shapes_file->get_info(shnum);
798 name = studio->get_shape_name(shnum);
799 }
800 studio->open_shape_window(shnum, frnum, file_info, name, info);
801 }
802
803 /*
804 * Bring up the user's image-editor for the selected shape.
805 */
806
edit_shape(int tiles,bool bycols)807 void Shape_chooser::edit_shape(
808 int tiles, // If #0, write all frames as tiles,
809 // this many in each row (col).
810 bool bycols // Write tiles columns-first.
811 ) {
812 ExultStudio *studio = ExultStudio::get_instance();
813 int shnum = info[selected].shapenum;
814 int frnum = info[selected].framenum;
815 string filestr("<SAVEGAME>"); // Set up filename.
816 filestr += "/itmp"; // "Image tmp" directory.
817 U7mkdir(filestr.c_str(), 0755); // Create if not already there.
818 // Lookup <SAVEGAME>.
819 filestr = get_system_path(filestr);
820 char *ext;
821 if (!tiles) // Create name from file,shape,frame.
822 ext = g_strdup_printf("/%s.s%d_f%d.png",
823 file_info->get_basename(), shnum, frnum);
824 else // Tiled.
825 ext = g_strdup_printf("/%s.s%d_%c%d.png",
826 file_info->get_basename(), shnum,
827 (bycols ? 'c' : 'r'), tiles);
828 filestr += ext;
829 g_free(ext);
830 const char *fname = filestr.c_str();
831 cout << "Writing image '" << fname << "'" << endl;
832 time_t mtime;
833 if (!tiles) { // One frame?
834 mtime = export_png(fname);
835 if (!mtime)
836 return;
837 // Store info. about file.
838 editing_files.push_back(std::make_unique<Editing_file>(
839 file_info->get_basename(), fname, mtime, shnum, frnum));
840 } else {
841 mtime = export_tiled_png(fname, tiles, bycols);
842 if (!mtime)
843 return;
844 editing_files.push_back(std::make_unique<Editing_file>(
845 file_info->get_basename(), fname, mtime, shnum,
846 tiles, bycols));
847 }
848 string cmd(studio->get_image_editor());
849 cmd += ' ';
850 string imgpath;
851 #ifdef _WIN32
852 if (fname[0] == '.' && (fname[1] == '\\' || fname[1] == '/')) {
853 char currdir[MAX_PATH];
854 GetCurrentDirectory(MAX_PATH, currdir);
855 imgpath += currdir;
856 if (imgpath[imgpath.length() - 1] != '\\')
857 imgpath += '\\';
858 }
859 #endif
860 imgpath += fname;
861 // handle spaces in path.
862 if (imgpath.find(' ') != string::npos)
863 imgpath = "\"" + imgpath + "\"";
864 cmd += imgpath;
865 #ifndef _WIN32
866 cmd += " &"; // Background.
867 int ret = system(cmd.c_str());
868 if (ret == 127 || ret == -1)
869 Alert("Can't launch '%s'", studio->get_image_editor());
870 #else
871 for (char &ch : cmd)
872 if (ch == '/')
873 ch = '\\';
874 PROCESS_INFORMATION pi;
875 STARTUPINFO si;
876 std::memset(&si, 0, sizeof(si));
877 si.cb = sizeof(si);
878 int ret = CreateProcess(nullptr, &cmd[0],
879 nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
880 if (!ret)
881 Alert("Can't launch '%s'", studio->get_image_editor());
882 #endif
883 if (check_editing_timer == -1) // Monitor files every 6 seconds.
884 check_editing_timer = g_timeout_add(6000,
885 Shape_chooser::check_editing_files_cb, nullptr);
886 }
887
888 /*
889 * Check the list of files being edited externally, and read in any that
890 * have changed.
891 *
892 * Output: 1 always.
893 */
894
check_editing_files_cb(gpointer data)895 gint Shape_chooser::check_editing_files_cb(
896 gpointer data
897 ) {
898 ignore_unused_variable_warning(data);
899 ExultStudio *studio = ExultStudio::get_instance();
900 // Is focus in main window?
901 if (studio->has_focus())
902 check_editing_files();
903 return 1; // Keep repeating.
904 }
905 // This one doesn't check focus.
check_editing_files()906 gint Shape_chooser::check_editing_files(
907 ) {
908 bool modified = false;
909 for (auto &ed : editing_files) {
910 struct stat fs; // Check mod. time of file.
911 if (stat(ed->pathname.c_str(), &fs) != 0) {
912 // Gone?
913 ed.reset();
914 continue;
915 }
916 if (fs.st_mtime <= ed->mtime)
917 continue; // Still not changed.
918 ed->mtime = fs.st_mtime;
919 read_back_edited(ed.get());
920 modified = true; // Did one.
921 }
922 editing_files.erase(std::remove_if(editing_files.begin(), editing_files.end(),
923 [](const auto & ed) {
924 return ed == nullptr;
925 }), editing_files.end());
926 if (modified) { // Repaint if modified.
927 ExultStudio *studio = ExultStudio::get_instance();
928 Object_browser *browser = studio->get_browser();
929 if (browser) {
930 // Repaint main window.
931 browser->render();
932 }
933 studio->update_group_windows(nullptr);
934 }
935 return 1; // Continue timeouts.
936 }
937
938 /*
939 * Find the closest color in a palette to a given one.
940 *
941 * Output: 0-254, whichever has closest color.
942 */
943
Find_closest_color(const unsigned char * pal,int r,int g,int b)944 static int Find_closest_color(
945 const unsigned char *pal, // 3*255 bytes.
946 int r, int g, int b // Color to match.
947 ) {
948 int best_index = -1;
949 long best_distance = 0xfffffff;
950 // Be sure to search rotating colors too.
951 for (int i = 0; i < 0xff; i++) {
952 // Get deltas.
953 long dr = r - pal[3 * i];
954 long dg = g - pal[3 * i + 1];
955 long db = b - pal[3 * i + 2];
956 // Figure distance-squared.
957 long dist = dr * dr + dg * dg + db * db;
958 if (dist < best_distance) {
959 // Better than prev?
960 best_index = i;
961 best_distance = dist;
962 }
963 }
964 return best_index;
965 }
966
967 /*
968 * Convert an 8-bit image in one palette to another.
969 */
970
Convert_indexed_image(unsigned char * pixels,int count,unsigned char * oldpal,int oldpalsize,unsigned char * newpal)971 static void Convert_indexed_image(
972 unsigned char *pixels, // Pixels.
973 int count, // # pixels.
974 unsigned char *oldpal, // Palette pixels currently uses.
975 int oldpalsize, // Size of old palette.
976 unsigned char *newpal // Palette (255 bytes) to convert to.
977 ) {
978 if (memcmp(oldpal, newpal, oldpalsize) == 0)
979 return; // Old palette matches new.
980 int map[256]; // Set up old->new map.
981 int i;
982 for (i = 0; i < 256; i++) // Set to 'unknown'.
983 map[i] = -1;
984 map[transp] = transp; // But leave transparent pix. alone.
985 // Go through pixels.
986 for (i = 0; i < count; i++) {
987 unsigned char pix = *pixels;
988 if (map[pix] == -1) // New one?
989 map[pix] = Find_closest_color(newpal, oldpal[3 * pix],
990 oldpal[3 * pix + 1], oldpal[3 * pix + 2]);
991 *pixels++ = map[pix];
992 }
993 }
994
995 /*
996 * Import a PNG file into a given shape,frame.
997 */
998
Import_png(const char * fname,Shape_file_info * finfo,unsigned char * pal,int shapenum,int framenum)999 static void Import_png(
1000 const char *fname, // Filename.
1001 Shape_file_info *finfo, // What we're updating.
1002 unsigned char *pal, // 3*255 bytes game palette.
1003 int shapenum, int framenum // Shape, frame to update
1004 ) {
1005 ExultStudio *studio = ExultStudio::get_instance();
1006 Vga_file *ifile = finfo->get_ifile();
1007 if (!ifile)
1008 return; // Shouldn't happen.
1009 Shape *shape = ifile->extract_shape(shapenum);
1010 if (!shape)
1011 return;
1012 int w;
1013 int h;
1014 int rowsize;
1015 int xoff;
1016 int yoff;
1017 int palsize;
1018 unsigned char *pixels;
1019 unsigned char *oldpal;
1020 // Import, with 255 = transp. index.
1021 if (!Import_png8(fname, 255, w, h, rowsize, xoff, yoff,
1022 pixels, oldpal, palsize))
1023 return; // Just return if error, for now.
1024 // Convert to game palette.
1025 Convert_indexed_image(pixels, h * rowsize, oldpal, palsize, pal);
1026 delete [] oldpal;
1027 // Low shape in 'shapes.vga'?
1028 bool flat = IS_FLAT(shapenum) && finfo == studio->get_vgafile();
1029 int xleft;
1030 int yabove;
1031 if (flat) {
1032 xleft = yabove = c_tilesize;
1033 if (w != c_tilesize || h != c_tilesize || rowsize != c_tilesize) {
1034 char *msg = g_strdup_printf(
1035 "Shape %d must be %dx%d", shapenum, c_tilesize, c_tilesize);
1036 studio->prompt(msg, "Continue");
1037 g_free(msg);
1038 delete [] pixels;
1039 return;
1040 }
1041 } else { // RLE. xoff,yoff are neg. from bottom.
1042 xleft = w + xoff - 1;
1043 yabove = h + yoff - 1;
1044 }
1045 shape->set_frame(make_unique<Shape_frame>(pixels,
1046 w, h, xleft, yabove, !flat), framenum);
1047 delete [] pixels;
1048 finfo->set_modified();
1049 }
1050
1051 /*
1052 * Import a tiled PNG file into a given shape's frames.
1053 */
1054
Import_png_tiles(const char * fname,Shape_file_info * finfo,unsigned char * pal,int shapenum,int tiles,bool bycols)1055 static void Import_png_tiles(
1056 const char *fname, // Filename.
1057 Shape_file_info *finfo, // What we're updating.
1058 unsigned char *pal, // 3*255 bytes game palette.
1059 int shapenum,
1060 int tiles, // #tiles per row/col.
1061 bool bycols // Write tiles columns-first.
1062 ) {
1063 Vga_file *ifile = finfo->get_ifile();
1064 if (!ifile)
1065 return; // Shouldn't happen.
1066 Shape *shape = ifile->extract_shape(shapenum);
1067 if (!shape)
1068 return;
1069 int nframes = shape->get_num_frames();
1070 cout << "Reading " << fname << " tiled"
1071 << (bycols ? ", by cols" : ", by rows") << " first" << endl;
1072 // Figure #tiles in other dim.
1073 int dim0_cnt = tiles;
1074 int dim1_cnt = (nframes + dim0_cnt - 1) / dim0_cnt;
1075 int needw;
1076 int needh; // Figure min. image dims.
1077 if (bycols) {
1078 needh = dim0_cnt * c_tilesize;
1079 needw = dim1_cnt * c_tilesize;
1080 } else {
1081 needw = dim0_cnt * c_tilesize;
1082 needh = dim1_cnt * c_tilesize;
1083 }
1084 int w;
1085 int h;
1086 int rowsize;
1087 int xoff;
1088 int yoff;
1089 int palsize;
1090 unsigned char *pixels;
1091 unsigned char *oldpal;
1092 // Import, with 255 = transp. index.
1093 if (!Import_png8(fname, 255, w, h, rowsize, xoff, yoff,
1094 pixels, oldpal, palsize)) {
1095 Alert("Error reading '%s'", fname);
1096 return;
1097 }
1098 // Convert to game palette.
1099 Convert_indexed_image(pixels, h * rowsize, oldpal, palsize, pal);
1100 delete [] oldpal;
1101 if (w < needw || h < needh) {
1102 Alert("File '%s' image is too small. %dx%d required.",
1103 fname, needw, needh);
1104 delete [] pixels;
1105 return;
1106 }
1107 for (int frnum = 0; frnum < nframes; frnum++) {
1108 int x;
1109 int y;
1110 if (bycols) {
1111 y = frnum % dim0_cnt;
1112 x = frnum / dim0_cnt;
1113 } else {
1114 x = frnum % dim0_cnt;
1115 y = frnum / dim0_cnt;
1116 }
1117 unsigned char *src = pixels + w * c_tilesize * y + c_tilesize * x;
1118 unsigned char buf[c_num_tile_bytes]; // Move tile to buffer.
1119 unsigned char *ptr = &buf[0];
1120 for (int row = 0; row < c_tilesize; row++) {
1121 // Write it out.
1122 memcpy(ptr, src, c_tilesize);
1123 ptr += c_tilesize;
1124 src += w;
1125 }
1126 shape->set_frame(make_unique<Shape_frame>(&buf[0], c_tilesize, c_tilesize,
1127 c_tilesize, c_tilesize, false), frnum);
1128 }
1129 delete [] pixels;
1130 finfo->set_modified();
1131 }
1132
1133 /*
1134 * Read in a shape that was changed by an external program (i.e., Gimp).
1135 */
1136
read_back_edited(Editing_file * ed)1137 void Shape_chooser::read_back_edited(
1138 Editing_file *ed
1139 ) {
1140 ExultStudio *studio = ExultStudio::get_instance();
1141 Shape_file_info *finfo = studio->open_shape_file(
1142 ed->vga_basename.c_str());
1143 if (!finfo)
1144 return;
1145 unsigned char pal[3 * 256]; // Convert to 0-255 RGB's.
1146 unsigned char *palbuf = studio->get_palbuf();
1147 for (int i = 0; i < 3 * 256; i++)
1148 pal[i] = palbuf[i] * 4;
1149 if (!ed->tiles)
1150 Import_png(ed->pathname.c_str(), finfo, pal,
1151 ed->shapenum, ed->framenum);
1152 else
1153 Import_png_tiles(ed->pathname.c_str(), finfo, pal,
1154 ed->shapenum, ed->tiles, ed->bycolumns);
1155 }
1156
1157 /*
1158 * Delete all the files being edited after doing a final check.
1159 */
1160
clear_editing_files()1161 void Shape_chooser::clear_editing_files(
1162 ) {
1163 check_editing_files(); // Import any that changed.
1164 for (auto &ed : editing_files) {
1165 unlink(ed->pathname.c_str());
1166 }
1167 editing_files.clear();
1168 if (check_editing_timer != -1)
1169 g_source_remove(check_editing_timer);
1170 check_editing_timer = -1;
1171 }
1172
1173 /*
1174 * Export current frame.
1175 */
1176
export_frame(const char * fname,gpointer user_data)1177 void Shape_chooser::export_frame(
1178 const char *fname,
1179 gpointer user_data
1180 ) {
1181 auto *chooser = static_cast<Shape_chooser *>(user_data);
1182 if (U7exists(fname)) {
1183 char *msg = g_strdup_printf(
1184 "'%s' already exists. Overwrite?", fname);
1185 int answer = Prompt(msg, "Yes", "No");
1186 g_free(msg);
1187 if (answer != 0)
1188 return;
1189 }
1190 if (chooser->selected < 0)
1191 return; // Shouldn't happen.
1192 chooser->export_png(fname);
1193 }
1194
1195 /*
1196 * Import current frame.
1197 */
1198
import_frame(const char * fname,gpointer user_data)1199 void Shape_chooser::import_frame(
1200 const char *fname,
1201 gpointer user_data
1202 ) {
1203 auto *chooser = static_cast<Shape_chooser *>(user_data);
1204 if (chooser->selected < 0)
1205 return; // Shouldn't happen.
1206 int shnum = chooser->info[chooser->selected].shapenum;
1207 int frnum = chooser->info[chooser->selected].framenum;
1208 unsigned char pal[3 * 256]; // Get current palette.
1209 Get_rgb_palette(chooser->palette, pal);
1210 Import_png(fname, chooser->file_info, pal, shnum, frnum);
1211 chooser->render();
1212 ExultStudio *studio = ExultStudio::get_instance();
1213 studio->update_group_windows(nullptr);
1214 }
1215
1216 /*
1217 * Export all frames of current shape.
1218 */
1219
export_all_pngs(const char * fname,int shnum)1220 void Shape_chooser::export_all_pngs(
1221 const char *fname,
1222 int shnum
1223 ) {
1224 for (int i = 0; i < ifile->get_num_frames(shnum); i++) {
1225 char *fullname = new char[strlen(fname) + 30];
1226 sprintf(fullname, "%s%02d.png", fname, i);
1227 cout << "Writing " << fullname << endl;
1228 Shape_frame *frame = ifile->get_shape(shnum, i);
1229 int w = frame->get_width();
1230 int h = frame->get_height();
1231 Image_buffer8 img(w, h); // Render into a buffer.
1232 img.fill8(transp); // Fill with transparent pixel.
1233 frame->paint(&img, frame->get_xleft(), frame->get_yabove());
1234 int xoff = 0;
1235 int yoff = 0;
1236 if (frame->is_rle()) {
1237 xoff = -frame->get_xright();
1238 yoff = -frame->get_ybelow();
1239 }
1240 export_png(fullname, img, xoff, yoff);
1241 delete [] fullname;
1242 }
1243 }
1244
export_all_frames(const char * fname,gpointer user_data)1245 void Shape_chooser::export_all_frames(
1246 const char *fname,
1247 gpointer user_data
1248 ) {
1249 auto *chooser = static_cast<Shape_chooser *>(user_data);
1250 int shnum = chooser->info[chooser->selected].shapenum;
1251 chooser->export_all_pngs(fname, shnum);
1252 }
1253
export_shape(const char * fname,gpointer user_data)1254 void Shape_chooser::export_shape(
1255 const char *fname,
1256 gpointer user_data
1257 ) {
1258 auto *chooser = static_cast<Shape_chooser *>(user_data);
1259 int shnum = chooser->info[chooser->selected].shapenum;
1260 Shape *shp = chooser->ifile->extract_shape(shnum);
1261 Image_file_info::write_file(fname, &shp, 1, true);
1262 }
1263
1264 /*
1265 * Import all frames into current shape.
1266 */
1267
import_all_pngs(const char * fname,int shnum)1268 void Shape_chooser::import_all_pngs(
1269 const char *fname,
1270 int shnum
1271 ) {
1272 char *fullname = new char[strlen(fname) + 30];
1273 sprintf(fullname, "%s%02d.png", fname, 0);
1274 if (!U7exists(fullname)) {
1275 cerr << "Invalid base file name for import of all frames!" << endl;
1276 delete [] fullname;
1277 return;
1278 }
1279 int i = 0;
1280 unsigned char pal[3 * 256]; // Get current palette.
1281 Get_rgb_palette(palette, pal);
1282 Shape *shape = ifile->extract_shape(shnum);
1283 if (!shape) {
1284 delete [] fullname;
1285 return;
1286 }
1287 ExultStudio *studio = ExultStudio::get_instance();
1288 while (U7exists(fullname)) {
1289 int w;
1290 int h;
1291 int rowsize;
1292 int xoff;
1293 int yoff;
1294 int palsize;
1295 unsigned char *pixels;
1296 unsigned char *oldpal;
1297 // Import, with 255 = transp. index.
1298 if (!Import_png8(fullname, 255, w, h, rowsize, xoff, yoff,
1299 pixels, oldpal, palsize)) {
1300 delete [] fullname;
1301 return; // Just return if error, for now.
1302 }
1303 // Convert to game palette.
1304 Convert_indexed_image(pixels, h * rowsize, oldpal, palsize, pal);
1305 delete [] oldpal;
1306 int xleft = w + xoff - 1;
1307 int yabove = h + yoff - 1;
1308 auto frame = make_unique<Shape_frame>(pixels,
1309 w, h, xleft, yabove, true);
1310 if (i < ifile->get_num_frames(shnum))
1311 shape->set_frame(std::move(frame), i);
1312 else
1313 shape->add_frame(std::move(frame), i);
1314 delete [] pixels;
1315
1316 i++;
1317 sprintf(fullname, "%s%02d.png", fname, i);
1318 }
1319 render();
1320 file_info->set_modified();
1321 studio->update_group_windows(nullptr);
1322 delete [] fullname;
1323 }
1324
import_all_frames(const char * fname,gpointer user_data)1325 void Shape_chooser::import_all_frames(
1326 const char *fname,
1327 gpointer user_data
1328 ) {
1329 auto *chooser = static_cast<Shape_chooser *>(user_data);
1330 if (chooser->selected < 0)
1331 return; // Shouldn't happen.
1332 int shnum = chooser->info[chooser->selected].shapenum;
1333 int len = strlen(fname);
1334 // Ensure we have a valid file name.
1335 std::string file = fname;
1336 if (file[len - 4] == '.')
1337 file[len - 6] = 0;
1338 chooser->import_all_pngs(file.c_str(), shnum);
1339 }
1340
import_shape(const char * fname,gpointer user_data)1341 void Shape_chooser::import_shape(
1342 const char *fname,
1343 gpointer user_data
1344 ) {
1345 if (U7exists(fname)) {
1346 auto *chooser = static_cast<Shape_chooser *>(user_data);
1347 if (chooser->selected < 0)
1348 return; // Shouldn't happen.
1349 IFileDataSource ds(fname);
1350 // Check to see if it is a valid shape file.
1351 // We never get here through a flat, so we don't deal
1352 // with that case. These tests aren't perfect!
1353 const int size = ds.getSize();
1354 const int len = ds.read4();
1355 int first;
1356 if (len != size || (first = ds.read4()) > size || (first % 4) != 0)
1357 return;
1358 int shnum = chooser->info[chooser->selected].shapenum;
1359 Shape *shp = chooser->ifile->extract_shape(shnum);
1360 ds.seek(0);
1361 shp->load(&ds);
1362 shp->set_modified();
1363 chooser->render();
1364 chooser->file_info->set_modified();
1365 }
1366 }
1367
1368 /*
1369 * Add a frame.
1370 */
1371
new_frame()1372 void Shape_chooser::new_frame(
1373 ) {
1374 if (selected < 0)
1375 return;
1376 int shnum = info[selected].shapenum;
1377 int frnum = info[selected].framenum;
1378 Vga_file *ifile = file_info->get_ifile();
1379 // Read entire shape.
1380 Shape *shape = ifile->extract_shape(shnum);
1381 if (!shape || // Shouldn't happen.
1382 // We'll insert AFTER frnum.
1383 frnum > shape->get_num_frames())
1384 return;
1385 // Low shape in 'shapes.vga'?
1386 ExultStudio *studio = ExultStudio::get_instance();
1387 bool flat = IS_FLAT(shnum) && file_info == studio->get_vgafile();
1388 int w = 0;
1389 int h = 0;
1390 int xleft;
1391 int yabove;
1392 if (flat)
1393 w = h = xleft = yabove = c_tilesize;
1394 else { // Find largest frame.
1395 int cnt = shape->get_num_frames();
1396 for (int i = 0; i < cnt; i++) {
1397 Shape_frame *fr = shape->get_frame(i);
1398 if (!fr) {
1399 continue;
1400 }
1401 int ht = fr->get_height();
1402 if (ht > h)
1403 h = ht;
1404 int wd = fr->get_width();
1405 if (wd > w)
1406 w = wd;
1407 }
1408 if (h == 0)
1409 h = c_tilesize;
1410 if (w == 0)
1411 w = c_tilesize;
1412 xleft = w - 1;
1413 yabove = h - 1;
1414 }
1415 Image_buffer8 img(w, h);
1416 img.fill8(1); // Just use color #1.
1417 if (w > 2 && h > 2)
1418 img.fill8(2, w - 2, h - 2, 1, 1);
1419 shape->add_frame(make_unique<Shape_frame>(img.get_bits(),
1420 w, h, xleft, yabove, !flat), frnum + 1);
1421 file_info->set_modified();
1422 Object_browser *browser = studio->get_browser();
1423 if (browser) {
1424 // Repaint main window.
1425 if (frames_mode)
1426 browser->setup_info(true);
1427 browser->render();
1428 }
1429 studio->update_group_windows(nullptr);
1430 }
1431
1432 /*
1433 * Callback for new-shape 'okay'.
1434 */
1435 C_EXPORT void
on_new_shape_okay_clicked(GtkButton * button,gpointer user_data)1436 on_new_shape_okay_clicked(GtkButton *button,
1437 gpointer user_data) {
1438 ignore_unused_variable_warning(user_data);
1439 GtkWidget *win = gtk_widget_get_toplevel(GTK_WIDGET(button));
1440 auto *chooser = static_cast<Shape_chooser *>(
1441 g_object_get_data(G_OBJECT(win), "user_data"));
1442 chooser->create_new_shape();
1443 gtk_widget_hide(win);
1444 }
1445 // Toggled 'From font' button:
on_new_shape_font_toggled(GtkToggleButton * btn,gpointer user_data)1446 C_EXPORT void on_new_shape_font_toggled(
1447 GtkToggleButton *btn,
1448 gpointer user_data
1449 ) {
1450 ignore_unused_variable_warning(user_data);
1451 bool on = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(btn));
1452 GtkWidget *win = gtk_widget_get_toplevel(GTK_WIDGET(btn));
1453 auto *chooser = static_cast<Shape_chooser *>(
1454 g_object_get_data(G_OBJECT(win), "user_data"));
1455 chooser->from_font_toggled(on);
1456 }
on_new_shape_font_color_draw_expose_event(GtkWidget * widget,cairo_t * cairo,gpointer data)1457 gboolean Shape_chooser::on_new_shape_font_color_draw_expose_event(
1458 GtkWidget *widget, // The draw area.
1459 cairo_t *cairo,
1460 gpointer data // -> Shape_chooser.
1461 ) {
1462 ignore_unused_variable_warning(data);
1463 ExultStudio *studio = ExultStudio::get_instance();
1464 int index = studio->get_spin("new_shape_font_color");
1465 auto *chooser = static_cast<Shape_chooser *>(
1466 g_object_get_data(G_OBJECT(widget), "user_data"));
1467 guint32 color = chooser->get_color(index);
1468 GdkRectangle area = { 0, 0, 0, 0 };
1469 gdk_cairo_get_clip_rectangle(cairo, &area);
1470 cairo_set_source_rgb(cairo,
1471 ((color >> 16) & 255) / 255.0,
1472 ((color >> 8) & 255) / 255.0,
1473 (color & 255) / 255.0);
1474 cairo_rectangle(cairo, area.x, area.y, area.width, area.height);
1475 cairo_fill(cairo);
1476 return TRUE;
1477 }
1478
on_new_shape_font_color_changed(GtkSpinButton * button,gpointer user_data)1479 C_EXPORT void on_new_shape_font_color_changed(
1480 GtkSpinButton *button,
1481 gpointer user_data
1482 ) {
1483 ignore_unused_variable_warning(button, user_data);
1484 ExultStudio *studio = ExultStudio::get_instance();
1485 // Show new color.
1486 gtk_widget_queue_draw(studio->get_widget("new_shape_font_color_draw"));
1487 }
1488
1489 /*
1490 * Font file was selected.
1491 */
1492
font_file_chosen(const char * fname,gpointer user_data)1493 static void font_file_chosen(
1494 const char *fname,
1495 gpointer user_data
1496 ) {
1497 ignore_unused_variable_warning(user_data);
1498 ExultStudio *studio = ExultStudio::get_instance();
1499 studio->set_entry("new_shape_font_name", fname);
1500 studio->set_spin("new_shape_nframes", 128);
1501 }
1502
1503 /*
1504 * 'From font' toggled in 'New shape' dialog.
1505 */
1506
from_font_toggled(bool on)1507 void Shape_chooser::from_font_toggled(
1508 bool on
1509 ) {
1510 ExultStudio *studio = ExultStudio::get_instance();
1511 studio->set_sensitive("new_shape_font_name", on);
1512 if (!on)
1513 return;
1514 studio->set_sensitive("new_shape_font_color", true);
1515 studio->set_sensitive("new_shape_font_height", true);
1516 Create_file_selection("Choose font file",
1517 "<PATCH>", "Fonts",
1518 {"*.ttf", "*.ttc", "*.otf",
1519 "*.otc", "*.pcf", "*.fnt", "*.fon"},
1520 GTK_FILE_CHOOSER_ACTION_OPEN,
1521 font_file_chosen,
1522 nullptr);
1523 }
1524
1525 /*
1526 * Add a new shape.
1527 */
1528
new_shape()1529 void Shape_chooser::new_shape(
1530 ) {
1531 ExultStudio *studio = ExultStudio::get_instance();
1532 GtkWidget *win = studio->get_widget("new_shape_window");
1533 gtk_window_set_modal(GTK_WINDOW(win), true);
1534 g_object_set_data(G_OBJECT(win), "user_data", this);
1535 // Get current selection.
1536 int shnum = selected >= 0 ? info[selected].shapenum : 0;
1537 GtkWidget *spin = studio->get_widget("new_shape_num");
1538 GtkAdjustment *adj = gtk_spin_button_get_adjustment(
1539 GTK_SPIN_BUTTON(spin));
1540 gtk_adjustment_set_upper(adj, c_max_shapes - 1);
1541 Vga_file *ifile = file_info->get_ifile();
1542 int shstart; // Find an unused shape.
1543 for (shstart = shnum; shstart <= gtk_adjustment_get_upper(adj); shstart++)
1544 if (shstart >= ifile->get_num_shapes() ||
1545 !ifile->get_num_frames(shstart))
1546 break;
1547 if (shstart > gtk_adjustment_get_upper(adj)) {
1548 for (shstart = shnum - 1; shstart >= 0; shstart--)
1549 if (!ifile->get_num_frames(shstart))
1550 break;
1551 if (shstart < 0)
1552 shstart = shnum;
1553 }
1554 gtk_adjustment_set_value(adj, shstart);
1555 spin = studio->get_widget("new_shape_nframes");
1556 adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
1557 bool flat = IS_FLAT(shnum) && file_info == studio->get_vgafile();
1558 if (flat)
1559 gtk_adjustment_set_upper(adj, 31);
1560 else
1561 gtk_adjustment_set_upper(adj, 255);
1562 spin = studio->get_widget("new_shape_font_height");
1563 studio->set_sensitive("new_shape_font_height", false);
1564 adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
1565 gtk_adjustment_set_lower(adj, 4);
1566 gtk_adjustment_set_upper(adj, 64);
1567 spin = studio->get_widget("new_shape_font_color");
1568 studio->set_sensitive("new_shape_font_color", false);
1569 adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(spin));
1570 gtk_adjustment_set_lower(adj, 0);
1571 gtk_adjustment_set_upper(adj, 255);
1572 // Unset 'From font:'.
1573 studio->set_toggle("new_shape_font", false);
1574 #ifndef HAVE_FREETYPE2 /* No freetype? No fonts. */
1575 studio->set_sensitive("new_shape_font", false);
1576 #endif
1577 // Store our pointer in color drawer.
1578 GtkWidget *draw = studio->get_widget("new_shape_font_color_draw");
1579 g_object_set_data(G_OBJECT(draw), "user_data", this);
1580 g_signal_connect(G_OBJECT(draw), "draw",
1581 G_CALLBACK(on_new_shape_font_color_draw_expose_event), this);
1582 gtk_widget_show(win);
1583 }
1584
1585 /*
1586 * Add a new shape after the user has clicked 'Okay' in the new-shape
1587 * dialog.
1588 */
1589
create_new_shape()1590 void Shape_chooser::create_new_shape(
1591 ) {
1592 ExultStudio *studio = ExultStudio::get_instance();
1593 int shnum = studio->get_spin("new_shape_num");
1594 int nframes = studio->get_spin("new_shape_nframes");
1595 if (nframes <= 0)
1596 nframes = 1;
1597 else if (nframes > 256)
1598 nframes = 256;
1599 Vga_file *ifile = file_info->get_ifile();
1600 if (shnum < ifile->get_num_shapes() && ifile->get_num_frames(shnum)) {
1601 if (Prompt("Replace existing shape?", "Yes", "No") != 0)
1602 return;
1603 }
1604 Shape *shape = ifile->new_shape(shnum);
1605 if (!shape) {
1606 Alert("Can't create shape %d", shnum);
1607 return;
1608 }
1609 // Create frames.
1610 bool flat = IS_FLAT(shnum) && file_info == studio->get_vgafile();
1611 bool use_font = false;
1612 #ifdef HAVE_FREETYPE2
1613 // Want to create from a font?
1614 use_font = studio->get_toggle("new_shape_font");
1615 const char *fontname = studio->get_text_entry("new_shape_font_name");
1616 use_font = use_font && (fontname != nullptr) && *fontname != 0;
1617 if (use_font) {
1618 if (flat) {
1619 Alert("Can't load font into a 'flat' shape");
1620 return;
1621 }
1622 int ht = studio->get_spin("new_shape_font_height");
1623 int fg = studio->get_spin("new_shape_font_color");
1624 if (!Gen_font_shape(shape, fontname, nframes,
1625 // Use transparent color for bgnd.
1626 ht, fg, 255))
1627 Alert("Error loading font file '%s'", fontname);
1628 }
1629 #endif
1630 if (!use_font) {
1631 int w = c_tilesize;
1632 int h = c_tilesize;
1633 int xleft = flat ? c_tilesize : w - 1;
1634 int yabove = flat ? c_tilesize : h - 1;
1635 Image_buffer8 img(w, h);
1636 img.fill8(1); // Just use color #1.
1637 img.fill8(2, w - 2, h - 2, 1, 1);
1638 // Include some transparency.
1639 img.fill8(255, w / 2, h / 2, w / 4, h / 4);
1640 for (int i = 0; i < nframes; i++)
1641 shape->add_frame(make_unique<Shape_frame>(img.get_bits(),
1642 w, h, xleft, yabove, !flat), i);
1643 }
1644 file_info->set_modified();
1645 Object_browser *browser = studio->get_browser();
1646 if (browser) {
1647 // Repaint main window.
1648 browser->setup_info(true);
1649 browser->render();
1650 }
1651 studio->update_group_windows(nullptr);
1652 }
1653
1654 /*
1655 * Delete a frame, and the shape itself if this is its last.
1656 */
1657
del_frame()1658 void Shape_chooser::del_frame(
1659 ) {
1660 if (selected < 0)
1661 return;
1662 int shnum = info[selected].shapenum;
1663 int frnum = info[selected].framenum;
1664 Vga_file *ifile = file_info->get_ifile();
1665 // Read entire shape.
1666 Shape *shape = ifile->extract_shape(shnum);
1667 if (!shape || // Shouldn't happen.
1668 frnum > shape->get_num_frames() - 1)
1669 return;
1670 // 1-shape file & last frame?
1671 if (!ifile->is_flex() && shape->get_num_frames() == 1)
1672 return;
1673 shape->del_frame(frnum);
1674 file_info->set_modified();
1675 ExultStudio *studio = ExultStudio::get_instance();
1676 Object_browser *browser = studio->get_browser();
1677 if (browser) {
1678 // Repaint main window.
1679 browser->render();
1680 }
1681 studio->update_group_windows(nullptr);
1682 }
1683
1684 /*
1685 * Someone wants the dragged shape.
1686 */
1687
drag_data_get(GtkWidget * widget,GdkDragContext * context,GtkSelectionData * seldata,guint info,guint time,gpointer data)1688 void Shape_chooser::drag_data_get(
1689 GtkWidget *widget, // The view window.
1690 GdkDragContext *context,
1691 GtkSelectionData *seldata, // Fill this in.
1692 guint info,
1693 guint time,
1694 gpointer data // ->Shape_chooser.
1695 ) {
1696 ignore_unused_variable_warning(widget, context, time);
1697 cout << "In DRAG_DATA_GET of Shape for '"
1698 << gdk_atom_name(gtk_selection_data_get_target(seldata))
1699 << "'" << endl;
1700 auto *chooser = static_cast<Shape_chooser *>(data);
1701 if (chooser->selected < 0 || info != U7_TARGET_SHAPEID)
1702 return; // Not sure about this.
1703 guchar buf[U7DND_DATA_LENGTH(3)];
1704 int file = chooser->ifile->get_u7drag_type();
1705 if (file == U7_SHAPE_UNK)
1706 file = U7_SHAPE_SHAPES; // Just assume it's shapes.vga.
1707 Shape_entry &shinfo = chooser->info[chooser->selected];
1708 int len = Store_u7_shapeid(buf, file, shinfo.shapenum,
1709 shinfo.framenum);
1710 cout << "Setting selection data (" << shinfo.shapenum <<
1711 '/' << shinfo.framenum << ')' << endl;
1712 // Set data.
1713 gtk_selection_data_set(seldata,
1714 gtk_selection_data_get_target(seldata),
1715 8, buf, len);
1716 }
1717
1718 /*
1719 * Beginning of a drag.
1720 */
1721
drag_begin(GtkWidget * widget,GdkDragContext * context,gpointer data)1722 gint Shape_chooser::drag_begin(
1723 GtkWidget *widget, // The view window.
1724 GdkDragContext *context,
1725 gpointer data // ->Shape_chooser.
1726 ) {
1727 ignore_unused_variable_warning(widget);
1728 cout << "In DRAG_BEGIN of Shape" << endl;
1729 auto *chooser = static_cast<Shape_chooser *>(data);
1730 if (chooser->selected < 0)
1731 return FALSE; // ++++Display a halt bitmap.
1732 // Get ->shape.
1733 Shape_entry &shinfo = chooser->info[chooser->selected];
1734 Shape_frame *shape = chooser->ifile->get_shape(shinfo.shapenum,
1735 shinfo.framenum);
1736 if (!shape)
1737 return FALSE;
1738 chooser->set_drag_icon(context, shape); // Set icon for dragging.
1739 return TRUE;
1740 }
1741
1742 /*
1743 * Scroll to a new shape/frame.
1744 */
1745
scroll_row_vertical(unsigned newrow)1746 void Shape_chooser::scroll_row_vertical(
1747 unsigned newrow // Abs. index of row to show.
1748 ) {
1749 if (newrow >= rows.size())
1750 return;
1751 row0 = newrow;
1752 row0_voffset = 0;
1753 render();
1754 }
1755
1756 /*
1757 * Scroll to new pixel offset.
1758 */
1759
scroll_vertical(int newoffset)1760 void Shape_chooser::scroll_vertical(
1761 int newoffset
1762 ) {
1763 int delta = newoffset - voffset;
1764 while (delta > 0 && row0 < rows.size() - 1) {
1765 // Going down.
1766 int rowh = rows[row0].height - row0_voffset;
1767 if (delta < rowh) {
1768 // Part of current row.
1769 voffset += delta;
1770 row0_voffset += delta;
1771 delta = 0;
1772 } else {
1773 // Go down to next row.
1774 voffset += rowh;
1775 delta -= rowh;
1776 ++row0;
1777 row0_voffset = 0;
1778 }
1779 }
1780 while (delta < 0) {
1781 if (-delta <= row0_voffset) {
1782 voffset += delta;
1783 row0_voffset += delta;
1784 delta = 0;
1785 } else if (row0_voffset) {
1786 voffset -= row0_voffset;
1787 delta += row0_voffset;
1788 row0_voffset = 0;
1789 } else {
1790 if (row0 == 0)
1791 break;
1792 --row0;
1793 row0_voffset = 0;
1794 voffset -= rows[row0].height;
1795 delta += rows[row0].height;
1796 if (delta > 0) {
1797 row0_voffset = delta;
1798 voffset += delta;
1799 delta = 0;
1800 }
1801 }
1802 }
1803 render();
1804 update_statusbar();
1805 }
1806
1807 /*
1808 * Adjust vertical scroll amounts after laying out shapes.
1809 */
1810
setup_vscrollbar()1811 void Shape_chooser::setup_vscrollbar(
1812 ) {
1813 GtkAdjustment *adj = gtk_range_get_adjustment(
1814 GTK_RANGE(vscroll));
1815 gtk_adjustment_set_value(adj, 0);
1816 gtk_adjustment_set_lower(adj, 0);
1817 gtk_adjustment_set_upper(adj, total_height);
1818 gtk_adjustment_set_step_increment(adj, 16); // +++++FOR NOW.
1819 gtk_adjustment_set_page_increment(adj, config_height);
1820 gtk_adjustment_set_page_size(adj, config_height);
1821 g_signal_emit_by_name(G_OBJECT(adj), "changed");
1822 }
1823
1824 /*
1825 * Adjust horizontal scroll amounts.
1826 */
1827
setup_hscrollbar(int newmax)1828 void Shape_chooser::setup_hscrollbar(
1829 int newmax // New max., or -1 to leave alone.
1830 ) {
1831 GtkAdjustment *adj = gtk_range_get_adjustment(
1832 GTK_RANGE(hscroll));
1833 if (newmax > 0)
1834 gtk_adjustment_set_upper(adj, newmax);
1835 GtkAllocation alloc = {0, 0, 0, 0};
1836 gtk_widget_get_allocation(draw, &alloc);
1837 gtk_adjustment_set_page_increment(adj, alloc.width);
1838 gtk_adjustment_set_page_size(adj, alloc.width);
1839 if (gtk_adjustment_get_page_size(adj) > gtk_adjustment_get_upper(adj))
1840 gtk_adjustment_set_upper(adj, gtk_adjustment_get_page_size(adj));
1841 g_signal_emit_by_name(G_OBJECT(adj), "changed");
1842 }
1843
1844 /*
1845 * Handle a scrollbar event.
1846 */
1847
vscrolled(GtkAdjustment * adj,gpointer data)1848 void Shape_chooser::vscrolled( // For vertical scrollbar.
1849 GtkAdjustment *adj, // The adjustment.
1850 gpointer data // ->Shape_chooser.
1851 ) {
1852 auto *chooser = static_cast<Shape_chooser *>(data);
1853 cout << "Scrolled to " << gtk_adjustment_get_value(adj) << '\n';
1854 gint newindex = static_cast<gint>(gtk_adjustment_get_value(adj));
1855 chooser->scroll_vertical(newindex);
1856 }
hscrolled(GtkAdjustment * adj,gpointer data)1857 void Shape_chooser::hscrolled( // For horizontal scrollbar.
1858 GtkAdjustment *adj, // The adjustment.
1859 gpointer data // ->Shape_chooser.
1860 ) {
1861 auto *chooser = static_cast<Shape_chooser *>(data);
1862 chooser->hoffset = static_cast<gint>(gtk_adjustment_get_value(adj));
1863 chooser->render();
1864 }
1865
1866 /*
1867 * Handle a change to the 'frame' spin button.
1868 */
1869
frame_changed(GtkAdjustment * adj,gpointer data)1870 void Shape_chooser::frame_changed(
1871 GtkAdjustment *adj, // The adjustment.
1872 gpointer data // ->Shape_chooser.
1873 ) {
1874 auto *chooser = static_cast<Shape_chooser *>(data);
1875 gint newframe = static_cast<gint>(gtk_adjustment_get_value(adj));
1876 if (chooser->selected >= 0) {
1877 Shape_entry &shinfo = chooser->info[chooser->selected];
1878 int nframes = chooser->ifile->get_num_frames(shinfo.shapenum);
1879 if (newframe >= nframes) // Just checking
1880 return;
1881 shinfo.framenum = newframe;
1882 if (chooser->frames_mode) // Get sel. frame in view.
1883 chooser->scroll_to_frame();
1884 chooser->render();
1885 chooser->update_statusbar();
1886 }
1887 }
1888
1889 /*
1890 * 'All frames' toggled.
1891 */
1892
all_frames_toggled(GtkToggleButton * btn,gpointer data)1893 void Shape_chooser::all_frames_toggled(
1894 GtkToggleButton *btn,
1895 gpointer data
1896 ) {
1897 auto *chooser = static_cast<Shape_chooser *>(data);
1898 bool on = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(btn));
1899 chooser->frames_mode = on;
1900 if (on) // Frame => show horiz. scrollbar.
1901 gtk_widget_show(chooser->hscroll);
1902 else
1903 gtk_widget_hide(chooser->hscroll);
1904 // The old index is no longer valid, so we need to remember the shape.
1905 int indx = chooser->selected >= 0 ? chooser->selected
1906 : static_cast<int>(chooser->rows[chooser->row0].index0);
1907 int shnum = chooser->info[indx].shapenum;
1908 chooser->selected = -1;
1909 chooser->setup_info();
1910 indx = chooser->find_shape(shnum);
1911 if (indx >= 0) // Get back to given shape.
1912 chooser->goto_index(indx);
1913 }
1914
1915 /*
1916 * Handle popup menu items.
1917 */
1918
on_shapes_popup_info_activate(GtkMenuItem * item,gpointer udata)1919 void Shape_chooser::on_shapes_popup_info_activate(
1920 GtkMenuItem *item,
1921 gpointer udata
1922 ) {
1923 ignore_unused_variable_warning(item);
1924 static_cast<Shape_chooser *>(udata)->edit_shape_info();
1925 }
1926
on_shapes_popup_edit_activate(GtkMenuItem * item,gpointer udata)1927 void Shape_chooser::on_shapes_popup_edit_activate(
1928 GtkMenuItem *item,
1929 gpointer udata
1930 ) {
1931 ignore_unused_variable_warning(item);
1932 static_cast<Shape_chooser *>(udata)->edit_shape();
1933 }
1934
on_shapes_popup_edtiles_activate(GtkMenuItem * item,gpointer udata)1935 void Shape_chooser::on_shapes_popup_edtiles_activate(
1936 GtkMenuItem *item,
1937 gpointer udata
1938 ) {
1939 ignore_unused_variable_warning(item);
1940 auto *ch = static_cast<Shape_chooser *>(udata);
1941 if (ch->selected < 0)
1942 return; // Shouldn't happen.
1943 ExultStudio *studio = ExultStudio::get_instance();
1944 GtkWidget *win = studio->get_widget("export_tiles_window");
1945 gtk_window_set_modal(GTK_WINDOW(win), true);
1946 g_object_set_data(G_OBJECT(win), "user_data", ch);
1947 // Get current selection.
1948 int shnum = ch->info[ch->selected].shapenum;
1949 Vga_file *ifile = ch->file_info->get_ifile();
1950 int nframes = ifile->get_num_frames(shnum);
1951 GtkWidget *spin = studio->get_widget("export_tiles_count");
1952 GtkAdjustment *adj = gtk_spin_button_get_adjustment(
1953 GTK_SPIN_BUTTON(spin));
1954 gtk_adjustment_set_lower(adj, 1);
1955 gtk_adjustment_set_upper(adj, nframes);
1956 gtk_widget_show(win);
1957 }
1958
on_shapes_popup_import(GtkMenuItem * item,gpointer udata)1959 static void on_shapes_popup_import(
1960 GtkMenuItem *item,
1961 gpointer udata
1962 ) {
1963 ignore_unused_variable_warning(item);
1964 Create_file_selection("Import frame from a .png file",
1965 "<PATCH>", "PNG Images",
1966 {"*.png"},
1967 GTK_FILE_CHOOSER_ACTION_OPEN,
1968 Shape_chooser::import_frame,
1969 udata);
1970 }
on_shapes_popup_export(GtkMenuItem * item,gpointer udata)1971 static void on_shapes_popup_export(
1972 GtkMenuItem *item,
1973 gpointer udata
1974 ) {
1975 ignore_unused_variable_warning(item);
1976 Create_file_selection("Export frame to a .png file",
1977 "<PATCH>", "PNG Images",
1978 {"*.png"},
1979 GTK_FILE_CHOOSER_ACTION_SAVE,
1980 Shape_chooser::export_frame,
1981 udata);
1982 }
on_shapes_popup_export_all(GtkMenuItem * item,gpointer udata)1983 static void on_shapes_popup_export_all(
1984 GtkMenuItem *item,
1985 gpointer udata
1986 ) {
1987 ignore_unused_variable_warning(item);
1988 Create_file_selection("Choose the base .png file name for all frames",
1989 "<PATCH>", nullptr,
1990 {},
1991 GTK_FILE_CHOOSER_ACTION_SAVE,
1992 Shape_chooser::export_all_frames,
1993 udata);
1994 }
on_shapes_popup_import_all(GtkMenuItem * item,gpointer udata)1995 static void on_shapes_popup_import_all(
1996 GtkMenuItem *item,
1997 gpointer udata
1998 ) {
1999 ignore_unused_variable_warning(item);
2000 Create_file_selection("Choose the one of the .png sprites to import",
2001 "<PATCH>", "PNG Images",
2002 {"*.png"},
2003 GTK_FILE_CHOOSER_ACTION_OPEN,
2004 Shape_chooser::import_all_frames,
2005 udata);
2006 }
on_shapes_popup_export_shape(GtkMenuItem * item,gpointer udata)2007 static void on_shapes_popup_export_shape(
2008 GtkMenuItem *item,
2009 gpointer udata
2010 ) {
2011 ignore_unused_variable_warning(item);
2012 Create_file_selection("Choose the shp file name",
2013 "<PATCH>", "Shape files",
2014 {"*.shp"},
2015 GTK_FILE_CHOOSER_ACTION_SAVE,
2016 Shape_chooser::export_shape,
2017 udata);
2018 }
on_shapes_popup_import_shape(GtkMenuItem * item,gpointer udata)2019 static void on_shapes_popup_import_shape(
2020 GtkMenuItem *item,
2021 gpointer udata
2022 ) {
2023 ignore_unused_variable_warning(item);
2024 Create_file_selection("Choose the shp file to import",
2025 "<PATCH>", "Shape files",
2026 {"*.shp"},
2027 GTK_FILE_CHOOSER_ACTION_OPEN,
2028 Shape_chooser::import_shape,
2029 udata);
2030 }
on_shapes_popup_new_frame(GtkMenuItem * item,gpointer udata)2031 static void on_shapes_popup_new_frame(
2032 GtkMenuItem *item,
2033 gpointer udata
2034 ) {
2035 ignore_unused_variable_warning(item);
2036 static_cast<Shape_chooser *>(udata)->new_frame();
2037 }
on_shapes_popup_new_shape(GtkMenuItem * item,gpointer udata)2038 static void on_shapes_popup_new_shape(
2039 GtkMenuItem *item,
2040 gpointer udata
2041 ) {
2042 ignore_unused_variable_warning(item);
2043 static_cast<Shape_chooser *>(udata)->new_shape();
2044 }
2045
2046 /*
2047 * Callback for edit-tiles 'okay'.
2048 */
2049 C_EXPORT void
on_export_tiles_okay_clicked(GtkButton * button,gpointer user_data)2050 on_export_tiles_okay_clicked(GtkButton *button,
2051 gpointer user_data) {
2052 ignore_unused_variable_warning(user_data);
2053 GtkWidget *win = gtk_widget_get_toplevel(GTK_WIDGET(button));
2054 auto *chooser = static_cast<Shape_chooser *>(
2055 g_object_get_data(G_OBJECT(win), "user_data"));
2056 ExultStudio *studio = ExultStudio::get_instance();
2057 int tiles = studio->get_spin("export_tiles_count");
2058 bool bycol = studio->get_toggle("tiled_by_columns");
2059 chooser->edit_shape(tiles, bycol);
2060 gtk_widget_hide(win);
2061 }
2062
2063 /*
2064 * Handle a shape dropped on our draw area.
2065 */
2066
shape_dropped_here(int file,int shape,int frame)2067 void Shape_chooser::shape_dropped_here(
2068 int file, // U7_SHAPE_SHAPES.
2069 int shape,
2070 int frame
2071 ) {
2072 ignore_unused_variable_warning(frame);
2073 // Got to be from same file type.
2074 if (ifile->get_u7drag_type() == file && group != nullptr) {
2075 // Add to group.
2076 if (group->is_builtin()) {
2077 Alert("Can't modify builtin group.");
2078 return;
2079 }
2080 group->add(shape);
2081 // Update all windows for this group.
2082 ExultStudio::get_instance()->update_group_windows(group);
2083 }
2084 }
2085
2086 /*
2087 * Get # shapes we can display.
2088 */
2089
get_count()2090 int Shape_chooser::get_count(
2091 ) {
2092 return group ? group->size() : ifile->get_num_shapes();
2093 }
2094
2095 /*
2096 * Search for an entry.
2097 */
2098
search(const char * search,int dir)2099 void Shape_chooser::search(
2100 const char *search, // What to search for.
2101 int dir // 1 or -1.
2102 ) {
2103 if (!shapes_file) // Not 'shapes.vga'.
2104 return; // In future, maybe find shape #?
2105 int total = get_count();
2106 if (!total)
2107 return; // Empty.
2108 // Read info if not read.
2109 ExultStudio *studio = ExultStudio::get_instance();
2110 if (shapes_file->read_info(studio->get_game_type(), true))
2111 studio->set_shapeinfo_modified();
2112 // Start with selection, or top.
2113 int start = selected >= 0 ? selected : static_cast<int>(rows[row0].index0);
2114 int i;
2115 start += dir;
2116 int stop = dir == -1 ? -1 : static_cast<int>(info.size());
2117 codepageStr srch(search);
2118 for (i = start; i != stop; i += dir) {
2119 int shnum = info[i].shapenum;
2120 const char *nm = studio->get_shape_name(shnum);
2121 if (nm && search_name(nm, srch))
2122 break; // Found it.
2123 const Shape_info &info = shapes_file->get_info(shnum);
2124 if (info.has_frame_name_info()) {
2125 bool found = false;
2126 const std::vector<Frame_name_info> &nminf = info.get_frame_name_info();
2127 for (const auto &it : nminf) {
2128 int type = it.get_type();
2129 int msgid = it.get_msgid();
2130 if (type == -255 || type == -1 || msgid >= get_num_misc_names()
2131 || !get_misc_name(msgid))
2132 continue; // Keep looking.
2133 if (search_name(get_misc_name(msgid), srch)) {
2134 found = true;
2135 break; // Found it.
2136 }
2137 }
2138 if (found)
2139 break;
2140 }
2141 }
2142 if (i == stop)
2143 return; // Not found.
2144 goto_index(i);
2145 select(i);
2146 render();
2147 }
2148
2149 /*
2150 * Locate shape on game map.
2151 */
2152
locate(bool upwards)2153 void Shape_chooser::locate(
2154 bool upwards
2155 ) {
2156 if (selected < 0)
2157 return; // Shouldn't happen.
2158 unsigned char data[Exult_server::maxlength];
2159 unsigned char *ptr = &data[0];
2160 int qual = ExultStudio::get_num_entry(get_loc_q(), -1);
2161 int frnum = ExultStudio::get_num_entry(get_loc_f(), -1);
2162 Write2(ptr, info[selected].shapenum);
2163 Write2(ptr, frnum < 0 ? c_any_framenum : frnum);
2164 Write2(ptr, qual < 0 ? c_any_qual : qual);
2165 *ptr++ = upwards ? 1 : 0;
2166 ExultStudio *studio = ExultStudio::get_instance();
2167 studio->send_to_server(
2168 Exult_server::locate_shape, data, ptr - data);
2169 }
2170
2171 /*
2172 * Set up popup menu for shape browser.
2173 */
2174
create_popup()2175 GtkWidget *Shape_chooser::create_popup(
2176 ) {
2177 ExultStudio *studio = ExultStudio::get_instance();
2178 create_popup_internal(true); // Create popup with groups, files.
2179 if (selected >= 0) { // Add editing choices.
2180 Add_menu_item(popup, "Info...",
2181 G_CALLBACK(on_shapes_popup_info_activate), this);
2182 if (studio->get_image_editor()) {
2183 Add_menu_item(popup, "Edit...",
2184 G_CALLBACK(on_shapes_popup_edit_activate),
2185 this);
2186 if (IS_FLAT(info[selected].shapenum) &&
2187 file_info == studio->get_vgafile())
2188 Add_menu_item(popup, "Edit tiled...",
2189 G_CALLBACK(
2190 on_shapes_popup_edtiles_activate), this);
2191 }
2192 // Separator.
2193 Add_menu_item(popup);
2194 // Add/del.
2195 Add_menu_item(popup, "New frame",
2196 G_CALLBACK(on_shapes_popup_new_frame), this);
2197 // Export/import.
2198 Add_menu_item(popup, "Export frame...",
2199 G_CALLBACK(on_shapes_popup_export), this);
2200 Add_menu_item(popup, "Import frame...",
2201 G_CALLBACK(on_shapes_popup_import), this);
2202 if (!IS_FLAT(info[selected].shapenum) ||
2203 file_info != studio->get_vgafile()) {
2204 // Separator.
2205 Add_menu_item(popup);
2206 // Export/import all frames.
2207 Add_menu_item(popup, "Export all frames...",
2208 G_CALLBACK(on_shapes_popup_export_all), this);
2209 Add_menu_item(popup, "Import all frames...",
2210 G_CALLBACK(on_shapes_popup_import_all), this);
2211 }
2212 }
2213 if (ifile->is_flex()) { // Multiple-shapes file (.vga)?
2214 if (selected >= 0 && (!IS_FLAT(info[selected].shapenum) ||
2215 file_info != studio->get_vgafile())) {
2216 // Separator.
2217 Add_menu_item(popup);
2218 // Export/import shape.
2219 Add_menu_item(popup, "Export shape...",
2220 G_CALLBACK(on_shapes_popup_export_shape), this);
2221 Add_menu_item(popup, "Import shape...",
2222 G_CALLBACK(on_shapes_popup_import_shape), this);
2223 }
2224 // Separator.
2225 Add_menu_item(popup);
2226 Add_menu_item(popup, "New shape",
2227 G_CALLBACK(on_shapes_popup_new_shape), this);
2228 }
2229 return popup;
2230 }
2231
2232 /*
2233 * Create the list.
2234 */
2235
Shape_chooser(Vga_file * i,unsigned char * palbuf,int w,int h,Shape_group * g,Shape_file_info * fi)2236 Shape_chooser::Shape_chooser(
2237 Vga_file *i, // Where they're kept.
2238 unsigned char *palbuf, // Palette, 3*256 bytes (rgb triples).
2239 int w, int h, // Dimensions.
2240 Shape_group *g,
2241 Shape_file_info *fi
2242 ) : Object_browser(g, fi),
2243 Shape_draw(i, palbuf, gtk_drawing_area_new()),
2244 shapes_file(nullptr), framenum0(0),
2245 info(0), rows(0), row0(0),
2246 row0_voffset(0), total_height(0),
2247 frames_mode(false), hoffset(0), voffset(0), status_id(-1), sel_changed(nullptr) {
2248 rows.reserve(40);
2249
2250 // Put things in a vert. box.
2251 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
2252 gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE);
2253 set_widget(vbox); // This is our "widget"
2254 gtk_widget_show(vbox);
2255
2256 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
2257 gtk_box_set_homogeneous(GTK_BOX(hbox), FALSE);
2258 gtk_widget_show(hbox);
2259 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
2260
2261 // A frame looks nice.
2262 GtkWidget *frame = gtk_frame_new(nullptr);
2263 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
2264 widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2265 gtk_widget_show(frame);
2266 gtk_box_pack_start(GTK_BOX(hbox), frame, TRUE, TRUE, 0);
2267
2268 // NOTE: draw is in Shape_draw.
2269 // Indicate the events we want.
2270 gtk_widget_set_events(draw, GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK
2271 | GDK_BUTTON_RELEASE_MASK
2272 | GDK_BUTTON1_MOTION_MASK | GDK_KEY_PRESS_MASK);
2273 // Set "configure" handler.
2274 g_signal_connect(G_OBJECT(draw), "configure-event",
2275 G_CALLBACK(Configure_chooser), this);
2276 // Set "expose-event" - "draw" handler.
2277 g_signal_connect(G_OBJECT(draw), "draw",
2278 G_CALLBACK(expose), this);
2279 // Keystroke.
2280 g_signal_connect(G_OBJECT(draw), "key-press-event",
2281 G_CALLBACK(on_draw_key_press),
2282 this);
2283 gtk_widget_set_can_focus(GTK_WIDGET(draw), TRUE);
2284 // Set mouse click handler.
2285 g_signal_connect(G_OBJECT(draw), "button-press-event",
2286 G_CALLBACK(Mouse_press), this);
2287 g_signal_connect(G_OBJECT(draw), "button-release-event",
2288 G_CALLBACK(Mouse_release), this);
2289 // Mouse motion.
2290 g_signal_connect(G_OBJECT(draw), "drag-begin",
2291 G_CALLBACK(drag_begin), this);
2292 g_signal_connect(G_OBJECT(draw), "motion-notify-event",
2293 G_CALLBACK(drag_motion), this);
2294 g_signal_connect(G_OBJECT(draw), "drag-data-get",
2295 G_CALLBACK(drag_data_get), this);
2296 gtk_container_add(GTK_CONTAINER(frame), draw);
2297 widget_set_margins(draw, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2298 gtk_widget_set_size_request(draw, w, h);
2299 gtk_widget_show(draw);
2300 // Want vert. scrollbar for the shapes.
2301 GtkAdjustment *shape_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0,
2302 std::ceil(get_count() / 4.0), 1, 1, 1));
2303 vscroll = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, GTK_ADJUSTMENT(shape_adj));
2304 gtk_box_pack_start(GTK_BOX(hbox), vscroll, FALSE, TRUE, 0);
2305 // Set scrollbar handler.
2306 g_signal_connect(G_OBJECT(shape_adj), "value-changed",
2307 G_CALLBACK(vscrolled), this);
2308 gtk_widget_show(vscroll);
2309 // Horizontal scrollbar.
2310 shape_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, 1600, 8, 16, 16));
2311 hscroll = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, GTK_ADJUSTMENT(shape_adj));
2312 gtk_box_pack_start(GTK_BOX(vbox), hscroll, FALSE, TRUE, 0);
2313 // Set scrollbar handler.
2314 g_signal_connect(G_OBJECT(shape_adj), "value-changed",
2315 G_CALLBACK(hscrolled), this);
2316 //++++ gtk_widget_hide(hscroll); // Only shown in 'frames' mode.
2317
2318 // At the bottom, status bar & frame:
2319 GtkWidget *hbox1 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
2320 gtk_box_set_homogeneous(GTK_BOX(hbox1), FALSE);
2321 gtk_box_pack_start(GTK_BOX(vbox), hbox1, FALSE, FALSE, 0);
2322 gtk_widget_show(hbox1);
2323 // At left, a status bar.
2324 sbar = gtk_statusbar_new();
2325 sbar_sel = gtk_statusbar_get_context_id(GTK_STATUSBAR(sbar),
2326 "selection");
2327 gtk_box_pack_start(GTK_BOX(hbox1), sbar, TRUE, TRUE, 0);
2328 widget_set_margins(sbar, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2329 gtk_widget_show(sbar);
2330 GtkWidget *label = gtk_label_new("Frame:");
2331 gtk_box_pack_start(GTK_BOX(hbox1), label, FALSE, FALSE, 0);
2332 widget_set_margins(label, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2333 gtk_widget_show(label);
2334 // A spin button for frame#.
2335 frame_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0,
2336 16, 1,
2337 0, 0.0));
2338 fspin = gtk_spin_button_new(GTK_ADJUSTMENT(frame_adj),
2339 1, 0);
2340 g_signal_connect(G_OBJECT(frame_adj), "value-changed",
2341 G_CALLBACK(frame_changed), this);
2342 gtk_box_pack_start(GTK_BOX(hbox1), fspin, FALSE, FALSE, 0);
2343 widget_set_margins(fspin, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2344 gtk_widget_show(fspin);
2345 // A toggle for 'All Frames'.
2346 GtkWidget *allframes = gtk_toggle_button_new_with_label("Frames");
2347 gtk_box_pack_start(GTK_BOX(hbox1), allframes, FALSE, FALSE, 0);
2348 widget_set_margins(allframes, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
2349 gtk_widget_show(allframes);
2350 g_signal_connect(G_OBJECT(allframes), "toggled",
2351 G_CALLBACK(all_frames_toggled), this);
2352 // Add search controls to bottom.
2353 gtk_box_pack_start(GTK_BOX(vbox),
2354 create_controls(find_controls | locate_controls |
2355 locate_quality | locate_frame),
2356 FALSE, FALSE, 0);
2357 }
2358
2359 /*
2360 * Delete.
2361 */
2362
~Shape_chooser()2363 Shape_chooser::~Shape_chooser(
2364 ) {
2365 gtk_widget_destroy(get_widget());
2366 }
2367
2368 /*
2369 * Unselect.
2370 */
2371
unselect(bool need_render)2372 void Shape_chooser::unselect(
2373 bool need_render // 1 to render and show.
2374 ) {
2375 if (selected >= 0) {
2376 selected = -1;
2377 // Update spin button for frame #.
2378 gtk_adjustment_set_value(frame_adj, 0);
2379 gtk_widget_set_sensitive(fspin, false);
2380 if (need_render) {
2381 render();
2382 }
2383 if (sel_changed) // Tell client.
2384 (*sel_changed)();
2385 }
2386 update_statusbar();
2387 }
2388
2389 /*
2390 * Show selection or range in window.
2391 */
2392
update_statusbar()2393 void Shape_chooser::update_statusbar(
2394 ) {
2395 char buf[150];
2396 if (status_id >= 0) // Remove prev. selection msg.
2397 gtk_statusbar_remove(GTK_STATUSBAR(sbar), sbar_sel, status_id);
2398 if (selected >= 0) {
2399 int shapenum = info[selected].shapenum;
2400 int nframes = ifile->get_num_frames(shapenum);
2401 g_snprintf(buf, sizeof(buf), "Shape %d (0x%03x, %d frames)",
2402 shapenum, shapenum, nframes);
2403 ExultStudio *studio = ExultStudio::get_instance();
2404 if (shapes_file) {
2405 const char *nm;
2406 if ((nm = studio->get_shape_name(shapenum))) {
2407 int len = strlen(buf);
2408 g_snprintf(buf + len, sizeof(buf) - len,
2409 ": '%s'", nm);
2410 }
2411 if (shapes_file->read_info(studio->get_game_type(), true))
2412 studio->set_shapeinfo_modified();
2413 int frnum = info[selected].framenum;
2414 const Shape_info &inf = shapes_file->get_info(shapenum);
2415 const Frame_name_info *nminf;
2416 if (inf.has_frame_name_info() &&
2417 (nminf = inf.get_frame_name(frnum, -1)) != nullptr) {
2418 int type = nminf->get_type();
2419 int msgid = nminf->get_msgid();
2420 if (type >= 0 && msgid < get_num_misc_names()) {
2421 const char *msgstr = get_misc_name(msgid);
2422 int len = strlen(buf);
2423 // For safety.
2424 if (!nm) nm = "";
2425 if (!msgstr) msgstr = "";
2426 if (type > 0) {
2427 const char *otmsgstr;
2428 if (type > 2)
2429 otmsgstr = "<NPC Name>";
2430 else {
2431 int otmsg = nminf->get_othermsg();
2432 otmsgstr = otmsg == -255 ? nm :
2433 (otmsg == -1 || otmsg >= get_num_misc_names() ? "" :
2434 get_misc_name(otmsg));
2435 if (!otmsgstr) otmsgstr = "";
2436 }
2437 const char *prefix = nullptr;
2438 const char *suffix = nullptr;
2439 if (type & 1) {
2440 prefix = otmsgstr;
2441 suffix = msgstr;
2442 } else {
2443 prefix = msgstr;
2444 suffix = otmsgstr;
2445 }
2446 g_snprintf(buf + len, sizeof(buf) - len,
2447 " - '%s%s'", prefix, suffix);
2448 } else
2449 g_snprintf(buf + len, sizeof(buf) - len,
2450 " - '%s'", msgstr);
2451 }
2452 }
2453 }
2454 status_id = gtk_statusbar_push(GTK_STATUSBAR(sbar),
2455 sbar_sel, buf);
2456 } else if (!info.empty() && !group) {
2457 int first_shape = info[rows[row0].index0].shapenum;
2458 g_snprintf(buf, sizeof(buf), "Shapes %d to %d (0x%03x to 0x%03x)",
2459 first_shape, last_shape, first_shape, last_shape);
2460 status_id = gtk_statusbar_push(GTK_STATUSBAR(sbar),
2461 sbar_sel, buf);
2462 } else
2463 status_id = -1;
2464 }
2465
2466
2467