text: // SciTE - Scintilla based Text Editor
  // Widget.cxx - code for manipulating  GTK widgets
  // Copyright 2010 by Neil Hodgson <neilh@scintilla.org>
  // The License.txt file describes the conditions under which this software may be distributed.
  
  #include <stdlib.h>
  #include <string.h>
  
  #include <string>
  #include <vector>
  #include <chrono>
  
  #include <gtk/gtk.h>
  #include <gdk/gdkkeysyms.h>
  
  #include "Scintilla.h"
  
  #include "GUI.h"
  #include "StringHelpers.h"
  #include "Widget.h"
  
  // Key names are longer for GTK 3
  #if GTK_CHECK_VERSION(3,0,0)
  #define GKEY_Escape GDK_KEY_Escape
  #define GKEY_Void GDK_KEY_VoidSymbol
  #else
  #define GKEY_Escape GDK_Escape
  #define GKEY_Void GDK_VoidSymbol
  #endif
  
  WBase::operator GtkWidget*() const {
  	return GTK_WIDGET(GetID());
  }
  
  GtkWidget* WBase::Pointer() {
  	return GTK_WIDGET(GetID());
  }
  
  bool WBase::Sensitive() {
  	return gtk_widget_get_sensitive(GTK_WIDGET(Pointer()));
  }
  
  void WStatic::Create(GUI::gui_string text) {
  	SetID(gtk_label_new_with_mnemonic(text.c_str()));
  }
  
  bool WStatic::HasMnemonic() {
  	return gtk_label_get_mnemonic_keyval(GTK_LABEL(Pointer())) != GKEY_Void;
  }
  
  void WStatic::SetMnemonicFor(WBase &w) {
  	gtk_label_set_mnemonic_widget(GTK_LABEL(Pointer()), w);
  }
  
  void WEntry::Create(const GUI::gui_char *text) {
  	SetID(gtk_entry_new());
  	if (text)
  		gtk_entry_set_text(GTK_ENTRY(GetID()), text);
  }
  
  void WEntry::ActivatesDefault() {
  	gtk_entry_set_activates_default(GTK_ENTRY(GetID()), TRUE);
  }
  
  const GUI::gui_char *WEntry::Text() {
  	return gtk_entry_get_text(GTK_ENTRY(GetID()));
  }
  
  int WEntry::Value() {
  	return atoi(Text());
  }
  
  void WEntry::SetText(const GUI::gui_char *text) {
  	return gtk_entry_set_text(GTK_ENTRY(GetID()), text);
  }
  
  void WEntry::SetValid(GtkEntry *entry, bool valid) {
  #if GTK_CHECK_VERSION(3,0,0)
  	gtk_widget_set_name(GTK_WIDGET(entry), valid ? "" : "entryInvalid");
  #else
  	if (valid) {
  		// Reset widget's background color
  		// to the one given by the GTK theme
  		gtk_widget_modify_base(GTK_WIDGET(entry), GTK_STATE_NORMAL, NULL);
  	} else {
  		GdkColor red = { 0, 0xFFFF, 0x6666, 0x6666 };
  		gtk_widget_modify_base(GTK_WIDGET(entry), GTK_STATE_NORMAL, &red);
  	}
  #endif
  }
  
  void WComboBoxEntry::Create() {
  #if GTK_CHECK_VERSION(3,0,0)
  	SetID(gtk_combo_box_text_new_with_entry());
  #else
  	SetID(gtk_combo_box_entry_new_text());
  #endif
  }
  
  GtkEntry *WComboBoxEntry::Entry() {
  	return GTK_ENTRY(gtk_bin_get_child(GTK_BIN(GetID())));
  }
  
  void WComboBoxEntry::ActivatesDefault() {
  	gtk_entry_set_activates_default(Entry(), TRUE);
  }
  
  const GUI::gui_char *WComboBoxEntry::Text() {
  	return gtk_entry_get_text(Entry());
  }
  
  void WComboBoxEntry::SetText(const GUI::gui_char *text) {
  	return gtk_entry_set_text(Entry(), text);
  }
  
  bool WComboBoxEntry::HasFocusOnSelfOrChild() {
  	return HasFocus() || gtk_widget_has_focus(GTK_WIDGET(Entry()));
  }
  
  void WComboBoxEntry::RemoveText(int position) {
  #if GTK_CHECK_VERSION(3,0,0)
  	gtk_combo_box_text_remove(GTK_COMBO_BOX_TEXT(GetID()), position);
  #else
  	gtk_combo_box_remove_text(GTK_COMBO_BOX(GetID()), position);
  #endif
  }
  
  void WComboBoxEntry::AppendText(const char *text) {
  #if GTK_CHECK_VERSION(3,0,0)
  	gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(GetID()), text);
  #if GTK_CHECK_VERSION(3,14,0)
  	gtk_combo_box_set_button_sensitivity(GTK_COMBO_BOX(GetID()), GTK_SENSITIVITY_ON);
  #endif
  #else
  	gtk_combo_box_append_text(GTK_COMBO_BOX(GetID()), text);
  #endif
  }
  
  void WComboBoxEntry::ClearList() {
  #if GTK_CHECK_VERSION(3,0,0)
  	gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(GetID()));
  #if GTK_CHECK_VERSION(3,14,0)
  	gtk_combo_box_set_button_sensitivity(GTK_COMBO_BOX(GetID()), GTK_SENSITIVITY_OFF);
  #endif
  #else
  	for (int i = 0; i < 10; i++) {
  		RemoveText(0);
  	}
  #endif
  }
  
  void WComboBoxEntry::FillFromMemory(const std::vector<std::string> &mem, bool useTop) {
  	ClearList();
  	for (const std::string &s : mem) {
  		AppendText(s.c_str());
  	}
  	if (useTop) {
  		gtk_entry_set_text(Entry(), mem[0].c_str());
  	}
  }
  
  void WButton::Create(GUI::gui_string text, GCallback func, gpointer data) {
  	SetID(gtk_button_new_with_mnemonic(text.c_str()));
  	gtk_widget_set_can_default(GTK_WIDGET(GetID()), TRUE);
  	g_signal_connect(G_OBJECT(GetID()), "clicked", func, data);
  }
  
  void WButton::Create(GUI::gui_string text) {
  	SetID(gtk_button_new_with_mnemonic(text.c_str()));
  	gtk_widget_set_can_default(GTK_WIDGET(GetID()), TRUE);
  }
  
  void WToggle::Create(const GUI::gui_string &text) {
  	SetID(gtk_check_button_new_with_mnemonic(text.c_str()));
  }
  bool WToggle::Active() {
  	return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(GetID()));
  }
  void WToggle::SetActive(bool active) {
  	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(GetID()), active);
  }
  
  void WProgress::Create() {
  	SetID(gtk_progress_bar_new());
  }
  
  WCheckDraw::WCheckDraw() : cdfn(NULL), user(NULL) {
  }
  
  WCheckDraw::~WCheckDraw() {
  }
  
  static void GreyToAlpha(GdkPixbuf *ppb, GdkColor fore) {
  	guchar *pixels = gdk_pixbuf_get_pixels(ppb);
  	int rowStride = gdk_pixbuf_get_rowstride(ppb);
  	int width = gdk_pixbuf_get_width(ppb);
  	int height = gdk_pixbuf_get_height(ppb);
  	for (int y =0; y<height; y++) {
  		guchar *pixelsRow = pixels + rowStride * y;
  		for (int x =0; x<width; x++) {
  			guchar alpha = pixelsRow[0];
  			pixelsRow[3] = 255 - alpha;
  			pixelsRow[0] = fore.red / 256;
  			pixelsRow[1] = fore.green / 256;
  			pixelsRow[2] = fore.blue / 256;
  			pixelsRow += 4;
  		}
  	}
  }
  
  void WCheckDraw::Create(const char **xpmImage, const GUI::gui_string &toolTip, GtkStyle *pStyle_) {
  	GtkWidget *button = gtk_toggle_button_new();
  	gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
  	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
  
  	GdkPixbuf *pbGrey = gdk_pixbuf_new_from_xpm_data(xpmImage);
  	GdkPixbuf *pbAlpha = gdk_pixbuf_add_alpha(pbGrey, TRUE, 0xff, 0xff, 0);
  	g_object_unref(pbGrey);
  #if GTK_CHECK_VERSION(3, 4, 0)
  	(void)pStyle_;
  	GdkRGBA rgbaFore;
  	GtkStyleContext *context = gtk_widget_get_style_context(button);
  	gtk_style_context_get_color(context, gtk_style_context_get_state(context), &rgbaFore);
  	GdkColor fore;
  	fore.red = rgbaFore.red * 65535;
  	fore.green = rgbaFore.green * 65535;
  	fore.blue = rgbaFore.blue * 65535;
  	fore.pixel = 0;
  #else
  	GdkColor fore = pStyle_->fg[gtk_widget_get_state(button)];
  #endif
  	// Convert the grey to alpha and make black
  	GreyToAlpha(pbAlpha, fore);
  	GtkWidget *image = gtk_image_new_from_pixbuf(pbAlpha);
  	g_object_unref(pbAlpha);
  
  	SetID(button);
  	gtk_container_add(GTK_CONTAINER(button), image);
  
  	g_signal_connect(button, "toggled", G_CALLBACK(Toggled), this);
  	gtk_widget_set_name(GTK_WIDGET(button), "toggler");
  
  	GUI::gui_string toolTipNoMnemonic = toolTip;
  	size_t posMnemonic = toolTipNoMnemonic.find("_");
  	if (posMnemonic != GUI::gui_string::npos)
  		toolTipNoMnemonic.replace(posMnemonic, 1, "");
  	gtk_widget_set_tooltip_text(Pointer(), toolTipNoMnemonic.c_str());
  }
  
  void WCheckDraw::Toggled(GtkWidget *, WCheckDraw *pcd) {
  	if (pcd->cdfn)
  		pcd->cdfn(pcd, pcd->user);
  }
  
  GtkToggleButton *WCheckDraw::ToggleButton() {
  	return reinterpret_cast<GtkToggleButton*>(GetID());
  }
  
  bool WCheckDraw::Active() {
  	return gtk_toggle_button_get_active(ToggleButton());
  }
  
  void WCheckDraw::SetActive(bool active) {
  	gtk_toggle_button_set_active(ToggleButton(), active);
  }
  
  void WCheckDraw::Toggle() {
  	SetActive(!Active());
  }
  
  void WCheckDraw::SetChangeFunction(ChangeFunction cdfn_, void *user_) {
  	cdfn = cdfn_;
  	user = user_;
  }
  
  #if GTK_CHECK_VERSION(3,4,0)
  #define USE_GRID 1
  #else
  #define USE_GRID 0
  #endif
  
  WTable::WTable(int rows_, int columns_) :
  	rows(rows_), columns(columns_), next(0) {
  #if USE_GRID
  	SetID(gtk_grid_new());
  #else
  	SetID(gtk_table_new(rows, columns, FALSE));
  #endif
  }
  
  void WTable::Add(GtkWidget *child, int width, bool expand, int xpadding, int ypadding) {
  	if (child) {
  #if USE_GRID
  		gtk_widget_set_hexpand(child, expand);
  #if GTK_CHECK_VERSION(3,14,0)
  		gtk_widget_set_margin_end(child, xpadding);
  #else
  		gtk_widget_set_margin_right(child, xpadding);
  #endif
  		gtk_widget_set_margin_bottom(child, ypadding);
  		gtk_grid_attach(GTK_GRID(GetID()), child,
  			next % columns, next / columns,
  			width, 1);
  #else
  		GtkAttachOptions opts = static_cast<GtkAttachOptions>(
  			GTK_SHRINK | GTK_FILL);
  		GtkAttachOptions optsExpand = static_cast<GtkAttachOptions>(
  			GTK_SHRINK | GTK_FILL | GTK_EXPAND);
  
  		gtk_table_attach(GTK_TABLE(GetID()), child,
  			next % columns, next % columns + width,
  			next / columns, (next / columns) + 1,
  			expand ? optsExpand : opts, opts,
  			xpadding, ypadding);
  #endif
  	}
  	next += width;
  }
  
  void WTable::Label(GtkWidget *child) {
  #if GTK_CHECK_VERSION(3,14,0)
  	gtk_widget_set_halign(child, GTK_ALIGN_END);
  	gtk_widget_set_valign(child, GTK_ALIGN_BASELINE);
  #else
  	gtk_misc_set_alignment(GTK_MISC(child), 1.0, 0.5);
  #endif
  	Add(child);
  }
  
  void WTable::PackInto(GtkBox *box, gboolean expand) {
  	gtk_box_pack_start(box, Pointer(), expand, expand, 0);
  }
  
  void WTable::Resize(int rows_, int columns_) {
  	rows = rows_;
  	columns = columns_;
  #if !USE_GRID
  	gtk_table_resize(GTK_TABLE(GetID()), rows, columns);
  #endif
  	next = 0;
  }
  
  void WTable::NextLine() {
  	next = ((next - 1) / columns + 1) * columns;
  }
  
  GUI::gui_char KeyFromLabel(GUI::gui_string label) {
  	if (!label.empty()) {
  		size_t posMnemonic = label.find('_');
  		return tolower(label[posMnemonic + 1]);
  	}
  	return 0;
  }
  
  void Dialog::Create(const GUI::gui_string &title) {
  	wid = gtk_dialog_new();
  	gtk_window_set_title(GTK_WINDOW(GetID()), title.c_str());
  	gtk_window_set_resizable(GTK_WINDOW(GetID()), TRUE);
  }
  
  void Dialog::Display(GtkWidget *parent, bool modal) {
  	// Mark it as a modal transient dialog
  	gtk_window_set_modal(GTK_WINDOW(GetID()), modal);
  	if (parent) {
  		gtk_window_set_transient_for(GTK_WINDOW(GetID()), GTK_WINDOW(parent));
  	}
  	g_signal_connect(G_OBJECT(GetID()), "destroy", G_CALLBACK(SignalDestroy), this);
  	gtk_widget_show_all(GTK_WIDGET(GetID()));
  	if (modal) {
  		while (Created()) {
  			gtk_main_iteration();
  		}
  	}
  }
  
  GtkWidget *Dialog::ResponseButton(const GUI::gui_string &text, int responseID) {
  	return gtk_dialog_add_button(GTK_DIALOG(GetID()), text.c_str(), responseID);
  }
  
  void Dialog::Present() {
  	gtk_window_present(GTK_WINDOW(GetID()));
  }
  
  GtkWidget *Dialog::ContentArea() {
  #if GTK_CHECK_VERSION(3,0,0)
  	return gtk_dialog_get_content_area(GTK_DIALOG(GetID()));
  #else
  	return GTK_DIALOG(GetID())->vbox;
  #endif
  }
  
  void Dialog::SignalDestroy(GtkWidget *, Dialog *d) {
  	if (d) {
  		d->SetID(0);
  	}
  }
  
  void Strip::Creation(GtkWidget *) {
  	g_signal_connect(G_OBJECT(GetID()), "button-press-event", G_CALLBACK(ButtonsPress), this);
  }
  
  void Strip::Show(int) {
  	gtk_widget_show(Widget(*this));
  	visible = true;
  }
  
  void Strip::Close() {
  	gtk_widget_hide(Widget(*this));
  	visible = false;
  }
  
  bool Strip::KeyDown(GdkEventKey *event) {
  	bool retVal = false;
  
  	if (visible) {
  		if (event->keyval == GKEY_Escape) {
  			Close();
  			return true;
  		}
  
  		if (event->state & GDK_MOD1_MASK) {
  			GList *childWidgets = gtk_container_get_children(GTK_CONTAINER(GetID()));
  			for (GList *child = g_list_first(childWidgets); child; child = g_list_next(child)) {
  				GtkWidget **w = (GtkWidget **)child;
  				std::string name = gtk_widget_get_name(*w);
  				std::string label;
  				if (name == "GtkButton" || name == "GtkCheckButton") {
  					label = gtk_button_get_label(GTK_BUTTON(*w));
  				} else if (name == "GtkLabel") {
  					label = gtk_label_get_label(GTK_LABEL(*w));
  				}
  				char key = KeyFromLabel(label);
  				if (static_cast<unsigned int>(key) == event->keyval) {
  					//fprintf(stderr, "%p %s %s %c\n", *w, name.c_str(), label.c_str(), key);
  					if (name == "GtkButton" || name == "GtkCheckButton") {
  						gtk_button_clicked(GTK_BUTTON(*w));
  					} else if (name == "GtkLabel") {
  						// Only ever use labels to label ComboBoxEntry
  						GtkWidget *pwidgetSelect = gtk_label_get_mnemonic_widget(GTK_LABEL(*w));
  						if (pwidgetSelect) {
  							gtk_widget_grab_focus(pwidgetSelect);
  						}
  					}
  					retVal = true;
  					break;
  				}
  			}
  			g_list_free(childWidgets);
  		}
  	}
  	return retVal;
  }
  
  void Strip::MenuSignal(GtkMenuItem *menuItem, Strip *pStrip) {
  	sptr_t cmd = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuItem), "CmdNum"));
  	pStrip->MenuAction(cmd);
  }
  
  void Strip::AddToPopUp(GUI::Menu &popup, const char *label, int cmd, bool checked) {
  	allowMenuActions = false;
  	GUI::gui_string localised = localiser->Text(label);
  	GtkWidget *menuItem = gtk_check_menu_item_new_with_mnemonic(localised.c_str());
  	gtk_menu_shell_append(GTK_MENU_SHELL(popup.GetID()), menuItem);
  	g_object_set_data(G_OBJECT(menuItem), "CmdNum", GINT_TO_POINTER(cmd));
  	g_signal_connect(G_OBJECT(menuItem),"activate", G_CALLBACK(MenuSignal), this);
  	gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuItem), checked ? TRUE : FALSE);
  	allowMenuActions = true;
  }
  
  void Strip::ChildFocus(GtkWidget *widget) {
  	childHasFocus = widget != 0;
  }
  
  gboolean Strip::ChildFocusSignal(GtkContainer */*container*/, GtkWidget *widget, Strip *pStrip) {
  	pStrip->ChildFocus(widget);
  	return FALSE;
  }
  
  gboolean Strip::FocusSignal(GtkWidget */*widget*/, GtkDirectionType direction, Strip *pStrip) {
  	return pStrip->Focus(direction);
  }
  
  bool Strip::VisibleHasFocus() const {
  	return visible && childHasFocus;
  }
  
  gint Strip::ButtonsPress(GtkWidget *, GdkEventButton *event, Strip *pstrip) {
  	if (event->button == 3) {
  		pstrip->ShowPopup();
  		return TRUE;
  	}
  	return FALSE;
  }
  