/**
 ** A GTK widget showing a palette's colors.
 **
 ** Written: 12/24/2000 - JSF
 **/

/*
Copyright (C) 2000-2022 The Exult Team

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "paledit.h"

#include "shapefile.h"
#include "u7drag.h"
#include "utils.h"

#include <glib.h>

#include <cctype>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>

using EStudio::Alert;
using EStudio::Prompt;
using std::cout;
using std::endl;
using std::ifstream;
using std::make_unique;
using std::ofstream;
using std::ostream;
using std::setw;
using std::string;

/*
 *  Write out a single palette to a buffer.
 */

static void Write_palette(
    unsigned char *buf,     // 3*256 bytes.
    ExultRgbCmap *pal       // Palette to write.
) {
	for (int i = 0; i < 256; i++) {
		int r = (pal->colors[i] >> 16) & 255;
		int g = (pal->colors[i] >> 8) & 255;
		int b = pal->colors[i] & 255;
		buf[3 * i] = r / 4; // Range 0-63.
		buf[3 * i + 1] = g / 4;
		buf[3 * i + 2] = b / 4;
	}
}

/*
 *  Blit onto screen.
 */

inline void Palette_edit::show(
    int x, int y, int w, int h  // Area to blit.
) {
	GtkAllocation alloc = {0, 0, 0, 0};
	gtk_widget_get_allocation(draw, &alloc);
	if (drawgc != nullptr) {   // In expose Callback
		int bw = alloc.width  / 16;
		int bh = alloc.height / 16;
		int rw = alloc.width  % 16;
		int rh = alloc.height % 16;
		// Two segments intersect iff neither is entirely right of the other
		for (int ly = 0; ly < 16; ly++) {
			int yp = (ly * bh) + (ly <= rh ? ly : rh);
			int hp = bh + (ly <= rh ? 1 : 0);
			if ((yp <= (y + h)) && (y <= (yp + hp))) {
				for (int lx = 0; lx < 16; lx++) {
					int xp = (lx * bw) + (lx <= rw ? lx : rw);
					int wp = bw + (lx <= rw ? 1 : 0);
					if ((xp <= (x + w)) && (x <= (xp + wp))) {
						guint32 color = palettes[cur_pal]->colors[lx + ly * 16];
						cairo_set_source_rgb(drawgc,
						                     ((color >> 16) & 255) / 255.0,
						                     ((color >> 8) & 255) / 255.0,
						                     (color & 255) / 255.0);
						cairo_rectangle(drawgc, xp, yp, wp, hp);
						cairo_fill(drawgc);
					}
				}
			}
		}
	}
	if ((selected >= 0) && (drawgc != nullptr)) {   // Show selected.
		// Draw yellow box.
		cairo_set_line_width(drawgc, 1.0);
		cairo_set_source_rgb(drawgc,
		                     ((drawfg >> 16) & 255) / 255.0,
		                     ((drawfg >> 8) & 255) / 255.0,
		                     (drawfg & 255) / 255.0);
		cairo_rectangle(drawgc, selected_box.x, selected_box.y, selected_box.w, selected_box.h);
		cairo_stroke(drawgc);
	}
}

/*
 *  Select an entry.  This should be called after rendering
 *  the shape.
 */

void Palette_edit::select(
    int new_sel
) {
	selected = new_sel;
	// Remove prev. selection msg.
	gtk_statusbar_pop(GTK_STATUSBAR(sbar), sbar_sel);
	char buf[150];          // Show new selection.
	g_snprintf(buf, sizeof(buf), "Color %d (0x%02x)", new_sel, new_sel);
	gtk_statusbar_push(GTK_STATUSBAR(sbar), sbar_sel, buf);
}

/*
 *  Load/reload from file.
 */

void Palette_edit::load_internal(
) {
	// Free old.
	for (auto *palette : palettes)
		delete palette;
	unsigned cnt = flex_info->size();
	palettes.resize(cnt);       // Set size of list.
	if (!cnt)           // No palettes?
		new_palette();      // Create 1 blank palette.
	else {
		for (unsigned pnum = 0; pnum < cnt; pnum++) {
			size_t len;
			unsigned char *buf = flex_info->get(pnum, len);
			palettes[pnum] = new ExultRgbCmap;
			for (size_t i = 0; i < len / 3; i++)
				palettes[pnum]->colors[i] = (buf[3 * i] << 16) * 4 +
				            (buf[3 * i + 1] << 8) * 4 + buf[3 * i + 2] * 4;
			for (size_t i = len / 3; i < 256; i++)
				palettes[pnum]->colors[i] = 0;
		}
	}
}

/*
 *  Draw the palette.  This need only be called when it changes.
 */

void Palette_edit::render(
) {
	GtkAllocation alloc = {0, 0, 0, 0};
	gtk_widget_get_allocation(draw, &alloc);
	int neww = alloc.width;
	int newh = alloc.height;
	// Changed size?
	if (neww != width || newh != height) {
		width = neww;
		height = newh;
	}
	// Figure cell size.
	int eachw = width / 16;
	int eachh = height / 16;
	// Figure extra pixels.
	int extraw = width % 16;
	int extrah = height % 16;
	if (selected >= 0) {    // Update selected box.
		int selx = selected % 16;
		int sely = selected / 16;
		selected_box.x = selx * eachw;
		selected_box.y = sely * eachh;
		selected_box.w = eachw;
		selected_box.h = eachh;
		if (selx < extraw) { // Watch for extra pixels.
			selected_box.w++;
			selected_box.x += selx;
		} else
			selected_box.x += extraw;
		if (sely < extrah) {
			selected_box.h++;
			selected_box.y += sely;
		} else
			selected_box.y += extrah;
		select(selected);
	}
	gtk_widget_queue_draw(draw);
}

/*
 *  Handle double-click on a color by bringing up a color-selector.
 */

void Palette_edit::double_clicked(
) {
	cout << "Double-clicked" << endl;
	if (selected < 0 || colorsel)   // Only one at a time.
		return;         // Nothing selected.
	char buf[150];          // Show new selection.
	g_snprintf(buf, sizeof(buf), "Color %d (0x%02x)", selected, selected);
	colorsel = GTK_COLOR_CHOOSER_DIALOG(
	               gtk_color_chooser_dialog_new(buf, nullptr));
	// Get color.
	guint32 c = palettes[cur_pal]->colors[selected];
	GdkRGBA  rgba;
	rgba.red   = ((c >> 16) & 0xff) / 255.0;
	rgba.green = ((c >>  8) & 0xff) / 255.0;
	rgba.blue  = ((c >>  0) & 0xff) / 255.0;
	rgba.alpha = 1.0;
	gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(colorsel), &rgba);
	if (gtk_dialog_run(GTK_DIALOG(colorsel)) == GTK_RESPONSE_OK) {
		gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(colorsel), &rgba);
		auto r = static_cast<unsigned char>(rgba.red   * 255.0);
		auto g = static_cast<unsigned char>(rgba.green * 255.0);
		auto b = static_cast<unsigned char>(rgba.blue  * 255.0);
		if (selected >= 0)
			palettes[cur_pal]->colors[selected] =
			    (r << 16) + (g << 8) + b;
		// Send to flex file.
		update_flex(cur_pal);
		render();
	}
	gtk_widget_destroy(GTK_WIDGET(colorsel));
	colorsel = nullptr;
}

/*
 *  Configure the viewing window.
 */

gint Palette_edit::configure(
    GtkWidget *widget,      // The view window.
    GdkEventConfigure *event,
    gpointer data           // ->Palette_edit
) {
	ignore_unused_variable_warning(event, widget);
	auto *paled = static_cast<Palette_edit *>(data);
	if (!paled->width) {    // First time?
		// Foreground = yellow.
		paled->drawfg = (255 << 16) + (255 << 8);
	}
	paled->render();
	return TRUE;
}

/*
 *  Handle an expose event.
 */

gint Palette_edit::expose(
    GtkWidget *widget,      // The view window.
    cairo_t *cairo,
    gpointer data           // ->Palette_edit.
) {
	ignore_unused_variable_warning(widget);
	auto *paled = static_cast<Palette_edit *>(data);
	paled->set_graphic_context(cairo);
	GdkRectangle area = { 0, 0, 0, 0 };
	gdk_cairo_get_clip_rectangle(cairo, &area);
	paled->show(area.x, area.y, area.width, area.height);
	paled->set_graphic_context(nullptr);
	return TRUE;
}

/*
 *  Handle a mouse button press event.
 */

gint Palette_edit::mouse_press(
    GtkWidget *widget,      // The view window.
    GdkEventButton *event,
    gpointer data           // ->Palette_edit.
) {
	ignore_unused_variable_warning(widget);
	auto *paled = static_cast<Palette_edit *>(data);

	if (event->button == 4 || event->button == 5) // mouse wheel
		return TRUE;

	if (paled->colorsel)
		return TRUE;      // Already editing a color.
	int old_selected = paled->selected;
	int width = paled->width;
	int height = paled->height;
	int eventx = static_cast<int>(event->x);
	int eventy = static_cast<int>(event->y);
	// Figure cell size.
	int eachw = width / 16;
	int eachh = height / 16;
	// Figure extra pixels.
	int extraw = width % 16;
	int extrah = height % 16;
	int extrax = extraw * (eachw + 1); // Total length of extra-sized boxes.
	int extray = extrah * (eachh + 1);
	int selx;
	int sely;         // Gets box indices.
	if (eventx < extrax)
		selx = eventx / (eachw + 1);
	else
		selx = extraw + (eventx - extrax) / eachw;
	if (eventy < extray)
		sely = eventy / (eachh + 1);
	else
		sely = extrah + (eventy - extray) / eachh;
	paled->selected = sely * 16 + selx;
	if (paled->selected == old_selected) {
		// Same square.  Check for dbl-click.
		if (reinterpret_cast<GdkEvent *>(event)->type == GDK_2BUTTON_PRESS)
			paled->double_clicked();
	} else {
		paled->render();
	}
	if (event->button == 3)
		gtk_menu_popup_at_pointer(GTK_MENU(paled->create_popup()),
		                          reinterpret_cast<GdkEvent *>(event));
	return TRUE;
}

/*
 *  Someone wants the dragged shape.
 */

void Palette_edit::drag_data_get(
    GtkWidget *widget,      // The view window.
    GdkDragContext *context,
    GtkSelectionData *seldata,  // Fill this in.
    guint info,
    guint time,
    gpointer data           // ->Palette_edit.
) {
	ignore_unused_variable_warning(widget, context, seldata, info, time, data);
	cout << "In DRAG_DATA_GET" << endl;
}

/*
 *  Beginning of a drag.
 */

gint Palette_edit::drag_begin(
    GtkWidget *widget,      // The view window.
    GdkDragContext *context,
    gpointer data           // ->Palette_edit.
) {
	ignore_unused_variable_warning(widget, context, data);
	cout << "In DRAG_BEGIN of Palette" << endl;
	//Palette_edit *paled = static_cast<Palette_edit *>(data);
	// Maybe someday.
	return TRUE;
}

/*
 *  Handle a change to the 'Palette #' spin button.
 */

void Palette_edit::palnum_changed(
    GtkAdjustment *adj,     // The adjustment.
    gpointer data           // ->Shape_chooser.
) {
	auto *paled = static_cast<Palette_edit *>(data);
	gint newnum = static_cast<gint>(gtk_adjustment_get_value(adj));
	paled->show_palette(newnum);
	paled->render();
	paled->enable_controls();
}

/*
 *  Callbacks for buttons:
 */
void
on_exportbtn_clicked(GtkButton       *button,
                     gpointer         user_data) {
	ignore_unused_variable_warning(button);
	Create_file_selection("Export palette to text format",
	                      "<PATCH>", nullptr,
	                      {},
	                      GTK_FILE_CHOOSER_ACTION_SAVE,
	                      Palette_edit::export_palette,
	                      user_data);
}

void
on_importbtn_clicked(GtkButton       *button,
                     gpointer         user_data) {
	ignore_unused_variable_warning(button);
	Create_file_selection("Import palette from text format",
	                      "<STATIC>", nullptr,
	                      {},
	                      GTK_FILE_CHOOSER_ACTION_OPEN,
	                      Palette_edit::import_palette,
	                      user_data);
}

void
on_insert_btn_clicked(GtkButton       *button,
                      gpointer         user_data) {
	ignore_unused_variable_warning(button);
	auto *paled = static_cast<Palette_edit *>(user_data);
	paled->add_palette();
}
void
on_remove_btn_clicked(GtkButton       *button,
                      gpointer         user_data) {
	ignore_unused_variable_warning(button);
	auto *paled = static_cast<Palette_edit *>(user_data);
	paled->remove_palette();
}
void
on_up_btn_clicked(GtkButton       *button,
                  gpointer         user_data) {
	ignore_unused_variable_warning(button);
	auto *paled = static_cast<Palette_edit *>(user_data);
	paled->move_palette(true);
}
void
on_down_btn_clicked(GtkButton       *button,
                    gpointer         user_data) {
	ignore_unused_variable_warning(button);
	auto *paled = static_cast<Palette_edit *>(user_data);
	paled->move_palette(false);
}

/*
 *  Create box with 'Palette #', 'Import', 'Move' controls.
 */

GtkWidget *Palette_edit::create_controls(
) {
	// Create main box.
	GtkWidget *topframe = gtk_frame_new(nullptr);
	widget_set_margins(topframe, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(topframe);

	GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE);
	gtk_widget_show(vbox);
	gtk_container_add(GTK_CONTAINER(topframe), vbox);

	GtkWidget *hbox0 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
	gtk_box_set_homogeneous(GTK_BOX(hbox0), FALSE);
	widget_set_margins(hbox0, 1*HMARGIN, 1*HMARGIN, 1*VMARGIN, 1*VMARGIN);
	gtk_widget_show(hbox0);
	gtk_box_pack_start(GTK_BOX(vbox), hbox0, TRUE, TRUE, 0);
	/*
	 *  The 'Edit' controls.
	 */
	GtkWidget *frame = gtk_frame_new("Edit");
	widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(frame);
	gtk_box_pack_start(GTK_BOX(hbox0), frame, FALSE, FALSE, 0);

	GtkWidget *hbuttonbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
	gtk_widget_show(hbuttonbox);
	gtk_container_add(GTK_CONTAINER(frame), hbuttonbox);
	gtk_button_box_set_layout(GTK_BUTTON_BOX(hbuttonbox),
	                          GTK_BUTTONBOX_START);
	gtk_box_set_spacing(GTK_BOX(hbuttonbox), 0);

	insert_btn = gtk_button_new_with_label("New");
	widget_set_margins(insert_btn, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(insert_btn);
	gtk_container_add(GTK_CONTAINER(hbuttonbox), insert_btn);
	gtk_widget_set_can_default(GTK_WIDGET(insert_btn), TRUE);

	remove_btn = gtk_button_new_with_label("Remove");
	widget_set_margins(remove_btn, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(remove_btn);
	gtk_container_add(GTK_CONTAINER(hbuttonbox), remove_btn);
	gtk_widget_set_can_default(GTK_WIDGET(remove_btn), TRUE);

	g_signal_connect(G_OBJECT(insert_btn), "clicked",
	                 G_CALLBACK(on_insert_btn_clicked),
	                 this);
	g_signal_connect(G_OBJECT(remove_btn), "clicked",
	                 G_CALLBACK(on_remove_btn_clicked),
	                 this);
	/*
	 *  The 'Move' controls.
	 */
	frame = gtk_frame_new("Move");
	widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(frame);
	gtk_box_pack_start(GTK_BOX(hbox0), frame, FALSE, FALSE, 0);

	GtkWidget *bbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
	gtk_box_set_homogeneous(GTK_BOX(bbox), TRUE);
	gtk_widget_show(bbox);
	gtk_container_add(GTK_CONTAINER(frame), bbox);

	down_btn = gtk_button_new();
	widget_set_margins(down_btn, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(down_btn);
	gtk_box_pack_start(GTK_BOX(bbox), down_btn, FALSE, FALSE, 0);
	gtk_widget_set_can_default(GTK_WIDGET(down_btn), TRUE);
	GtkWidget *arrow = gtk_image_new_from_icon_name("go-down", GTK_ICON_SIZE_BUTTON);
	gtk_button_set_image(GTK_BUTTON(down_btn), arrow);

	up_btn = gtk_button_new();
	widget_set_margins(up_btn, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(up_btn);
	gtk_box_pack_start(GTK_BOX(bbox), up_btn, FALSE, FALSE, 0);
	gtk_widget_set_can_default(GTK_WIDGET(up_btn), TRUE);
	arrow = gtk_image_new_from_icon_name("go-up", GTK_ICON_SIZE_BUTTON);
	gtk_button_set_image(GTK_BUTTON(up_btn), arrow);

	g_signal_connect(G_OBJECT(down_btn), "clicked",
	                 G_CALLBACK(on_down_btn_clicked),
	                 this);
	g_signal_connect(G_OBJECT(up_btn), "clicked",
	                 G_CALLBACK(on_up_btn_clicked),
	                 this);
	/*
	 *  The 'File' controls.
	 */
	frame = gtk_frame_new("File");
	widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(frame);
	gtk_box_pack_start(GTK_BOX(hbox0), frame, FALSE, FALSE, 0);

	hbuttonbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
	gtk_widget_show(hbuttonbox);
	gtk_container_add(GTK_CONTAINER(frame), hbuttonbox);
	gtk_button_box_set_layout(GTK_BUTTON_BOX(hbuttonbox),
	                          GTK_BUTTONBOX_START);
	gtk_box_set_spacing(GTK_BOX(hbuttonbox), 0);

	GtkWidget *importbtn = gtk_button_new_with_label("Import");
	widget_set_margins(importbtn, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(importbtn);
	gtk_container_add(GTK_CONTAINER(hbuttonbox), importbtn);
	gtk_widget_set_can_default(GTK_WIDGET(importbtn), TRUE);

	GtkWidget *exportbtn = gtk_button_new_with_label("Export");
	widget_set_margins(exportbtn, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(exportbtn);
	gtk_container_add(GTK_CONTAINER(hbuttonbox), exportbtn);
	gtk_widget_set_can_default(GTK_WIDGET(exportbtn), TRUE);

	g_signal_connect(G_OBJECT(importbtn), "clicked",
	                 G_CALLBACK(on_importbtn_clicked),
	                 this);
	g_signal_connect(G_OBJECT(exportbtn), "clicked",
	                 G_CALLBACK(on_exportbtn_clicked),
	                 this);
	return topframe;
}

/*
 *  Enable/disable controls after changes.
 */

void Palette_edit::enable_controls(
) {
	// Can't delete last one.
	gtk_widget_set_sensitive(remove_btn, cur_pal >= 0 &&
	                         palettes.size() > 1);
	if (cur_pal == -1) {    // No palette?
		gtk_widget_set_sensitive(down_btn, false);
		gtk_widget_set_sensitive(up_btn, false);
		gtk_widget_set_sensitive(remove_btn, false);
	} else {
		gtk_widget_set_sensitive(down_btn,
		                         static_cast<unsigned>(cur_pal) < palettes.size() - 1);
		gtk_widget_set_sensitive(up_btn, cur_pal > 0);
		gtk_widget_set_sensitive(remove_btn, palettes.size() > 1);
	}
}

/*
 *  Set up box.
 */

void Palette_edit::setup(
) {
	// Put things in a vert. box.
	GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE);
	gtk_widget_show(vbox);
	set_widget(vbox);

	// A frame looks nice.
	GtkWidget *frame = gtk_frame_new(nullptr);
	gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
	widget_set_margins(frame, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(frame);
	gtk_box_pack_start(GTK_BOX(vbox), frame, TRUE, TRUE, 0);

	draw = gtk_drawing_area_new();  // Create drawing area window.
//	gtk_widget_set_size_request(draw, w, h);
	// Indicate the events we want.
	gtk_widget_set_events(draw, GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK
	                      | GDK_BUTTON1_MOTION_MASK);
	// Set "configure" handler.
	g_signal_connect(G_OBJECT(draw), "configure-event",
	                 G_CALLBACK(configure), this);
	// Set "expose-event" - "draw" handler.
	g_signal_connect(G_OBJECT(draw), "draw",
	                 G_CALLBACK(expose), this);
	// Set mouse click handler.
	g_signal_connect(G_OBJECT(draw), "button-press-event",
	                 G_CALLBACK(mouse_press), this);
	// Mouse motion.
	g_signal_connect(G_OBJECT(draw), "drag-begin",
	                 G_CALLBACK(drag_begin), this);
	g_signal_connect(G_OBJECT(draw), "drag-data-get",
	                 G_CALLBACK(drag_data_get), this);
	gtk_container_add(GTK_CONTAINER(frame), draw);
	widget_set_margins(draw, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(draw);

	// At bottom, a status bar.
	sbar = gtk_statusbar_new();
	sbar_sel = gtk_statusbar_get_context_id(GTK_STATUSBAR(sbar),
	                                        "selection");
	// At the bottom, status bar & frame:
	GtkWidget *hbox1 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
	gtk_box_set_homogeneous(GTK_BOX(hbox1), FALSE);
	gtk_box_pack_start(GTK_BOX(vbox), hbox1, FALSE, FALSE, 0);
	gtk_widget_show(hbox1);
	gtk_box_pack_start(GTK_BOX(hbox1), sbar, TRUE, TRUE, 0);
	// Palette # to right of sbar.
	GtkWidget *label = gtk_label_new("Palette #:");
	widget_set_margins(label, 2*HMARGIN, 1*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_box_pack_start(GTK_BOX(hbox1), label, FALSE, FALSE, 0);
	gtk_widget_show(label);

	// A spin button for palette#.
	palnum_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0,
	                            palettes.size() - 1, 1,
	                            2, 2));
	pspin = gtk_spin_button_new(palnum_adj, 1, 0);
	g_signal_connect(G_OBJECT(palnum_adj), "value-changed",
	                 G_CALLBACK(palnum_changed), this);
	widget_set_margins(pspin, 1*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_box_pack_start(GTK_BOX(hbox1), pspin, FALSE, FALSE, 0);
	gtk_widget_show(pspin);

	// Add edit controls to bottom.
	gtk_box_pack_start(GTK_BOX(vbox), create_controls(), FALSE, FALSE, 0);
	widget_set_margins(sbar, 2*HMARGIN, 2*HMARGIN, 2*VMARGIN, 2*VMARGIN);
	gtk_widget_show(sbar);
	enable_controls();
}

/*
 *  Create/add a new palette.
 */

void Palette_edit::new_palette(
) {
	auto *newpal = new ExultRgbCmap;        // R, G, B, then all black.
	memset(&newpal->colors[0], 0, 256 * sizeof(guint32));
	newpal->colors[0] = 255 << 16;
	newpal->colors[1] = 255 << 8;
	newpal->colors[2] = 255;
	int index = palettes.size();    // Index of new palette.
	palettes.push_back(newpal);
	update_flex(index);     // Add to file.
}

/*
 *  Update palette entry in flex file.
 */

void Palette_edit::update_flex(
    int pnum            // Palette # to send to file.
) {
	auto buf = make_unique<unsigned char[]>(3 * 256);
	Write_palette(buf.get(), palettes[pnum]);
	// Update or append file data.
	flex_info->set(pnum, std::move(buf), 3 * 256);
	flex_info->set_modified();
}

/*
 *  Create the list for a single palette.
 */

Palette_edit::Palette_edit(
    Flex_file_info *flinfo      // Flex-file info.
) : Object_browser(nullptr, flinfo),
	flex_info(flinfo), /* image(nullptr),*/ width(0), height(0),
	cur_pal(0), colorsel(nullptr) {
	load_internal();             // Load from file.
	setup();
}

/*
 *  Delete.
 */

Palette_edit::~Palette_edit(
) {
	for (auto *palette : palettes)
		delete palette;
	gtk_widget_destroy(get_widget());
}

/*
 *  Get i'th palette.
 */

void Palette_edit::show_palette(
    int palnum
) {
	cur_pal = palnum;
}

/*
 *  Unselect.
 */

void Palette_edit::unselect(
    bool need_render            // 1 to render and show.
) {
	if (selected >= 0) {
		selected = -1;
		if (need_render) {
			render();
		}
	}
}

/*
 *  Move a palette within the list.
 */

void Palette_edit::move_palette(
    bool up
) {
	if (cur_pal < 0)
		return;
	ExultRgbCmap *tmp;
	if (up) {
		if (cur_pal > 0) {
			tmp = palettes[cur_pal - 1];
			palettes[cur_pal - 1] = palettes[cur_pal];
			palettes[cur_pal] = tmp;
			cur_pal--;
			flex_info->swap(cur_pal);// Update flex-file list.
		}
	} else {
		if (static_cast<unsigned>(cur_pal) < palettes.size() - 1) {
			tmp = palettes[cur_pal + 1];
			palettes[cur_pal + 1] = palettes[cur_pal];
			palettes[cur_pal] = tmp;
			flex_info->swap(cur_pal);// Update flex-file list.
			cur_pal++;
		}
	}
	flex_info->set_modified();
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(pspin), cur_pal);
}

/*
 *  Update upper bound of a range widget.
 */

void Update_range_upper(
    GtkAdjustment *adj,
    int new_upper
) {
	gtk_adjustment_set_upper(adj, new_upper);
	g_signal_emit_by_name(G_OBJECT(adj), "changed");
}

/*
 *  Add a new palette at the end of the list.
 */

void Palette_edit::add_palette(
) {
	new_palette();
	cur_pal = palettes.size() - 1;  // Set to display new palette.
	Update_range_upper(palnum_adj, palettes.size() - 1);
	// This will update the display:
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(pspin), cur_pal);
}

/*
 *  Remove the current palette.
 */

void Palette_edit::remove_palette(
) {
	// Don't delete the last one.
	if (cur_pal < 0 || palettes.size() < 2)
		return;
	if (Prompt(
	            "Do you really want to delete the palette you're viewing?",
	            "Yes", "No") != 0)
		return;
	delete palettes[cur_pal];
	palettes.erase(palettes.begin() + cur_pal);
	flex_info->remove(cur_pal);
	flex_info->set_modified();
	if (static_cast<unsigned>(cur_pal) >= palettes.size())
		cur_pal = palettes.size() - 1;
	Update_range_upper(palnum_adj, palettes.size() - 1);
	// This will update the display:
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(pspin), cur_pal);
	render();           // Cur_pal may not have changed.
}

/*
 *  Export current palette.
 */

void Palette_edit::export_palette(
    const char *fname,
    gpointer user_data
) {
	auto *paled = static_cast<Palette_edit *>(user_data);
	if (U7exists(fname)) {
		char *msg = g_strdup_printf(
		                "'%s' already exists.  Overwrite?", fname);
		int answer = Prompt(msg, "Yes", "No");
		g_free(msg);
		if (answer != 0)
			return;
	}
	// Write out current palette.
	ExultRgbCmap *pal = paled->palettes[paled->cur_pal];
	ofstream out(fname);        // OKAY that it's a 'text' file.
	out << "GIMP Palette" << endl;
	out << "# Exported from ExultStudio" << endl;
	int i;              // Skip 0's at end.
	for (i = 255; i > 0; i--)
		if (pal->colors[i] != 0)
			break;
	int last_color = i;
	for (i = 0; i <= last_color; i++) {
		int r = (pal->colors[i] >> 16) & 255;
		int g = (pal->colors[i] >> 8) & 255;
		int b = pal->colors[i] & 255;
		out << setw(3) << r << ' ' << setw(3) << g << ' ' <<
		    setw(3) << b << endl;
	}
	out.close();
}

/*
 *  Import current palette.
 */

void Palette_edit::import_palette(
    const char *fname,
    gpointer user_data
) {
	auto *paled = static_cast<Palette_edit *>(user_data);
	char *msg = g_strdup_printf(
	                "Overwrite current palette from '%s'?", fname);
	int answer = Prompt(msg, "Yes", "No");
	g_free(msg);
	if (answer != 0)
		return;
	// Read in current palette.
	ExultRgbCmap *pal = paled->palettes[paled->cur_pal];
	ifstream in(fname);     // OKAY that it's a 'text' file.
	char buf[256];
	in.getline(buf, sizeof(buf));   // Skip 1st line.
	if (!in.good()) {
		Alert("Error reading '%s'", fname);
		return;
	}
	int i = 0;          // Color #.
	while (i < 256 && !in.eof()) {
		in.getline(buf, sizeof(buf));
		char *ptr = &buf[0];
		// Skip spaces.
		while (ptr < buf + sizeof(buf) && *ptr && isspace(static_cast<unsigned char>(*ptr)))
			ptr++;
		if (*ptr == '#')
			continue;   // Comment.
		int r;
		int g;
		int b;
		if (sscanf(buf, "%d %d %d", &r, &g, &b) == 3)
			pal->colors[i++] = (r << 16) + (g << 8) + b;
	}
	in.close();
	// Add to file.
	paled->update_flex(paled->cur_pal);
	paled->render();
}

