/* vim: ts=4 sw=4 colorcolumn=80
 * -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */

/* gtkhex.c - definition of a HexWidget widget

   Copyright © 1997 - 2004 Free Software Foundation

   Copyright © 2005-2020 Various individual contributors, including
   but not limited to: Jonathon Jongsma, Kalev Lember, who continued
   to maintain the source code under the licensing terms described
   herein and below.

   Copyright © 2021 Logan Rathbone <poprocks@gmail.com>

   GHex 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.

   GHex 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 GHex; see the file COPYING.
   If not, write to the Free Software Foundation, Inc.,
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

   Original Author: Jaka Mocnik <jaka@gnu.org>
*/

#include "gtkhex.h"
#include "gtkhex-layout-manager.h"
#include "common-macros.h"

#include <string.h>

#include <config.h>

/* Don't move these from the source file as they are not part of the public
 * header.
 */

/* DEFINES */

/* default minimum drawing area size (for ascii and hex widgets) in pixels. */
#define DEFAULT_DA_SIZE 50
#define SCROLL_TIMEOUT 100
#define STARTING_OFFSET 0

#define is_displayable(c) (((c) >= 0x20) && ((c) < 0x7f))
#define is_copyable(c) (is_displayable(c) || (c) == 0x0a || (c) == 0x0d)

#define HEX_BUFFER_PAYLOAD(X)	\
	hex_buffer_get_payload_size (hex_document_get_buffer (X))

/* ENUMS */

enum {
	UPPER_NIBBLE,
	LOWER_NIBBLE
};

typedef enum {
	VIEW_HEX,
	VIEW_ASCII
} HexWidgetViewType;

enum {
	CURSOR_MOVED_SIGNAL,
	DATA_CHANGED_SIGNAL,
	CUT_CLIPBOARD_SIGNAL,
	COPY_CLIPBOARD_SIGNAL,
	PASTE_CLIPBOARD_SIGNAL,
	DRAW_COMPLETE_SIGNAL,
	LAST_SIGNAL
};

/* highlighting information.
 */
typedef struct _HexWidget_Highlight HexWidget_Highlight;

/* start_line and end_line only have to be set (and valid) of
 * valid is set to TRUE. */
struct _HexWidget_Highlight
{
	gint64 start, end;
	gint64 start_line, end_line;

	gboolean valid;

	HexWidget_Highlight *prev, *next;
};

/**
 * HexWidgetAutoHighlight:
 *
 * A structure used to automatically highlight all visible occurrences
 * of a given string.
 */
struct _HexWidgetAutoHighlight
{
	char *search_string;
	int search_len;

	gint64 view_min;
	gint64 view_max;

	HexWidget_Highlight *highlights;
	HexWidgetAutoHighlight *next, *prev;
};

/* FIXME - only defined to create a boxed type and may be unreliable. */
static HexWidgetAutoHighlight *
hex_widget_autohighlight_copy (HexWidgetAutoHighlight *ahl)
{
	return ahl;
}

G_DEFINE_BOXED_TYPE (HexWidgetAutoHighlight, hex_widget_autohighlight,
		hex_widget_autohighlight_copy, g_free)


/* ------------------------------
 * Main HexWidget GObject definition
 * ------------------------------
 */

/**
 * HexWidget:
 *
 * #HexWidget is a widget which can display #HexDocument data as a
 * side-by-side representation of offets, hexadecimal nibbles, and ASCII
 * characters.
 */
struct _HexWidget
{
	GtkWidget parent_instance;

	HexDocument *document;

	GtkLayoutManager *layout_manager;

	GtkCssProvider *provider;

	GtkWidget *xdisp, *adisp;	/* DrawingArea */
	GtkWidget *offsets;			/* DrawingArea */
	GtkWidget *scrollbar;

	GtkWidget *busy_spinner;

	PangoLayout *xlayout, *alayout, *olayout;

	GtkWidget *context_menu;

	GtkAdjustment *adj;

	HexWidgetViewType active_view;

	int char_width, char_height;

	/* Cache of the mouse button. guint to marry up with GtkGesture. */
	guint button;

	gint64 cursor_pos;
	HexWidget_Highlight selection;
	gboolean lower_nibble;

	HexWidgetGroupType group_type;

	/* cpl == `characters per line` */
	int cpl;
	/* number of lines visible in the display */
	int vis_lines;

	/* These are the lines in absolute terms, shown within the display. So they
	 * will be some fraction of `index`, but could still theoretically be quite
	 * large if dealing with a huge file.
	 */
	gint64 lines, top_line;
	gboolean cursor_shown;

	/* width of the hex display `xdisp` and ascii display `adisp` */
	int xdisp_width, adisp_width;

	HexWidgetAutoHighlight *auto_highlight;

	/* scroll direction: 0 means no scrolling; a -ve number means we're
	 * scrolling up, and a +ve number means we're scrolling down. */
	int scroll_dir;
	guint scroll_timeout;

	gboolean show_offsets;
	gboolean insert;
	gboolean selecting;

	/* Buffer for storing formatted data for rendering;
	   dynamically adjusts its size to the display size */
	guchar *disp_buffer;

	/* default characters per line and number of lines. */
	int default_cpl;
	int default_lines;
};

G_DEFINE_TYPE (HexWidget, hex_widget, GTK_TYPE_WIDGET)

/* ----- */

/* STATIC FORWARD DECLARATIONS */

static guint gtkhex_signals[LAST_SIGNAL] = { 0 };

static char *char_widths = NULL;

static void render_highlights (HexWidget *self, cairo_t *cr, gint64 cursor_line,
		HexWidgetViewType type);
static void render_lines (HexWidget *self, cairo_t *cr, int min_lines, int max_lines,
		HexWidgetViewType type);

static void hex_widget_validate_highlight (HexWidget *self, HexWidget_Highlight *hl);
static void hex_widget_invalidate_highlight (HexWidget *self, HexWidget_Highlight *hl);
static void hex_widget_invalidate_all_highlights (HexWidget *self);

static void hex_widget_update_all_auto_highlights (HexWidget *self);

static HexWidget_Highlight *hex_widget_insert_highlight (HexWidget *self,
		HexWidgetAutoHighlight *ahl, gint64 start, gint64 end);

static void hex_widget_delete_highlight (HexWidget *self, HexWidgetAutoHighlight *ahl,
		HexWidget_Highlight *hl);

static void hex_widget_update_auto_highlight (HexWidget *self, HexWidgetAutoHighlight *ahl,
		gboolean delete, gboolean add);

static void recalc_displays (HexWidget *self);

static void show_cursor (HexWidget *self, gboolean show);

/* HexWidget - Method Definitions */

/* ACTIONS */

static void
copy_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET(widget);

	g_return_if_fail (HEX_IS_WIDGET(self));
	(void)action_name, (void)parameter;

	hex_widget_copy_to_clipboard (self);
}

static void
cut_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET(widget);

	g_return_if_fail (HEX_IS_WIDGET(self));
	(void)action_name, (void)parameter;

	hex_widget_cut_to_clipboard (self);
}

static void
paste_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET(widget);

	g_return_if_fail (HEX_IS_WIDGET(self));
	(void)action_name, (void)parameter;

	hex_widget_paste_from_clipboard (self);
}

static void
redo_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET(widget);
	HexChangeData *cd;

	(void)action_name, (void)parameter;

	g_return_if_fail (HEX_IS_WIDGET(self));
	g_return_if_fail (HEX_IS_DOCUMENT(self->document));

	if (hex_document_can_redo (self->document))
	{
		hex_document_redo (self->document);

		cd = hex_document_get_undo_data (self->document);

		hex_widget_set_cursor (self, cd->start);
		hex_widget_set_nibble (self, cd->lower_nibble);
	}
}

static void
doc_undo_redo_cb (HexDocument *doc, gpointer user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	g_return_if_fail (HEX_IS_WIDGET (self));
	
	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.undo", hex_document_can_undo (doc));
	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.redo", hex_document_can_redo (doc));
}

static void
undo_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET(widget);
	HexChangeData *cd;

	(void)action_name, (void)parameter;

	g_return_if_fail (HEX_IS_WIDGET(self));
	g_return_if_fail (HEX_IS_DOCUMENT(self->document));

	if (hex_document_can_undo (self->document))
	{
		cd = hex_document_get_undo_data (self->document);

		hex_document_undo (self->document);

		hex_widget_set_cursor (self, cd->start);
		hex_widget_set_nibble (self, cd->lower_nibble);
	}
}

static void
toggle_hex_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET (widget);

	self->active_view = VIEW_HEX;
	gtk_widget_queue_draw (widget);
}

static void
toggle_ascii_action (GtkWidget *widget,
		const char *action_name,
		GVariant *parameter)
{
	HexWidget *self = HEX_WIDGET (widget);

	self->active_view = VIEW_ASCII;
	gtk_widget_queue_draw (widget);
}

/*
 * ?_to_pointer translates mouse coordinates in hex/ascii view
 * to cursor coordinates.
 */
static void
hex_to_pointer (HexWidget *self, int mx, int my)
{
	int x = 0, cx = 0;
	gint64 cy;
	
	cy = self->top_line + my/self->char_height;

	while (cx < 2 * self->cpl)
	{
		x += self->char_width;
		
		if (x > mx)
		{
			hex_widget_set_cursor_by_row_and_col (self, cx/2, cy);
			hex_widget_set_nibble (self, ((cx%2 == 0)?UPPER_NIBBLE:LOWER_NIBBLE));
			
			cx = 2*self->cpl;
		}
		
		cx++;
		
		if(cx%(2*self->group_type) == 0)
			x += self->char_width;
	}
}

static void
ascii_to_pointer (HexWidget *self, int mx, int my)
{
	gint64 cy;
	
	cy = self->top_line + my/self->char_height;
	
	hex_widget_set_cursor_by_row_and_col (self, mx/self->char_width, cy);
}

static int
get_char_height (HexWidget *self)
{
	PangoContext *context;
	PangoFontMetrics *metrics;
	int height;

	context = gtk_widget_get_pango_context (GTK_WIDGET(self));
	metrics = pango_context_get_metrics (context, NULL, NULL);

	height =
		PANGO_PIXELS(pango_font_metrics_get_height (metrics));
	
	return height;
}

static int
get_char_width (HexWidget *self)
{
	PangoContext *context;
	PangoFontMetrics *metrics;
	int width;

	context = gtk_widget_get_pango_context (GTK_WIDGET(self));
	metrics = pango_context_get_metrics (context, NULL, NULL);

	/* generally the digit width returned will be bigger, but let's take
	 * the max for now and run with it.
	 */
	width = MAX (pango_font_metrics_get_approximate_digit_width (metrics),
			pango_font_metrics_get_approximate_char_width (metrics));

	/* scale down from pango units to pixels */
	width = PANGO_PIXELS(width);
	
	/* update layout manager */
	if (HEX_IS_WIDGET_LAYOUT (self->layout_manager)) {
		hex_widget_layout_set_char_width (HEX_WIDGET_LAYOUT(self->layout_manager),
				width);
	}
	return width;
}

/*
 * format_[x|a]block() formats contents of the buffer (out) into displayable
 * text in hex or ascii, respectively.
 * Returns: length of resulting number of bytes/characters in buffer.
 */
static int
format_xblock (HexWidget *self, char *out, gint64 start, gint64 end)
{
	gint64 i, j;
	int low, high;
	guchar c;

	for (i = start + 1, j = 0; i <= end; i++)
	{
		c = hex_widget_get_byte (self, i - 1);
		low = c & 0x0F;
		high = (c & 0xF0) >> 4;
		
		out[j++] = (high < 10) ? (high + '0') : (high - 10 + 'A');
		out[j++] = (low < 10) ? (low + '0') : (low - 10 + 'A');
		
		if (i % self->group_type == 0)
			out[j++] = ' ';
	}
	return j;
}

static int
format_ablock (HexWidget *self, char *out, gint64 start, gint64 end)
{
	gint64 i, j;
	guchar c;

	for (i = start, j = 0; i < end; i++, j++) {
		c = hex_widget_get_byte (self, i);
		if (is_displayable(c))
			out[j] = c;
		else
			out[j] = '.';
	}
	return end - start;
}

/*
 * get_[x|a]coords() translates offset from the beginning of
 * the block into x,y coordinates of the xdisp/adisp, respectively
 */
static gboolean
get_xcoords (HexWidget *self, gint64 pos, int *x, int *y)
{
	gint64 cx, cy, spaces;
	
	if (self->cpl == 0)
		return FALSE;
	
	cy = pos / self->cpl;
	cy -= self->top_line;

	if (cy < 0)
		return FALSE;
	
	cx = 2 * (pos % self->cpl);
	spaces = (pos % self->cpl) / self->group_type;
	
	cx *= self->char_width;
	cy *= self->char_height;
	spaces *= self->char_width;

	/* nb: Having this as an assertion warning is annoying. Happens a lot
	 * in that case if you scroll way up/down in a big file and suddenly start
	 * dragging. */
	if (cx + spaces > INT_MAX && cy > INT_MAX)
		return FALSE;

	*x = cx + spaces;
	*y = cy;
	
	return TRUE;
}

static gboolean
get_acoords (HexWidget *self, gint64 pos, int *x, int *y)
{
	gint64 cx, cy;
	
	if (self->cpl == 0)
		return FALSE;

	cy = pos / self->cpl;
	cy -= self->top_line;

	if (cy < 0)
		return FALSE;

	cy *= self->char_height;
	cx = self->char_width*(pos % self->cpl);
	
	/* nb: Having this as an assertion warning is annoying. Happens a lot
	 * in that case if you scroll way up/down in a big file and suddenly start
	 * dragging. */
	if (cy > INT_MAX && cx > INT_MAX)
		return FALSE;

	*x = cx;
	*y = cy;
	
	return TRUE;
}

static void
invalidate_xc (HexWidget *self)
{
	GtkWidget *widget = self->xdisp;
	int cx, cy;

	if (get_xcoords (self, self->cursor_pos, &cx, &cy))
	{
		if (self->lower_nibble)
			cx += self->char_width;

		gtk_widget_queue_draw (widget);
	}
}

static void
invalidate_ac (HexWidget *self)
{
	GtkWidget *widget = self->adisp;
	int cx, cy;

	if (get_acoords (self, self->cursor_pos, &cx, &cy))
	{
		gtk_widget_queue_draw (widget);
	}
}

static void
render_cursor (HexWidget *self,
		cairo_t *cr,
		GtkWidget *widget,
		PangoLayout *layout,
		int y,
		HexWidgetViewType cursor_type)
{
	GdkRGBA fg_color;
	gint64 index;
	int range[2];
	GtkStateFlags state;
	GtkStyleContext *context;
	cairo_region_t *region;
	double x1, x2, y1, y2;
	gboolean at_file_end = FALSE;
	gboolean at_new_row = FALSE;

	g_return_if_fail (gtk_widget_get_realized (widget));

	context = gtk_widget_get_style_context (widget);
	state = gtk_widget_get_state_flags (widget);
	gtk_style_context_get_color (context, &fg_color);

	/* Find out if we're at the end of the row and/or the end of the file,
	 * since this will make a difference when in insert mode
	 */
	if (self->cursor_pos >= HEX_BUFFER_PAYLOAD (self->document))
		at_file_end = TRUE;

	if (self->cursor_pos % self->cpl == 0)
		at_new_row = TRUE;

	if (cursor_type == VIEW_HEX)
	{
		int spaces;

		spaces = (self->cursor_pos % self->cpl) / self->group_type;
		index = 2 * (self->cursor_pos % self->cpl);
		index += spaces;

		if (self->lower_nibble) {
			range[0] = index + 1;
			range[1] = index + 2;
		} else {
			range[0] = index;
			range[1] = index + 1;
		}
	}
	else	/* VIEW_ASCII */
	{
		index = self->cursor_pos % self->cpl;
		range[0] = index;
		range[1] = index + 1;
	}

	if (self->insert && at_file_end)
	{
		if (at_new_row) {
			range[0] = 0;
			range[1] = 1;
		} else {
			--range[0];
			--range[1];
		}
	}

	region = gdk_pango_layout_get_clip_region (layout,
			0,	/* x */
			y,
			range,
			1);

	if (self->insert && at_file_end && !at_new_row)
	{
		cairo_rectangle_int_t tmp_rect = { 0 };

		cairo_region_get_extents (region, &tmp_rect);
		cairo_region_translate (region, tmp_rect.width, 0);
	}

	gdk_cairo_region (cr, region);
	cairo_save (cr);
	cairo_clip (cr);
	cairo_clip_extents (cr, &x1, &y1, &x2, &y2);

	/* - If don't have focus: draw dashed square.
	 * - If have focus, draw filled in square (colour defined w/ css using
	 *   `:checked` pseudoclass) if the pane we're drawing is selected;
	 *   otherwise, draw a solid square.
	 */
	if (! gtk_widget_has_focus (GTK_WIDGET(self)))
	{
		cairo_save (cr);
		cairo_set_source_rgba (cr,
				fg_color.red, fg_color.green, fg_color.blue, 0.75);

		cairo_set_line_width (cr, 1.5);
		cairo_set_dash (cr, (double[]){1.0}, 1, 0.0);

		cairo_rectangle (cr, x1, y1, ABS(x2-x1), ABS(y2-y1));

		cairo_stroke (cr);
		cairo_restore (cr);
	}
	else if (self->active_view == cursor_type)
	{
		gtk_style_context_save (context);

		state |= GTK_STATE_FLAG_CHECKED;
		gtk_style_context_set_state (context, state);

		gtk_render_background (context, cr,
				x1,
				y1,
				ABS(x2-x1), 
				ABS(y2-y1));

		/* Don't render layout if we're in insert mode and at file end, since
		 * the cursor should be blank at that point.
		 */
		if (at_file_end && self->insert)
			;
		else
			gtk_render_layout (context, cr, 0, y, layout);

		gtk_style_context_restore (context);
	}
	else
	{
		cairo_save (cr);
		cairo_set_source_rgba (cr,
				fg_color.red, fg_color.green, fg_color.blue, fg_color.alpha);
		cairo_set_line_width (cr, 1.5);

		cairo_rectangle (cr, x1, y1, ABS(x2-x1), ABS(y2-y1));

		cairo_stroke (cr);
		cairo_restore (cr);
	}
	cairo_restore (cr);		/* from cairo_clip above */
}

static void
show_cursor (HexWidget *self, gboolean show)
{
	if (self->cursor_shown == show)
		return;
	
	if (gtk_widget_get_realized (self->xdisp) ||
			gtk_widget_get_realized (self->adisp))
	{
		invalidate_xc (self);
		invalidate_ac (self);
	}
	self->cursor_shown = show;
}

inline static int
get_hex_cpl (HexWidget *self)
{
	int hex_cpl;

	if (! self->default_cpl)
		hex_cpl = hex_widget_layout_get_hex_cpl (
				HEX_WIDGET_LAYOUT(self->layout_manager));
	else
		hex_cpl = hex_widget_layout_util_hex_cpl_from_ascii_cpl (
				self->default_cpl, self->group_type);

	return hex_cpl;
}

static void
render_highlights (HexWidget *self,
		cairo_t *cr,
		gint64 cursor_line,
		HexWidgetViewType type)
{
	/* Shorthand tracers to walk through highlight lists */
	HexWidget_Highlight *highlight = &self->selection;
	HexWidgetAutoHighlight *auto_highlight = self->auto_highlight;
	GtkWidget *widget;		/* shorthand for the hex or ascii drawing area */
	PangoLayout *layout;	/* shorthand for the hex or ascii pango layout */
	GtkStyleContext *context;
	GtkStateFlags state;
	int hex_cpl;
	cairo_region_t *region;
	int y;
	gboolean is_autohighlight = FALSE;

	if (type == VIEW_HEX)
	{
		widget = self->xdisp;
		layout = self->xlayout;
	}
	else
	{
		widget = self->adisp;
		layout = self->alayout;
	}

	hex_cpl = get_hex_cpl (self);

	y = cursor_line * self->char_height;

	while (highlight)
	{
		gint64 cursor_off = 0;
		size_t len;

		int range[2];
		double x1, x2, y1, y2;

		/* Shorthands for readability of this loop */
		gint64 start, end, start_line, end_line;

		context = gtk_widget_get_style_context (widget);
		gtk_style_context_save (context);

		state = gtk_style_context_get_state (context);
		if (is_autohighlight)
			state |= GTK_STATE_FLAG_LINK;
		else
		{
			if (gtk_widget_has_focus (GTK_WIDGET(self)))
			{
				state |= GTK_STATE_FLAG_FOCUS_WITHIN;
			}
			state |= GTK_STATE_FLAG_SELECTED;
		}

		gtk_style_context_set_state (context, state);

		hex_widget_validate_highlight (self, highlight);

		start = MIN(highlight->start, highlight->end);
		end = MAX(highlight->start, highlight->end);
		start_line = highlight->start_line;
		end_line = highlight->end_line;

		if (start == end)
			goto end_of_loop;

		if (cursor_line == start_line)
		{
			if (type == VIEW_HEX) {
				cursor_off = 2 * (start % self->cpl) +
					(start % self->cpl) / self->group_type;

				if (cursor_line == end_line)
					len = 2 * (end % self->cpl + 1) +
						(end % self->cpl) / self->group_type;
				else
					len = hex_cpl;

				len = len - cursor_off;
			}
			else {	/* VIEW_ASCII */
				cursor_off = start % self->cpl;

				if (cursor_line == end_line)
					len = end - start + 1;
				else
					len = self->cpl - cursor_off;
			}

			range[0] = cursor_off;
			range[1] = cursor_off + len;
		}
		else if (cursor_line == end_line)
		{
			if (type == VIEW_HEX) {
				cursor_off = 2 * (end % self->cpl + 1) + 
					(end % self->cpl) / self->group_type;
			}
			else {	/* VIEW_ASCII */
				cursor_off = end % self->cpl + 1;
			}

			range[0] = 0;
			range[1] = cursor_off;
		}
		else if (cursor_line > start_line && cursor_line < end_line)
		{
			int cpl = (type == VIEW_HEX) ? hex_cpl : self->cpl;

			range[0] = 0;
			range[1] = cpl;
		}
		else
			goto end_of_loop;

		cairo_save (cr);

		region = gdk_pango_layout_get_clip_region (layout,
				0,	/* x */
				y,
				range,
				1);

		gdk_cairo_region (cr, region);
		cairo_clip (cr);
		cairo_clip_extents (cr, &x1, &y1, &x2, &y2);

		gtk_render_background (context, cr,
				x1,
				y1,
				ABS(x2-x1),
				ABS(y2-y1));

		cairo_restore (cr);

end_of_loop:
		highlight = highlight->next;

		/* if there are no selection highlights left, look for auto-highlights
		 */
		while (highlight == NULL && auto_highlight)
		{
			highlight = auto_highlight->highlights;
			auto_highlight = auto_highlight->next;
			is_autohighlight = TRUE;
		}
		gtk_style_context_restore (context);
	}
}

/* FIXME - Previously, this function was more sophisticated, and only
 * redrew part of the drawing area requested. Need to make an executive
 * decision as to whether that will be feasible to do for ghex4, or just
 * eliminate those and queue a redraw for the drawing area in question.
 */
/*
 * when calling invalidate_*_lines() the imin and imax arguments are the
 * numbers of the first and last line TO BE INVALIDATED in the range
 * [0 .. self->vis_lines-1] AND NOT [0 .. self->lines]!
 */
static void
invalidate_lines (HexWidget *self,
                  GtkWidget *widget,
                  int imin,
                  int imax)
{
#if 0
    gtk_widget_get_allocation (widget, &allocation);
#endif

	(void)self, (void)imin, (void)imax; /* unused for now. See comment above. */

    gtk_widget_queue_draw (widget);
#if 0
    gtk_widget_queue_draw_area (widget,
                                0,
                                imin * self->char_height,
                                allocation.width,
                                (imax - imin + 1) * self->char_height);
#endif
}

static void
invalidate_hex_lines (HexWidget *self,
                      int imin,		/* FIXME - imin/imax for next few funcs */
                      int imax)		/* presently ignored. */
{
    invalidate_lines (self, self->xdisp, imin, imax);
}

static void
invalidate_ascii_lines (HexWidget *self,
                        int imin,
                        int imax)
{
    invalidate_lines (self, self->adisp, imin, imax);
}

static void
invalidate_offsets (HexWidget *self,
                    int imin,
                    int imax)
{
    invalidate_lines (self, self->offsets, imin, imax);
}

/*
 * when calling render_lines() the min_lines and max_lines arguments are the
 * numbers of the first and last line TO BE DISPLAYED in the range
 * [0 .. self->vis_lines-1] AND NOT [0 .. self->lines]!
 */

#define DO_RENDER_CURSOR \
			render_cursor (self, cr, widget, layout,				\
					cursor_line * self->char_height,	/* y */		\
					type);
static void
render_lines (HexWidget *self,
		cairo_t *cr,
		int min_lines,
		int max_lines,
		HexWidgetViewType type)
{
	GtkWidget *widget;
	GtkStateFlags state;
	PangoLayout *layout;
	int (*block_format_func) (HexWidget *self, char *out, gint64 start, gint64 end);
	int block_format_len;
	GtkAllocation allocation;
	GtkStyleContext *context;
	gint64 cursor_line;
	int cpl;
	gboolean cursor_drawn = FALSE;

	g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET(self)));
	g_return_if_fail (self->cpl);

	/* Setup variables depending on widget type (hex or ascii) */

	if (type == VIEW_HEX)
	{
		widget = self->xdisp;
		layout = self->xlayout;
		block_format_func = format_xblock;

		if (! self->default_cpl)
			cpl = hex_widget_layout_get_hex_cpl (
					HEX_WIDGET_LAYOUT(self->layout_manager));
		else
			cpl = hex_widget_layout_util_hex_cpl_from_ascii_cpl (
					self->default_cpl, self->group_type);

	}
	else	/* VIEW_ASCII */
	{
		widget = self->adisp;
		layout = self->alayout;
		block_format_func = format_ablock;
		cpl = self->cpl;
	}

	/* Grab styling and render the (blank) background. */

	context = gtk_widget_get_style_context (widget);
	cursor_line = self->cursor_pos / self->cpl - self->top_line;
	gtk_widget_get_allocation (widget, &allocation);

	gtk_render_background (context, cr,
			/* x: */		0,
			/* y: */		min_lines * self->char_height,
			/* width: */	allocation.width,
			/* height: */	(max_lines - min_lines + 1) * self->char_height);

	/* If it's a blank canvass, we know there'll be no lines to render. But we
	 * draw a single space in the pango layout as a workaround for the fact
	 * that it will have no context as to how large to draw the cursor.
	 */
	if (HEX_BUFFER_PAYLOAD (self->document) == 0 &&
			! hex_document_has_changed (self->document))
	{
		pango_layout_set_text (layout, " ", -1);
		goto no_more_lines_to_draw;
	}

	/* Work out how many lines need to be drawn, and draw them from the
	 * buffer.
	 */
	max_lines = MIN(max_lines, self->vis_lines);
	max_lines = MIN(max_lines, self->lines);

	block_format_len = block_format_func (self,
			(char *)self->disp_buffer,
			(self->top_line + min_lines) * self->cpl,
			MIN( (self->top_line + max_lines + 1) * self->cpl,
				HEX_BUFFER_PAYLOAD (self->document) ));
	
	for (int i = min_lines; i <= max_lines; i++)
	{
		int tmp = block_format_len - ((i - min_lines) * cpl);

		if (tmp <= 0)
			break;

		pango_layout_set_text (layout,
				(char *)self->disp_buffer + (i - min_lines) * cpl,
				MIN(cpl, tmp));

		render_highlights (self, cr, i, type);

		gtk_render_layout (context, cr,
				/* x: */ 0,
				/* y: */ i * self->char_height,
				layout);

		if (i == cursor_line)
		{
			DO_RENDER_CURSOR
			cursor_drawn = TRUE;
		}
	}

no_more_lines_to_draw:
	if (! cursor_drawn && cursor_line <= self->vis_lines)
	{
		DO_RENDER_CURSOR
	}
}
#undef DO_RENDER_CURSOR

#define BUFLEN	32
#define MIN_CPL	8
static void
render_offsets (HexWidget *self,
                cairo_t *cr,
                int min_lines,
                int max_lines)
{
	GtkWidget *widget = self->offsets;
	GdkRGBA fg_color;
	GtkAllocation allocation;
	GtkStyleContext *context;
	char buf[BUFLEN] = {0};
	char fmt[BUFLEN] = {0};
	char off_str[BUFLEN];
	int off_cpl;

	g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (self)));

	context = gtk_widget_get_style_context (widget);

	gtk_widget_get_allocation (widget, &allocation);

	gtk_render_background (context, cr,
			/* x: */		0,
			/* y: */		min_lines * self->char_height,
			/* width: */	allocation.width,
			/* height: */	(max_lines - min_lines + 1) * self->char_height);
  
	/* update max_lines and min_lines */
	max_lines = MIN(max_lines, self->vis_lines);
	max_lines = MIN(max_lines, self->lines - self->top_line - 1);

	/* find out how many chars wide our offset column should be based on
	 * how many chars will be in the last offset marker of the screen */
	
	snprintf (buf, BUFLEN-1, "%lX",
			MAX ((self->top_line + max_lines), 0) * self->cpl);
	off_cpl = MAX (MIN_CPL, strlen (buf));
	snprintf (fmt, BUFLEN-1, "%%0%dlX", off_cpl); 
	hex_widget_layout_set_offset_cpl (HEX_WIDGET_LAYOUT(self->layout_manager), off_cpl);

	for (int i = min_lines; i <= max_lines; i++)
	{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
		/* generate offset string and place in temporary buffer */
		sprintf(off_str, fmt,
				(self->top_line + i) * self->cpl + STARTING_OFFSET);
#pragma GCC diagnostic pop

		/* build pango layout for offset line; draw line with gtk. */
		pango_layout_set_text (self->olayout, off_str, off_cpl);
		gtk_render_layout (context, cr,
				/* x: */ 0,
				/* y: */ i * self->char_height,
				self->olayout);
	}
}
#undef BUFLEN
#undef MIN_CPL

static void
allocate_display_buffer (HexWidget *self)
{
	GtkWidget *widget = GTK_WIDGET (self);
	int hex_cpl = get_hex_cpl (self);

	if (self->disp_buffer)
		g_free (self->disp_buffer);

	self->disp_buffer = g_malloc ((hex_cpl + 1) * (self->vis_lines + 1));
}

/* draw_func for the hex drawing area
 */
static void
hex_draw (GtkDrawingArea *drawing_area,
                           cairo_t *cr,
                           int width,
                           int height,
                           gpointer user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);

	g_return_if_fail (HEX_IS_WIDGET(self));

	/* Now that we have self->cpl defined, run this function to bump all
	 * required values:
	 */
	recalc_displays (self);
	allocate_display_buffer (self);

	/* Finally, we can do what we wanted to do to begin with: draw our hex
	 * lines!
	 */
	render_lines (self, cr, 0, self->vis_lines, VIEW_HEX);
}

static void
ascii_draw (GtkDrawingArea *drawing_area,
                           cairo_t *cr,
                           int width,
                           int height,
                           gpointer user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	g_return_if_fail(HEX_IS_WIDGET(self));

	recalc_displays (self);
	allocate_display_buffer (self);
	render_lines (self, cr, 0, self->vis_lines, VIEW_ASCII);
}

static void
offsets_draw (GtkDrawingArea *drawing_area,
                           cairo_t *cr,
                           int width,
                           int height,
                           gpointer user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	g_return_if_fail(HEX_IS_WIDGET(self));

	recalc_displays (self);
	render_offsets (self, cr, 0, self->vis_lines);
}

/*
 * this calculates how many bytes we can stuff into one line and how many
 * lines we can display according to the current size of the widget
 */
static void
recalc_displays (HexWidget *self)
{
	GtkWidget *widget = GTK_WIDGET (self);
	gint64 payload_size;

	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	/*
	 * Only change the value of the adjustment to put the cursor on screen
	 * if the cursor is currently within the displayed portion.
	 */
	if (payload_size == 0 || self->cpl == 0)
		self->lines = 1;
	else {
		self->lines = payload_size / self->cpl;
		if (payload_size % self->cpl)
			self->lines++;
	}
}

static void
recalc_scrolling (HexWidget *self)
{
	gboolean scroll_to_cursor;
	double value;

	scroll_to_cursor = (self->cpl == 0) ||
		((self->cursor_pos / self->cpl >= gtk_adjustment_get_value (self->adj)) &&
		 (self->cursor_pos / self->cpl <= gtk_adjustment_get_value (self->adj) +
			  self->vis_lines - 1));

	/* calculate new display position */
	if (self->cpl == 0)		/* avoid divide by zero (FIXME - feels hackish) */
		value = 0;
	else
		value = MIN (self->top_line, self->lines - self->vis_lines);

	/* clamp value */
	value = MAX (0, value);

	/* keep cursor on screen if it was on screen before */
	if (self->cpl == 0) {		/* avoid divide by zero (FIXME - feels hackish) */
		value = 0;
	}
	else if (scroll_to_cursor &&
	    ((self->cursor_pos / self->cpl < value) ||
	     (self->cursor_pos / self->cpl > value + self->vis_lines - 1))) {
		value = MIN (self->cursor_pos / self->cpl, self->lines - self->vis_lines);
		value = MAX (0, value);
	}

	/* adjust the scrollbar and display position to new values */
	gtk_adjustment_configure (self->adj,
	                          value,             /* value */
	                          0,                 /* lower */
	                          self->lines,         /* upper */
	                          1,                 /* step increment */
	                          self->vis_lines - 1, /* page increment */
	                          self->vis_lines      /* page size */);
}

/*
 * takes care of xdisp and adisp scrolling
 * connected to value-changed signal of scrollbar's GtkAdjustment
 */
static void
adj_value_changed_cb (GtkAdjustment *adj, HexWidget *self)
{
	int dx, dy;
	
	g_return_if_fail (gtk_widget_is_drawable (self->xdisp) &&
			gtk_widget_is_drawable (self->adisp));

	self->top_line = gtk_adjustment_get_value (adj);

	gtk_widget_queue_draw (GTK_WIDGET(self));
}

/*
 * mouse signal handlers (button 1 and motion) for both displays
 */
static gboolean
scroll_timeout_handler (HexWidget *self)
{
	if (self->scroll_dir < 0)
	{
		hex_widget_set_cursor (self,
				MAX (0, self->cursor_pos - self->cpl));
	}
	else if (self->scroll_dir > 0)
	{
		hex_widget_set_cursor (self,
				MIN (HEX_BUFFER_PAYLOAD (self->document) - 1,
						self->cursor_pos + self->cpl));
	}
	return G_SOURCE_CONTINUE;
}

static gboolean
scroll_cb (GtkEventControllerScroll *controller,
               double                    dx,
               double                    dy,
               gpointer                  user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);
	double old_value, new_value;

	g_return_val_if_fail (HEX_IS_WIDGET(self), FALSE);

	old_value = gtk_adjustment_get_value (self->adj);
	new_value = old_value + dy;

	gtk_adjustment_set_value (self->adj, new_value);

	/* TFM: returns true if scroll event was handled; false otherwise.
	 */
	return TRUE;
}

/* Helper function for *_pressed_cb 's
 */
static void
pressed_gesture_helper (HexWidget *self,
		GtkGestureClick *gesture,
		double x,
		double y,
		HexWidgetViewType type)
{
	GtkWidget *widget;
	guint button;

	if (type == VIEW_HEX)
		widget = self->xdisp;
	else
		widget = self->adisp;

	g_return_if_fail (HEX_IS_WIDGET(self));
	g_return_if_fail (GTK_IS_WIDGET(widget));

	button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE(gesture));

	/* Single-press */
	if (button == GDK_BUTTON_PRIMARY)
	{
		if (! gtk_widget_has_focus (widget)) 
			gtk_widget_grab_focus (GTK_WIDGET(self));
		
		self->button = button;
		
		if (self->active_view == type)
		{
			if (type == VIEW_HEX)
				hex_to_pointer (self, x, y);
			else
				ascii_to_pointer (self, x, y);

			if (! self->selecting)
			{
				self->selecting = TRUE;
				hex_widget_set_selection (self, self->cursor_pos, self->cursor_pos);
			}
		}
		else
		{
			show_cursor (self, FALSE);
			self->active_view = type;
			show_cursor (self, TRUE);
			pressed_gesture_helper (self, gesture, x, y, type);
		}
	}
	else
	{
		self->button = 0;
	}
}

static void
released_gesture_helper (HexWidget *self,
		GtkGestureClick *gesture,
		int				n_press,
		double			x,
		double			y,
		HexWidgetViewType type)
{
	GtkWidget *widget;
	guint button;

	if (type == VIEW_HEX)
		widget = self->xdisp;
	else
		widget = self->adisp;

	g_return_if_fail (HEX_IS_WIDGET(self));
	g_return_if_fail (GTK_IS_WIDGET(widget));
	
	button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE(gesture));

	/* Single-click */
	if (button == GDK_BUTTON_PRIMARY && n_press == 1)
	{
		if (self->scroll_timeout != 0)
		{
			g_source_remove (self->scroll_timeout);
			self->scroll_timeout = 0;
			self->scroll_dir = 0;
		}
		self->selecting = FALSE;
		self->button = 0;
	}
}

/* nb: this gesture is only associated with the right-click, so there is
 * no need to test for that. If the impl in _init changes, this will need
 * to change
 */
static void
gh_pressed_cb (GtkGestureClick *gesture,
	int			n_press,
	double		x,
	double		y,
	gpointer	user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	hex_widget_layout_set_cursor_pos (HEX_WIDGET_LAYOUT(self->layout_manager), x, y);
	gtk_popover_popup (GTK_POPOVER(self->context_menu));
}

static void
hex_pressed_cb (GtkGestureClick *gesture,
	int			n_press,
	double		x,
	double		y,
	gpointer	user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	pressed_gesture_helper (self, gesture, x, y, VIEW_HEX);
}

static void
hex_released_cb (GtkGestureClick *gesture,
               int              n_press,
               double           x,
               double           y,
               gpointer         user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	released_gesture_helper (self, gesture, n_press, x, y, VIEW_HEX);
}

static void
drag_update_helper (HexWidget *self,
		GtkGestureDrag	*gesture,
		double			offset_x,
		double			offset_y,
		HexWidgetViewType	type)
{
	GtkWidget *widget;
	double start_x, start_y;
	double x, y;
	GtkAllocation allocation;

	if (type == VIEW_HEX)
		widget = self->xdisp;
	else
		widget = self->adisp;

	g_return_if_fail (HEX_IS_WIDGET(self));
	g_return_if_fail (GTK_IS_WIDGET(widget));

	gtk_widget_get_allocation (widget, &allocation);
	gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);

	x = start_x + offset_x;
	y = start_y + offset_y;

	if (y < 0) {
		self->scroll_dir = -1;
	} else if (y >= allocation.height) {
		self->scroll_dir = 1;
	} else {
		self->scroll_dir = 0;
	}

	if (self->scroll_dir != 0) {
		if (! self->scroll_timeout) {
			self->scroll_timeout =
				g_timeout_add (SCROLL_TIMEOUT,
							  G_SOURCE_FUNC(scroll_timeout_handler),
							  self);
		}
		return;
	}
	else {
		if (self->scroll_timeout != 0) {
			g_source_remove (self->scroll_timeout);
			self->scroll_timeout = 0;
		}
	}
			
	if (self->active_view == type && self->button == GDK_BUTTON_PRIMARY)
	{
		if (type == VIEW_HEX)
			hex_to_pointer (self, x, y);
		else
			ascii_to_pointer (self, x, y);
	}
}

static void
hex_drag_update_cb (GtkGestureDrag *gesture,
               double          offset_x,
               double          offset_y,
               gpointer        user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	drag_update_helper (self, gesture, offset_x, offset_y, VIEW_HEX);
}

/* ASCII Widget - click and drag callbacks. */

static void
ascii_pressed_cb (GtkGestureClick *gesture,
               int              n_press,
               double           x,
               double           y,
               gpointer         user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	pressed_gesture_helper (self, gesture, x, y, VIEW_ASCII);
}

static void
ascii_released_cb (GtkGestureClick *gesture,
               int              n_press,
               double           x,
               double           y,
               gpointer         user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	released_gesture_helper (self, gesture, n_press, x, y, VIEW_ASCII);
}

static void
ascii_drag_update_cb (GtkGestureDrag *gesture,
               double          offset_x,
               double          offset_y,
               gpointer        user_data)
{
	HexWidget *self = HEX_WIDGET (user_data);

	drag_update_helper (self, gesture, offset_x, offset_y, VIEW_ASCII);
}

static gboolean
key_press_cb (GtkEventControllerKey *controller,
               guint                  keyval,
               guint                  keycode,
               GdkModifierType        state,
               gpointer               user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	GtkWidget *widget = GTK_WIDGET(user_data);
	gboolean ret = GDK_EVENT_PROPAGATE;
	size_t payload_size;

	g_return_val_if_fail (HEX_IS_DOCUMENT (self->document), FALSE);

	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	show_cursor (self, FALSE);

	/* Figure out if we're holding shift or not. */
	if (state & GDK_SHIFT_MASK)
		self->selecting = TRUE;
	else
		self->selecting = FALSE;

	/* FIXME - This could use a cleanup. Mostly flown in from old code.
	 */
	switch(keyval)
	{
		case GDK_KEY_BackSpace:
			if (self->cursor_pos > 0) {
				hex_document_set_data (self->document, self->cursor_pos - 1,
						0, 1, NULL, TRUE);
				if (self->selecting)
					self->selecting = FALSE;
				hex_widget_set_cursor (self, self->cursor_pos - 1);
				ret = GDK_EVENT_STOP;
			}
			break;

		case GDK_KEY_Delete:
			if (self->cursor_pos < payload_size) {
				hex_document_set_data (self->document, self->cursor_pos,
						0, 1, NULL, TRUE);
				hex_widget_set_cursor (self, self->cursor_pos);
				ret = GDK_EVENT_STOP;
			}
			break;

		case GDK_KEY_Up:
			hex_widget_set_cursor (self, self->cursor_pos - self->cpl);
			ret = GDK_EVENT_STOP;
			break;

		case GDK_KEY_Down:
			hex_widget_set_cursor (self, self->cursor_pos + self->cpl);
			ret = GDK_EVENT_STOP;
			break;

		case GDK_KEY_Page_Up:
			hex_widget_set_cursor (self, MAX (0, self->cursor_pos - self->vis_lines*self->cpl));
			ret = GDK_EVENT_STOP;
			break;

		case GDK_KEY_Page_Down:
			hex_widget_set_cursor(self, MIN (payload_size,
						self->cursor_pos + self->vis_lines*self->cpl));
			ret = GDK_EVENT_STOP;
			break;

		case GDK_KEY_Home:
			if (state & GDK_CONTROL_MASK)
			{
				hex_widget_set_cursor (self, 0);
			}
			else
			{
				gint64 line_beg = self->cursor_pos; 

				while (line_beg % self->cpl != 0)
					--line_beg;

				hex_widget_set_cursor (self, line_beg);
			}
			ret = GDK_EVENT_STOP;
			break;

		case GDK_KEY_End:
			if (state & GDK_CONTROL_MASK)
			{
				hex_widget_set_cursor (self, payload_size);
			}
			else
			{
				gint64 line_end = self->cursor_pos; 

				while (line_end % self->cpl != self->cpl - 1)
					++line_end;

				hex_widget_set_cursor (self, MIN (line_end, payload_size));
			}
			ret = GDK_EVENT_STOP;
			break;

		default:
			/* don't trample over Ctrl or Alt (reserved for actions) */
			if (state & GDK_CONTROL_MASK || state & GDK_ALT_MASK) {
				return FALSE;
			}

			if (self->active_view == VIEW_HEX)
			{
				switch(keyval)
				{
					case GDK_KEY_Left:
						if (state & GDK_SHIFT_MASK) {
							hex_widget_set_cursor (self, self->cursor_pos - 1);
						}
						else {
							self->lower_nibble = !self->lower_nibble;
							if (self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos - 1);
						}
						ret = GDK_EVENT_STOP;
						break;

					case GDK_KEY_Right:
						if (self->cursor_pos >= payload_size) {
							ret = GDK_EVENT_STOP;
							break;
						}

						if (state & GDK_SHIFT_MASK) {
							hex_widget_set_cursor (self, self->cursor_pos + 1);
						}
						else {
							self->lower_nibble = !self->lower_nibble;
							if (!self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos + 1);
						}
						ret = GDK_EVENT_STOP;
						break;

					default:
						if (keyval >= '0' && keyval <= '9')
						{
							hex_document_set_nibble (self->document, keyval - '0',
									self->cursor_pos, self->lower_nibble,
									self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							self->lower_nibble = !self->lower_nibble;
							if (!self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						else if (keyval >= 'A' && keyval <= 'F')
						{
							hex_document_set_nibble (self->document, keyval - 'A' + 10,
									self->cursor_pos, self->lower_nibble,
									self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							self->lower_nibble = !self->lower_nibble;
							if (!self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						else if (keyval >= 'a' && keyval <= 'f')
						{
							hex_document_set_nibble (self->document, keyval - 'a' + 10,
									self->cursor_pos, self->lower_nibble,
									self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							self->lower_nibble = !self->lower_nibble;
							if (!self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9)
						{
							hex_document_set_nibble (self->document, keyval - GDK_KEY_KP_0,
									self->cursor_pos, self->lower_nibble,
									self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							self->lower_nibble = !self->lower_nibble;
							if (!self->lower_nibble)
								hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						break;      
				}
			}
			else	/* VIEW_ASCII */
			{
				switch (keyval)
				{
					case GDK_KEY_Left:
						hex_widget_set_cursor (self, self->cursor_pos - 1);
						ret = GDK_EVENT_STOP;
						break;

					case GDK_KEY_Right:
						hex_widget_set_cursor (self, self->cursor_pos + 1);
						ret = GDK_EVENT_STOP;
						break;

					default:
						if (is_displayable (keyval))
						{
							hex_document_set_byte (self->document, keyval,
									self->cursor_pos, self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						else if (keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9)
						{
							hex_document_set_byte (self->document, keyval - GDK_KEY_KP_0 + '0',
									self->cursor_pos, self->insert, TRUE);
							if (self->selecting)
								self->selecting = FALSE;
							hex_widget_set_cursor (self, self->cursor_pos + 1);
							ret = GDK_EVENT_STOP;
						}
						break;
				}
			}
			break;
	}
	show_cursor (self, TRUE);
	return ret;
}

static gboolean
key_release_cb (GtkEventControllerKey *controller,
               guint                  keyval,
               guint                  keycode,
               GdkModifierType        state,
               gpointer               user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	GtkWidget *widget = GTK_WIDGET(user_data);
	gboolean ret = GDK_EVENT_PROPAGATE;

	/* avoid shift key getting 'stuck'
	 */
	if (state & GDK_SHIFT_MASK) {
		self->selecting = FALSE;
	}
	return ret;
}

static void
show_offsets_widget(HexWidget *self)
{
	g_return_if_fail (GTK_IS_WIDGET (self->offsets));

	gtk_widget_show (self->offsets);
}

static void
hide_offsets_widget(HexWidget *self)
{
	g_return_if_fail (gtk_widget_get_realized (self->offsets));

	gtk_widget_hide (self->offsets);
}

/* FIXME - Reorganize/clean up. Mostly flown in from old code.
 */
/*
 * default data_changed signal handler
 */
static void
hex_widget_real_data_changed (HexWidget *self, gpointer data)
{
	HexChangeData *change_data = (HexChangeData *)data;
	gint64 start_line, end_line;
	gint64 lines;
	size_t payload_size;

	g_return_if_fail (HEX_IS_DOCUMENT (self->document));

	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	if (self->cpl == 0)
		return;

	if (change_data->start - change_data->end + 1 != change_data->rep_len) {
		lines = payload_size / self->cpl;
		if (payload_size % self->cpl)
			lines++;
		if (lines != self->lines) {
			self->lines = lines;
			gtk_adjustment_set_value(self->adj, MIN(gtk_adjustment_get_value(self->adj), self->lines - self->vis_lines));
			gtk_adjustment_set_value(self->adj, MAX(0, gtk_adjustment_get_value(self->adj)));
			if((self->cursor_pos/self->cpl < gtk_adjustment_get_value(self->adj)) ||
			   (self->cursor_pos/self->cpl > gtk_adjustment_get_value(self->adj) + self->vis_lines - 1)) {
				gtk_adjustment_set_value(self->adj, MIN(self->cursor_pos/self->cpl, self->lines - self->vis_lines));
				gtk_adjustment_set_value(self->adj, MAX(0, gtk_adjustment_get_value(self->adj)));
			}
			gtk_adjustment_set_lower(self->adj, 0);
			gtk_adjustment_set_upper(self->adj, self->lines);
			gtk_adjustment_set_step_increment(self->adj, 1);
			gtk_adjustment_set_page_increment(self->adj, self->vis_lines - 1);
			gtk_adjustment_set_page_size(self->adj, self->vis_lines);
		}
	}

	start_line = change_data->start/self->cpl - self->top_line;
	end_line = change_data->end/self->cpl - self->top_line;

	if(end_line < 0 ||
	   start_line > self->vis_lines)
		return;

	start_line = MAX(start_line, 0);
	if(change_data->rep_len - change_data->end + change_data->start - 1 != 0)
		end_line = self->vis_lines;
	else
		end_line = MIN(end_line, self->vis_lines);

    invalidate_hex_lines (self, start_line, end_line);
    invalidate_ascii_lines (self, start_line, end_line);
    if (self->show_offsets)
    {
        invalidate_offsets (self, start_line, end_line);
    }
}

static void
bytes_changed (HexWidget *self, gint64 start, gint64 end)
{
	gint64 start_line;
	gint64 end_line;

	/* check for divide-by-zero issues */
	g_return_if_fail (self->cpl);

	start_line = start / self->cpl - self->top_line;
	start_line = MAX (start_line, 0);

	end_line = end / self->cpl - self->top_line;

	/* Nothing needs to be done in some instances */
	if (end_line < 0 || start_line > self->vis_lines)
		return;

    invalidate_hex_lines (self, start_line, end_line);
    invalidate_ascii_lines (self, start_line, end_line);

    if (self->show_offsets)
    {
        invalidate_offsets (self, start_line, end_line);
    }
}

static void
hex_widget_validate_highlight(HexWidget *self, HexWidget_Highlight *hl)
{
	if (!hl->valid)
	{
		hl->start_line = MIN(hl->start, hl->end) / self->cpl - self->top_line;
		hl->end_line = MAX(hl->start, hl->end) / self->cpl - self->top_line;
		hl->valid = TRUE;
	}
}

static void
hex_widget_invalidate_highlight (HexWidget *self, HexWidget_Highlight *hl)
{
	hl->valid = FALSE;
}

static void
hex_widget_invalidate_all_highlights (HexWidget *self)
{
	HexWidget_Highlight *cur = &self->selection;
	HexWidgetAutoHighlight *nextList = self->auto_highlight;

	while (cur)
	{
		hex_widget_invalidate_highlight (self, cur);
		cur = cur->next;

		while (cur == NULL && nextList)
		{
			cur = nextList->highlights;
			nextList = nextList->next;
		}
	}
}

static HexWidget_Highlight *
hex_widget_insert_highlight (HexWidget *self,
		HexWidgetAutoHighlight *ahl,
		gint64 start,
		gint64 end)
{
	size_t payload_size;

	g_return_val_if_fail (HEX_IS_DOCUMENT (self->document), NULL);

	payload_size = HEX_BUFFER_PAYLOAD (self->document);
	HexWidget_Highlight *new = g_new0 (HexWidget_Highlight, 1);

	new->start = CLAMP(MIN(start, end), 0, payload_size);
	new->end = MIN(MAX(start, end), payload_size);

	new->valid = FALSE;

	new->prev = NULL;
	new->next = ahl->highlights;
	if (new->next) new->next->prev = new;
	ahl->highlights = new;

	bytes_changed (self, new->start, new->end);

	return new;
}

static void
hex_widget_delete_highlight (HexWidget *self, HexWidgetAutoHighlight *ahl,
		HexWidget_Highlight *hl)
{
	gint64 start, end;
	start = hl->start;
	end = hl->end;
	if (hl->prev) hl->prev->next = hl->next;
	if (hl->next) hl->next->prev = hl->prev;

	if (hl == ahl->highlights) ahl->highlights = hl->next;

	g_free(hl);
	bytes_changed(self, start, end);
}

/* stolen from hex_document_compare_data - but uses
 * hex_widget_* stuff rather than hex_document_* directly
 * and simply returns a gboolean.
 */
static gboolean
hex_widget_compare_data (HexWidget *self, guchar *cmp, gint64 pos, int len)
{
	for (int i = 0; i < len; i++)
	{
		guchar c = hex_widget_get_byte (self, pos + i);

		if (c != *(cmp + i))
			return FALSE;
	}
	return TRUE;
}

static gboolean
hex_widget_find_limited (HexWidget *self, char *find, int findlen,
		gint64 lower, gint64 upper, gint64 *found)
{
	gint64 pos = lower;

	while (pos < upper)
	{
		if (hex_widget_compare_data (self, (guchar *)find, pos, findlen))
		{
			*found = pos;
			return TRUE;
		}
		pos++;
	}
	return FALSE;
}


/* Helper for the autohighlight functions to set a reasonable default
 * view_min () and view_max () value.
 */
static inline void
ahl_set_view_min_max (HexWidget *self, HexWidgetAutoHighlight *ahl)
{
	ahl->view_min = self->top_line * self->cpl;
	ahl->view_max = (self->top_line + self->vis_lines) * self->cpl;
}

static void
hex_widget_update_auto_highlight (HexWidget *self, HexWidgetAutoHighlight *ahl,
		gboolean delete, gboolean add)
{
	gint64 del_min, del_max;
	gint64 add_min, add_max;
	gint64 foundpos = -1;
	gint64 prev_min = ahl->view_min;
	gint64 prev_max = ahl->view_max;
	HexWidget_Highlight *cur;

	ahl_set_view_min_max (self, ahl);

	if (prev_min < ahl->view_min && prev_max < ahl->view_max)
	{
		del_min = prev_min - ahl->search_len;
		del_max = ahl->view_min - ahl->search_len;
		add_min = prev_max;
		add_max = ahl->view_max;
	}
	else if (prev_min > ahl->view_min && prev_max > ahl->view_max)
	{
		add_min = ahl->view_min - ahl->search_len;
		add_max = prev_min - ahl->search_len;
		del_min = ahl->view_max;
		del_max = prev_max;
	}
	else /* just refresh the lot */
	{
		del_min = 0;
		del_max = self->cpl * self->lines;
		add_min = ahl->view_min;
		add_max = ahl->view_max;
	}

	add_min = MAX(add_min, 0);
	del_min = MAX(del_min, 0);

	cur = ahl->highlights;
	while (delete && cur)
	{
		if (cur->start >= del_min && cur->start <= del_max)
		{
			HexWidget_Highlight *next = cur->next;

			hex_widget_delete_highlight (self, ahl, cur);
			cur = next;
		}
		else cur = cur->next;
	}

	while (add &&
		   hex_widget_find_limited (self, ahl->search_string, ahl->search_len,
			   /* lower */ MAX(add_min, foundpos+1), add_max, &foundpos))
	{
		hex_widget_insert_highlight (self, ahl,
				foundpos, foundpos+(ahl->search_len)-1);
	}
}

static void
hex_widget_update_all_auto_highlights (HexWidget *self)
{
	HexWidgetAutoHighlight *cur = self->auto_highlight;

	int mult = 10;

	gint64 top_line_pos = self->top_line * self->cpl;
	gint64 bot_line_pos = (self->top_line + self->vis_lines) * self->cpl;

	while (cur)
	{
		/* only refresh possibilities within a certain number of line numbers
		 */
		if (top_line_pos - mult * self->vis_lines * self->cpl > cur->view_min ||
			bot_line_pos + mult * self->vis_lines * self->cpl < cur->view_max)
		{
			/* do nothing */
		}
		else
		{
			hex_widget_update_auto_highlight (self, cur, TRUE, TRUE);
		}
		cur = cur->next;
	}
}

static void
hex_widget_real_copy_to_clipboard (HexWidget *self)
{
	GtkWidget *widget = GTK_WIDGET(self);
	GdkClipboard *clipboard;
	HexPasteData *paste;
	GdkContentProvider *provider_union;
	GdkContentProvider *provider_array[2];
	gint64 start_pos, end_pos;
	size_t len;
	char *doc_data;
	char *string;

	clipboard = gtk_widget_get_clipboard (widget);

	start_pos = MIN(self->selection.start, self->selection.end);
	end_pos = MAX(self->selection.start, self->selection.end);

	/* +1 because we're counting the number of characters to grab here.
	 * You have to actually include the first character in the range.
	 */
	len = end_pos - start_pos + 1;
	g_return_if_fail (len);

	/* Grab the raw data from the HexDocument. */
	doc_data = hex_buffer_get_data (hex_document_get_buffer(self->document),
			start_pos, len);

	/* Setup a union of HexPasteData and a plain C string */
	paste = hex_paste_data_new (doc_data, len);
	g_return_if_fail (HEX_IS_PASTE_DATA(paste));
	string = hex_paste_data_get_string (paste);

	provider_array[0] =
		gdk_content_provider_new_typed (HEX_TYPE_PASTE_DATA, paste);
	provider_array[1] =
		gdk_content_provider_new_typed (G_TYPE_STRING, string);

	provider_union = gdk_content_provider_new_union (provider_array, 2);

	/* Finally, set our content to our newly created union. */
	gdk_clipboard_set_content (clipboard, provider_union);
}

static void
hex_widget_real_cut_to_clipboard(HexWidget *self,
		gpointer user_data)
{
	(void)user_data;

	if (self->selection.start != -1 && self->selection.end != -1) {
		hex_widget_real_copy_to_clipboard(self);
		hex_widget_delete_selection(self);
	}
}

static void
plaintext_paste_received_cb (GObject *source_object,
		GAsyncResult *result,
		gpointer user_data)
{
	HexWidget *self = HEX_WIDGET(user_data);
	GdkClipboard *clipboard;
	char *text;
	GError *error = NULL;

	g_debug ("%s: We DON'T have our special HexPasteData. Falling back "
			"to plaintext paste.",
			__func__);

	clipboard = GDK_CLIPBOARD (source_object);

	/* Get the resulting text of the read operation */
	text = gdk_clipboard_read_text_finish (clipboard, result, &error);

	if (text) {
		hex_document_set_data (self->document,
				self->cursor_pos,
				strlen(text),
				0,	/* rep_len (0 to insert w/o replacing; what we want) */
				text,
				TRUE);

		hex_widget_set_cursor (self, self->cursor_pos + strlen(text));
		
		g_free(text);
	}
	else {
		g_critical ("Error pasting text: %s", 
				error->message);
		g_error_free (error);
	}
}

static void
hex_widget_real_paste_from_clipboard (HexWidget *self,
		gpointer user_data)
{
	GtkWidget *widget = GTK_WIDGET(self);
	GdkClipboard *clipboard;
	GdkContentProvider *content;
	GValue value = G_VALUE_INIT;
	HexPasteData *paste;
	gboolean have_hex_paste_data = FALSE;

	(void)user_data;	/* unused */

	clipboard = gtk_widget_get_clipboard (widget);
	content = gdk_clipboard_get_content (clipboard);
	g_value_init (&value, HEX_TYPE_PASTE_DATA);

	/* If the clipboard contains our special HexPasteData, we'll use it.
	 * If not, just fall back to plaintext.
	 *
	 * Note the double test here; it seems the test is semi-superfluous for
	 * *this* purpose because _get_content will itself return NULL if the
	 * clipboard data we're getting is not owned by the process; that will
	 * pretty much *always* be the case when we're falling back to plaintext,
	 * ie, when pasting from external apps. Oh well.
	 */
	have_hex_paste_data =
		GDK_IS_CONTENT_PROVIDER (content) &&
		gdk_content_provider_get_value (content,
				&value,
				NULL);	/* GError - NULL to ignore */

	if (have_hex_paste_data)
	{
		char *doc_data;
		int elems;

		g_debug("%s: We HAVE our special HexPasteData.",
				__func__);

		paste = HEX_PASTE_DATA(g_value_get_object (&value));
		doc_data = hex_paste_data_get_doc_data (paste);
		elems = hex_paste_data_get_elems (paste);

		hex_document_set_data (self->document,
				self->cursor_pos,
				elems,
				0,	/* rep_len (0 to insert w/o replacing; what we want) */
				doc_data,
				TRUE);

		hex_widget_set_cursor (self, self->cursor_pos + elems);
	}
	else {
		gdk_clipboard_read_text_async (clipboard,
				NULL,	/* GCancellable *cancellable */
				plaintext_paste_received_cb,
				self);
	}
}

static void
hex_widget_real_draw_complete (HexWidget *self,
		gpointer user_data)
{
	(void)user_data;

	recalc_scrolling (self);
}

static void
hex_widget_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
{
	HexWidget *self = HEX_WIDGET(widget);
	GtkWidget *child;
	float height;

	height = gtk_widget_get_allocated_height (widget);

	/* Update character width & height */
	self->char_width = get_char_width (self);
	self->char_height = get_char_height (self);

	/* Get cpl from layout manager or geometry (if set) */
	if (! self->default_cpl)
		self->cpl = hex_widget_layout_get_cpl (
				HEX_WIDGET_LAYOUT(self->layout_manager));
	else
		self->cpl = self->default_cpl;

	/* set visible lines based on widget height or geometry (if set) */
	if (! self->default_lines)
		self->vis_lines = height / self->char_height;
	else
		self->vis_lines = self->default_lines;

	hex_widget_update_all_auto_highlights (self);
	hex_widget_invalidate_all_highlights (self);

	/* queue child draw functions
	 */

	/* manually specify these as sometimes _snapshot_child doesn't `think'
	 * they need to be redrawn. */
	gtk_widget_queue_draw (self->offsets);
	gtk_widget_queue_draw (self->xdisp);
	gtk_widget_queue_draw (self->adisp);

	for (child = gtk_widget_get_first_child (widget);
			child != NULL;
			child = gtk_widget_get_next_sibling (child))
	{
		gtk_widget_snapshot_child (widget, child, snapshot);
	}

	g_signal_emit_by_name(G_OBJECT(self), "draw-complete");
}

static void
set_busy_state (HexWidget *self, gboolean busy)
{
	GtkWidget *widget = GTK_WIDGET(self);

	if (busy)
	{
		gtk_widget_hide (self->offsets);
		gtk_widget_hide (self->xdisp);
		gtk_widget_hide (self->adisp);
		gtk_widget_hide (self->scrollbar);
		gtk_widget_show (self->busy_spinner);
	}
	else
	{
		gtk_widget_hide (self->busy_spinner);
		gtk_widget_set_visible (self->offsets, self->show_offsets);
		gtk_widget_show (self->xdisp);
		gtk_widget_show (self->adisp);
		gtk_widget_show (self->scrollbar);
	}
}

static void
file_read_started_cb (HexDocument *doc, gpointer data)
{
	HexWidget *self = HEX_WIDGET (data);

	set_busy_state (self, TRUE);
}

static void
file_loaded_cb (HexDocument *doc, gpointer data)
{
	HexWidget *self = HEX_WIDGET(data);

	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.undo", hex_document_can_undo (doc));
	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.redo", hex_document_can_redo (doc));

	set_busy_state (self, FALSE);

	gtk_widget_queue_draw (GTK_WIDGET(self));
}

static void
document_changed_cb (HexDocument* doc, gpointer change_data,
        gboolean push_undo, gpointer data)
{
	HexWidget *self = HEX_WIDGET(data);
	g_return_if_fail (HEX_IS_WIDGET (self));

    hex_widget_real_data_changed (self, change_data);

	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.undo", hex_document_can_undo (doc));
	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.redo", hex_document_can_redo (doc));
}

static void
hex_widget_dispose (GObject *object)
{
	HexWidget *self = HEX_WIDGET(object);
	GtkWidget *widget = GTK_WIDGET(self);
	GtkWidget *child;

	/* Unparent children
	 */
	g_clear_pointer (&self->xdisp, gtk_widget_unparent);
	g_clear_pointer (&self->adisp, gtk_widget_unparent);
	g_clear_pointer (&self->offsets, gtk_widget_unparent);
	g_clear_pointer (&self->scrollbar, gtk_widget_unparent);
	g_clear_pointer (&self->busy_spinner, gtk_widget_unparent);

	/* FIXME - This results in assertion errors upon exit. I have been told
	 * by ebassi on IRC (16-Dec-2021) that this may be a bug in gtk. See:
	 * https://gitlab.gnome.org/GNOME/gtk/-/issues/4548
	 */
	g_clear_pointer (&self->context_menu, gtk_widget_unparent);

	/* Clear pango layouts
	 */
	g_clear_object (&self->xlayout);
	g_clear_object (&self->alayout);
	g_clear_object (&self->olayout);

	g_clear_object (&self->document);
	
	/* Chain up */
	G_OBJECT_CLASS(hex_widget_parent_class)->dispose(object);
}

static void
hex_widget_finalize (GObject *gobject)
{
	HexWidget *self = HEX_WIDGET(gobject);
	
	if (self->disp_buffer)
		g_free (self->disp_buffer);

	/* Boilerplate; keep here. Chain up to the parent class.
	 */
	G_OBJECT_CLASS(hex_widget_parent_class)->finalize(gobject);
}


static void
hex_widget_class_init (HexWidgetClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	/* vfuncs */

	object_class->dispose = hex_widget_dispose;
	object_class->finalize = hex_widget_finalize;
	widget_class->snapshot = hex_widget_snapshot;

	/* Layout manager: box-style layout. */

	gtk_widget_class_set_layout_manager_type (widget_class,
			HEX_TYPE_WIDGET_LAYOUT);

	/* CSS name */

	gtk_widget_class_set_css_name (widget_class, "hexwidget");

	/* SIGNALS */

	/**
	 * HexWidget::cursor-moved:
	 * 
	 * Emitted when the cursor has moved.
	 */
	gtkhex_signals[CURSOR_MOVED_SIGNAL] =
		g_signal_new_class_handler ("cursor-moved",
				G_OBJECT_CLASS_TYPE(object_class),
			/* GSignalFlags signal_flags : */
				G_SIGNAL_RUN_FIRST,
			/* GCallback class_handler: */
				NULL,		/* no callback; plain signal. */
			/* no accumulator or accu_data */
				NULL, NULL,
			/* GSignalCMarshaller c_marshaller: */
				NULL,		/* use generic marshaller */
			/* GType return_type: */
				G_TYPE_NONE,
			/* guint n_params: */
				0);

	/**
	 * HexWidget::data-changed:
	 *
	 * Emitted when data has changed.
	 */
	gtkhex_signals[DATA_CHANGED_SIGNAL] = 
		g_signal_new_class_handler ("data-changed",
				G_OBJECT_CLASS_TYPE(object_class),
				G_SIGNAL_RUN_FIRST,
				G_CALLBACK(hex_widget_real_data_changed),
				NULL, NULL,
				NULL,
				G_TYPE_NONE,
				0);
	/**
	 * HexWidget::cut-clipboard:
	 *
	 * Emitted when a cut-to-clipboard operation has occurred.
	 */
	gtkhex_signals[CUT_CLIPBOARD_SIGNAL] =
		g_signal_new_class_handler ("cut-clipboard",
				G_OBJECT_CLASS_TYPE(object_class),
				G_SIGNAL_RUN_FIRST,
				G_CALLBACK(hex_widget_real_cut_to_clipboard),
				NULL, NULL,
				NULL,
				G_TYPE_NONE,
				0);

	/**
	 * HexWidget::copy-clipboard:
	 *
	 * Emitted when a copy-to-clipboard operation has occurred.
	 */
	gtkhex_signals[COPY_CLIPBOARD_SIGNAL] = 
		g_signal_new_class_handler ("copy-clipboard",
				G_OBJECT_CLASS_TYPE(object_class),
				G_SIGNAL_RUN_FIRST,
				G_CALLBACK(hex_widget_real_copy_to_clipboard),
				NULL, NULL,
				NULL,
				G_TYPE_NONE,
				0);

	/**
	 * HexWidget::paste-clipboard:
	 *
	 * Emitted when a paste-from-clipboard operation has occurred.
	 */
	gtkhex_signals[PASTE_CLIPBOARD_SIGNAL] = 
		g_signal_new_class_handler ("paste-clipboard",
				G_OBJECT_CLASS_TYPE(object_class),
				G_SIGNAL_RUN_FIRST,
				G_CALLBACK(hex_widget_real_paste_from_clipboard),
				NULL, NULL,
				NULL,
				G_TYPE_NONE,
				0);
	
	/**
	 * HexWidget:draw-complete:
	 *
	 * Emitted when the #HexWidget has been fully redrawn.
	 */
	gtkhex_signals[DRAW_COMPLETE_SIGNAL] = 
		g_signal_new_class_handler ("draw-complete",
				G_OBJECT_CLASS_TYPE(object_class),
				G_SIGNAL_RUN_FIRST,
				G_CALLBACK(hex_widget_real_draw_complete),
				NULL, NULL,
				NULL,
				G_TYPE_NONE,
				0);

	/* ACTIONS */

	gtk_widget_class_install_action (widget_class, "gtkhex.copy",
			NULL,   /* GVariant string param_type */
			copy_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.cut",
			NULL,
			cut_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.paste",
			NULL,
			paste_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.undo",
			NULL,
			undo_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.redo",
			NULL,
			redo_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.toggle-hex",
			NULL,
			toggle_hex_action);

	gtk_widget_class_install_action (widget_class, "gtkhex.toggle-ascii",
			NULL,
			toggle_ascii_action);

	/* SHORTCUTS FOR ACTIONS (not to be confused with keybindings, which are
	 * set up in hex_widget_init) */

	/* Ctrl+c - copy */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_c,
			GDK_CONTROL_MASK,
			"gtkhex.copy",
			NULL);	/* no args. */

	/* Ctrl+x - cut */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_x,
			GDK_CONTROL_MASK,
			"gtkhex.cut",
			NULL);

	/* Ctrl+v - paste */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_v,
			GDK_CONTROL_MASK,
			"gtkhex.paste",
			NULL);

	/* Ctrl+z - undo */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_z,
			GDK_CONTROL_MASK,
			"gtkhex.undo",
			NULL);

	/* Ctrl+y - redo */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_y,
			GDK_CONTROL_MASK,
			"gtkhex.redo",
			NULL);

	/* Alt+Left - toggle hex display */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_Left,
			GDK_ALT_MASK,
			"gtkhex.toggle-hex",
			NULL);

	/* Alt+Right - toggle ascii display */
	gtk_widget_class_add_binding_action (widget_class,
			GDK_KEY_Right,
			GDK_ALT_MASK,
			"gtkhex.toggle-ascii",
			NULL);
}

static void
hex_widget_init (HexWidget *self)
{
	GtkWidget *widget = GTK_WIDGET(self);

	HexWidgetLayoutChild *child_info;

	GtkStyleContext *context;

	GtkBuilder *builder;
	GMenuModel *menu;

	GtkGesture *gesture;
	GtkEventController *controller;

	self->layout_manager = gtk_widget_get_layout_manager (widget);

	self->disp_buffer = NULL;

	self->scroll_timeout = 0;

	self->document = NULL;

	self->active_view = VIEW_HEX;
	self->group_type = HEX_WIDGET_GROUP_BYTE;
	self->lines = self->vis_lines = self->top_line = self->cpl = 0;
	self->cursor_pos = 0;
	self->lower_nibble = FALSE;
	self->cursor_shown = FALSE;
	self->button = 0;
	self->insert = FALSE; /* default to overwrite mode */
	self->selecting = FALSE;

	self->selection.start = self->selection.end = 0;
	self->selection.next = self->selection.prev = NULL;
	self->selection.valid = FALSE;

	self->auto_highlight = NULL;

	gtk_widget_set_can_focus (widget, TRUE);
	gtk_widget_set_focusable (widget, TRUE);

	/* Init CSS */

	context = gtk_widget_get_style_context (GTK_WIDGET (widget));

	/* Add common custom `.hex` style class */
	gtk_style_context_add_class (context, "hex");

	self->provider = gtk_css_provider_new ();
	gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (self->provider),
		RESOURCE_BASE_PATH "/css/ghex.css");

	gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (self->provider),
			GTK_STYLE_PROVIDER_PRIORITY_THEME);

	/* Setup offsets widget. */

	self->offsets = gtk_drawing_area_new();
	gtk_widget_set_parent (self->offsets, widget);
	child_info = HEX_WIDGET_LAYOUT_CHILD (gtk_layout_manager_get_layout_child
			(self->layout_manager, self->offsets));
	hex_widget_layout_child_set_column (child_info, OFFSETS_COLUMN);

	/* Create the pango layout for the widget */
	self->olayout = gtk_widget_create_pango_layout (self->offsets, "");

	gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (self->offsets),
			offsets_draw,
			self,
			NULL);		/* GDestroyNotify destroy); */

	context = gtk_widget_get_style_context (GTK_WIDGET (self->offsets));

	gtk_style_context_add_class (context, "hex");

	gtk_style_context_add_provider (context,
	                                GTK_STYLE_PROVIDER (self->provider),
	                                GTK_STYLE_PROVIDER_PRIORITY_THEME);

	gtk_widget_set_name (self->offsets, "offsets");

	/* hide it by default. */
	gtk_widget_hide (self->offsets);


	/* Setup our Hex drawing area. */

	self->xdisp = gtk_drawing_area_new();
	gtk_widget_set_parent (self->xdisp, widget);
	child_info = HEX_WIDGET_LAYOUT_CHILD (gtk_layout_manager_get_layout_child
			(self->layout_manager, self->xdisp));
	hex_widget_layout_child_set_column (child_info, HEX_COLUMN);

	/* Create the pango layout for the widget */
	self->xlayout = gtk_widget_create_pango_layout (self->xdisp, "");

	gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (self->xdisp),
			hex_draw,
			self,
			NULL);		/* GDestroyNotify destroy); */

	context = gtk_widget_get_style_context (GTK_WIDGET (self->xdisp));

	gtk_style_context_add_class (context, "hex");

	gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (self->provider),
			GTK_STYLE_PROVIDER_PRIORITY_THEME);

	gtk_widget_set_name (self->xdisp, "hex-display");

	/* Setup our ASCII widget. */

	self->adisp = gtk_drawing_area_new();
	gtk_widget_set_parent (self->adisp, widget);
	child_info = HEX_WIDGET_LAYOUT_CHILD (gtk_layout_manager_get_layout_child
			(self->layout_manager, self->adisp));
	hex_widget_layout_child_set_column (child_info, ASCII_COLUMN);

	/* Create the pango layout for the widget */
	self->alayout = gtk_widget_create_pango_layout (self->adisp, "");

	gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (self->adisp),
			ascii_draw,
			self,
			NULL);		/* GDestroyNotify destroy); */

	context = gtk_widget_get_style_context (GTK_WIDGET (self->adisp));
	gtk_style_context_add_class (context, "hex");
	gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (self->provider),
			GTK_STYLE_PROVIDER_PRIORITY_THEME);

	gtk_widget_set_name (self->adisp, "ascii-display");

	/* Set a minimum size for hex/ascii drawing areas. */

	gtk_widget_set_size_request (self->adisp,
			DEFAULT_DA_SIZE, DEFAULT_DA_SIZE);
	gtk_widget_set_size_request (self->xdisp,
			DEFAULT_DA_SIZE, DEFAULT_DA_SIZE);

	/* Context Menu */

	builder = gtk_builder_new_from_resource (RESOURCE_BASE_PATH "/context-menu.ui");
	menu = G_MENU_MODEL(gtk_builder_get_object (builder, "context-menu"));
	self->context_menu = gtk_popover_menu_new_from_model (menu);

	gtk_widget_set_parent (self->context_menu, widget);

	g_object_unref (builder);
	g_object_unref (menu);		/* ref'd by gtk_popover_menu_new_from_model */

	/* Initialize Adjustment */
	self->adj = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

	/* Setup scrollbar. */

	self->scrollbar = gtk_scrollbar_new (GTK_ORIENTATION_VERTICAL,
			self->adj);

	context = gtk_widget_get_style_context (GTK_WIDGET (self->scrollbar));
	gtk_style_context_add_class (context, "hex");
	gtk_style_context_add_provider (context,
	                                GTK_STYLE_PROVIDER (self->provider),
	                                GTK_STYLE_PROVIDER_PRIORITY_THEME);

	gtk_widget_set_parent (self->scrollbar, widget);
	child_info = HEX_WIDGET_LAYOUT_CHILD (gtk_layout_manager_get_layout_child
			(self->layout_manager, self->scrollbar));
	hex_widget_layout_child_set_column (child_info, SCROLLBAR_COLUMN);
	
	/* Setup busy spinner */

	self->busy_spinner = gtk_spinner_new ();
	gtk_spinner_start (GTK_SPINNER(self->busy_spinner));
	gtk_widget_set_visible (self->busy_spinner, FALSE);
	gtk_widget_set_parent (self->busy_spinner, widget);

	/* SETUP SIGNALS */

	/* GESTURES */

	/* Whole HexWidget widget (for right-click context menu) */

	gesture = gtk_gesture_click_new ();

	/* listen for right-click only */
	gtk_gesture_single_set_button (GTK_GESTURE_SINGLE(gesture),
			GDK_BUTTON_SECONDARY);

	g_signal_connect (gesture, "pressed",
			G_CALLBACK(gh_pressed_cb),
			self);

	gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER(gesture));

	/* Hex widget: */

	/* click gestures */
	gesture = gtk_gesture_click_new ();

	/* listen for any button */
	gtk_gesture_single_set_button (GTK_GESTURE_SINGLE(gesture), 0);

	g_signal_connect (gesture, "pressed",
			G_CALLBACK(hex_pressed_cb),
			self);
	g_signal_connect (gesture, "released",
			G_CALLBACK(hex_released_cb),
			self);
	gtk_widget_add_controller (self->xdisp,
			GTK_EVENT_CONTROLLER(gesture));

	/* drag gestures */
	gesture = gtk_gesture_drag_new ();

	g_signal_connect (gesture, "drag-update",
			G_CALLBACK (hex_drag_update_cb),
			self);
	gtk_widget_add_controller (self->xdisp,
			GTK_EVENT_CONTROLLER(gesture));

	/* ASCII widget: */

	/* click gestures */
	gesture = gtk_gesture_click_new ();

	/* listen for any button */
	gtk_gesture_single_set_button (GTK_GESTURE_SINGLE(gesture), 0);

	g_signal_connect (gesture, "pressed",
			G_CALLBACK(ascii_pressed_cb),
			self);
	g_signal_connect (gesture, "released",
			G_CALLBACK(ascii_released_cb),
			self);
	gtk_widget_add_controller (self->adisp,
			GTK_EVENT_CONTROLLER(gesture));

	/* drag gestures */
	gesture = gtk_gesture_drag_new ();

	g_signal_connect (gesture, "drag-update",
			G_CALLBACK(ascii_drag_update_cb),
			self);
	gtk_widget_add_controller (self->adisp,
			GTK_EVENT_CONTROLLER(gesture));

	/* Event controller - scrolling */

	controller =
		gtk_event_controller_scroll_new
			(GTK_EVENT_CONTROLLER_SCROLL_VERTICAL |
			 GTK_EVENT_CONTROLLER_SCROLL_DISCRETE);

	g_signal_connect (controller, "scroll",
			G_CALLBACK(scroll_cb),
			self);
	gtk_widget_add_controller (widget,
			GTK_EVENT_CONTROLLER(controller));

	/* Event controller - keyboard - for the widget *as a whole* */
	
	controller = gtk_event_controller_key_new ();

	g_signal_connect(controller, "key-pressed",
			G_CALLBACK(key_press_cb),
			self);
	g_signal_connect(controller, "key-released",
			G_CALLBACK(key_release_cb),
			self);

	gtk_widget_add_controller (widget,
			GTK_EVENT_CONTROLLER(controller));

	/* Event controller - focus */

	controller = gtk_event_controller_focus_new ();

	g_signal_connect_swapped (controller, "enter",
			G_CALLBACK(gtk_widget_queue_draw), widget);
	g_signal_connect_swapped (controller, "leave",
			G_CALLBACK(gtk_widget_queue_draw), widget);

	gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER(controller));

	/* Connect signal for adjustment */

	g_signal_connect (self->adj, "value-changed",
					 G_CALLBACK (adj_value_changed_cb), self);

	/* ACTIONS - Undo / Redo should start out disabled. */

	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.undo", FALSE);
	gtk_widget_action_set_enabled (GTK_WIDGET(self),
			"gtkhex.redo", FALSE);
}

/*-------- public API starts here --------*/

/**
 * hex_widget_new:
 * @owner: the [class@Hex.Document] object to be associated with the
 *   newly created #HexWidget
 *
 * Create a new #HexWidget object.
 *
 * Returns: a newly created #HexWidget object, or %NULL
 */
GtkWidget *
hex_widget_new (HexDocument *owner)
{
	HexWidget *self;

	self = HEX_WIDGET (g_object_new (HEX_TYPE_WIDGET, NULL));
	g_return_val_if_fail (self != NULL, NULL);

	self->document = owner;

	/* Setup document signals */

    g_signal_connect (G_OBJECT (self->document), "file-read-started",
            G_CALLBACK (file_read_started_cb), self);

    g_signal_connect (G_OBJECT (self->document), "file-loaded",
            G_CALLBACK (file_loaded_cb), self);

    g_signal_connect (G_OBJECT (self->document), "document-changed",
            G_CALLBACK (document_changed_cb), self);

    g_signal_connect (G_OBJECT (self->document), "undo",
            G_CALLBACK (doc_undo_redo_cb), self);
	
    g_signal_connect (G_OBJECT (self->document), "redo",
            G_CALLBACK (doc_undo_redo_cb), self);
	
	return GTK_WIDGET(self);
}

/**
 * hex_widget_copy_to_clipboard:
 *
 * Copy selection to clipboard.
 */
void
hex_widget_copy_to_clipboard (HexWidget *self)
{
	g_signal_emit_by_name(G_OBJECT(self), "copy-clipboard");
}

/**
 * hex_widget_cut_to_clipboard:
 *
 * Cut selection to clipboard.
 */
void
hex_widget_cut_to_clipboard (HexWidget *self)
{
	g_signal_emit_by_name(G_OBJECT(self), "cut-clipboard");
}

/**
 * hex_widget_paste_from_clipboard:
 *
 * Paste clipboard data to widget at position of cursor.
 */
void
hex_widget_paste_from_clipboard (HexWidget *self)
{
	g_signal_emit_by_name(G_OBJECT(self), "paste-clipboard");
}

/**
 * hex_widget_set_selection:
 * @start: starting offset by byte within the buffer
 * @end: ending offset by byte within the buffer
 *
 * Set the widget selection (highlights).
 */
void
hex_widget_set_selection (HexWidget *self, gint64 start, gint64 end)
{
	size_t payload_size;
	gint64 oe, os, ne, ns;

	g_return_if_fail (HEX_IS_DOCUMENT (self->document));

	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	if (end < 0)
		end = payload_size;

	os = MIN(self->selection.start, self->selection.end);
	oe = MAX(self->selection.start, self->selection.end);

	self->selection.start = CLAMP(start, 0, payload_size);
	self->selection.end = MIN(end, payload_size);

	hex_widget_invalidate_highlight(self, &self->selection);

	ns = MIN(self->selection.start, self->selection.end);
	ne = MAX(self->selection.start, self->selection.end);

	if (ns != os && ne != oe) {
		bytes_changed(self, MIN(ns, os), MAX(ne, oe));
	}
	else if (ne != oe) {
		bytes_changed(self, MIN(ne, oe), MAX(ne, oe));
	}
	else if (ns != os) {
		bytes_changed(self, MIN(ns, os), MAX(ns, os));
	}
}

/**
 * hex_widget_get_selection:
 * @start: (out): where to store the start of the current selection, as offset
 *   by byte within the buffer
 * @end: (out): where to store the end of the current selection, as offset by
 *   byte within the buffer
 *
 * Get the current widget selection (highlights).
 *
 * Returns: %TRUE if the operation was successful; %FALSE otherwise.
 */
gboolean
hex_widget_get_selection (HexWidget *self, gint64 *start, gint64 *end)
{
	gint64 ss, se;

	if (self->selection.start > self->selection.end) {
		se = self->selection.start;
		ss = self->selection.end;
	}
	else {
		ss = self->selection.start;
		se = self->selection.end;
	}

	if (start)
		*start = ss;
	if (end)
		*end = se;

	return !(ss == se);
}

/**
 * hex_widget_clear_selection:
 *
 * Clear the selection (if any). Any autohighlights will remain intact.
 */
void
hex_widget_clear_selection (HexWidget *self)
{
	hex_widget_set_selection (self, 0, 0);
}

/**
 * hex_widget_delete_selection:
 *
 * Delete the current selection. The resulting action will be undoable. 
 */
void
hex_widget_delete_selection (HexWidget *self)
{
	gint64 start, end;
	size_t len;

	start = MIN(self->selection.start, self->selection.end);
	end = MAX(self->selection.start, self->selection.end);

	len = end - start + 1;
	g_return_if_fail (len);

	hex_widget_clear_selection (self);

	hex_document_delete_data (self->document,
			MIN(start, end), len, TRUE);

	hex_widget_set_cursor (self, start);
}

/**
 * hex_widget_set_nibble:
 * @lower_nibble: %TRUE if the lower nibble of the current byte should be
 *   selected; %FALSE for the upper nibble
 *
 * Move the cursor to upper nibble or lower nibble of the current byte.
 */
void
hex_widget_set_nibble (HexWidget *self, gboolean lower_nibble)
{
	g_return_if_fail (HEX_IS_WIDGET(self));

	if (self->selecting) {
		bytes_changed(self, self->cursor_pos, self->cursor_pos);
		self->lower_nibble = lower_nibble;
	}
	else if(self->selection.end != self->selection.start) {
		guint start = MIN(self->selection.start, self->selection.end);
		guint end = MAX(self->selection.start, self->selection.end);
		self->selection.end = self->selection.start = 0;
		bytes_changed(self, start, end);
		self->lower_nibble = lower_nibble;
	}
	else {
		show_cursor (self, FALSE);
		self->lower_nibble = lower_nibble;
		show_cursor(self, TRUE);
	}
}

/**
 * hex_widget_set_cursor:
 * @index: where the cursor should be moved to, as offset by byte within
 *   the buffer
 *
 * Move cursor to @index.
 */
void
hex_widget_set_cursor (HexWidget *self, gint64 index)
{
	gint64 y;
	gint64 old_pos;
	size_t payload_size;

	g_return_if_fail (HEX_IS_WIDGET (self));

	old_pos = self->cursor_pos;
	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	if ((index >= 0) && (index <= payload_size))
	{
		if (!self->insert && index == payload_size)
			index--;

		index = MAX(index, 0);

		show_cursor (self, FALSE);
		
		self->cursor_pos = index;

		if (self->cpl == 0)
			return;
		
		y = index / self->cpl;
		if (y >= self->top_line + self->vis_lines)
		{
			gtk_adjustment_set_value (self->adj,
					MIN (y - self->vis_lines + 1, self->lines - self->vis_lines));

			gtk_adjustment_set_value (self->adj,
					MAX (gtk_adjustment_get_value(self->adj), 0));
		}
		else if (y < self->top_line)
		{
			gtk_adjustment_set_value (self->adj, y);
		}      

		if (index == payload_size)
			self->lower_nibble = FALSE;

		if (self->selecting)
		{
			hex_widget_set_selection(self, self->selection.start, self->cursor_pos);

			bytes_changed (self,
					MIN (self->cursor_pos, old_pos),
					MAX (self->cursor_pos, old_pos));
		}
		else
		{
			gint64 start = MIN (self->selection.start, self->selection.end);
			gint64 end = MAX (self->selection.start, self->selection.end);

			bytes_changed (self, start, end);
			self->selection.end = self->selection.start = self->cursor_pos;
		}

		g_signal_emit_by_name (self, "cursor-moved");

		bytes_changed (self, old_pos, old_pos);
		show_cursor (self, TRUE);
	}
}

/**
 * hex_widget_set_cursor_by_row_and_col:
 * @col_x: column to which the cursor should be moved
 * @line_y: line to which the cursor should be moved, by absolute value, within
 *   the whole buffer (not just the currently visible part)
 *
 * Move the cursor by row and column, as absolute values.
 */
void
hex_widget_set_cursor_by_row_and_col (HexWidget *self, int col_x, gint64 line_y)
{
	gint64 tmp_cursor_pos;
	gint64 old_pos;
	gint64 payload_size;

	g_return_if_fail (HEX_IS_WIDGET(self));

	old_pos = self->cursor_pos;
	tmp_cursor_pos = line_y * self->cpl + col_x;
	payload_size = HEX_BUFFER_PAYLOAD (self->document);

	if ((line_y >= 0) && (line_y < self->lines) && (col_x >= 0) &&
	   (col_x < self->cpl) && (tmp_cursor_pos <= payload_size))
	{
		if (!self->insert && tmp_cursor_pos == payload_size)
			tmp_cursor_pos--;

		tmp_cursor_pos = MAX(tmp_cursor_pos, 0);

		show_cursor (self, FALSE);
		
		self->cursor_pos = tmp_cursor_pos;
		
		if (line_y >= self->top_line + self->vis_lines) {
			gtk_adjustment_set_value (self->adj,
					MIN(line_y - self->vis_lines + 1, self->lines - self->vis_lines));
			gtk_adjustment_set_value (self->adj,
					MAX(0, gtk_adjustment_get_value(self->adj)));
		}
		else if (line_y < self->top_line) {
			gtk_adjustment_set_value(self->adj, line_y);
		}      
	
		g_signal_emit_by_name(G_OBJECT(self), "cursor-moved");
		
		if (self->selecting) {
			hex_widget_set_selection(self, self->selection.start, self->cursor_pos);
			bytes_changed(self,
					MIN(self->cursor_pos, old_pos), MAX(self->cursor_pos,
						old_pos));
		}
		else if (self->selection.end != self->selection.start) {
			gint64 start = MIN(self->selection.start, self->selection.end);
			gint64 end = MAX(self->selection.start, self->selection.end);
			self->selection.end = self->selection.start = 0;
			bytes_changed(self, start, end);
		}
		bytes_changed(self, old_pos, old_pos);
		show_cursor (self, TRUE);
	}
}

/**
 * hex_widget_get_cursor:
 *
 * Get the cursor position.
 *
 * Returns: the cursor position, as index within the whole of the buffer
 */
gint64
hex_widget_get_cursor (HexWidget *self)
{
	g_return_val_if_fail (self != NULL, -1);
	g_return_val_if_fail (HEX_IS_WIDGET(self), -1);

	return self->cursor_pos;
}

/**
 * hex_widget_get_byte:
 * @offset: index of the requested byte within the whole of the buffer
 *
 * Get the value of the byte at requested offset position.
 */
guchar
hex_widget_get_byte (HexWidget *self, gint64 offset)
{
	g_return_val_if_fail (HEX_IS_WIDGET(self), 0);

	if ((offset >= 0) && (offset < HEX_BUFFER_PAYLOAD (self->document)))
		return hex_buffer_get_byte (hex_document_get_buffer (self->document), offset);

	return 0;
}

/**
 * hex_widget_set_group_type:
 * @gt: group type
 *
 * Set the group type of the #HexWidget.
 */
void
hex_widget_set_group_type (HexWidget *self, HexWidgetGroupType gt)
{
	g_return_if_fail(self != NULL);
	g_return_if_fail(HEX_IS_WIDGET(self));

	show_cursor (self, FALSE);
	self->group_type = gt;

	hex_widget_layout_set_group_type (HEX_WIDGET_LAYOUT(self->layout_manager), gt);

	recalc_displays(self);
	gtk_widget_queue_resize (GTK_WIDGET(self));
	show_cursor (self, TRUE);
}

/**
 * hex_widget_show_offsets:
 * @show: %TRUE if the offsets column should be shown, %FALSE if it should
 *   be hidden
 *   
 * Set whether the offsets column of the widget should be shown.
 */
void
hex_widget_show_offsets (HexWidget *self, gboolean show)
{
	g_return_if_fail (HEX_IS_WIDGET (self));

	self->show_offsets = show;
	if (show)
		show_offsets_widget(self);
	else
		hide_offsets_widget(self);
}

/**
 * hex_widget_set_insert_mode:
 * @insert: %TRUE if insert mode should be enabled, %FALSE if overwrite mode
 *   should be enabled
 *   
 * Set whether the #HexWidget should be in insert or overwrite mode.
 */
void
hex_widget_set_insert_mode (HexWidget *self, gboolean insert)
{
	size_t payload_size;

	g_return_if_fail (HEX_IS_DOCUMENT (self->document));

	payload_size = HEX_BUFFER_PAYLOAD (self->document);
	self->insert = insert;

	if (!self->insert && self->cursor_pos > 0 && self->cursor_pos >= payload_size)
			self->cursor_pos = payload_size - 1;
}

/**
 * hex_widget_insert_autohighlight:
 * @search: (array length=len) (transfer full): search string to auto-highlight
 * @len: length of the @search string
 *   
 * Insert an auto-highlight of a given search string.
 *
 * Returns: a newly created [struct@Hex.WidgetAutoHighlight] structure
 */
HexWidgetAutoHighlight *
hex_widget_insert_autohighlight (HexWidget *self,
		const char *search,
		int len)
{
	HexWidgetAutoHighlight *new = g_new0 (HexWidgetAutoHighlight, 1);

	new->search_string = g_memdup2 (search, len);
	new->search_len = len;

	new->highlights = NULL;

	new->next = self->auto_highlight;
	new->prev = NULL;
	if (new->next) new->next->prev = new;
	self->auto_highlight = new;

	ahl_set_view_min_max (self, new);

	hex_widget_update_auto_highlight (self, new, FALSE, TRUE);

	return new;
}

/* FIXME - use _free func. */
/**
 * hex_widget_delete_autohighlight:
 * @ahl: the autohighlight to be deleted
 *
 * Delete the requested autohighlight.
 */
void hex_widget_delete_autohighlight (HexWidget *self,
		HexWidgetAutoHighlight *ahl)
{
	g_free (ahl->search_string);

	while (ahl->highlights)
	{
		hex_widget_delete_highlight (self, ahl, ahl->highlights);
	}

	if (ahl->next) ahl->next->prev = ahl->prev;
	if (ahl->prev) ahl->prev->next = ahl->next;

	if (self->auto_highlight == ahl) self->auto_highlight = ahl->next;

	g_free (ahl);
}

/**
 * hex_widget_set_geometry:
 * @cpl: columns per line which should be displayed, or 0 for default
 * @vis_lines: number of lines which should be displayed, or 0 for default
 *
 * Set the geometry of the widget to specified dimensions.
 */
void hex_widget_set_geometry (HexWidget *self, int cpl, int vis_lines)
{
    self->default_cpl = cpl;
    self->default_lines = vis_lines;
}

/**
 * hex_widget_get_adjustment:
 * 
 * Get the [class@Gtk.Adjustment] of the #HexWidget.
 *
 * Returns: (transfer none): #GtkAdjustment of the widget.
 */
GtkAdjustment *
hex_widget_get_adjustment (HexWidget *self)
{
	g_return_val_if_fail (GTK_IS_ADJUSTMENT(self->adj), NULL);

	return self->adj;
}

/**
 * hex_widget_get_document:
 *
 * Get the [class@Hex.Document] owned by the #HexWidget.
 *
 * Returns: (transfer none): the #HexDocument owned by the #HexWidget, or
 *   %NULL.
 */
HexDocument *
hex_widget_get_document (HexWidget *self)
{
	g_return_val_if_fail (HEX_IS_DOCUMENT(self->document), NULL);

	return self->document;
}

/**
 * hex_widget_get_insert_mode:
 *
 * Get whether the widget is insert mode.
 *
 * Returns: %TRUE if the #HexWidget is in insert mode; %FALSE if it is in
 *   overwrite mode.
 */
gboolean
hex_widget_get_insert_mode (HexWidget *self)
{
	g_return_val_if_fail (HEX_IS_WIDGET (self), FALSE);

	return self->insert;
}

/**
 * hex_widget_get_group_type:
 *
 * Get the group type of the data of the #HexWidget.
 *
 * Returns: the group type of the data of the #HexWidget, by
 *   [enum@Hex.WidgetGroupType]
 */
HexWidgetGroupType
hex_widget_get_group_type (HexWidget *self)
{
	g_assert (HEX_IS_WIDGET (self));

	return self->group_type;
}
