mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-14 17:47:13 +00:00
When nick indentation is off, lines after the first wrap back to the left edge, under the timestamp. Add a text_wrap_align preference (start of line, prefix, message) so they can line up under the nick or the message text instead. It is applied at wrap time, so existing scrollback is re-wrapped when the setting changes, and the menu is greyed out while nick indentation is on.
6056 lines
147 KiB
C
6056 lines
147 KiB
C
/* X-Chat
|
||
* Copyright (C) 1998 Peter Zelezny.
|
||
*
|
||
* This program is free software; you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation; either version 2 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with this program; if not, write to the Free Software
|
||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||
* =========================================================================
|
||
*
|
||
* xtext, the text widget used by X-Chat.
|
||
* By Peter Zelezny <[email protected]>.
|
||
*
|
||
*/
|
||
|
||
#define MARGIN 2 /* dont touch. */
|
||
#define REFRESH_TIMEOUT 20
|
||
#define WORDWRAP_LIMIT 24
|
||
|
||
#include <string.h>
|
||
#include <ctype.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
#include <errno.h>
|
||
|
||
#include "config.h"
|
||
#include "../common/zoitechat.h"
|
||
#include "../common/fe.h"
|
||
#include "../common/util.h"
|
||
#include "../common/hexcolor.h"
|
||
#include "../common/zoitechatc.h"
|
||
#include "../common/url.h"
|
||
|
||
#include "fe-gtk.h"
|
||
#include "xtext.h"
|
||
#include "fkeys.h"
|
||
#include "emoji-font.h"
|
||
#include "theme/theme-access.h"
|
||
|
||
#define charlen(str) g_utf8_skip[*(guchar *)(str)]
|
||
|
||
#ifdef WIN32
|
||
#include <windows.h>
|
||
#include <io.h>
|
||
#include <gdk/gdk.h>
|
||
#include <gdk/gdkcairo.h>
|
||
#include <gdk/gdkwin32.h>
|
||
#else
|
||
#include <unistd.h>
|
||
#include <gdk/gdkcairo.h>
|
||
#ifdef GDK_WINDOWING_X11
|
||
#include <gdk/gdkx.h>
|
||
#endif
|
||
#endif
|
||
#include <pango/pangocairo.h>
|
||
|
||
/* is delimiter */
|
||
#define is_del(c) \
|
||
(c == ' ' || c == '\n' || c == '>' || c == '<' || c == 0)
|
||
|
||
/* force scrolling off */
|
||
#define dontscroll(buf) ((buf)->last_pixel_pos = G_MAXINT)
|
||
|
||
struct textentry
|
||
{
|
||
struct textentry *next;
|
||
struct textentry *prev;
|
||
unsigned char *str;
|
||
time_t stamp;
|
||
int str_width;
|
||
gint16 str_len;
|
||
gint16 mark_start;
|
||
gint16 mark_end;
|
||
int indent;
|
||
gint16 left_len;
|
||
gint16 stamp_len; /* bytes of str taken by the timestamp (no-indent mode) */
|
||
GSList *slp;
|
||
GArray *sublines; /* byte offsets at the end of each screen row */
|
||
int subline_count;
|
||
guchar tag;
|
||
GList *marks; /* List of found strings */
|
||
GdkPixbuf *image; /* inline image shown below the text, or NULL */
|
||
GdkPixbuf *image_full; /* larger version for the image viewer */
|
||
char *image_url; /* the URL the image was loaded from */
|
||
};
|
||
|
||
/* inline image display */
|
||
#define XTEXT_IMAGE_PADDING 4
|
||
#define XTEXT_IMAGE_MAX_WIDTH 480
|
||
#define XTEXT_IMAGE_MAX_HEIGHT 360
|
||
#define XTEXT_IMAGE_FULL_MAX_WIDTH 1600
|
||
#define XTEXT_IMAGE_FULL_MAX_HEIGHT 1200
|
||
|
||
/* an inline image load that has been started but not yet completed;
|
||
ent/buf/xtext are invalidated (entry removed) when any of them dies */
|
||
typedef struct
|
||
{
|
||
guint id;
|
||
GtkXText *xtext;
|
||
xtext_buffer *buf;
|
||
textentry *ent;
|
||
char *url;
|
||
} xtext_image_pending;
|
||
|
||
static GSList *image_pending_list = NULL;
|
||
static guint image_pending_next_id = 1;
|
||
|
||
static void
|
||
gtk_xtext_image_pending_forget (gboolean (*match) (xtext_image_pending *, gconstpointer), gconstpointer data)
|
||
{
|
||
GSList *list = image_pending_list;
|
||
|
||
while (list)
|
||
{
|
||
GSList *next = list->next;
|
||
xtext_image_pending *pending = list->data;
|
||
|
||
if (match (pending, data))
|
||
{
|
||
image_pending_list = g_slist_delete_link (image_pending_list, list);
|
||
g_free (pending->url);
|
||
g_free (pending);
|
||
}
|
||
list = next;
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_image_pending_match_ent (xtext_image_pending *pending, gconstpointer ent)
|
||
{
|
||
return pending->ent == ent;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_image_pending_match_buf (xtext_image_pending *pending, gconstpointer buf)
|
||
{
|
||
return pending->buf == buf;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_image_pending_match_xtext (xtext_image_pending *pending, gconstpointer xtext)
|
||
{
|
||
return pending->xtext == xtext;
|
||
}
|
||
|
||
/* number of extra screen lines an entry needs for its inline image */
|
||
|
||
static int
|
||
gtk_xtext_image_rows (GtkXText *xtext, textentry *ent)
|
||
{
|
||
if (ent->image == NULL || xtext->fontsize <= 0)
|
||
return 0;
|
||
|
||
return (gdk_pixbuf_get_height (ent->image) + XTEXT_IMAGE_PADDING * 2 +
|
||
xtext->fontsize - 1) / xtext->fontsize;
|
||
}
|
||
|
||
/* free an entry's inline image and drop any pending load aimed at it;
|
||
must be called whenever a textentry is freed */
|
||
|
||
static void
|
||
gtk_xtext_ent_free_image (textentry *ent)
|
||
{
|
||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_ent, ent);
|
||
|
||
if (ent->image)
|
||
{
|
||
g_object_unref (ent->image);
|
||
ent->image = NULL;
|
||
}
|
||
if (ent->image_full)
|
||
{
|
||
g_object_unref (ent->image_full);
|
||
ent->image_full = NULL;
|
||
}
|
||
g_free (ent->image_url);
|
||
ent->image_url = NULL;
|
||
}
|
||
|
||
enum
|
||
{
|
||
WORD_CLICK,
|
||
SET_SCROLL_ADJUSTMENTS,
|
||
LAST_SIGNAL
|
||
};
|
||
|
||
/* values for selection info */
|
||
enum
|
||
{
|
||
TARGET_UTF8_STRING,
|
||
TARGET_STRING,
|
||
TARGET_TEXT,
|
||
TARGET_COMPOUND_TEXT
|
||
};
|
||
|
||
enum
|
||
{
|
||
PROP_0,
|
||
PROP_HADJUSTMENT,
|
||
PROP_VADJUSTMENT,
|
||
PROP_HSCROLL_POLICY,
|
||
PROP_VSCROLL_POLICY
|
||
};
|
||
|
||
|
||
/* Selection targets for PRIMARY selection / copy-paste.
|
||
*
|
||
* On Wayland, GtkWidget has no GdkWindow until it is realized. Registering
|
||
* selection targets during instance init is too early and can crash under the
|
||
* Wayland backend (window/display is NULL).
|
||
*/
|
||
static const GtkTargetEntry gtk_xtext_selection_targets[] = {
|
||
{ "UTF8_STRING", 0, TARGET_UTF8_STRING },
|
||
{ "STRING", 0, TARGET_STRING },
|
||
{ "TEXT", 0, TARGET_TEXT },
|
||
{ "COMPOUND_TEXT", 0, TARGET_COMPOUND_TEXT }
|
||
};
|
||
|
||
static void
|
||
gtk_xtext_install_selection_targets_on_realize (GtkWidget *widget, gpointer user_data)
|
||
{
|
||
(void)user_data;
|
||
|
||
if (gtk_widget_get_window (widget) == NULL)
|
||
return;
|
||
|
||
gtk_selection_add_targets (widget,
|
||
GDK_SELECTION_PRIMARY,
|
||
(GtkTargetEntry *)gtk_xtext_selection_targets,
|
||
(gint)G_N_ELEMENTS (gtk_xtext_selection_targets));
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_install_selection_targets (GtkWidget *widget)
|
||
{
|
||
if (gtk_widget_get_realized (widget) && gtk_widget_get_window (widget) != NULL)
|
||
{
|
||
gtk_selection_add_targets (widget,
|
||
GDK_SELECTION_PRIMARY,
|
||
(GtkTargetEntry *)gtk_xtext_selection_targets,
|
||
(gint)G_N_ELEMENTS (gtk_xtext_selection_targets));
|
||
return;
|
||
}
|
||
|
||
g_signal_connect (widget,
|
||
"realize",
|
||
G_CALLBACK (gtk_xtext_install_selection_targets_on_realize),
|
||
NULL);
|
||
}
|
||
|
||
static guint xtext_signals[LAST_SIGNAL];
|
||
|
||
/* Font measurements belong to the widget: raw logs, event previews and chat
|
||
* windows can use different fonts at the same time. Keep public structs intact. */
|
||
typedef struct
|
||
{
|
||
PangoAttrList *attr_lists[4];
|
||
int fontwidths[4][128];
|
||
} GtkXTextPrivate;
|
||
|
||
static GQuark xtext_private_quark;
|
||
|
||
static GtkXTextPrivate *
|
||
gtk_xtext_get_private (GtkXText *xtext)
|
||
{
|
||
return g_object_get_qdata (G_OBJECT (xtext), xtext_private_quark);
|
||
}
|
||
|
||
G_DEFINE_TYPE_WITH_CODE (GtkXText, gtk_xtext, GTK_TYPE_WIDGET,
|
||
G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
|
||
|
||
int xtext_get_stamp_str (time_t, char **);
|
||
static void gtk_xtext_render_page (GtkXText * xtext);
|
||
static void gtk_xtext_calc_lines (xtext_buffer *buf, int);
|
||
static gboolean gtk_xtext_is_selecting (GtkXText *xtext);
|
||
static char *gtk_xtext_selection_get_text (GtkXText *xtext, int *len_ret);
|
||
static void gtk_xtext_clipboard_get (GtkClipboard *, GtkSelectionData *, guint, gpointer);
|
||
static void gtk_xtext_clipboard_clear (GtkClipboard *, gpointer);
|
||
static textentry *gtk_xtext_nth (GtkXText *xtext, int line, int *subline);
|
||
static void gtk_xtext_adjustment_changed (GtkAdjustment * adj,
|
||
GtkXText * xtext);
|
||
static void gtk_xtext_scroll_adjustments (GtkXText *xtext, GtkAdjustment *hadj,
|
||
GtkAdjustment *vadj);
|
||
static void gtk_xtext_set_hadjustment (GtkXText *xtext, GtkAdjustment *adj);
|
||
static void gtk_xtext_set_property (GObject *object, guint prop_id,
|
||
const GValue *value, GParamSpec *pspec);
|
||
static void gtk_xtext_get_property (GObject *object, guint prop_id,
|
||
GValue *value, GParamSpec *pspec);
|
||
static void gtk_xtext_render_ents (GtkXText * xtext, textentry *, textentry *);
|
||
static void gtk_xtext_recalc_widths (xtext_buffer *buf, int);
|
||
static void gtk_xtext_fix_indent (xtext_buffer *buf);
|
||
static int gtk_xtext_text_width (GtkXText *xtext, unsigned char *text, int len);
|
||
static int gtk_xtext_find_subline (GtkXText *xtext, textentry *ent, int line);
|
||
/* For use by gtk_xtext_strip_color() and its callers -- */
|
||
struct offlen_s {
|
||
guint16 off;
|
||
guint16 len;
|
||
guint16 emph;
|
||
int width;
|
||
};
|
||
typedef struct offlen_s offlen_t;
|
||
static unsigned char *
|
||
gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf,
|
||
int *newlen, GSList **slp, int strip_hidden);
|
||
static gboolean gtk_xtext_check_ent_visibility (GtkXText * xtext, textentry *find_ent, int add);
|
||
static gboolean gtk_xtext_render_page_timeout (gpointer data);
|
||
static int gtk_xtext_search_offset (xtext_buffer *buf, textentry *ent, unsigned int off);
|
||
static GList * gtk_xtext_search_textentry (xtext_buffer *, textentry *);
|
||
static void gtk_xtext_search_textentry_add (xtext_buffer *, textentry *, GList *, gboolean);
|
||
static void gtk_xtext_search_textentry_del (xtext_buffer *, textentry *);
|
||
static void gtk_xtext_search_textentry_fini (gpointer, gpointer);
|
||
static void gtk_xtext_search_fini (xtext_buffer *);
|
||
static gboolean gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk_xtext_search_flags flags, GError **perr);
|
||
static char * gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent, int *ret_off, int *ret_len, GSList **slp);
|
||
static gboolean gtk_xtext_word_select_char (const unsigned char *ch);
|
||
static gboolean gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len);
|
||
static textentry * gtk_xtext_image_hit (GtkXText *xtext, int x, int y);
|
||
|
||
/* Source IDs are reset before removal so cleanup is safe when re-entered. */
|
||
static void
|
||
gtk_xtext_cancel_source (gint *tag)
|
||
{
|
||
guint id = *tag;
|
||
*tag = 0;
|
||
if (id)
|
||
g_source_remove (id);
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_subline_end (textentry *ent, guint line)
|
||
{
|
||
if (!ent->sublines || line >= ent->sublines->len)
|
||
return ent->str_len;
|
||
return g_array_index (ent->sublines, int, line);
|
||
}
|
||
|
||
/* Every entry-removal path owns the same resources. Search-list bookkeeping
|
||
* is done by the caller before this releases the entry itself. */
|
||
static void
|
||
gtk_xtext_entry_free (GtkXText *xtext, textentry *ent)
|
||
{
|
||
if (xtext->hilight_ent == ent)
|
||
{
|
||
xtext->hilight_ent = NULL;
|
||
xtext->hilight_start = xtext->hilight_end = -1;
|
||
}
|
||
if (xtext->clicked_ent == ent)
|
||
xtext->clicked_ent = NULL;
|
||
if (xtext->clicked_image_ent == ent)
|
||
xtext->clicked_image_ent = NULL;
|
||
g_slist_free_full (ent->slp, g_free);
|
||
g_clear_pointer (&ent->sublines, g_array_unref);
|
||
g_list_free (ent->marks);
|
||
gtk_xtext_ent_free_image (ent);
|
||
g_free (ent);
|
||
}
|
||
|
||
static inline void
|
||
xtext_set_source_color (cairo_t *cr, const XTextColor *color, gdouble alpha)
|
||
{
|
||
cairo_set_source_rgba (cr, color->red, color->green,
|
||
color->blue, color->alpha * alpha);
|
||
}
|
||
|
||
static void
|
||
xtext_adjustment_apply (GtkAdjustment *adj, gdouble lower, gdouble upper, gdouble value, gdouble page_size)
|
||
{
|
||
gdouble step_increment = page_size * prefs.hex_gui_mouse_scroll_speed / 100.0;
|
||
|
||
if (step_increment < 1)
|
||
step_increment = 1;
|
||
|
||
gtk_adjustment_configure (adj, value, lower, upper, step_increment, page_size, page_size);
|
||
}
|
||
|
||
/* All painting uses GTK's draw context. Event handlers only invalidate the
|
||
* widget, so X11, Wayland and HiDPI displays share the same render path. */
|
||
static cairo_t *
|
||
xtext_create_context (GtkXText *xtext)
|
||
{
|
||
g_assert (xtext->draw_cr != NULL);
|
||
return cairo_reference (xtext->draw_cr);
|
||
}
|
||
|
||
static inline void
|
||
xtext_draw_rectangle (GtkXText *xtext, cairo_t *cr, const XTextColor *color, int x, int y, int width, int height)
|
||
{
|
||
cairo_save (cr);
|
||
|
||
xtext_set_source_color (cr, color, 1.0);
|
||
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
|
||
cairo_rectangle (cr, (double)x, (double)y, (double)width, (double)height);
|
||
cairo_fill (cr);
|
||
|
||
cairo_restore (cr);
|
||
}
|
||
|
||
static inline void
|
||
xtext_draw_line (GtkXText *xtext, cairo_t *cr, const XTextColor *color, int x1, int y1, int x2, int y2)
|
||
{
|
||
cairo_save (cr);
|
||
|
||
/* Disable antialiasing for crispy 1-pixel lines */
|
||
cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE);
|
||
xtext_set_source_color (cr, color, 1.0);
|
||
cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
|
||
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
|
||
cairo_set_line_width (cr, 1.0);
|
||
cairo_move_to (cr, x1, y1);
|
||
cairo_line_to (cr, x2, y2);
|
||
cairo_stroke (cr);
|
||
|
||
cairo_restore (cr);
|
||
}
|
||
|
||
static void
|
||
xtext_draw_bg_offset (GtkXText *xtext, int x, int y, int width, int height, int tile_x, int tile_y)
|
||
{
|
||
cairo_t *cr = xtext_create_context (xtext);
|
||
|
||
if (xtext->background_surface)
|
||
{
|
||
GtkAllocation allocation;
|
||
int clip_x;
|
||
int clip_y;
|
||
int clip_w;
|
||
int clip_h;
|
||
|
||
if (cairo_surface_status (xtext->background_surface) != CAIRO_STATUS_SUCCESS)
|
||
{
|
||
xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height);
|
||
cairo_destroy (cr);
|
||
return;
|
||
}
|
||
|
||
gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation);
|
||
clip_x = 0;
|
||
clip_y = 0;
|
||
clip_w = allocation.width;
|
||
clip_h = allocation.height;
|
||
|
||
if (clip_w < 1 || clip_h < 1 || clip_w > 8192 || clip_h > 8192)
|
||
{
|
||
xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height);
|
||
cairo_destroy (cr);
|
||
return;
|
||
}
|
||
|
||
if (xtext->background_clip_surface == NULL ||
|
||
xtext->background_clip_cycle != xtext->render_cycle ||
|
||
xtext->background_clip_x != clip_x ||
|
||
xtext->background_clip_y != clip_y ||
|
||
xtext->background_clip_width != clip_w ||
|
||
xtext->background_clip_height != clip_h)
|
||
{
|
||
cairo_t *bg_cr;
|
||
|
||
if (xtext->background_clip_surface)
|
||
{
|
||
cairo_surface_destroy (xtext->background_clip_surface);
|
||
xtext->background_clip_surface = NULL;
|
||
}
|
||
|
||
xtext->background_clip_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, clip_w, clip_h);
|
||
if (cairo_surface_status (xtext->background_clip_surface) != CAIRO_STATUS_SUCCESS)
|
||
{
|
||
cairo_surface_destroy (xtext->background_clip_surface);
|
||
xtext->background_clip_surface = NULL;
|
||
xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height);
|
||
cairo_destroy (cr);
|
||
return;
|
||
}
|
||
bg_cr = cairo_create (xtext->background_clip_surface);
|
||
if (cairo_surface_get_type (xtext->background_surface) == CAIRO_SURFACE_TYPE_IMAGE)
|
||
{
|
||
int src_w = cairo_image_surface_get_width (xtext->background_surface);
|
||
int src_h = cairo_image_surface_get_height (xtext->background_surface);
|
||
if (src_w > 0 && src_h > 0)
|
||
{
|
||
double scale_x = (double)clip_w / (double)src_w;
|
||
double scale_y = (double)clip_h / (double)src_h;
|
||
double scale = scale_x < scale_y ? scale_x : scale_y;
|
||
double draw_w = src_w * scale;
|
||
double draw_h = src_h * scale;
|
||
double draw_x = ((double)clip_w - draw_w) / 2.0;
|
||
double draw_y = ((double)clip_h - draw_h) / 2.0;
|
||
cairo_set_source_rgb (bg_cr, 0.0, 0.0, 0.0);
|
||
cairo_paint (bg_cr);
|
||
cairo_save (bg_cr);
|
||
cairo_translate (bg_cr, draw_x, draw_y);
|
||
cairo_scale (bg_cr, scale, scale);
|
||
cairo_set_source_surface (bg_cr, xtext->background_surface, 0.0, 0.0);
|
||
cairo_pattern_set_extend (cairo_get_source (bg_cr), CAIRO_EXTEND_NONE);
|
||
cairo_rectangle (bg_cr, 0.0, 0.0, (double)src_w, (double)src_h);
|
||
cairo_fill (bg_cr);
|
||
cairo_restore (bg_cr);
|
||
}
|
||
else
|
||
{
|
||
cairo_set_source_surface (bg_cr, xtext->background_surface, tile_x - clip_x, tile_y - clip_y);
|
||
cairo_pattern_set_extend (cairo_get_source (bg_cr), CAIRO_EXTEND_REPEAT);
|
||
cairo_rectangle (bg_cr, 0.0, 0.0, (double)clip_w, (double)clip_h);
|
||
cairo_fill (bg_cr);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
cairo_set_source_surface (bg_cr, xtext->background_surface, tile_x - clip_x, tile_y - clip_y);
|
||
cairo_pattern_set_extend (cairo_get_source (bg_cr), CAIRO_EXTEND_REPEAT);
|
||
cairo_rectangle (bg_cr, 0.0, 0.0, (double)clip_w, (double)clip_h);
|
||
cairo_fill (bg_cr);
|
||
}
|
||
cairo_destroy (bg_cr);
|
||
|
||
xtext->background_clip_x = clip_x;
|
||
xtext->background_clip_y = clip_y;
|
||
xtext->background_clip_width = clip_w;
|
||
xtext->background_clip_height = clip_h;
|
||
xtext->background_clip_cycle = xtext->render_cycle;
|
||
}
|
||
|
||
cairo_set_source_surface (cr, xtext->background_clip_surface,
|
||
(double)xtext->background_clip_x, (double)xtext->background_clip_y);
|
||
cairo_rectangle (cr, (double)x, (double)y, (double)width, (double)height);
|
||
cairo_fill (cr);
|
||
}
|
||
else
|
||
{
|
||
xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y, width, height);
|
||
}
|
||
|
||
cairo_destroy (cr);
|
||
}
|
||
|
||
static inline void
|
||
xtext_draw_bg (GtkXText *xtext, int x, int y, int width, int height)
|
||
{
|
||
xtext_draw_bg_offset (xtext, x, y, width, height, xtext->ts_x, xtext->ts_y);
|
||
}
|
||
|
||
/* ======================================= */
|
||
/* ============ PANGO BACKEND ============ */
|
||
/* ======================================= */
|
||
|
||
#define EMPH_ITAL 1
|
||
#define EMPH_BOLD 2
|
||
#define EMPH_HIDDEN 4
|
||
|
||
static PangoAttribute *
|
||
xtext_pango_attr (PangoAttribute *attr)
|
||
{
|
||
attr->start_index = PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING;
|
||
attr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
||
return attr;
|
||
}
|
||
|
||
static void
|
||
xtext_pango_init (GtkXText *xtext)
|
||
{
|
||
GtkXTextPrivate *priv = gtk_xtext_get_private (xtext);
|
||
guint i;
|
||
int j;
|
||
char buf[2] = "\000";
|
||
|
||
for (i = 0; i < G_N_ELEMENTS (priv->attr_lists); i++)
|
||
{
|
||
g_clear_pointer (&priv->attr_lists[i], pango_attr_list_unref);
|
||
priv->attr_lists[i] = pango_attr_list_new ();
|
||
switch (i)
|
||
{
|
||
case 0: /* Roman */
|
||
break;
|
||
case EMPH_ITAL: /* Italic */
|
||
pango_attr_list_insert (priv->attr_lists[i],
|
||
xtext_pango_attr (pango_attr_style_new (PANGO_STYLE_ITALIC)));
|
||
break;
|
||
case EMPH_BOLD: /* Bold */
|
||
pango_attr_list_insert (priv->attr_lists[i],
|
||
xtext_pango_attr (pango_attr_weight_new (PANGO_WEIGHT_BOLD)));
|
||
break;
|
||
case EMPH_ITAL | EMPH_BOLD: /* Italic Bold */
|
||
pango_attr_list_insert (priv->attr_lists[i],
|
||
xtext_pango_attr (pango_attr_style_new (PANGO_STYLE_ITALIC)));
|
||
pango_attr_list_insert (priv->attr_lists[i],
|
||
xtext_pango_attr (pango_attr_weight_new (PANGO_WEIGHT_BOLD)));
|
||
break;
|
||
}
|
||
|
||
/* Now initialize priv->fontwidths[i] */
|
||
pango_layout_set_attributes (xtext->layout, priv->attr_lists[i]);
|
||
for (j = 0; j < 128; j++)
|
||
{
|
||
buf[0] = j;
|
||
pango_layout_set_text (xtext->layout, buf, 1);
|
||
pango_layout_get_pixel_size (xtext->layout, &priv->fontwidths[i][j], NULL);
|
||
}
|
||
}
|
||
xtext->space_width = priv->fontwidths[0][' '];
|
||
}
|
||
|
||
static void
|
||
backend_font_close (GtkXText *xtext)
|
||
{
|
||
g_clear_pointer (&xtext->font->font, pango_font_description_free);
|
||
xtext->font = NULL;
|
||
}
|
||
|
||
static void
|
||
backend_init (GtkXText *xtext)
|
||
{
|
||
if (xtext->layout == NULL)
|
||
{
|
||
emoji_font_apply (GTK_WIDGET (xtext));
|
||
xtext->layout = gtk_widget_create_pango_layout (GTK_WIDGET (xtext), 0);
|
||
if (xtext->font)
|
||
pango_layout_set_font_description (xtext->layout, xtext->font->font);
|
||
}
|
||
}
|
||
|
||
static void
|
||
backend_deinit (GtkXText *xtext)
|
||
{
|
||
if (xtext->layout)
|
||
{
|
||
g_object_unref (xtext->layout);
|
||
xtext->layout = NULL;
|
||
}
|
||
}
|
||
|
||
static PangoFontDescription *
|
||
backend_font_open_real (char *name)
|
||
{
|
||
PangoFontDescription *font;
|
||
|
||
font = pango_font_description_from_string (name);
|
||
if (font && pango_font_description_get_size (font) == 0)
|
||
{
|
||
pango_font_description_free (font);
|
||
font = pango_font_description_from_string ("sans 11");
|
||
}
|
||
if (!font)
|
||
font = pango_font_description_from_string ("sans 11");
|
||
|
||
return font;
|
||
}
|
||
|
||
static void
|
||
backend_font_open (GtkXText *xtext, char *name)
|
||
{
|
||
PangoLanguage *lang;
|
||
PangoContext *context;
|
||
PangoFontMetrics *metrics;
|
||
|
||
xtext->font = &xtext->pango_font;
|
||
xtext->font->font = backend_font_open_real (name);
|
||
if (!xtext->font->font)
|
||
{
|
||
xtext->font = NULL;
|
||
return;
|
||
}
|
||
|
||
backend_init (xtext);
|
||
pango_layout_set_font_description (xtext->layout, xtext->font->font);
|
||
xtext_pango_init (xtext);
|
||
|
||
/* vte does it this way */
|
||
context = gtk_widget_get_pango_context (GTK_WIDGET (xtext));
|
||
lang = pango_context_get_language (context);
|
||
metrics = pango_context_get_metrics (context, xtext->font->font, lang);
|
||
xtext->font->ascent = pango_font_metrics_get_ascent (metrics) / PANGO_SCALE;
|
||
xtext->font->descent = pango_font_metrics_get_descent (metrics) / PANGO_SCALE;
|
||
|
||
/*
|
||
* In later versions of pango, a font's height should be calculated like
|
||
* this to account for line gap; a typical symptom of not doing so is
|
||
* cutting off the underscore on some fonts.
|
||
*/
|
||
#if PANGO_VERSION_CHECK(1, 44, 0)
|
||
xtext->fontsize = pango_font_metrics_get_height (metrics) / PANGO_SCALE + 1;
|
||
|
||
if (xtext->fontsize <= 1)
|
||
xtext->fontsize = MAX (1, xtext->font->ascent + xtext->font->descent);
|
||
#else
|
||
xtext->fontsize = MAX (1, xtext->font->ascent + xtext->font->descent);
|
||
#endif
|
||
|
||
pango_font_metrics_unref (metrics);
|
||
}
|
||
|
||
static int
|
||
backend_get_text_width_emph (GtkXText *xtext, guchar *str, int len, int emphasis)
|
||
{
|
||
GtkXTextPrivate *priv = gtk_xtext_get_private (xtext);
|
||
int width;
|
||
int deltaw;
|
||
int mbl;
|
||
|
||
if (*str == 0)
|
||
return 0;
|
||
|
||
if ((emphasis & EMPH_HIDDEN) && !xtext->ignore_hidden)
|
||
return 0;
|
||
emphasis &= (EMPH_ITAL | EMPH_BOLD);
|
||
|
||
width = 0;
|
||
pango_layout_set_attributes (xtext->layout, priv->attr_lists[emphasis]);
|
||
while (len > 0)
|
||
{
|
||
mbl = charlen(str);
|
||
if (*str < 128)
|
||
deltaw = priv->fontwidths[emphasis][*str];
|
||
else
|
||
{
|
||
pango_layout_set_text (xtext->layout, str, mbl);
|
||
pango_layout_get_pixel_size (xtext->layout, &deltaw, NULL);
|
||
}
|
||
width += deltaw;
|
||
str += mbl;
|
||
len -= mbl;
|
||
}
|
||
|
||
return width;
|
||
}
|
||
|
||
static int
|
||
backend_get_text_width_slp (GtkXText *xtext, guchar *str, GSList *slp)
|
||
{
|
||
int width = 0;
|
||
|
||
while (slp)
|
||
{
|
||
offlen_t *meta;
|
||
|
||
meta = slp->data;
|
||
width += backend_get_text_width_emph (xtext, str, meta->len, meta->emph);
|
||
str += meta->len;
|
||
slp = g_slist_next (slp);
|
||
}
|
||
|
||
return width;
|
||
}
|
||
|
||
static void
|
||
backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y,
|
||
char *str, int len, int str_width, int emphasis)
|
||
{
|
||
GtkXTextPrivate *priv = gtk_xtext_get_private (xtext);
|
||
cairo_t *cr;
|
||
PangoLayoutLine *line;
|
||
|
||
cr = xtext_create_context (xtext);
|
||
|
||
pango_layout_set_attributes (xtext->layout, priv->attr_lists[emphasis]);
|
||
pango_layout_set_text (xtext->layout, str, len);
|
||
|
||
if (dofill)
|
||
{
|
||
xtext_draw_rectangle (xtext, cr, &xtext->bgc, x, y -
|
||
xtext->font->ascent, str_width, xtext->fontsize);
|
||
}
|
||
|
||
xtext_set_source_color (cr, &xtext->fgc, 1.0);
|
||
line = pango_layout_get_line_readonly (xtext->layout, 0);
|
||
|
||
cairo_save (cr);
|
||
cairo_move_to (cr, x, y);
|
||
pango_cairo_show_layout_line (cr, line);
|
||
cairo_restore (cr);
|
||
|
||
cairo_destroy (cr);
|
||
}
|
||
|
||
static XTextColor
|
||
xtext_color_for_index (GtkXText *xtext, int index)
|
||
{
|
||
if (index == XTEXT_HEX_FG)
|
||
return xtext->hex_fg;
|
||
if (index == XTEXT_HEX_BG)
|
||
return xtext->hex_bg;
|
||
return xtext->palette[index];
|
||
}
|
||
|
||
static void
|
||
xtext_set_fg (GtkXText *xtext, int index)
|
||
{
|
||
xtext->fgc = xtext_color_for_index (xtext, index);
|
||
}
|
||
|
||
static void
|
||
xtext_set_bg (GtkXText *xtext, int index)
|
||
{
|
||
xtext->bgc = xtext_color_for_index (xtext, index);
|
||
}
|
||
|
||
static void
|
||
xtext_rgb_to_color (guint32 rgb, XTextColor *color)
|
||
{
|
||
color->red = ((rgb >> 16) & 0xFF) / 255.0;
|
||
color->green = ((rgb >> 8) & 0xFF) / 255.0;
|
||
color->blue = (rgb & 0xFF) / 255.0;
|
||
color->alpha = 1.0;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_sync_palette_from_theme (GtkXText *xtext)
|
||
{
|
||
XTextColor palette[XTEXT_COLS];
|
||
|
||
theme_get_xtext_colors_for_widget (GTK_WIDGET (xtext), palette, G_N_ELEMENTS (palette));
|
||
gtk_xtext_set_palette (xtext, palette);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_style_updated (GtkWidget *widget, gpointer user_data)
|
||
{
|
||
(void) user_data;
|
||
gtk_xtext_sync_palette_from_theme (GTK_XTEXT (widget));
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_init (GtkXText * xtext)
|
||
{
|
||
/* Object data preserves both the public struct layout and GLib 2.36 API. */
|
||
g_object_set_qdata_full (G_OBJECT (xtext), xtext_private_quark,
|
||
g_new0 (GtkXTextPrivate, 1), g_free);
|
||
xtext->col_back = XTEXT_BG;
|
||
xtext->col_fore = XTEXT_FG;
|
||
xtext->clip_x2 = xtext->clip_y2 = G_MAXINT;
|
||
xtext->hscroll_policy = GTK_SCROLL_MINIMUM;
|
||
xtext->vscroll_policy = GTK_SCROLL_MINIMUM;
|
||
gtk_widget_set_has_window (GTK_WIDGET (xtext), TRUE);
|
||
xtext->buffer = gtk_xtext_buffer_new (xtext);
|
||
xtext->orig_buffer = xtext->buffer;
|
||
backend_font_open (xtext, "sans 11");
|
||
gtk_xtext_scroll_adjustments (xtext, NULL, NULL);
|
||
|
||
gtk_xtext_install_selection_targets (GTK_WIDGET (xtext));
|
||
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (xtext)), "view");
|
||
g_signal_connect (G_OBJECT (xtext), "style-updated", G_CALLBACK (gtk_xtext_style_updated), NULL);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_adjustment_set (xtext_buffer *buf, int fire_signal)
|
||
{
|
||
GtkAdjustment *adj = buf->xtext->adj;
|
||
|
||
if (buf->xtext->buffer == buf && buf->xtext->fontsize > 0)
|
||
{
|
||
gdouble lower = 0;
|
||
gdouble upper = buf->num_lines;
|
||
gdouble value = gtk_adjustment_get_value (adj);
|
||
gdouble page_size;
|
||
|
||
if (upper == 0)
|
||
upper = 1;
|
||
|
||
GtkAllocation allocation;
|
||
|
||
gtk_widget_get_allocation (GTK_WIDGET (buf->xtext), &allocation);
|
||
page_size = allocation.height / buf->xtext->fontsize;
|
||
|
||
if (value > upper - page_size)
|
||
{
|
||
buf->scrollbar_down = TRUE;
|
||
value = upper - page_size;
|
||
}
|
||
|
||
if (value < 0)
|
||
value = 0;
|
||
|
||
xtext_adjustment_apply (adj, lower, upper, value, page_size);
|
||
gtk_adjustment_set_page_increment (adj, page_size);
|
||
|
||
if (fire_signal)
|
||
{
|
||
g_signal_emit_by_name (adj, "value-changed");
|
||
}
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_adjustment_timeout (gpointer data)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (data);
|
||
xtext->io_tag = 0;
|
||
gtk_xtext_render_page (xtext);
|
||
return 0;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_adjustment_changed (GtkAdjustment * adj, GtkXText * xtext)
|
||
{
|
||
gdouble value = gtk_adjustment_get_value (xtext->adj);
|
||
gdouble upper = gtk_adjustment_get_upper (xtext->adj);
|
||
gdouble page_size = gtk_adjustment_get_page_size (xtext->adj);
|
||
|
||
if (!gtk_widget_get_realized (GTK_WIDGET (xtext)))
|
||
return;
|
||
|
||
if (xtext->buffer->old_value != value)
|
||
{
|
||
if (value >= upper - page_size)
|
||
xtext->buffer->scrollbar_down = TRUE;
|
||
else
|
||
xtext->buffer->scrollbar_down = FALSE;
|
||
|
||
if (value + 1 == xtext->buffer->old_value ||
|
||
value - 1 == xtext->buffer->old_value) /* clicked an arrow? */
|
||
{
|
||
gtk_xtext_cancel_source (&xtext->io_tag);
|
||
gtk_xtext_render_page (xtext);
|
||
} else
|
||
{
|
||
if (!xtext->io_tag)
|
||
xtext->io_tag = g_timeout_add (REFRESH_TIMEOUT,
|
||
gtk_xtext_adjustment_timeout,
|
||
xtext);
|
||
}
|
||
}
|
||
xtext->buffer->old_value = gtk_adjustment_get_value (adj);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (object);
|
||
GtkAdjustment *adj;
|
||
|
||
switch (prop_id)
|
||
{
|
||
case PROP_HADJUSTMENT:
|
||
adj = g_value_get_object (value);
|
||
gtk_xtext_set_hadjustment (xtext, adj);
|
||
g_object_notify (object, "hadjustment");
|
||
break;
|
||
case PROP_VADJUSTMENT:
|
||
gtk_xtext_scroll_adjustments (xtext, xtext->hadj, g_value_get_object (value));
|
||
g_object_notify (object, "vadjustment");
|
||
break;
|
||
case PROP_HSCROLL_POLICY:
|
||
xtext->hscroll_policy = g_value_get_enum (value);
|
||
g_object_notify (object, "hscroll-policy");
|
||
break;
|
||
case PROP_VSCROLL_POLICY:
|
||
xtext->vscroll_policy = g_value_get_enum (value);
|
||
g_object_notify (object, "vscroll-policy");
|
||
break;
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (object);
|
||
|
||
switch (prop_id)
|
||
{
|
||
case PROP_HADJUSTMENT:
|
||
g_value_set_object (value, xtext->hadj);
|
||
break;
|
||
case PROP_VADJUSTMENT:
|
||
g_value_set_object (value, xtext->adj);
|
||
break;
|
||
case PROP_HSCROLL_POLICY:
|
||
g_value_set_enum (value, xtext->hscroll_policy);
|
||
break;
|
||
case PROP_VSCROLL_POLICY:
|
||
g_value_set_enum (value, xtext->vscroll_policy);
|
||
break;
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
}
|
||
|
||
GtkWidget *
|
||
gtk_xtext_new (const XTextColor *palette, int separator)
|
||
{
|
||
GtkXText *xtext;
|
||
|
||
xtext = g_object_new (gtk_xtext_get_type (), NULL);
|
||
xtext->separator = separator;
|
||
xtext->wordwrap = TRUE;
|
||
|
||
gtk_xtext_set_palette (xtext, palette);
|
||
gtk_xtext_sync_palette_from_theme (xtext);
|
||
|
||
return GTK_WIDGET (xtext);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_release_selections (GtkXText *xtext)
|
||
{
|
||
GdkAtom selections[] = { GDK_SELECTION_PRIMARY, GDK_SELECTION_SECONDARY };
|
||
guint i;
|
||
|
||
if (!gtk_widget_has_screen (GTK_WIDGET (xtext)))
|
||
return;
|
||
|
||
for (i = 0; i < G_N_ELEMENTS (selections); i++)
|
||
{
|
||
GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET (xtext), selections[i]);
|
||
|
||
if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (xtext))
|
||
gtk_clipboard_clear (clipboard);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_cleanup (GtkXText *xtext)
|
||
{
|
||
gtk_xtext_release_selections (xtext);
|
||
|
||
gtk_xtext_cancel_source (&xtext->add_io_tag);
|
||
|
||
gtk_xtext_cancel_source (&xtext->scroll_tag);
|
||
|
||
gtk_xtext_cancel_source (&xtext->io_tag);
|
||
|
||
if (xtext->background_surface)
|
||
{
|
||
cairo_surface_destroy (xtext->background_surface);
|
||
xtext->background_surface = NULL;
|
||
}
|
||
|
||
if (xtext->background_clip_surface)
|
||
{
|
||
cairo_surface_destroy (xtext->background_clip_surface);
|
||
xtext->background_clip_surface = NULL;
|
||
}
|
||
|
||
if (xtext->font)
|
||
{
|
||
backend_font_close (xtext);
|
||
xtext->font = NULL;
|
||
}
|
||
|
||
if (xtext->adj)
|
||
{
|
||
g_signal_handlers_disconnect_matched (G_OBJECT (xtext->adj),
|
||
G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, xtext);
|
||
g_object_unref (G_OBJECT (xtext->adj));
|
||
xtext->adj = NULL;
|
||
}
|
||
|
||
if (xtext->hadj)
|
||
{
|
||
g_object_unref (G_OBJECT (xtext->hadj));
|
||
xtext->hadj = NULL;
|
||
}
|
||
|
||
if (xtext->hand_cursor)
|
||
{
|
||
g_object_unref (xtext->hand_cursor);
|
||
xtext->hand_cursor = NULL;
|
||
}
|
||
|
||
if (xtext->resize_cursor)
|
||
{
|
||
g_object_unref (xtext->resize_cursor);
|
||
xtext->resize_cursor = NULL;
|
||
}
|
||
|
||
if (xtext->orig_buffer)
|
||
{
|
||
gtk_xtext_buffer_free (xtext->orig_buffer);
|
||
xtext->orig_buffer = NULL;
|
||
}
|
||
}
|
||
|
||
|
||
static void
|
||
gtk_xtext_dispose (GObject *object)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (object);
|
||
|
||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_xtext, xtext);
|
||
g_clear_pointer (&xtext->tooltip_url, g_free);
|
||
GtkXTextPrivate *priv = gtk_xtext_get_private (xtext);
|
||
guint i;
|
||
|
||
gtk_xtext_cleanup (xtext);
|
||
backend_deinit (xtext);
|
||
for (i = 0; i < G_N_ELEMENTS (priv->attr_lists); i++)
|
||
g_clear_pointer (&priv->attr_lists[i], pango_attr_list_unref);
|
||
|
||
if (G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose)
|
||
(*G_OBJECT_CLASS (gtk_xtext_parent_class)->dispose) (object);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_unrealize (GtkWidget * widget)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
|
||
gtk_xtext_release_selections (xtext);
|
||
gtk_xtext_cancel_source (&xtext->io_tag);
|
||
gtk_xtext_cancel_source (&xtext->add_io_tag);
|
||
gtk_xtext_cancel_source (&xtext->scroll_tag);
|
||
backend_deinit (xtext);
|
||
g_clear_object (&xtext->hand_cursor);
|
||
g_clear_object (&xtext->resize_cursor);
|
||
xtext->draw_window = NULL;
|
||
xtext->draw_cr = NULL;
|
||
|
||
/*
|
||
* Keep GtkWidget/GdkWindow association intact until parent unrealize.
|
||
* GtkWidget's unrealize path unregisters the window and expects
|
||
* gdk_window_get_user_data(window) == widget.
|
||
*/
|
||
|
||
if (GTK_WIDGET_CLASS (gtk_xtext_parent_class)->unrealize)
|
||
(*GTK_WIDGET_CLASS (gtk_xtext_parent_class)->unrealize) (widget);
|
||
|
||
gtk_widget_set_window (widget, NULL);
|
||
gtk_widget_set_realized (widget, FALSE);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_get_pointer (GdkWindow *window, gint *x, gint *y, GdkModifierType *mask)
|
||
{
|
||
GdkSeat *seat;
|
||
GdkDevice *device;
|
||
|
||
if (x) *x = 0;
|
||
if (y) *y = 0;
|
||
if (mask) *mask = 0;
|
||
if (!window)
|
||
return;
|
||
|
||
seat = gdk_display_get_default_seat (gdk_window_get_display (window));
|
||
device = seat ? gdk_seat_get_pointer (seat) : NULL;
|
||
if (device)
|
||
gdk_window_get_device_position (window, device, x, y, mask);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_realize (GtkWidget * widget)
|
||
{
|
||
GtkXText *xtext;
|
||
GdkWindowAttr attributes;
|
||
GdkWindow *window;
|
||
GtkAllocation allocation;
|
||
GdkWindow *parent_window;
|
||
gint attributes_mask;
|
||
|
||
xtext = GTK_XTEXT (widget);
|
||
|
||
gtk_widget_set_realized (widget, TRUE);
|
||
gtk_widget_get_allocation (widget, &allocation);
|
||
parent_window = gtk_widget_get_parent_window (widget);
|
||
attributes.visual = gtk_widget_get_visual (widget);
|
||
attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
|
||
|
||
attributes.x = allocation.x;
|
||
attributes.y = allocation.y;
|
||
attributes.width = allocation.width;
|
||
attributes.height = allocation.height;
|
||
attributes.wclass = GDK_INPUT_OUTPUT;
|
||
attributes.window_type = GDK_WINDOW_CHILD;
|
||
attributes.event_mask = gtk_widget_get_events (widget) |
|
||
GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
|
||
| GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_SCROLL_MASK
|
||
| GDK_SMOOTH_SCROLL_MASK;
|
||
|
||
window = gdk_window_new (parent_window, &attributes, attributes_mask);
|
||
|
||
gtk_widget_set_window (widget, window);
|
||
|
||
gdk_window_set_user_data (window, widget);
|
||
|
||
xtext->depth = gdk_visual_get_depth (gdk_window_get_visual (window));
|
||
|
||
/* for the separator bar (light) */
|
||
xtext->light_gc.red = 1.0;
|
||
xtext->light_gc.green = 1.0;
|
||
xtext->light_gc.blue = 1.0;
|
||
xtext->light_gc.alpha = 1.0;
|
||
|
||
/* for the separator bar (dark) */
|
||
xtext->dark_gc.red = 0x1111 / 65535.0;
|
||
xtext->dark_gc.green = 0x1111 / 65535.0;
|
||
xtext->dark_gc.blue = 0x1111 / 65535.0;
|
||
xtext->dark_gc.alpha = 1.0;
|
||
|
||
/* for the separator bar (thinline) */
|
||
xtext->thin_gc.red = 0x8e38 / 65535.0;
|
||
xtext->thin_gc.green = 0x8e38 / 65535.0;
|
||
xtext->thin_gc.blue = 0x9f38 / 65535.0;
|
||
xtext->thin_gc.alpha = 1.0;
|
||
|
||
/* for the marker bar (marker) */
|
||
xtext->marker_gc = xtext->palette[XTEXT_MARKER];
|
||
|
||
xtext_set_fg (xtext, XTEXT_FG);
|
||
xtext_set_bg (xtext, XTEXT_BG);
|
||
|
||
/* Retain the public window field for existing callers. */
|
||
xtext->draw_window = window;
|
||
|
||
if (xtext->background_surface)
|
||
{
|
||
xtext->ts_x = xtext->ts_y = 0;
|
||
}
|
||
|
||
if (window && GDK_IS_WINDOW (window))
|
||
{
|
||
GdkDisplay *display = gdk_window_get_display (window);
|
||
|
||
if (display)
|
||
{
|
||
xtext->hand_cursor = gdk_cursor_new_for_display (display, GDK_HAND1);
|
||
xtext->resize_cursor = gdk_cursor_new_for_display (display, GDK_LEFT_SIDE);
|
||
}
|
||
}
|
||
|
||
backend_init (xtext);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_size_request_internal (GtkWidget *widget, GtkRequisition *requisition)
|
||
{
|
||
requisition->width = 200;
|
||
requisition->height = 90;
|
||
}
|
||
|
||
|
||
static void
|
||
gtk_xtext_get_preferred_width (GtkWidget *widget, gint *minimum, gint *natural)
|
||
{
|
||
GtkRequisition requisition;
|
||
|
||
gtk_xtext_size_request_internal (widget, &requisition);
|
||
*minimum = requisition.width;
|
||
*natural = requisition.width;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_get_preferred_height (GtkWidget *widget, gint *minimum, gint *natural)
|
||
{
|
||
GtkRequisition requisition;
|
||
|
||
gtk_xtext_size_request_internal (widget, &requisition);
|
||
*minimum = requisition.height;
|
||
*natural = requisition.height;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_get_preferred_height_for_width (GtkWidget *widget, gint width,
|
||
gint *minimum, gint *natural)
|
||
{
|
||
GtkRequisition requisition;
|
||
|
||
(void)width;
|
||
gtk_xtext_size_request_internal (widget, &requisition);
|
||
*minimum = requisition.height;
|
||
*natural = requisition.height;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_size_allocate (GtkWidget * widget, GtkAllocation * allocation)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
int height_only = FALSE;
|
||
GdkWindow *window;
|
||
|
||
if (allocation->width == xtext->buffer->window_width)
|
||
height_only = TRUE;
|
||
|
||
gtk_widget_set_allocation (widget, allocation);
|
||
if (gtk_widget_get_realized (GTK_WIDGET(widget)))
|
||
{
|
||
xtext->buffer->window_width = allocation->width;
|
||
xtext->buffer->window_height = allocation->height;
|
||
|
||
window = gtk_widget_get_window (widget);
|
||
if (window)
|
||
gdk_window_move_resize (window, allocation->x, allocation->y,
|
||
allocation->width, allocation->height);
|
||
dontscroll (xtext->buffer); /* force scrolling off */
|
||
if (!height_only)
|
||
gtk_xtext_calc_lines (xtext->buffer, FALSE);
|
||
else
|
||
{
|
||
xtext->buffer->pagetop_ent = NULL;
|
||
gtk_xtext_adjustment_set (xtext->buffer, FALSE);
|
||
}
|
||
if (xtext->buffer->scrollbar_down)
|
||
gtk_adjustment_set_value (xtext->adj,
|
||
gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj));
|
||
}
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_selection_clear (xtext_buffer *buf)
|
||
{
|
||
textentry *ent;
|
||
int ret = 0;
|
||
|
||
ent = buf->last_ent_start;
|
||
while (ent)
|
||
{
|
||
if (ent->mark_start != -1)
|
||
ret = 1;
|
||
ent->mark_start = -1;
|
||
ent->mark_end = -1;
|
||
if (ent == buf->last_ent_end)
|
||
break;
|
||
ent = ent->next;
|
||
}
|
||
|
||
return ret;
|
||
}
|
||
|
||
/* x position that the lines after the first of 'ent' start at */
|
||
|
||
static int
|
||
gtk_xtext_cont_indent (xtext_buffer *buf, textentry *ent)
|
||
{
|
||
int w, off;
|
||
|
||
if (ent->left_len != -1 || prefs.hex_text_wrap_align < 1)
|
||
return buf->indent;
|
||
|
||
/* 1 = under the prefix (nick), 2 = under the message */
|
||
off = ent->stamp_len;
|
||
if (prefs.hex_text_wrap_align >= 2)
|
||
{
|
||
/* the nick (or "*") is the first word after the timestamp */
|
||
while (off < ent->str_len && ent->str[off] != ' ')
|
||
off++;
|
||
while (off < ent->str_len && ent->str[off] == ' ')
|
||
off++;
|
||
}
|
||
if (off <= 0 || off >= ent->str_len)
|
||
return buf->indent;
|
||
|
||
w = ent->indent + gtk_xtext_text_width (buf->xtext, ent->str, off);
|
||
/* leave room for the text, or just wrap normally */
|
||
if (w > (buf->window_width - MARGIN) / 2)
|
||
return buf->indent;
|
||
return w;
|
||
}
|
||
|
||
static int
|
||
find_x (GtkXText *xtext, textentry *ent, int x, int subline, int indent)
|
||
{
|
||
int xx = indent;
|
||
int suboff;
|
||
GSList *list;
|
||
GSList *hid = NULL;
|
||
offlen_t *meta;
|
||
int off, len, wid, mbl, mbw;
|
||
|
||
/* Skip to the first chunk of stuff for the subline */
|
||
if (subline > 0)
|
||
{
|
||
suboff = gtk_xtext_subline_end (ent, subline - 1);
|
||
for (list = ent->slp; list; list = g_slist_next (list))
|
||
{
|
||
meta = list->data;
|
||
if (meta->off + meta->len > suboff)
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
suboff = 0;
|
||
list = ent->slp;
|
||
}
|
||
/* Step to the first character of the subline */
|
||
if (list == NULL)
|
||
return 0;
|
||
meta = list->data;
|
||
off = meta->off;
|
||
len = meta->len;
|
||
if (meta->emph & EMPH_HIDDEN)
|
||
hid = list;
|
||
while (len > 0)
|
||
{
|
||
if (off >= suboff)
|
||
break;
|
||
mbl = charlen (ent->str + off);
|
||
len -= mbl;
|
||
off += mbl;
|
||
}
|
||
if (len < 0)
|
||
return ent->str_len; /* Bad char -- return max offset. */
|
||
|
||
/* Step through characters to find the one at the x position */
|
||
wid = x - indent;
|
||
len = meta->len - (off - meta->off);
|
||
while (wid > 0)
|
||
{
|
||
mbl = charlen (ent->str + off);
|
||
mbw = backend_get_text_width_emph (xtext, ent->str + off, mbl, meta->emph);
|
||
wid -= mbw;
|
||
xx += mbw;
|
||
if (xx >= x)
|
||
return off;
|
||
len -= mbl;
|
||
off += mbl;
|
||
if (len <= 0)
|
||
{
|
||
if (meta->emph & EMPH_HIDDEN)
|
||
hid = list;
|
||
list = g_slist_next (list);
|
||
if (list == NULL)
|
||
return ent->str_len;
|
||
meta = list->data;
|
||
off = meta->off;
|
||
len = meta->len;
|
||
}
|
||
}
|
||
|
||
/* If previous chunk exists and is marked hidden, regard it as unhidden */
|
||
if (hid && list && hid->next == list)
|
||
{
|
||
meta = hid->data;
|
||
off = meta->off;
|
||
}
|
||
|
||
/* Return offset of character at x within subline */
|
||
return off;
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
|
||
int line, int *out_of_bounds)
|
||
{
|
||
int indent;
|
||
unsigned char *str;
|
||
|
||
if (subline < 1)
|
||
indent = ent->indent;
|
||
else
|
||
indent = gtk_xtext_cont_indent (xtext->buffer, ent);
|
||
|
||
if (line > gtk_adjustment_get_page_size (xtext->adj) || line < 0)
|
||
{
|
||
*out_of_bounds = TRUE;
|
||
return 0;
|
||
}
|
||
|
||
str = ent->str + gtk_xtext_find_subline (xtext, ent, subline);
|
||
if (str >= ent->str + ent->str_len)
|
||
return ent->str_len; /* e.g. the blank rows of an inline image */
|
||
|
||
/* Let user select left a few pixels to grab hidden text e.g. '<' */
|
||
if (x < indent - xtext->space_width)
|
||
{
|
||
*out_of_bounds = 1;
|
||
return (str - ent->str);
|
||
}
|
||
|
||
*out_of_bounds = 0;
|
||
|
||
return find_x (xtext, ent, x, subline, indent);
|
||
}
|
||
|
||
static textentry *
|
||
gtk_xtext_find_char (GtkXText * xtext, int x, int y, int *off, int *out_of_bounds)
|
||
{
|
||
textentry *ent;
|
||
int line;
|
||
int subline;
|
||
int outofbounds = FALSE;
|
||
|
||
/* Adjust y value for negative rounding, double to int */
|
||
if (y < 0)
|
||
y -= xtext->fontsize;
|
||
|
||
line = (y + xtext->pixel_offset) / xtext->fontsize;
|
||
ent = gtk_xtext_nth (xtext,
|
||
line + (int)gtk_adjustment_get_value (xtext->adj), &subline);
|
||
if (!ent)
|
||
return NULL;
|
||
|
||
if (off)
|
||
*off = gtk_xtext_find_x (xtext, x, ent, subline, line, &outofbounds);
|
||
if (out_of_bounds)
|
||
*out_of_bounds = outofbounds;
|
||
|
||
return ent;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_draw_sep (GtkXText * xtext, int y)
|
||
{
|
||
int x, height;
|
||
cairo_t *cr;
|
||
GtkAllocation allocation;
|
||
|
||
if (y == -1)
|
||
{
|
||
y = 0;
|
||
gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation);
|
||
height = allocation.height;
|
||
} else
|
||
{
|
||
height = xtext->fontsize;
|
||
}
|
||
|
||
if (!xtext->draw_cr)
|
||
{
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
return;
|
||
}
|
||
|
||
/* draw the separator line */
|
||
if (xtext->separator && xtext->buffer->indent)
|
||
{
|
||
const XTextColor *light = &xtext->light_gc;
|
||
const XTextColor *dark = &xtext->dark_gc;
|
||
cr = xtext_create_context (xtext);
|
||
|
||
x = xtext->buffer->indent - ((xtext->space_width + 1) / 2);
|
||
if (x < 1)
|
||
{
|
||
cairo_destroy (cr);
|
||
return;
|
||
}
|
||
|
||
if (xtext->thinline)
|
||
{
|
||
if (xtext->moving_separator)
|
||
xtext_draw_line (xtext, cr, light, x, y, x, y + height);
|
||
else
|
||
xtext_draw_line (xtext, cr, &xtext->thin_gc, x, y, x, y + height);
|
||
} else
|
||
{
|
||
if (xtext->moving_separator)
|
||
{
|
||
xtext_draw_line (xtext, cr, light, x - 1, y, x - 1, y + height);
|
||
xtext_draw_line (xtext, cr, dark, x, y, x, y + height);
|
||
} else
|
||
{
|
||
xtext_draw_line (xtext, cr, dark, x - 1, y, x - 1, y + height);
|
||
xtext_draw_line (xtext, cr, light, x, y, x, y + height);
|
||
}
|
||
}
|
||
|
||
cairo_destroy (cr);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_draw_marker (GtkXText * xtext, textentry * ent, int y)
|
||
{
|
||
int x, width, render_y;
|
||
cairo_t *cr;
|
||
GtkAllocation allocation;
|
||
|
||
if (!xtext->marker) return;
|
||
|
||
if (xtext->buffer->marker_pos == ent)
|
||
{
|
||
render_y = y + xtext->font->descent;
|
||
}
|
||
else if (xtext->buffer->marker_pos == ent->next && ent->next != NULL)
|
||
{
|
||
render_y = y + xtext->font->descent + xtext->fontsize * ent->subline_count;
|
||
}
|
||
else return;
|
||
|
||
x = 0;
|
||
gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation);
|
||
width = allocation.width;
|
||
|
||
cr = xtext_create_context (xtext);
|
||
xtext_draw_line (xtext, cr, &xtext->marker_gc, x, render_y, x + width, render_y);
|
||
cairo_destroy (cr);
|
||
|
||
if (gtk_window_has_toplevel_focus (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (xtext)))))
|
||
{
|
||
xtext->buffer->marker_seen = TRUE;
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_render (GtkWidget *widget, GdkRectangle *area, cairo_t *cr)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
cairo_t *old_cr = xtext->draw_cr;
|
||
|
||
xtext->draw_cr = cr;
|
||
xtext->render_cycle++;
|
||
xtext->clip_x = area->x;
|
||
xtext->clip_x2 = area->x + area->width;
|
||
xtext->clip_y = area->y;
|
||
xtext->clip_y2 = area->y + area->height;
|
||
|
||
/* Recompute the page origin even for a partial exposure: scrolling or
|
||
* switching buffers may have invalidated the previous pagetop entry.
|
||
* Cairo retains GTK's dirty-region clip; text outside it is skipped. */
|
||
gtk_xtext_render_page (xtext);
|
||
|
||
xtext->clip_x = xtext->clip_y = 0;
|
||
xtext->clip_x2 = xtext->clip_y2 = G_MAXINT;
|
||
g_clear_pointer (&xtext->background_clip_surface, cairo_surface_destroy);
|
||
xtext->draw_cr = old_cr;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_draw (GtkWidget *widget, cairo_t *cr)
|
||
{
|
||
GdkRectangle area;
|
||
|
||
if (!gdk_cairo_get_clip_rectangle (cr, &area))
|
||
{
|
||
GtkAllocation allocation;
|
||
|
||
gtk_widget_get_allocation (widget, &allocation);
|
||
area.x = 0;
|
||
area.y = 0;
|
||
area.width = allocation.width;
|
||
area.height = allocation.height;
|
||
}
|
||
|
||
gtk_xtext_render (widget, &area, cr);
|
||
return FALSE;
|
||
}
|
||
|
||
|
||
/* Update selection state before scheduling the GTK frame. */
|
||
static void
|
||
gtk_xtext_selection_render (GtkXText *xtext, textentry *start_ent, textentry *end_ent)
|
||
{
|
||
xtext->buffer->last_ent_start = start_ent;
|
||
xtext->buffer->last_ent_end = end_ent;
|
||
xtext->buffer->last_offset_start = start_ent->mark_start;
|
||
xtext->buffer->last_offset_end = end_ent->mark_end;
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_selection_draw (GtkXText * xtext, GdkEventMotion * event, gboolean render)
|
||
{
|
||
textentry *ent;
|
||
textentry *ent_end;
|
||
textentry *ent_start;
|
||
int offset_start = 0;
|
||
int offset_end = 0;
|
||
textentry *low_ent, *high_ent;
|
||
int low_x, low_y, low_offs;
|
||
int high_x, high_y, high_offs, high_len;
|
||
|
||
if (xtext->buffer->text_first == NULL)
|
||
return;
|
||
|
||
ent_start = gtk_xtext_find_char (xtext, xtext->select_start_x, xtext->select_start_y, &offset_start, NULL);
|
||
ent_end = gtk_xtext_find_char (xtext, xtext->select_end_x, xtext->select_end_y, &offset_end, NULL);
|
||
if (ent_start == NULL && ent_end == NULL)
|
||
return;
|
||
|
||
if ((ent_start != ent_end && xtext->select_start_y > xtext->select_end_y) || /* different entries */
|
||
(ent_start == ent_end && offset_start > offset_end)) /* same entry, different character offsets */
|
||
{
|
||
/* marking up */
|
||
low_ent = ent_end;
|
||
low_x = xtext->select_end_x;
|
||
low_y = xtext->select_end_y;
|
||
low_offs = offset_end;
|
||
high_ent = ent_start;
|
||
high_x = xtext->select_start_x;
|
||
high_y = xtext->select_start_y;
|
||
high_offs = offset_start;
|
||
}
|
||
else
|
||
{
|
||
/* marking down */
|
||
low_ent = ent_start;
|
||
low_x = xtext->select_start_x;
|
||
low_y = xtext->select_start_y;
|
||
low_offs = offset_start;
|
||
high_ent = ent_end;
|
||
high_x = xtext->select_end_x;
|
||
high_y = xtext->select_end_y;
|
||
high_offs = offset_end;
|
||
}
|
||
if (low_ent == NULL)
|
||
{
|
||
low_ent = xtext->buffer->text_first;
|
||
low_offs = 0;
|
||
}
|
||
if (high_ent == NULL)
|
||
{
|
||
high_ent = xtext->buffer->text_last;
|
||
high_offs = high_ent->str_len;
|
||
}
|
||
|
||
/* word selection */
|
||
if (xtext->word_select)
|
||
{
|
||
/* a word selection cannot be started if the cursor is out of bounds in gtk_xtext_button_press */
|
||
if (!gtk_xtext_get_word_select_range (xtext, low_x, low_y, NULL, &low_offs, NULL))
|
||
gtk_xtext_get_word (xtext, low_x, low_y, NULL, &low_offs, NULL, NULL);
|
||
|
||
/* in case the cursor is out of bounds we keep offset_end from gtk_xtext_find_char and fix the length */
|
||
if (!gtk_xtext_get_word_select_range (xtext, high_x, high_y, NULL, &high_offs, &high_len) &&
|
||
gtk_xtext_get_word (xtext, high_x, high_y, NULL, &high_offs, &high_len, NULL) == NULL)
|
||
high_len = high_offs == high_ent->str_len? 0: -1; /* -1 for the space, 0 if at the end */
|
||
high_offs += high_len;
|
||
if (low_y < 0)
|
||
low_offs = xtext->buffer->last_offset_start;
|
||
if (high_y > xtext->buffer->window_height)
|
||
high_offs = xtext->buffer->last_offset_end;
|
||
}
|
||
/* line/ent selection */
|
||
else if (xtext->line_select)
|
||
{
|
||
low_offs = 0;
|
||
high_offs = high_ent->str_len;
|
||
}
|
||
/* character selection */
|
||
else
|
||
{
|
||
if (low_y < 0)
|
||
low_offs = xtext->buffer->last_offset_start;
|
||
if (high_y > xtext->buffer->window_height)
|
||
high_offs = xtext->buffer->last_offset_end;
|
||
}
|
||
|
||
/* set all the old mark_ fields to -1 */
|
||
gtk_xtext_selection_clear (xtext->buffer);
|
||
|
||
low_ent->mark_start = low_offs;
|
||
low_ent->mark_end = high_offs;
|
||
|
||
if (low_ent != high_ent)
|
||
{
|
||
low_ent->mark_end = low_ent->str_len;
|
||
if (high_offs != 0)
|
||
{
|
||
high_ent->mark_start = 0;
|
||
high_ent->mark_end = high_offs;
|
||
}
|
||
|
||
/* set all the mark_ fields of the ents within the selection */
|
||
ent = low_ent->next;
|
||
while (ent && ent != high_ent)
|
||
{
|
||
ent->mark_start = 0;
|
||
ent->mark_end = ent->str_len;
|
||
ent = ent->next;
|
||
}
|
||
}
|
||
|
||
if (render)
|
||
gtk_xtext_selection_render (xtext, low_ent, high_ent);
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_timeout_ms (GtkXText *xtext, int pixes)
|
||
{
|
||
int apixes = abs(pixes);
|
||
|
||
if (apixes < 6) return 100;
|
||
if (apixes < 12) return 50;
|
||
if (apixes < 20) return 20;
|
||
return 10;
|
||
}
|
||
static gboolean
|
||
gtk_xtext_scrolldown_timeout (gpointer data)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (data);
|
||
int p_y, win_height;
|
||
xtext_buffer *buf = xtext->buffer;
|
||
GtkAdjustment *adj = xtext->adj;
|
||
GdkWindow *window;
|
||
|
||
xtext->scroll_tag = 0;
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return 0;
|
||
gtk_xtext_get_pointer (window, NULL, &p_y, NULL);
|
||
win_height = gdk_window_get_height (window);
|
||
|
||
if (buf->last_ent_end == NULL || /* If context has changed OR */
|
||
buf->pagetop_ent == NULL || /* pagetop_ent is reset OR */
|
||
p_y <= win_height || /* pointer not below bottom margin OR */
|
||
gtk_adjustment_get_value (adj) >=
|
||
(gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj)))
|
||
{
|
||
xtext->scroll_tag = 0;
|
||
return 0;
|
||
}
|
||
|
||
xtext->select_start_y -= xtext->fontsize;
|
||
xtext->select_start_adj++;
|
||
gtk_adjustment_set_value (adj, gtk_adjustment_get_value (adj) + 1);
|
||
g_signal_emit_by_name (adj, "value-changed");
|
||
gtk_xtext_selection_draw (xtext, NULL, TRUE);
|
||
gtk_xtext_render_ents (xtext, buf->pagetop_ent->next, buf->last_ent_end);
|
||
xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y - win_height),
|
||
gtk_xtext_scrolldown_timeout,
|
||
xtext);
|
||
|
||
return 0;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_scrollup_timeout (gpointer data)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (data);
|
||
int p_y;
|
||
xtext_buffer *buf = xtext->buffer;
|
||
GtkAdjustment *adj = xtext->adj;
|
||
int delta_y;
|
||
GdkWindow *window;
|
||
|
||
xtext->scroll_tag = 0;
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return 0;
|
||
gtk_xtext_get_pointer (window, NULL, &p_y, NULL);
|
||
|
||
if (buf->last_ent_start == NULL || /* If context has changed OR */
|
||
buf->pagetop_ent == NULL || /* pagetop_ent is reset OR */
|
||
p_y >= 0 || /* not above top margin OR */
|
||
gtk_adjustment_get_value (adj) == 0) /* we're scrolled to the top */
|
||
{
|
||
xtext->scroll_tag = 0;
|
||
return 0;
|
||
}
|
||
|
||
if (gtk_adjustment_get_value (adj) < 0)
|
||
{
|
||
delta_y = gtk_adjustment_get_value (adj) * xtext->fontsize;
|
||
gtk_adjustment_set_value (adj, 0);
|
||
} else {
|
||
delta_y = xtext->fontsize;
|
||
gtk_adjustment_set_value (adj, gtk_adjustment_get_value (adj) - 1);
|
||
}
|
||
xtext->select_start_y += delta_y;
|
||
xtext->select_start_adj = gtk_adjustment_get_value (adj);
|
||
g_signal_emit_by_name (adj, "value-changed");
|
||
gtk_xtext_selection_draw (xtext, NULL, TRUE);
|
||
gtk_xtext_render_ents (xtext, buf->pagetop_ent->prev, buf->last_ent_end);
|
||
xtext->scroll_tag = g_timeout_add (gtk_xtext_timeout_ms (xtext, p_y),
|
||
gtk_xtext_scrollup_timeout,
|
||
xtext);
|
||
|
||
return 0;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_selection_update (GtkXText * xtext, GdkEventMotion * event, int p_y, gboolean render)
|
||
{
|
||
int win_height;
|
||
int moved;
|
||
GdkWindow *window;
|
||
|
||
if (xtext->scroll_tag)
|
||
{
|
||
return;
|
||
}
|
||
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return;
|
||
win_height = gdk_window_get_height (window);
|
||
|
||
/* selecting past top of window, scroll up! */
|
||
if (p_y < 0 && gtk_adjustment_get_value (xtext->adj) >= 0)
|
||
{
|
||
gtk_xtext_scrollup_timeout (xtext);
|
||
}
|
||
|
||
/* selecting past bottom of window, scroll down! */
|
||
else if (p_y > win_height &&
|
||
gtk_adjustment_get_value (xtext->adj) <
|
||
(gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj)))
|
||
{
|
||
gtk_xtext_scrolldown_timeout (xtext);
|
||
}
|
||
else
|
||
{
|
||
moved = (int)gtk_adjustment_get_value (xtext->adj) -
|
||
xtext->select_start_adj;
|
||
xtext->select_start_y -= (moved * xtext->fontsize);
|
||
xtext->select_start_adj = gtk_adjustment_get_value (xtext->adj);
|
||
gtk_xtext_selection_draw (xtext, event, render);
|
||
}
|
||
}
|
||
|
||
static char *
|
||
gtk_xtext_get_word (GtkXText * xtext, int x, int y, textentry ** ret_ent,
|
||
int *ret_off, int *ret_len, GSList **slp)
|
||
{
|
||
textentry *ent;
|
||
int offset;
|
||
unsigned char *word;
|
||
unsigned char *last, *end;
|
||
int len;
|
||
int out_of_bounds = 0;
|
||
int len_to_offset = 0;
|
||
gboolean has_hidden;
|
||
|
||
ent = gtk_xtext_find_char (xtext, x, y, &offset, &out_of_bounds);
|
||
if (ent == NULL || out_of_bounds || offset < 0 || offset >= ent->str_len)
|
||
return NULL;
|
||
|
||
word = ent->str + offset;
|
||
while ((word = g_utf8_find_prev_char (ent->str, word)))
|
||
{
|
||
if (is_del (*word))
|
||
{
|
||
word++;
|
||
len_to_offset--;
|
||
break;
|
||
}
|
||
len_to_offset += charlen (word);
|
||
}
|
||
if (!word)
|
||
word = ent->str;
|
||
|
||
/* remove color characters from the length */
|
||
gtk_xtext_strip_color (word, len_to_offset, xtext->scratch_buffer, &len_to_offset, NULL, FALSE);
|
||
|
||
last = word;
|
||
end = ent->str + ent->str_len;
|
||
len = 0;
|
||
do
|
||
{
|
||
if (is_del (*last))
|
||
break;
|
||
len += charlen (last);
|
||
last = g_utf8_find_next_char (last, end);
|
||
}
|
||
while (last);
|
||
|
||
if (len > 0 && word[len-1]=='.')
|
||
len--;
|
||
|
||
if (ret_ent)
|
||
*ret_ent = ent;
|
||
if (ret_off)
|
||
*ret_off = word - ent->str;
|
||
if (ret_len)
|
||
*ret_len = len; /* Length before stripping */
|
||
|
||
/* a word with hidden text is a placeholder (e.g. an inline image link
|
||
label); its whole visible part acts as the link */
|
||
has_hidden = memchr (word, ATTR_HIDDEN, len) != NULL;
|
||
|
||
word = gtk_xtext_strip_color (word, len, xtext->scratch_buffer, NULL, slp, FALSE);
|
||
|
||
/* avoid turning the cursor into a hand for non-url part of the word */
|
||
if (!has_hidden &&
|
||
xtext->urlcheck_function && xtext->urlcheck_function (GTK_WIDGET (xtext), word))
|
||
{
|
||
int start = 0, end = len;
|
||
url_last (&start, &end);
|
||
|
||
/* make sure we're not before the start of the match */
|
||
if (len_to_offset < start)
|
||
return NULL;
|
||
|
||
/* and not after it */
|
||
if (len_to_offset - start >= end - start)
|
||
return NULL;
|
||
}
|
||
|
||
return word;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_word_select_char (const unsigned char *ch)
|
||
{
|
||
gunichar uc;
|
||
|
||
if (!ch || !*ch)
|
||
return FALSE;
|
||
|
||
uc = g_utf8_get_char_validated ((const gchar *)ch, -1);
|
||
if (uc == (gunichar)-1 || uc == (gunichar)-2)
|
||
return FALSE;
|
||
return g_unichar_isalnum (uc) || uc == '_' || uc == '-';
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_get_word_select_range (GtkXText *xtext, int x, int y, textentry **ret_ent, int *ret_off, int *ret_len)
|
||
{
|
||
textentry *ent;
|
||
int offset;
|
||
unsigned char *start, *end;
|
||
|
||
ent = gtk_xtext_find_char (xtext, x, y, &offset, NULL);
|
||
if (!ent || offset < 0 || offset >= ent->str_len)
|
||
return FALSE;
|
||
|
||
start = ent->str + offset;
|
||
end = g_utf8_find_next_char (start, ent->str + ent->str_len);
|
||
if (!gtk_xtext_word_select_char (start))
|
||
return FALSE;
|
||
|
||
while (start > ent->str)
|
||
{
|
||
unsigned char *prev = g_utf8_find_prev_char (ent->str, start);
|
||
if (!prev || !gtk_xtext_word_select_char (prev))
|
||
break;
|
||
start = prev;
|
||
}
|
||
|
||
while (end && end < ent->str + ent->str_len && gtk_xtext_word_select_char (end))
|
||
end = g_utf8_find_next_char (end, ent->str + ent->str_len);
|
||
|
||
if (ret_ent)
|
||
*ret_ent = ent;
|
||
if (ret_off)
|
||
*ret_off = (int)(start - ent->str);
|
||
if (ret_len)
|
||
*ret_len = (int)(end - start);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_unrender_hilight (GtkXText *xtext)
|
||
{
|
||
xtext->render_hilights_only = TRUE;
|
||
xtext->skip_border_fills = TRUE;
|
||
xtext->skip_stamp = TRUE;
|
||
xtext->un_hilight = TRUE;
|
||
|
||
gtk_xtext_render_ents (xtext, xtext->hilight_ent, NULL);
|
||
|
||
xtext->render_hilights_only = FALSE;
|
||
xtext->skip_border_fills = FALSE;
|
||
xtext->skip_stamp = FALSE;
|
||
xtext->un_hilight = FALSE;
|
||
}
|
||
|
||
/* show (or stop showing) the destination URL of a hovered placeholder
|
||
word as the widget's tooltip */
|
||
|
||
static void
|
||
gtk_xtext_set_url_tooltip (GtkXText *xtext, const char *url, int url_len)
|
||
{
|
||
if (url == NULL)
|
||
{
|
||
if (xtext->tooltip_url)
|
||
{
|
||
g_free (xtext->tooltip_url);
|
||
xtext->tooltip_url = NULL;
|
||
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), NULL);
|
||
xtext->tooltip_stamp_set = FALSE;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (xtext->tooltip_url &&
|
||
strlen (xtext->tooltip_url) == (gsize) url_len &&
|
||
memcmp (xtext->tooltip_url, url, url_len) == 0)
|
||
return;
|
||
|
||
g_free (xtext->tooltip_url);
|
||
xtext->tooltip_url = g_strndup (url, url_len);
|
||
gtk_widget_set_tooltip_text (GTK_WIDGET (xtext), xtext->tooltip_url);
|
||
xtext->tooltip_stamp_set = FALSE;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_leave_notify (GtkWidget * widget, GdkEventCrossing * event)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
GdkWindow *window = gtk_widget_get_window (widget);
|
||
|
||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||
|
||
if (xtext->cursor_hand)
|
||
{
|
||
gtk_xtext_unrender_hilight (xtext);
|
||
xtext->hilight_start = -1;
|
||
xtext->hilight_end = -1;
|
||
xtext->cursor_hand = FALSE;
|
||
if (window)
|
||
gdk_window_set_cursor (window, 0);
|
||
xtext->hilight_ent = NULL;
|
||
}
|
||
|
||
if (xtext->cursor_resize)
|
||
{
|
||
gtk_xtext_unrender_hilight (xtext);
|
||
xtext->hilight_start = -1;
|
||
xtext->hilight_end = -1;
|
||
xtext->cursor_resize = FALSE;
|
||
if (window)
|
||
gdk_window_set_cursor (window, 0);
|
||
xtext->hilight_ent = NULL;
|
||
}
|
||
|
||
if (xtext->tooltip_stamp_set)
|
||
{
|
||
gtk_widget_set_tooltip_text (widget, NULL);
|
||
xtext->tooltip_stamp_set = FALSE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* check if we should mark time stamps, and if a redraw is needed */
|
||
|
||
static gboolean
|
||
gtk_xtext_check_mark_stamp (GtkXText *xtext, GdkModifierType mask)
|
||
{
|
||
gboolean redraw = FALSE;
|
||
|
||
if ((mask & STATE_SHIFT || prefs.hex_text_autocopy_stamp)
|
||
&& (!prefs.hex_stamp_text || prefs.hex_text_indent))
|
||
{
|
||
if (!xtext->mark_stamp)
|
||
{
|
||
redraw = TRUE; /* must redraw all */
|
||
xtext->mark_stamp = TRUE;
|
||
}
|
||
} else
|
||
{
|
||
if (xtext->mark_stamp)
|
||
{
|
||
redraw = TRUE; /* must redraw all */
|
||
xtext->mark_stamp = FALSE;
|
||
}
|
||
}
|
||
return redraw;
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_get_word_adjust (GtkXText *xtext, int x, int y, textentry **word_ent, int *offset, int *len)
|
||
{
|
||
GSList *slp = NULL;
|
||
unsigned char *word;
|
||
int word_type = 0;
|
||
gboolean placeholder = FALSE;
|
||
|
||
word = gtk_xtext_get_word (xtext, x, y, word_ent, offset, len, &slp);
|
||
if (word)
|
||
{
|
||
int laststart, lastend;
|
||
|
||
word_type = xtext->urlcheck_function (GTK_WIDGET (xtext), word);
|
||
if (word_type > 0)
|
||
{
|
||
/* words with hidden text are placeholders (e.g. inline image
|
||
links): hilight the whole word, not just the URL inside it */
|
||
if (memchr ((*word_ent)->str + *offset, ATTR_HIDDEN, *len))
|
||
{
|
||
/* the label hides the real destination, so show the full
|
||
URL as a tooltip while hovering */
|
||
placeholder = TRUE;
|
||
if (url_last (&laststart, &lastend) && lastend > laststart)
|
||
gtk_xtext_set_url_tooltip (xtext,
|
||
(const char *) word + laststart, lastend - laststart);
|
||
}
|
||
else if (url_last (&laststart, &lastend))
|
||
{
|
||
int cumlen, startadj = 0, endadj = 0;
|
||
offlen_t *meta;
|
||
GSList *sl;
|
||
|
||
for (sl = slp, cumlen = 0; sl; sl = g_slist_next (sl))
|
||
{
|
||
meta = sl->data;
|
||
startadj = meta->off - cumlen;
|
||
cumlen += meta->len;
|
||
if (laststart < cumlen)
|
||
break;
|
||
}
|
||
for (sl = slp, cumlen = 0; sl; sl = g_slist_next (sl))
|
||
{
|
||
meta = sl->data;
|
||
endadj = meta->off - cumlen;
|
||
cumlen += meta->len;
|
||
if (lastend < cumlen)
|
||
break;
|
||
}
|
||
laststart += startadj;
|
||
*offset += laststart;
|
||
*len = lastend + endadj - laststart;
|
||
}
|
||
}
|
||
}
|
||
g_slist_free_full (slp, g_free);
|
||
|
||
if (!placeholder)
|
||
gtk_xtext_set_url_tooltip (xtext, NULL, 0);
|
||
|
||
return word_type;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_motion_notify (GtkWidget * widget, GdkEventMotion * event)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
GdkModifierType mask;
|
||
int redraw, tmp, x, y, offset, len, line_x;
|
||
textentry *word_ent;
|
||
int word_type;
|
||
GdkWindow *window;
|
||
GtkAllocation allocation;
|
||
|
||
window = gtk_widget_get_window (widget);
|
||
gtk_widget_get_allocation (widget, &allocation);
|
||
if (!window)
|
||
return FALSE;
|
||
|
||
if (event->is_hint)
|
||
{
|
||
gtk_xtext_get_pointer (window, &x, &y, &mask);
|
||
}
|
||
else
|
||
{
|
||
x = (int)event->x;
|
||
y = (int)event->y;
|
||
mask = event->state;
|
||
}
|
||
|
||
if (xtext->moving_separator)
|
||
{
|
||
if (x < (3 * allocation.width) / 5 && x > 15)
|
||
{
|
||
tmp = xtext->buffer->indent;
|
||
xtext->buffer->indent = x;
|
||
gtk_xtext_fix_indent (xtext->buffer);
|
||
if (tmp != xtext->buffer->indent)
|
||
{
|
||
gtk_xtext_recalc_widths (xtext->buffer, FALSE);
|
||
if (xtext->buffer->scrollbar_down)
|
||
gtk_adjustment_set_value (xtext->adj,
|
||
gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj));
|
||
if (!xtext->io_tag)
|
||
xtext->io_tag = g_timeout_add (REFRESH_TIMEOUT,
|
||
gtk_xtext_adjustment_timeout,
|
||
xtext);
|
||
}
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
if (xtext->button_down)
|
||
{
|
||
redraw = gtk_xtext_check_mark_stamp (xtext, mask);
|
||
gtk_grab_add (widget);
|
||
xtext->select_end_x = x;
|
||
xtext->select_end_y = y;
|
||
gtk_xtext_selection_update (xtext, event, y, !redraw);
|
||
|
||
/* user has pressed or released SHIFT, must redraw entire selection */
|
||
if (redraw)
|
||
{
|
||
xtext->force_stamp = TRUE;
|
||
gtk_xtext_render_ents (xtext, xtext->buffer->last_ent_start,
|
||
xtext->buffer->last_ent_end);
|
||
xtext->force_stamp = FALSE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
if (xtext->separator && xtext->buffer->indent)
|
||
{
|
||
line_x = xtext->buffer->indent - ((xtext->space_width + 1) / 2);
|
||
if (line_x == x || line_x == x + 1 || line_x == x - 1)
|
||
{
|
||
if (!xtext->cursor_resize)
|
||
{
|
||
gdk_window_set_cursor (window, xtext->resize_cursor);
|
||
xtext->cursor_hand = FALSE;
|
||
xtext->cursor_resize = TRUE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
if (xtext->urlcheck_function == NULL)
|
||
goto tooltip_check;
|
||
|
||
word_type = gtk_xtext_get_word_adjust (xtext, x, y, &word_ent, &offset, &len);
|
||
if (word_type > 0)
|
||
{
|
||
if (!xtext->cursor_hand ||
|
||
xtext->hilight_ent != word_ent ||
|
||
xtext->hilight_start != offset ||
|
||
xtext->hilight_end != offset + len)
|
||
{
|
||
if (!xtext->cursor_hand)
|
||
{
|
||
gdk_window_set_cursor (window, xtext->hand_cursor);
|
||
xtext->cursor_hand = TRUE;
|
||
xtext->cursor_resize = FALSE;
|
||
}
|
||
|
||
/* un-render the old hilight */
|
||
if (xtext->hilight_ent)
|
||
gtk_xtext_unrender_hilight (xtext);
|
||
|
||
xtext->hilight_ent = word_ent;
|
||
xtext->hilight_start = offset;
|
||
xtext->hilight_end = offset + len;
|
||
|
||
xtext->skip_border_fills = TRUE;
|
||
xtext->render_hilights_only = TRUE;
|
||
xtext->skip_stamp = TRUE;
|
||
|
||
gtk_xtext_render_ents (xtext, word_ent, NULL);
|
||
|
||
xtext->skip_border_fills = FALSE;
|
||
xtext->render_hilights_only = FALSE;
|
||
xtext->skip_stamp = FALSE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
tooltip_check:
|
||
/* hovering an inline image? it opens the viewer when clicked */
|
||
if (gtk_xtext_image_hit (xtext, x, y))
|
||
{
|
||
if (xtext->hilight_ent) /* moving here straight from a link */
|
||
gtk_xtext_leave_notify (widget, NULL);
|
||
if (!xtext->cursor_hand)
|
||
{
|
||
gdk_window_set_cursor (window, xtext->hand_cursor);
|
||
xtext->cursor_hand = TRUE;
|
||
xtext->cursor_resize = FALSE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
if (xtext->buffer->time_stamp && xtext->buffer->indent > 0 && x >= 0 && x < xtext->stamp_width)
|
||
{
|
||
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
|
||
if (ent && (!xtext->tooltip_stamp_set || xtext->tooltip_stamp != ent->stamp))
|
||
{
|
||
char tooltip[96];
|
||
strftime_utf8 (tooltip, sizeof (tooltip), "%Y-%m-%d", ent->stamp);
|
||
gtk_widget_set_tooltip_text (widget, tooltip);
|
||
xtext->tooltip_stamp = ent->stamp;
|
||
xtext->tooltip_stamp_set = TRUE;
|
||
}
|
||
if (ent)
|
||
return FALSE;
|
||
}
|
||
else if (!xtext->buffer->time_stamp && x >= xtext->buffer->indent)
|
||
{
|
||
textentry *ent = gtk_xtext_find_char (xtext, x, y, NULL, NULL);
|
||
if (ent && ent->stamp && (!xtext->tooltip_stamp_set || xtext->tooltip_stamp != ent->stamp))
|
||
{
|
||
char tooltip[128];
|
||
char date[64];
|
||
char *stamp_text;
|
||
strftime_utf8 (date, sizeof (date), "%Y-%m-%d", ent->stamp);
|
||
xtext_get_stamp_str (ent->stamp, &stamp_text);
|
||
g_snprintf (tooltip, sizeof (tooltip), "%s %s", date, stamp_text);
|
||
gtk_widget_set_tooltip_text (widget, tooltip);
|
||
g_free (stamp_text);
|
||
xtext->tooltip_stamp = ent->stamp;
|
||
xtext->tooltip_stamp_set = TRUE;
|
||
}
|
||
if (ent)
|
||
return FALSE;
|
||
}
|
||
else if (xtext->tooltip_stamp_set)
|
||
{
|
||
gtk_widget_set_tooltip_text (widget, NULL);
|
||
xtext->tooltip_stamp_set = FALSE;
|
||
}
|
||
|
||
gtk_xtext_leave_notify (widget, NULL);
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_set_clip_owner (GtkWidget * xtext, GdkEventButton * event)
|
||
{
|
||
char *str;
|
||
int len;
|
||
|
||
if (GTK_XTEXT (xtext)->selection_buffer &&
|
||
GTK_XTEXT (xtext)->selection_buffer != GTK_XTEXT (xtext)->buffer)
|
||
gtk_xtext_selection_clear (GTK_XTEXT (xtext)->selection_buffer);
|
||
|
||
GTK_XTEXT (xtext)->selection_buffer = GTK_XTEXT (xtext)->buffer;
|
||
|
||
str = gtk_xtext_selection_get_text (GTK_XTEXT (xtext), &len);
|
||
if (str)
|
||
{
|
||
if (str[0])
|
||
{
|
||
gtk_clipboard_set_text (gtk_widget_get_clipboard (xtext, GDK_SELECTION_CLIPBOARD), str, len);
|
||
|
||
/* Let GtkClipboard own the X11 selection window. Making xtext
|
||
* the selection owner forces its window native and bypasses
|
||
* GTK's toplevel backing buffer on subsequent redraws.
|
||
*/
|
||
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_PRIMARY),
|
||
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
|
||
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
|
||
gtk_clipboard_set_with_owner (gtk_widget_get_clipboard (xtext, GDK_SELECTION_SECONDARY),
|
||
gtk_xtext_selection_targets, G_N_ELEMENTS (gtk_xtext_selection_targets),
|
||
gtk_xtext_clipboard_get, gtk_xtext_clipboard_clear, G_OBJECT (xtext));
|
||
}
|
||
|
||
g_free (str);
|
||
}
|
||
}
|
||
|
||
void
|
||
gtk_xtext_copy_selection (GtkXText *xtext)
|
||
{
|
||
gtk_xtext_set_clip_owner (GTK_WIDGET (xtext), NULL);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_unselect (GtkXText *xtext)
|
||
{
|
||
gtk_xtext_selection_clear (xtext->buffer);
|
||
xtext->buffer->last_ent_start = NULL;
|
||
xtext->buffer->last_ent_end = NULL;
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
}
|
||
|
||
/* check whether the given widget coordinates hit an entry's inline image;
|
||
returns the entry if so */
|
||
|
||
static textentry *
|
||
gtk_xtext_image_hit (GtkXText *xtext, int x, int y)
|
||
{
|
||
textentry *ent;
|
||
int line, subline, image_rows, text_rows, rel_y, img_x;
|
||
|
||
if (xtext->fontsize <= 0)
|
||
return NULL;
|
||
|
||
/* adjust y value for negative rounding, double to int */
|
||
if (y < 0)
|
||
y -= xtext->fontsize;
|
||
|
||
line = (y + xtext->pixel_offset) / xtext->fontsize;
|
||
ent = gtk_xtext_nth (xtext, line + (int) gtk_adjustment_get_value (xtext->adj),
|
||
&subline);
|
||
if (ent == NULL || ent->image == NULL)
|
||
return NULL;
|
||
|
||
image_rows = gtk_xtext_image_rows (xtext, ent);
|
||
text_rows = ent->subline_count - image_rows;
|
||
if (subline < text_rows)
|
||
return NULL;
|
||
|
||
img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
|
||
if (x < img_x || x >= img_x + gdk_pixbuf_get_width (ent->image))
|
||
return NULL;
|
||
|
||
rel_y = (subline - text_rows) * xtext->fontsize +
|
||
(y + xtext->pixel_offset) - (line * xtext->fontsize) -
|
||
XTEXT_IMAGE_PADDING;
|
||
if (rel_y < 0 || rel_y >= gdk_pixbuf_get_height (ent->image))
|
||
return NULL;
|
||
|
||
return ent;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_button_release (GtkWidget * widget, GdkEventButton * event)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
unsigned char *word;
|
||
int old;
|
||
int event_x, event_y;
|
||
GtkAllocation allocation;
|
||
|
||
gtk_widget_get_allocation (widget, &allocation);
|
||
event_x = (int)event->x;
|
||
event_y = (int)event->y;
|
||
|
||
if (xtext->moving_separator)
|
||
{
|
||
xtext->moving_separator = FALSE;
|
||
old = xtext->buffer->indent;
|
||
if (event->x < (4 * allocation.width) / 5 && event->x > 15)
|
||
xtext->buffer->indent = event->x;
|
||
gtk_xtext_fix_indent (xtext->buffer);
|
||
if (xtext->buffer->indent != old)
|
||
{
|
||
gtk_xtext_recalc_widths (xtext->buffer, FALSE);
|
||
gtk_xtext_adjustment_set (xtext->buffer, TRUE);
|
||
gtk_xtext_render_page (xtext);
|
||
} else
|
||
gtk_xtext_draw_sep (xtext, -1);
|
||
return FALSE;
|
||
}
|
||
|
||
if (event->button == 1)
|
||
{
|
||
xtext->button_down = FALSE;
|
||
gtk_xtext_cancel_source (&xtext->scroll_tag);
|
||
|
||
gtk_grab_remove (widget);
|
||
|
||
/* got a new selection? */
|
||
if (xtext->buffer->last_ent_start)
|
||
{
|
||
xtext->color_paste = FALSE;
|
||
if (event->state & STATE_CTRL || prefs.hex_text_autocopy_color)
|
||
xtext->color_paste = TRUE;
|
||
if (prefs.hex_text_autocopy_text)
|
||
{
|
||
gtk_xtext_set_clip_owner (GTK_WIDGET (xtext), event);
|
||
}
|
||
}
|
||
|
||
if (xtext->word_select || xtext->line_select)
|
||
{
|
||
xtext->word_select = FALSE;
|
||
xtext->line_select = FALSE;
|
||
return FALSE;
|
||
}
|
||
|
||
if (xtext->select_start_x == event_x &&
|
||
xtext->select_start_y == event_y &&
|
||
xtext->buffer->last_ent_start)
|
||
{
|
||
gtk_xtext_unselect (xtext);
|
||
xtext->mark_stamp = FALSE;
|
||
return FALSE;
|
||
}
|
||
|
||
if (!gtk_xtext_is_selecting (xtext))
|
||
{
|
||
textentry *word_ent = NULL;
|
||
|
||
word = gtk_xtext_get_word (xtext, event_x, event_y, &word_ent, 0, 0, 0);
|
||
xtext->clicked_ent = word ? word_ent : NULL;
|
||
xtext->clicked_image_ent = word ? NULL :
|
||
gtk_xtext_image_hit (xtext, event_x, event_y);
|
||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0, word ? word : NULL, event);
|
||
xtext->clicked_ent = NULL;
|
||
xtext->clicked_image_ent = NULL;
|
||
}
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_button_press (GtkWidget * widget, GdkEventButton * event)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
GdkModifierType mask;
|
||
textentry *ent;
|
||
unsigned char *word;
|
||
int line_x, x, y, offset, len;
|
||
GdkWindow *window;
|
||
|
||
window = gtk_widget_get_window (widget);
|
||
if (!window)
|
||
return FALSE;
|
||
|
||
x = (int)event->x;
|
||
y = (int)event->y;
|
||
mask = event->state;
|
||
|
||
if (event->button == 3 || event->button == 2) /* right/middle click */
|
||
{
|
||
word = gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0);
|
||
if (word)
|
||
{
|
||
xtext->clicked_ent = ent;
|
||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||
word, event);
|
||
} else
|
||
g_signal_emit (G_OBJECT (xtext), xtext_signals[WORD_CLICK], 0,
|
||
"", event);
|
||
xtext->clicked_ent = NULL;
|
||
return FALSE;
|
||
}
|
||
|
||
if (event->button != 1) /* we only want left button */
|
||
return FALSE;
|
||
|
||
if (event->type == GDK_2BUTTON_PRESS) /* WORD select */
|
||
{
|
||
gtk_xtext_check_mark_stamp (xtext, mask);
|
||
if (gtk_xtext_get_word_select_range (xtext, x, y, &ent, &offset, &len) ||
|
||
gtk_xtext_get_word (xtext, x, y, &ent, &offset, &len, 0))
|
||
{
|
||
if (len == 0)
|
||
return FALSE;
|
||
gtk_xtext_selection_clear (xtext->buffer);
|
||
ent->mark_start = offset;
|
||
ent->mark_end = offset + len;
|
||
gtk_xtext_selection_render (xtext, ent, ent);
|
||
xtext->word_select = TRUE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
if (event->type == GDK_3BUTTON_PRESS) /* LINE select */
|
||
{
|
||
gtk_xtext_check_mark_stamp (xtext, mask);
|
||
if (gtk_xtext_get_word (xtext, x, y, &ent, 0, 0, 0))
|
||
{
|
||
gtk_xtext_selection_clear (xtext->buffer);
|
||
ent->mark_start = 0;
|
||
ent->mark_end = ent->str_len;
|
||
gtk_xtext_selection_render (xtext, ent, ent);
|
||
xtext->line_select = TRUE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* check if it was a separator-bar click */
|
||
if (xtext->separator && xtext->buffer->indent)
|
||
{
|
||
line_x = xtext->buffer->indent - ((xtext->space_width + 1) / 2);
|
||
if (line_x == x || line_x == x + 1 || line_x == x - 1)
|
||
{
|
||
xtext->moving_separator = TRUE;
|
||
/* draw the separator line */
|
||
gtk_xtext_draw_sep (xtext, -1);
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
xtext->button_down = TRUE;
|
||
xtext->select_start_x = x;
|
||
xtext->select_start_y = y;
|
||
xtext->select_start_adj = gtk_adjustment_get_value (xtext->adj);
|
||
if (xtext->buffer->last_ent_start)
|
||
{
|
||
gtk_xtext_unselect (xtext);
|
||
xtext->mark_stamp = FALSE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* another program has claimed the selection */
|
||
|
||
static gboolean
|
||
gtk_xtext_selection_kill (GtkXText *xtext, GdkEventSelection *event)
|
||
{
|
||
#ifndef WIN32
|
||
if (xtext->buffer->last_ent_start)
|
||
gtk_xtext_unselect (xtext);
|
||
#endif
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_selection_clear_event (GtkWidget *widget, GdkEventSelection *event)
|
||
{
|
||
return gtk_xtext_selection_kill (GTK_XTEXT (widget), event);
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_is_selecting (GtkXText *xtext)
|
||
{
|
||
textentry *ent;
|
||
xtext_buffer *buf;
|
||
|
||
buf = xtext->selection_buffer;
|
||
if (!buf)
|
||
return FALSE;
|
||
|
||
for (ent = buf->last_ent_start; ent; ent = ent->next)
|
||
{
|
||
if (ent->mark_start != -1 && ent->mark_end - ent->mark_start > 0)
|
||
return TRUE;
|
||
|
||
if (ent == buf->last_ent_end)
|
||
break;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static char *
|
||
gtk_xtext_selection_get_text (GtkXText *xtext, int *len_ret)
|
||
{
|
||
xtext_buffer *buf = xtext->selection_buffer;
|
||
textentry *ent;
|
||
GString *text;
|
||
char *result;
|
||
gboolean first = TRUE;
|
||
|
||
if (!buf)
|
||
return NULL;
|
||
|
||
text = g_string_new (NULL);
|
||
for (ent = buf->last_ent_start; ent; ent = ent->next)
|
||
{
|
||
if (ent->mark_start != -1)
|
||
{
|
||
if (!first)
|
||
g_string_append_c (text, '\n');
|
||
first = FALSE;
|
||
if (ent->mark_end > ent->mark_start)
|
||
{
|
||
if (ent->mark_start == 0 && xtext->mark_stamp)
|
||
{
|
||
char *stamp;
|
||
int len = xtext_get_stamp_str (ent->stamp, &stamp);
|
||
g_string_append_len (text, stamp, len);
|
||
g_free (stamp);
|
||
}
|
||
g_string_append_len (text, (char *)ent->str + ent->mark_start,
|
||
ent->mark_end - ent->mark_start);
|
||
}
|
||
}
|
||
if (ent == buf->last_ent_end)
|
||
break;
|
||
}
|
||
|
||
if (!text->len)
|
||
{
|
||
g_string_free (text, TRUE);
|
||
return NULL;
|
||
}
|
||
if (xtext->color_paste)
|
||
{
|
||
*len_ret = text->len;
|
||
return g_string_free (text, FALSE);
|
||
}
|
||
result = (char *)gtk_xtext_strip_color ((guchar *)text->str, text->len,
|
||
NULL, len_ret, NULL, FALSE);
|
||
g_string_free (text, TRUE);
|
||
return result;
|
||
}
|
||
|
||
/* another program is asking for our selection */
|
||
|
||
static void
|
||
gtk_xtext_selection_get (GtkWidget * widget,
|
||
GtkSelectionData * selection_data_ptr,
|
||
guint info, guint time)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
char *stripped;
|
||
guchar *new_text;
|
||
int len;
|
||
gsize glen = 0;
|
||
|
||
stripped = gtk_xtext_selection_get_text (xtext, &len);
|
||
if (!stripped)
|
||
return;
|
||
|
||
switch (info)
|
||
{
|
||
case TARGET_UTF8_STRING:
|
||
/* it's already in utf8 */
|
||
gtk_selection_data_set_text (selection_data_ptr, stripped, len);
|
||
break;
|
||
case TARGET_TEXT:
|
||
case TARGET_COMPOUND_TEXT:
|
||
#ifdef GDK_WINDOWING_X11
|
||
{
|
||
GdkDisplay *display;
|
||
GdkWindow *window = gtk_widget_get_window (widget);
|
||
|
||
if (!window || !GDK_IS_WINDOW (window))
|
||
break;
|
||
display = gdk_window_get_display (window);
|
||
if (!display || !GDK_IS_X11_DISPLAY (display))
|
||
break;
|
||
GdkAtom encoding;
|
||
gint format;
|
||
gint new_length;
|
||
|
||
gdk_x11_display_string_to_compound_text (display, stripped, &encoding,
|
||
&format, &new_text, &new_length);
|
||
gtk_selection_data_set (selection_data_ptr, encoding, format,
|
||
new_text, new_length);
|
||
gdk_x11_free_compound_text (new_text);
|
||
|
||
}
|
||
break;
|
||
#endif
|
||
default:
|
||
new_text = g_locale_from_utf8 (stripped, len, NULL, &glen, NULL);
|
||
if (new_text)
|
||
gtk_selection_data_set (selection_data_ptr, GDK_SELECTION_TYPE_STRING,
|
||
8, new_text, glen);
|
||
g_free (new_text);
|
||
}
|
||
|
||
g_free (stripped);
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_clipboard_get (GtkClipboard *clipboard, GtkSelectionData *selection_data,
|
||
guint info, gpointer owner)
|
||
{
|
||
int len;
|
||
char *text = gtk_xtext_selection_get_text (GTK_XTEXT (owner), &len);
|
||
|
||
if (text)
|
||
{
|
||
gtk_selection_data_set_text (selection_data, text, len);
|
||
g_free (text);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_clipboard_clear (GtkClipboard *clipboard, gpointer owner)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (owner);
|
||
|
||
if (xtext->orig_buffer)
|
||
gtk_xtext_selection_kill (xtext, NULL);
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_scroll (GtkWidget *widget, GdkEventScroll *event)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (widget);
|
||
gfloat new_value;
|
||
gfloat step;
|
||
gdouble dx;
|
||
gdouble dy;
|
||
int direction = 0;
|
||
int speed = prefs.hex_gui_mouse_scroll_speed;
|
||
|
||
if (speed < 1)
|
||
speed = 1;
|
||
step = (gtk_adjustment_get_page_increment (xtext->adj) * speed) / 100.0f;
|
||
|
||
if (event->direction == GDK_SCROLL_SMOOTH &&
|
||
gdk_event_get_scroll_deltas ((GdkEvent *)event, &dx, &dy))
|
||
{
|
||
if (dy > 0)
|
||
direction = 1;
|
||
else if (dy < 0)
|
||
direction = -1;
|
||
}
|
||
else if (event->direction == GDK_SCROLL_UP)
|
||
direction = -1;
|
||
else if (event->direction == GDK_SCROLL_DOWN)
|
||
direction = 1;
|
||
|
||
if (direction < 0)
|
||
{
|
||
new_value = gtk_adjustment_get_value (xtext->adj) -
|
||
step;
|
||
if (new_value < gtk_adjustment_get_lower (xtext->adj))
|
||
new_value = gtk_adjustment_get_lower (xtext->adj);
|
||
gtk_adjustment_set_value (xtext->adj, new_value);
|
||
}
|
||
else if (direction > 0)
|
||
{
|
||
new_value = gtk_adjustment_get_value (xtext->adj) +
|
||
step;
|
||
if (new_value > (gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj)))
|
||
new_value = gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj);
|
||
gtk_adjustment_set_value (xtext->adj, new_value);
|
||
}
|
||
|
||
return direction != 0;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_set_hadjustment (GtkXText *xtext, GtkAdjustment *adj)
|
||
{
|
||
if (adj == xtext->hadj && adj)
|
||
return;
|
||
if (!adj)
|
||
adj = GTK_ADJUSTMENT (gtk_adjustment_new (0, 0, 1, 1, 1, 1));
|
||
g_object_ref_sink (adj);
|
||
g_clear_object (&xtext->hadj);
|
||
xtext->hadj = adj;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_scroll_adjustments (GtkXText *xtext, GtkAdjustment *hadj, GtkAdjustment *vadj)
|
||
{
|
||
/* Horizontal scrolling stays disabled, but GtkScrollable still owns a
|
||
* valid adjustment. The legacy signal uses the same ownership rules. */
|
||
gtk_xtext_set_hadjustment (xtext, hadj);
|
||
|
||
if (vadj)
|
||
g_return_if_fail (GTK_IS_ADJUSTMENT (vadj));
|
||
else
|
||
vadj = GTK_ADJUSTMENT(gtk_adjustment_new (0, 0, 1, 1, 1, 1));
|
||
|
||
if (xtext->adj && (xtext->adj != vadj))
|
||
{
|
||
g_signal_handlers_disconnect_by_func (xtext->adj,
|
||
gtk_xtext_adjustment_changed,
|
||
xtext);
|
||
g_object_unref (xtext->adj);
|
||
}
|
||
|
||
if (xtext->adj != vadj)
|
||
{
|
||
xtext->adj = vadj;
|
||
g_object_ref_sink (xtext->adj);
|
||
|
||
xtext->vc_signal_tag = g_signal_connect (xtext->adj, "value-changed",
|
||
G_CALLBACK (gtk_xtext_adjustment_changed),
|
||
xtext);
|
||
|
||
gtk_xtext_adjustment_changed (xtext->adj, xtext);
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_class_init (GtkXTextClass * class)
|
||
{
|
||
GtkWidgetClass *widget_class;
|
||
GtkXTextClass *xtext_class;
|
||
GObjectClass *object_class;
|
||
|
||
xtext_private_quark = g_quark_from_static_string ("zoitechat-xtext-private");
|
||
object_class = G_OBJECT_CLASS (class);
|
||
widget_class = (GtkWidgetClass *) class;
|
||
xtext_class = (GtkXTextClass *) class;
|
||
|
||
object_class->set_property = gtk_xtext_set_property;
|
||
object_class->get_property = gtk_xtext_get_property;
|
||
g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment");
|
||
g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
|
||
g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
|
||
g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
|
||
|
||
xtext_signals[WORD_CLICK] =
|
||
g_signal_new ("word_click",
|
||
G_TYPE_FROM_CLASS (object_class),
|
||
G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
|
||
G_STRUCT_OFFSET (GtkXTextClass, word_click),
|
||
NULL, NULL,
|
||
g_cclosure_marshal_generic,
|
||
G_TYPE_NONE,
|
||
2, G_TYPE_POINTER, G_TYPE_POINTER);
|
||
xtext_signals[SET_SCROLL_ADJUSTMENTS] =
|
||
g_signal_new ("set_scroll_adjustments",
|
||
G_OBJECT_CLASS_TYPE (object_class),
|
||
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
||
G_STRUCT_OFFSET (GtkXTextClass, set_scroll_adjustments),
|
||
NULL, NULL,
|
||
g_cclosure_marshal_generic,
|
||
G_TYPE_NONE,
|
||
2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT);
|
||
|
||
object_class->dispose = gtk_xtext_dispose;
|
||
|
||
widget_class->realize = gtk_xtext_realize;
|
||
widget_class->unrealize = gtk_xtext_unrealize;
|
||
widget_class->size_allocate = gtk_xtext_size_allocate;
|
||
widget_class->button_press_event = gtk_xtext_button_press;
|
||
widget_class->button_release_event = gtk_xtext_button_release;
|
||
widget_class->motion_notify_event = gtk_xtext_motion_notify;
|
||
widget_class->selection_clear_event = gtk_xtext_selection_clear_event;
|
||
widget_class->selection_get = gtk_xtext_selection_get;
|
||
widget_class->draw = gtk_xtext_draw;
|
||
widget_class->get_preferred_width = gtk_xtext_get_preferred_width;
|
||
widget_class->get_preferred_height = gtk_xtext_get_preferred_height;
|
||
widget_class->get_preferred_height_for_width = gtk_xtext_get_preferred_height_for_width;
|
||
widget_class->scroll_event = gtk_xtext_scroll;
|
||
widget_class->leave_notify_event = gtk_xtext_leave_notify;
|
||
|
||
xtext_class->word_click = NULL;
|
||
xtext_class->set_scroll_adjustments = gtk_xtext_scroll_adjustments;
|
||
}
|
||
|
||
/* strip MIRC colors and other attribs. */
|
||
|
||
/* CL: needs to strip hidden when called by gtk_xtext_text_width, but not when copying text */
|
||
|
||
typedef struct chunk_s {
|
||
GSList *slp;
|
||
int off1, len1, emph;
|
||
} chunk_t;
|
||
|
||
static void
|
||
xtext_do_chunk(chunk_t *c)
|
||
{
|
||
offlen_t *meta;
|
||
|
||
if (c->len1 == 0)
|
||
return;
|
||
|
||
meta = g_new (offlen_t, 1);
|
||
meta->off = c->off1;
|
||
meta->len = c->len1;
|
||
meta->emph = c->emph;
|
||
meta->width = 0;
|
||
c->slp = g_slist_prepend (c->slp, meta);
|
||
|
||
c->len1 = 0;
|
||
}
|
||
|
||
static unsigned char *
|
||
gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf,
|
||
int *newlen, GSList **slpp, int strip_hidden)
|
||
{
|
||
chunk_t c;
|
||
int i = 0;
|
||
int rcol = 0, bgcol = 0;
|
||
int hidden = FALSE;
|
||
unsigned char *new_str;
|
||
unsigned char *text0 = text;
|
||
int mbl; /* multi-byte length */
|
||
|
||
if (outbuf == NULL)
|
||
new_str = g_malloc (len + 2);
|
||
else
|
||
new_str = outbuf;
|
||
|
||
c.slp = NULL;
|
||
c.off1 = 0;
|
||
c.len1 = 0;
|
||
c.emph = 0;
|
||
while (len > 0)
|
||
{
|
||
mbl = charlen (text);
|
||
if (mbl > len)
|
||
goto bad_utf8;
|
||
|
||
if (rcol > 0 && (isdigit (*text) || (*text == ',' && isdigit (text[1]) && !bgcol)))
|
||
{
|
||
if (text[1] != ',') rcol--;
|
||
if (*text == ',')
|
||
{
|
||
rcol = 2;
|
||
bgcol = 1;
|
||
}
|
||
} else
|
||
{
|
||
rcol = bgcol = 0;
|
||
switch (*text)
|
||
{
|
||
case ATTR_COLOR:
|
||
xtext_do_chunk (&c);
|
||
rcol = 2;
|
||
break;
|
||
case ATTR_HEXCOLOR:
|
||
xtext_do_chunk (&c);
|
||
{
|
||
int skip = hexcolor_parse ((char *)text + 1, len - 1, NULL, NULL, NULL);
|
||
text += skip;
|
||
len -= skip;
|
||
}
|
||
break;
|
||
case ATTR_BEEP:
|
||
case ATTR_RESET:
|
||
case ATTR_REVERSE:
|
||
case ATTR_BOLD:
|
||
case ATTR_UNDERLINE:
|
||
case ATTR_STRIKETHROUGH:
|
||
case ATTR_ITALICS:
|
||
xtext_do_chunk (&c);
|
||
if (*text == ATTR_RESET)
|
||
c.emph = 0;
|
||
if (*text == ATTR_ITALICS)
|
||
c.emph ^= EMPH_ITAL;
|
||
if (*text == ATTR_BOLD)
|
||
c.emph ^= EMPH_BOLD;
|
||
break;
|
||
case ATTR_HIDDEN:
|
||
xtext_do_chunk (&c);
|
||
c.emph ^= EMPH_HIDDEN;
|
||
hidden = !hidden;
|
||
break;
|
||
default:
|
||
if (strip_hidden == 2 || (!(hidden && strip_hidden)))
|
||
{
|
||
if (c.len1 == 0)
|
||
c.off1 = text - text0;
|
||
memcpy (new_str + i, text, mbl);
|
||
i += mbl;
|
||
c.len1 += mbl;
|
||
}
|
||
}
|
||
}
|
||
text += mbl;
|
||
len -= mbl;
|
||
}
|
||
|
||
bad_utf8: /* Normal ending sequence, and give up if bad utf8 */
|
||
xtext_do_chunk (&c);
|
||
|
||
new_str[i] = 0;
|
||
|
||
if (newlen != NULL)
|
||
*newlen = i;
|
||
|
||
if (slpp)
|
||
*slpp = g_slist_reverse (c.slp);
|
||
else
|
||
g_slist_free_full (c.slp, g_free);
|
||
|
||
return new_str;
|
||
}
|
||
|
||
/* gives width of a string, excluding the mIRC codes */
|
||
|
||
static int
|
||
gtk_xtext_text_width_ent (GtkXText *xtext, textentry *ent)
|
||
{
|
||
unsigned char *new_buf;
|
||
GSList *slp0, *slp;
|
||
int width;
|
||
|
||
if (ent->slp)
|
||
{
|
||
g_slist_free_full (ent->slp, g_free);
|
||
ent->slp = NULL;
|
||
}
|
||
|
||
new_buf = gtk_xtext_strip_color (ent->str, ent->str_len, xtext->scratch_buffer,
|
||
NULL, &slp0, 2);
|
||
|
||
width = backend_get_text_width_slp (xtext, new_buf, slp0);
|
||
ent->slp = slp0;
|
||
|
||
for (slp = slp0; slp; slp = g_slist_next (slp))
|
||
{
|
||
offlen_t *meta;
|
||
|
||
meta = slp->data;
|
||
meta->width = backend_get_text_width_emph (xtext, ent->str + meta->off, meta->len, meta->emph);
|
||
}
|
||
return width;
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_text_width (GtkXText *xtext, unsigned char *text, int len)
|
||
{
|
||
unsigned char *new_buf;
|
||
int new_len;
|
||
GSList *slp;
|
||
int width;
|
||
|
||
new_buf = gtk_xtext_strip_color (text, len, xtext->scratch_buffer,
|
||
&new_len, &slp, !xtext->ignore_hidden);
|
||
|
||
width = backend_get_text_width_slp (xtext, new_buf, slp);
|
||
g_slist_free_full (slp, g_free);
|
||
|
||
return width;
|
||
}
|
||
|
||
/* actually draw text to screen (one run with the same color/attribs) */
|
||
|
||
static int
|
||
gtk_xtext_render_flush (GtkXText * xtext, int x, int y, unsigned char *str,
|
||
int len, int *emphasis, int str_width)
|
||
{
|
||
int dofill;
|
||
|
||
if (xtext->dont_render || len < 1 || xtext->hidden)
|
||
return 0;
|
||
|
||
if (str_width < 0)
|
||
str_width = backend_get_text_width_emph (xtext, str, len, *emphasis);
|
||
|
||
if (xtext->dont_render2)
|
||
return str_width;
|
||
|
||
/* Avoid measuring and drawing runs outside the damaged region. */
|
||
if (x > xtext->clip_x2 || x + str_width < xtext->clip_x)
|
||
return str_width;
|
||
if (y - xtext->font->ascent > xtext->clip_y2 || (y - xtext->font->ascent) + xtext->fontsize < xtext->clip_y)
|
||
return str_width;
|
||
|
||
if (xtext->render_hilights_only)
|
||
{
|
||
if (!xtext->in_hilight) /* is it a hilight prefix? */
|
||
return str_width;
|
||
if (!xtext->un_hilight) /* doing a hilight? no need to draw the text */
|
||
goto dounder;
|
||
}
|
||
|
||
dofill = !xtext->background_surface || xtext->backcolor;
|
||
|
||
backend_draw_text_emph (xtext, dofill, x, y, str, len, str_width, *emphasis);
|
||
|
||
if (xtext->strikethrough)
|
||
{
|
||
cairo_t *cr;
|
||
int strike_y = y - xtext->font->ascent + (xtext->fontsize / 2);
|
||
|
||
cr = xtext_create_context (xtext);
|
||
xtext_draw_line (xtext, cr, &xtext->fgc, x, strike_y, x + str_width - 1, strike_y);
|
||
cairo_destroy (cr);
|
||
}
|
||
|
||
if (xtext->underline)
|
||
{
|
||
dounder:
|
||
{
|
||
cairo_t *cr;
|
||
int underline_y = y + 1;
|
||
|
||
cr = xtext_create_context (xtext);
|
||
xtext_draw_line (xtext, cr, &xtext->fgc, x, underline_y, x + str_width - 1, underline_y);
|
||
cairo_destroy (cr);
|
||
}
|
||
}
|
||
|
||
return str_width;
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_lookup_run_width (textentry *ent, GSList **slp_cache, int off, int len, int emphasis)
|
||
{
|
||
GSList *lp;
|
||
int emph = emphasis & (EMPH_ITAL | EMPH_BOLD);
|
||
|
||
if (len < 1)
|
||
return 0;
|
||
|
||
lp = slp_cache ? *slp_cache : NULL;
|
||
if (!lp)
|
||
lp = ent->slp;
|
||
|
||
while (lp)
|
||
{
|
||
offlen_t *meta = lp->data;
|
||
|
||
if (meta->off < off)
|
||
{
|
||
lp = g_slist_next (lp);
|
||
continue;
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
if (slp_cache)
|
||
*slp_cache = lp;
|
||
|
||
if (lp)
|
||
{
|
||
offlen_t *meta = lp->data;
|
||
|
||
if (meta->off == off && meta->len == len && (meta->emph & (EMPH_ITAL | EMPH_BOLD)) == emph)
|
||
return meta->width;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_reset (GtkXText * xtext, int mark, int attribs)
|
||
{
|
||
if (attribs)
|
||
{
|
||
xtext->underline = FALSE;
|
||
xtext->strikethrough = FALSE;
|
||
xtext->hidden = FALSE;
|
||
}
|
||
if (!mark)
|
||
{
|
||
xtext->backcolor = FALSE;
|
||
if (xtext->col_fore != XTEXT_FG)
|
||
xtext_set_fg (xtext, XTEXT_FG);
|
||
if (xtext->col_back != XTEXT_BG)
|
||
xtext_set_bg (xtext, XTEXT_BG);
|
||
}
|
||
xtext->col_fore = XTEXT_FG;
|
||
xtext->col_back = XTEXT_BG;
|
||
xtext->parsing_color = FALSE;
|
||
xtext->parsing_backcolor = FALSE;
|
||
xtext->nc = 0;
|
||
}
|
||
|
||
/*
|
||
* gtk_xtext_search_offset (buf, ent, off) --
|
||
* Look for arg offset in arg textentry
|
||
* Return one or more flags:
|
||
* GTK_MATCH_MID if we are in a match
|
||
* GTK_MATCH_START if we're at the first byte of it
|
||
* GTK_MATCH_END if we're at the first byte past it
|
||
* GTK_MATCH_CUR if it is the current match
|
||
*/
|
||
#define GTK_MATCH_START 1
|
||
#define GTK_MATCH_MID 2
|
||
#define GTK_MATCH_END 4
|
||
#define GTK_MATCH_CUR 8
|
||
static int
|
||
gtk_xtext_search_offset (xtext_buffer *buf, textentry *ent, unsigned int off)
|
||
{
|
||
GList *gl;
|
||
offsets_t o;
|
||
int flags = 0;
|
||
|
||
for (gl = g_list_first (ent->marks); gl; gl = g_list_next (gl))
|
||
{
|
||
o.u = GPOINTER_TO_UINT (gl->data);
|
||
if (off < o.o.start || off > o.o.end)
|
||
continue;
|
||
flags = GTK_MATCH_MID;
|
||
if (off == o.o.start)
|
||
flags |= GTK_MATCH_START;
|
||
if (off == o.o.end)
|
||
{
|
||
gl = g_list_next (gl);
|
||
if (gl)
|
||
{
|
||
o.u = GPOINTER_TO_UINT (gl->data);
|
||
if (off == o.o.start) /* If subseq match is adjacent */
|
||
{
|
||
flags |= (gl == buf->curmark)? GTK_MATCH_CUR: 0;
|
||
}
|
||
else /* If subseq match is not adjacent */
|
||
{
|
||
flags |= GTK_MATCH_END;
|
||
}
|
||
}
|
||
else /* If there is no subseq match */
|
||
{
|
||
flags |= GTK_MATCH_END;
|
||
}
|
||
}
|
||
else if (gl == buf->curmark) /* If not yet at the end of this match */
|
||
{
|
||
flags |= GTK_MATCH_CUR;
|
||
}
|
||
break;
|
||
}
|
||
return flags;
|
||
}
|
||
|
||
/* render a single line, which WONT wrap, and parse mIRC colors */
|
||
|
||
#define RENDER_FLUSH x += gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis, gtk_xtext_lookup_run_width (ent, &slp_cache, pstr - ent->str, j, *emphasis))
|
||
|
||
static int
|
||
gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent,
|
||
unsigned char *str, int len, int win_width, int indent,
|
||
int line, int left_only, int *x_size_ret, int *emphasis)
|
||
{
|
||
int i = 0, x = indent, j = 0;
|
||
unsigned char *pstr = str;
|
||
int col_num, tmp;
|
||
int offset;
|
||
int mark = FALSE;
|
||
int ret = 1;
|
||
int k;
|
||
int srch_underline = FALSE;
|
||
int srch_mark = FALSE;
|
||
GSList *slp_cache = ent->slp;
|
||
|
||
xtext->in_hilight = FALSE;
|
||
|
||
offset = str - ent->str;
|
||
|
||
if (ent->mark_start != -1 &&
|
||
ent->mark_start <= i + offset && ent->mark_end > i + offset)
|
||
{
|
||
xtext_set_bg (xtext, XTEXT_MARK_BG);
|
||
xtext_set_fg (xtext, XTEXT_MARK_FG);
|
||
xtext->backcolor = TRUE;
|
||
mark = TRUE;
|
||
}
|
||
if (xtext->hilight_ent == ent &&
|
||
xtext->hilight_start <= i + offset && xtext->hilight_end > i + offset)
|
||
{
|
||
if (!xtext->un_hilight)
|
||
{
|
||
xtext->underline = TRUE;
|
||
}
|
||
xtext->in_hilight = TRUE;
|
||
}
|
||
|
||
if (!xtext->skip_border_fills && !xtext->dont_render)
|
||
{
|
||
/* draw background to the left of the text */
|
||
if (str == ent->str && indent > MARGIN && xtext->buffer->time_stamp)
|
||
{
|
||
/* don't overwrite the timestamp */
|
||
if (indent > xtext->stamp_width)
|
||
{
|
||
xtext_draw_bg (xtext, xtext->stamp_width, y - xtext->font->ascent,
|
||
indent - xtext->stamp_width, xtext->fontsize);
|
||
}
|
||
} else
|
||
{
|
||
/* fill the indent area with background gc */
|
||
if (indent >= xtext->clip_x)
|
||
{
|
||
xtext_draw_bg (xtext, 0, y - xtext->font->ascent,
|
||
MIN (indent, xtext->clip_x2), xtext->fontsize);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (xtext->jump_in_offset > 0 && offset < xtext->jump_in_offset)
|
||
xtext->dont_render2 = TRUE;
|
||
|
||
while (i < len)
|
||
{
|
||
|
||
if (xtext->hilight_ent == ent && xtext->hilight_start == (i + offset))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
if (!xtext->un_hilight)
|
||
{
|
||
xtext->underline = TRUE;
|
||
}
|
||
|
||
xtext->in_hilight = TRUE;
|
||
}
|
||
|
||
if ((xtext->parsing_color && isdigit (str[i]) && xtext->nc < 2) ||
|
||
(xtext->parsing_color && str[i] == ',' && isdigit (str[i+1]) && xtext->nc < 3 && !xtext->parsing_backcolor))
|
||
{
|
||
pstr++;
|
||
if (str[i] == ',')
|
||
{
|
||
xtext->parsing_backcolor = TRUE;
|
||
if (xtext->nc)
|
||
{
|
||
xtext->num[xtext->nc] = 0;
|
||
xtext->nc = 0;
|
||
col_num = atoi (xtext->num);
|
||
if (col_num == 99) /* mIRC lameness */
|
||
col_num = XTEXT_FG;
|
||
else
|
||
if (col_num > XTEXT_MAX_COLOR)
|
||
col_num = col_num % XTEXT_MIRC_COLS;
|
||
xtext->col_fore = col_num;
|
||
if (!mark)
|
||
xtext_set_fg (xtext, col_num);
|
||
}
|
||
} else
|
||
{
|
||
xtext->num[xtext->nc] = str[i];
|
||
if (xtext->nc < 7)
|
||
xtext->nc++;
|
||
}
|
||
} else
|
||
{
|
||
if (xtext->parsing_color)
|
||
{
|
||
xtext->parsing_color = FALSE;
|
||
if (xtext->nc)
|
||
{
|
||
xtext->num[xtext->nc] = 0;
|
||
xtext->nc = 0;
|
||
col_num = atoi (xtext->num);
|
||
if (xtext->parsing_backcolor)
|
||
{
|
||
if (col_num == 99) /* mIRC lameness */
|
||
col_num = XTEXT_BG;
|
||
else
|
||
if (col_num > XTEXT_MAX_COLOR)
|
||
col_num = col_num % XTEXT_MIRC_COLS;
|
||
if (col_num == XTEXT_BG)
|
||
xtext->backcolor = FALSE;
|
||
else
|
||
xtext->backcolor = TRUE;
|
||
if (!mark)
|
||
xtext_set_bg (xtext, col_num);
|
||
xtext->col_back = col_num;
|
||
} else
|
||
{
|
||
if (col_num == 99) /* mIRC lameness */
|
||
col_num = XTEXT_FG;
|
||
else
|
||
if (col_num > XTEXT_MAX_COLOR)
|
||
col_num = col_num % XTEXT_MIRC_COLS;
|
||
if (!mark)
|
||
xtext_set_fg (xtext, col_num);
|
||
xtext->col_fore = col_num;
|
||
}
|
||
xtext->parsing_backcolor = FALSE;
|
||
} else
|
||
{
|
||
/* got a \003<non-digit>... i.e. reset colors */
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
gtk_xtext_reset (xtext, mark, FALSE);
|
||
}
|
||
}
|
||
|
||
if (!left_only && !mark &&
|
||
(k = gtk_xtext_search_offset (xtext->buffer, ent, offset + i)))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
if (!(xtext->buffer->search_flags & highlight))
|
||
{
|
||
if (k & GTK_MATCH_CUR)
|
||
{
|
||
xtext_set_bg (xtext, XTEXT_MARK_BG);
|
||
xtext_set_fg (xtext, XTEXT_MARK_FG);
|
||
xtext->backcolor = TRUE;
|
||
srch_mark = TRUE;
|
||
} else
|
||
{
|
||
xtext_set_bg (xtext, xtext->col_back);
|
||
xtext_set_fg (xtext, xtext->col_fore);
|
||
xtext->backcolor = (xtext->col_back != XTEXT_BG)? TRUE: FALSE;
|
||
srch_mark = FALSE;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
xtext->underline = (k & GTK_MATCH_CUR)? TRUE: FALSE;
|
||
if (k & (GTK_MATCH_START | GTK_MATCH_MID))
|
||
{
|
||
xtext_set_bg (xtext, XTEXT_MARK_BG);
|
||
xtext_set_fg (xtext, XTEXT_MARK_FG);
|
||
xtext->backcolor = TRUE;
|
||
srch_mark = TRUE;
|
||
}
|
||
if (k & GTK_MATCH_END)
|
||
{
|
||
xtext_set_bg (xtext, xtext->col_back);
|
||
xtext_set_fg (xtext, xtext->col_fore);
|
||
xtext->backcolor = (xtext->col_back != XTEXT_BG)? TRUE: FALSE;
|
||
srch_mark = FALSE;
|
||
xtext->underline = FALSE;
|
||
}
|
||
srch_underline = xtext->underline;
|
||
}
|
||
}
|
||
|
||
switch (str[i])
|
||
{
|
||
case '\n':
|
||
/*case ATTR_BEEP:*/
|
||
break;
|
||
case ATTR_REVERSE:
|
||
RENDER_FLUSH;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
tmp = xtext->col_fore;
|
||
xtext->col_fore = xtext->col_back;
|
||
xtext->col_back = tmp;
|
||
if (!mark)
|
||
{
|
||
xtext_set_fg (xtext, xtext->col_fore);
|
||
xtext_set_bg (xtext, xtext->col_back);
|
||
}
|
||
if (xtext->col_back != XTEXT_BG)
|
||
xtext->backcolor = TRUE;
|
||
else
|
||
xtext->backcolor = FALSE;
|
||
break;
|
||
case ATTR_BOLD:
|
||
RENDER_FLUSH;
|
||
*emphasis ^= EMPH_BOLD;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_UNDERLINE:
|
||
RENDER_FLUSH;
|
||
xtext->underline = !xtext->underline;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_STRIKETHROUGH:
|
||
RENDER_FLUSH;
|
||
xtext->strikethrough = !xtext->strikethrough;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_ITALICS:
|
||
RENDER_FLUSH;
|
||
*emphasis ^= EMPH_ITAL;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_HIDDEN:
|
||
RENDER_FLUSH;
|
||
xtext->hidden = (!xtext->hidden) & (!xtext->ignore_hidden);
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_RESET:
|
||
RENDER_FLUSH;
|
||
*emphasis = 0;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
gtk_xtext_reset (xtext, mark, !xtext->in_hilight);
|
||
break;
|
||
case ATTR_COLOR:
|
||
RENDER_FLUSH;
|
||
xtext->parsing_color = TRUE;
|
||
pstr += j + 1;
|
||
j = 0;
|
||
break;
|
||
case ATTR_HEXCOLOR:
|
||
RENDER_FLUSH;
|
||
{
|
||
guint32 fg = 0, bg = 0;
|
||
int has_bg;
|
||
int skip = hexcolor_parse ((char *)str + i + 1, len - i - 1, &fg, &bg, &has_bg);
|
||
|
||
if (skip == 0)
|
||
gtk_xtext_reset (xtext, mark, FALSE);
|
||
else
|
||
{
|
||
xtext_rgb_to_color (fg, &xtext->hex_fg);
|
||
xtext->col_fore = XTEXT_HEX_FG;
|
||
if (!mark)
|
||
xtext_set_fg (xtext, XTEXT_HEX_FG);
|
||
if (has_bg)
|
||
{
|
||
xtext_rgb_to_color (bg, &xtext->hex_bg);
|
||
xtext->col_back = XTEXT_HEX_BG;
|
||
xtext->backcolor = TRUE;
|
||
if (!mark)
|
||
xtext_set_bg (xtext, XTEXT_HEX_BG);
|
||
}
|
||
}
|
||
pstr += j + 1 + skip;
|
||
j = 0;
|
||
i += skip;
|
||
}
|
||
break;
|
||
default:
|
||
tmp = charlen (str + i);
|
||
/* invalid utf8 safe guard */
|
||
if (tmp + i > len)
|
||
tmp = len - i;
|
||
j += tmp; /* move to the next utf8 char */
|
||
}
|
||
}
|
||
i += charlen (str + i); /* move to the next utf8 char */
|
||
/* invalid utf8 safe guard */
|
||
if (i > len)
|
||
i = len;
|
||
|
||
/* Separate the left part, the space and the right part
|
||
into separate runs, and reset bidi state inbetween.
|
||
Perform this only on the first line of the message.
|
||
*/
|
||
if (offset == 0)
|
||
{
|
||
/* we've reached the end of the left part? */
|
||
if ((pstr-str)+j == ent->left_len)
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
}
|
||
else if ((pstr-str)+j == ent->left_len+1)
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
}
|
||
}
|
||
|
||
/* have we been told to stop rendering at this point? */
|
||
if (xtext->jump_out_offset > 0 && xtext->jump_out_offset <= (i + offset))
|
||
{
|
||
gtk_xtext_render_flush (xtext, x, y, pstr, j, emphasis,
|
||
gtk_xtext_lookup_run_width (ent, &slp_cache, pstr - ent->str, j, *emphasis));
|
||
ret = 0; /* skip the rest of the lines, we're done. */
|
||
j = 0;
|
||
break;
|
||
}
|
||
|
||
if (xtext->jump_in_offset > 0 && xtext->jump_in_offset == (i + offset))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
xtext->dont_render2 = FALSE;
|
||
}
|
||
|
||
if (xtext->hilight_ent == ent && xtext->hilight_end == (i + offset))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
xtext->underline = FALSE;
|
||
xtext->in_hilight = FALSE;
|
||
if (xtext->render_hilights_only)
|
||
{
|
||
/* stop drawing this ent */
|
||
ret = 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!mark && ent->mark_start == (i + offset))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
xtext_set_bg (xtext, XTEXT_MARK_BG);
|
||
xtext_set_fg (xtext, XTEXT_MARK_FG);
|
||
xtext->backcolor = TRUE;
|
||
if (srch_underline)
|
||
{
|
||
xtext->underline = FALSE;
|
||
srch_underline = FALSE;
|
||
}
|
||
mark = TRUE;
|
||
}
|
||
|
||
if (mark && ent->mark_end == (i + offset))
|
||
{
|
||
RENDER_FLUSH;
|
||
pstr += j;
|
||
j = 0;
|
||
xtext_set_bg (xtext, xtext->col_back);
|
||
xtext_set_fg (xtext, xtext->col_fore);
|
||
if (xtext->col_back != XTEXT_BG)
|
||
xtext->backcolor = TRUE;
|
||
else
|
||
xtext->backcolor = FALSE;
|
||
mark = FALSE;
|
||
}
|
||
|
||
}
|
||
|
||
if (j)
|
||
RENDER_FLUSH;
|
||
|
||
if (mark || srch_mark)
|
||
{
|
||
xtext_set_bg (xtext, xtext->col_back);
|
||
xtext_set_fg (xtext, xtext->col_fore);
|
||
if (xtext->col_back != XTEXT_BG)
|
||
xtext->backcolor = TRUE;
|
||
else
|
||
xtext->backcolor = FALSE;
|
||
}
|
||
|
||
/* draw background to the right of the text */
|
||
if (!left_only && !xtext->dont_render)
|
||
{
|
||
/* draw separator now so it doesn't appear to flicker */
|
||
gtk_xtext_draw_sep (xtext, y - xtext->font->ascent);
|
||
if (!xtext->skip_border_fills && xtext->clip_x2 >= x)
|
||
{
|
||
int xx = MAX (x, xtext->clip_x);
|
||
|
||
xtext_draw_bg (xtext,
|
||
xx, /* x */
|
||
y - xtext->font->ascent, /* y */
|
||
MIN (xtext->clip_x2 - xx, (win_width + MARGIN) - xx), /* width */
|
||
xtext->fontsize); /* height */
|
||
}
|
||
}
|
||
|
||
xtext->dont_render2 = FALSE;
|
||
|
||
/* return how much we drew in the x direction */
|
||
if (x_size_ret)
|
||
*x_size_ret = x - indent;
|
||
|
||
return ret;
|
||
}
|
||
|
||
/* walk through str until this line doesn't fit anymore */
|
||
|
||
static int
|
||
find_next_wrap (GtkXText * xtext, textentry * ent, unsigned char *str,
|
||
int win_width, int indent)
|
||
{
|
||
unsigned char *last_space = str;
|
||
unsigned char *orig_str = str;
|
||
int str_width = indent;
|
||
int rcol = 0, bgcol = 0;
|
||
int hidden = FALSE;
|
||
int mbl;
|
||
int char_width;
|
||
int ret;
|
||
int limit_offset = 0;
|
||
int emphasis = 0;
|
||
GSList *lp;
|
||
|
||
/* single liners */
|
||
if (win_width >= ent->str_width + ent->indent)
|
||
return ent->str_len;
|
||
|
||
/* it does happen! */
|
||
if (win_width < 1)
|
||
{
|
||
ret = ent->str_len - (str - ent->str);
|
||
goto done;
|
||
}
|
||
|
||
/* Find emphasis value for the offset that is the first byte of our string */
|
||
for (lp = ent->slp; lp; lp = g_slist_next (lp))
|
||
{
|
||
offlen_t *meta = lp->data;
|
||
unsigned char *start, *end;
|
||
|
||
start = ent->str + meta->off;
|
||
end = start + meta->len;
|
||
if (str >= start && str < end)
|
||
{
|
||
emphasis = meta->emph;
|
||
break;
|
||
}
|
||
}
|
||
|
||
while (1)
|
||
{
|
||
if (rcol > 0 && (isdigit (*str) || (*str == ',' && isdigit (str[1]) && !bgcol)))
|
||
{
|
||
if (str[1] != ',') rcol--;
|
||
if (*str == ',')
|
||
{
|
||
rcol = 2;
|
||
bgcol = 1;
|
||
}
|
||
limit_offset++;
|
||
str++;
|
||
} else
|
||
{
|
||
rcol = bgcol = 0;
|
||
switch (*str)
|
||
{
|
||
case ATTR_COLOR:
|
||
rcol = 2;
|
||
case ATTR_BEEP:
|
||
case ATTR_RESET:
|
||
case ATTR_REVERSE:
|
||
case ATTR_BOLD:
|
||
case ATTR_UNDERLINE:
|
||
case ATTR_STRIKETHROUGH:
|
||
case ATTR_ITALICS:
|
||
if (*str == ATTR_RESET)
|
||
emphasis = 0;
|
||
if (*str == ATTR_ITALICS)
|
||
emphasis ^= EMPH_ITAL;
|
||
if (*str == ATTR_BOLD)
|
||
emphasis ^= EMPH_BOLD;
|
||
limit_offset++;
|
||
str++;
|
||
break;
|
||
case ATTR_HEXCOLOR:
|
||
{
|
||
int skip = hexcolor_parse ((char *)str + 1, (ent->str + ent->str_len) - str - 1, NULL, NULL, NULL);
|
||
limit_offset += 1 + skip;
|
||
str += 1 + skip;
|
||
}
|
||
break;
|
||
case ATTR_HIDDEN:
|
||
if (xtext->ignore_hidden)
|
||
goto def;
|
||
hidden = !hidden;
|
||
limit_offset++;
|
||
str++;
|
||
break;
|
||
default:
|
||
def:
|
||
mbl = charlen (str);
|
||
char_width = backend_get_text_width_emph (xtext, str, mbl, emphasis);
|
||
if (!hidden) str_width += char_width;
|
||
if (str_width > win_width)
|
||
{
|
||
if (xtext->wordwrap)
|
||
{
|
||
if (str - last_space > WORDWRAP_LIMIT + limit_offset)
|
||
ret = str - orig_str; /* fall back to character wrap */
|
||
else
|
||
{
|
||
if (*last_space == ' ')
|
||
last_space++;
|
||
ret = last_space - orig_str;
|
||
if (ret == 0) /* fall back to character wrap */
|
||
ret = str - orig_str;
|
||
}
|
||
goto done;
|
||
}
|
||
ret = str - orig_str;
|
||
goto done;
|
||
}
|
||
|
||
/* keep a record of the last space, for wordwrapping */
|
||
if (is_del (*str))
|
||
{
|
||
last_space = str;
|
||
limit_offset = 0;
|
||
}
|
||
|
||
/* progress to the next char */
|
||
str += mbl;
|
||
|
||
}
|
||
}
|
||
|
||
if (str >= ent->str + ent->str_len)
|
||
{
|
||
ret = str - orig_str;
|
||
goto done;
|
||
}
|
||
}
|
||
|
||
done:
|
||
|
||
/* must make progress */
|
||
if (ret < 1)
|
||
ret = MIN ((int)charlen (orig_str), ent->str_len - (orig_str - ent->str));
|
||
|
||
return ret;
|
||
}
|
||
|
||
/* find the offset, in bytes, that wrap number 'line' starts at */
|
||
|
||
static int
|
||
gtk_xtext_find_subline (GtkXText *xtext, textentry *ent, int line)
|
||
{
|
||
int rlen = 0;
|
||
|
||
if (line > 0)
|
||
{
|
||
rlen = gtk_xtext_subline_end (ent, line - 1);
|
||
if (rlen == 0)
|
||
rlen = ent->str_len;
|
||
}
|
||
return rlen;
|
||
}
|
||
|
||
/* Render a timestamp with the normal IRC formatting and selection rules. */
|
||
|
||
static void
|
||
gtk_xtext_render_stamp (GtkXText * xtext, textentry * ent,
|
||
char *text, int len, int line, int win_width)
|
||
{
|
||
textentry tmp_ent;
|
||
int jo, ji, hs;
|
||
int xsize, y, emphasis;
|
||
|
||
tmp_ent = *ent;
|
||
ent = &tmp_ent;
|
||
ent->str = (guchar *)text;
|
||
ent->str_len = len;
|
||
ent->slp = NULL;
|
||
ent->marks = NULL;
|
||
ent->left_len = -1;
|
||
jo = xtext->jump_out_offset; /* back these up */
|
||
ji = xtext->jump_in_offset;
|
||
hs = xtext->hilight_start;
|
||
xtext->jump_out_offset = 0;
|
||
xtext->jump_in_offset = 0;
|
||
xtext->hilight_start = 0xffff; /* temp disable */
|
||
emphasis = 0;
|
||
|
||
if (xtext->mark_stamp)
|
||
{
|
||
/* if this line is marked, mark this stamp too */
|
||
if (ent->mark_start == 0)
|
||
{
|
||
ent->mark_start = 0;
|
||
ent->mark_end = len;
|
||
}
|
||
else
|
||
{
|
||
ent->mark_start = -1;
|
||
ent->mark_end = -1;
|
||
}
|
||
}
|
||
|
||
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
|
||
gtk_xtext_render_str (xtext, y, ent, text, len,
|
||
win_width, 2, line, TRUE, &xsize, &emphasis);
|
||
|
||
/* restore everything back to how it was */
|
||
xtext->jump_out_offset = jo;
|
||
xtext->jump_in_offset = ji;
|
||
xtext->hilight_start = hs;
|
||
|
||
/* with a non-fixed-width font, sometimes we don't draw enough
|
||
background i.e. when this stamp is shorter than xtext->stamp_width */
|
||
xsize += MARGIN;
|
||
if (xsize < xtext->stamp_width)
|
||
{
|
||
y -= xtext->font->ascent;
|
||
xtext_draw_bg (xtext,
|
||
xsize, /* x */
|
||
y, /* y */
|
||
xtext->stamp_width - xsize, /* width */
|
||
xtext->fontsize /* height */);
|
||
}
|
||
}
|
||
|
||
/* render a single line, which may wrap to more lines */
|
||
|
||
static int
|
||
gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
|
||
int lines_max, int subline, int win_width)
|
||
{
|
||
unsigned char *str;
|
||
int indent, taken, entline, len, y, start_subline;
|
||
int emphasis = 0;
|
||
int image_rows;
|
||
|
||
entline = taken = 0;
|
||
str = ent->str;
|
||
indent = ent->indent;
|
||
start_subline = subline;
|
||
|
||
/* draw each line one by one */
|
||
do
|
||
{
|
||
if (entline > 0)
|
||
len = gtk_xtext_subline_end (ent, entline) - gtk_xtext_subline_end (ent, entline - 1);
|
||
else
|
||
len = gtk_xtext_subline_end (ent, entline);
|
||
|
||
entline++;
|
||
|
||
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
|
||
if (!subline)
|
||
{
|
||
int bg_x;
|
||
int bg_w;
|
||
|
||
if (!xtext->dont_render)
|
||
{
|
||
bg_x = MAX (0, xtext->clip_x);
|
||
bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
|
||
if (bg_w > 0)
|
||
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
|
||
}
|
||
|
||
if (entline == 1 && xtext->auto_indent && xtext->buffer->time_stamp &&
|
||
(!xtext->skip_stamp || xtext->mark_stamp || xtext->force_stamp))
|
||
{
|
||
char *time_str;
|
||
int stamp_len;
|
||
|
||
stamp_len = xtext_get_stamp_str (ent->stamp, &time_str);
|
||
gtk_xtext_render_stamp (xtext, ent, time_str, stamp_len, line, win_width);
|
||
g_free (time_str);
|
||
}
|
||
|
||
if (!gtk_xtext_render_str (xtext, y, ent, str, len, win_width,
|
||
indent, line, FALSE, NULL, &emphasis))
|
||
{
|
||
/* small optimization */
|
||
gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline + 1));
|
||
return ent->subline_count - subline;
|
||
}
|
||
} else
|
||
{
|
||
xtext->dont_render = TRUE;
|
||
gtk_xtext_render_str (xtext, y, ent, str, len, win_width,
|
||
indent, line, FALSE, NULL, &emphasis);
|
||
xtext->dont_render = FALSE;
|
||
subline--;
|
||
line--;
|
||
taken--;
|
||
}
|
||
|
||
indent = gtk_xtext_cont_indent (xtext->buffer, ent);
|
||
line++;
|
||
taken++;
|
||
str += len;
|
||
|
||
if (line >= lines_max)
|
||
break;
|
||
|
||
}
|
||
while (str < ent->str + ent->str_len);
|
||
|
||
/* draw the inline image on its blank rows below the text. 'subline'
|
||
still holds the rows left to skip when the page starts inside the
|
||
image area (the text loop above only consumes text sublines). */
|
||
image_rows = gtk_xtext_image_rows (xtext, ent);
|
||
if (image_rows > 0)
|
||
{
|
||
int skip = MIN (subline, image_rows);
|
||
int rows = image_rows - skip;
|
||
int first_row_y = (xtext->fontsize * line) - xtext->pixel_offset;
|
||
int drawn = 0;
|
||
gboolean render_image = !xtext->dont_render && !xtext->skip_border_fills &&
|
||
!xtext->render_hilights_only;
|
||
|
||
while (rows > 0 && line < lines_max)
|
||
{
|
||
y = (xtext->fontsize * line) + xtext->font->ascent - xtext->pixel_offset;
|
||
if (render_image)
|
||
{
|
||
int bg_x = MAX (0, xtext->clip_x);
|
||
int bg_w = MIN (win_width + MARGIN, xtext->clip_x2) - bg_x;
|
||
|
||
if (bg_w > 0)
|
||
xtext_draw_bg (xtext, bg_x, y - xtext->font->ascent, bg_w, xtext->fontsize);
|
||
gtk_xtext_draw_sep (xtext, y - xtext->font->ascent);
|
||
}
|
||
line++;
|
||
taken++;
|
||
rows--;
|
||
drawn++;
|
||
}
|
||
|
||
if (drawn > 0 && render_image)
|
||
{
|
||
/* this may be the shared draw context, so the clip has to be
|
||
undone before anything else is rendered with it */
|
||
cairo_t *cr = xtext_create_context (xtext);
|
||
int img_x = xtext->buffer->indent + XTEXT_IMAGE_PADDING;
|
||
int img_y = first_row_y - (skip * xtext->fontsize) + XTEXT_IMAGE_PADDING;
|
||
|
||
cairo_save (cr);
|
||
cairo_rectangle (cr, 0, first_row_y, win_width + MARGIN,
|
||
drawn * xtext->fontsize);
|
||
cairo_clip (cr);
|
||
gdk_cairo_set_source_pixbuf (cr, ent->image, img_x, img_y);
|
||
cairo_paint (cr);
|
||
cairo_restore (cr);
|
||
cairo_destroy (cr);
|
||
}
|
||
}
|
||
|
||
gtk_xtext_draw_marker (xtext, ent, y - xtext->fontsize * (taken + start_subline));
|
||
|
||
return taken;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_palette (GtkXText * xtext, const XTextColor *palette)
|
||
{
|
||
int i;
|
||
|
||
for (i = (XTEXT_COLS-1); i >= 0; i--)
|
||
{
|
||
xtext->palette[i] = palette[i];
|
||
}
|
||
|
||
if (gtk_widget_get_realized (GTK_WIDGET(xtext)))
|
||
{
|
||
xtext_set_fg (xtext, XTEXT_FG);
|
||
xtext_set_bg (xtext, XTEXT_BG);
|
||
|
||
xtext->marker_gc = xtext->palette[XTEXT_MARKER];
|
||
}
|
||
xtext->col_fore = XTEXT_FG;
|
||
xtext->col_back = XTEXT_BG;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_fix_indent (xtext_buffer *buf)
|
||
{
|
||
int j;
|
||
|
||
/* make indent a multiple of the space width */
|
||
if (buf->indent && buf->xtext->space_width)
|
||
{
|
||
j = 0;
|
||
while (j < buf->indent)
|
||
{
|
||
j += buf->xtext->space_width;
|
||
}
|
||
buf->indent = j;
|
||
}
|
||
|
||
dontscroll (buf); /* force scrolling off */
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_recalc_widths (xtext_buffer *buf, int do_str_width)
|
||
{
|
||
textentry *ent;
|
||
int prefix_width;
|
||
|
||
/* since we have a new font, we have to recalc the text widths */
|
||
ent = buf->text_first;
|
||
while (ent)
|
||
{
|
||
if (do_str_width)
|
||
{
|
||
ent->str_width = gtk_xtext_text_width_ent (buf->xtext, ent);
|
||
}
|
||
if (ent->left_len != -1)
|
||
{
|
||
prefix_width = gtk_xtext_text_width (buf->xtext, ent->str,
|
||
ent->left_len + 1);
|
||
ent->indent =
|
||
(buf->indent - prefix_width);
|
||
if (ent->indent < MARGIN)
|
||
ent->indent = MARGIN;
|
||
}
|
||
ent = ent->next;
|
||
}
|
||
|
||
gtk_xtext_calc_lines (buf, FALSE);
|
||
}
|
||
|
||
int
|
||
gtk_xtext_set_font (GtkXText *xtext, char *name)
|
||
{
|
||
|
||
if (xtext->font)
|
||
backend_font_close (xtext);
|
||
|
||
/* realize now, so that font_open has a XDisplay */
|
||
gtk_widget_realize (GTK_WIDGET (xtext));
|
||
|
||
backend_font_open (xtext, name);
|
||
if (xtext->font == NULL)
|
||
return FALSE;
|
||
|
||
{
|
||
char *time_str;
|
||
int stamp_size = xtext_get_stamp_str (time(0), &time_str);
|
||
xtext->stamp_width =
|
||
gtk_xtext_text_width (xtext, time_str, stamp_size) + MARGIN;
|
||
g_free (time_str);
|
||
}
|
||
|
||
gtk_xtext_fix_indent (xtext->buffer);
|
||
|
||
if (gtk_widget_get_realized (GTK_WIDGET(xtext)))
|
||
gtk_xtext_recalc_widths (xtext->buffer, TRUE);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_background (GtkXText * xtext, cairo_surface_t *surface)
|
||
{
|
||
/* The caller may pass our current surface back to us. */
|
||
if (surface)
|
||
cairo_surface_reference (surface);
|
||
if (xtext->background_surface)
|
||
{
|
||
cairo_surface_destroy (xtext->background_surface);
|
||
xtext->background_surface = NULL;
|
||
}
|
||
|
||
if (xtext->background_clip_surface)
|
||
{
|
||
cairo_surface_destroy (xtext->background_clip_surface);
|
||
xtext->background_clip_surface = NULL;
|
||
}
|
||
|
||
dontscroll (xtext->buffer);
|
||
if (surface)
|
||
{
|
||
xtext->background_surface = surface;
|
||
}
|
||
|
||
if (surface && gtk_widget_get_realized (GTK_WIDGET(xtext)))
|
||
{
|
||
xtext->ts_x = xtext->ts_y = 0;
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_write_all (int fh, const char *text, int len)
|
||
{
|
||
while (len > 0)
|
||
{
|
||
int written = write (fh, text, len);
|
||
if (written < 0 && errno == EINTR)
|
||
continue;
|
||
if (written <= 0)
|
||
return FALSE;
|
||
text += written;
|
||
len -= written;
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_save (GtkXText * xtext, int fh)
|
||
{
|
||
textentry *ent;
|
||
int newlen;
|
||
char *buf;
|
||
|
||
ent = xtext->buffer->text_first;
|
||
while (ent)
|
||
{
|
||
gboolean saved;
|
||
buf = gtk_xtext_strip_color (ent->str, ent->str_len, NULL,
|
||
&newlen, NULL, FALSE);
|
||
saved = gtk_xtext_write_all (fh, buf, newlen) &&
|
||
gtk_xtext_write_all (fh, "\n", 1);
|
||
g_free (buf);
|
||
if (!saved)
|
||
return;
|
||
ent = ent->next;
|
||
}
|
||
}
|
||
|
||
/* count how many lines 'ent' will take (with wraps) */
|
||
|
||
static int
|
||
gtk_xtext_lines_taken (xtext_buffer *buf, textentry *ent)
|
||
{
|
||
guchar *str = ent->str;
|
||
int indent = ent->indent;
|
||
int win_width = buf->window_width - MARGIN;
|
||
int end, len, image_rows;
|
||
|
||
if (ent->sublines)
|
||
g_array_set_size (ent->sublines, 0);
|
||
else
|
||
ent->sublines = g_array_new (FALSE, FALSE, sizeof (int));
|
||
|
||
if (win_width >= ent->indent + ent->str_width || ent->str_len == 0)
|
||
{
|
||
end = ent->str_len;
|
||
g_array_append_val (ent->sublines, end);
|
||
}
|
||
else
|
||
{
|
||
do
|
||
{
|
||
len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
|
||
str += len;
|
||
end = str - ent->str;
|
||
g_array_append_val (ent->sublines, end);
|
||
indent = gtk_xtext_cont_indent (buf, ent);
|
||
}
|
||
while (str < ent->str + ent->str_len);
|
||
}
|
||
|
||
/* Blank image rows end at the text's final byte offset. */
|
||
end = ent->str_len;
|
||
image_rows = gtk_xtext_image_rows (buf->xtext, ent);
|
||
while (image_rows-- > 0)
|
||
g_array_append_val (ent->sublines, end);
|
||
|
||
ent->subline_count = ent->sublines->len;
|
||
return ent->subline_count;
|
||
}
|
||
|
||
/* Calculate number of actual lines (with wraps), to set adj->lower. *
|
||
* This should only be called when the window resizes. */
|
||
|
||
static void
|
||
gtk_xtext_calc_lines (xtext_buffer *buf, int fire_signal)
|
||
{
|
||
textentry *ent;
|
||
int width;
|
||
int height;
|
||
int lines;
|
||
GdkWindow *window;
|
||
|
||
window = gtk_widget_get_window (GTK_WIDGET (buf->xtext));
|
||
if (!window)
|
||
return;
|
||
height = gdk_window_get_height (window);
|
||
width = gdk_window_get_width (window);
|
||
width -= MARGIN;
|
||
|
||
if (width < 30 || height < buf->xtext->fontsize || width < buf->indent + 30)
|
||
return;
|
||
|
||
lines = 0;
|
||
ent = buf->text_first;
|
||
while (ent)
|
||
{
|
||
lines += gtk_xtext_lines_taken (buf, ent);
|
||
ent = ent->next;
|
||
}
|
||
|
||
buf->pagetop_ent = NULL;
|
||
buf->num_lines = lines;
|
||
gtk_xtext_adjustment_set (buf, fire_signal);
|
||
}
|
||
|
||
/* find the n-th line in the linked list, this includes wrap calculations */
|
||
|
||
static textentry *
|
||
gtk_xtext_nth (GtkXText *xtext, int line, int *subline)
|
||
{
|
||
int lines = 0;
|
||
textentry *ent;
|
||
|
||
ent = xtext->buffer->text_first;
|
||
|
||
/* -- optimization -- try to make a short-cut using the pagetop ent */
|
||
if (xtext->buffer->pagetop_ent)
|
||
{
|
||
if (line == xtext->buffer->pagetop_line)
|
||
{
|
||
*subline = xtext->buffer->pagetop_subline;
|
||
return xtext->buffer->pagetop_ent;
|
||
}
|
||
if (line > xtext->buffer->pagetop_line)
|
||
{
|
||
/* lets start from the pagetop instead of the absolute beginning */
|
||
ent = xtext->buffer->pagetop_ent;
|
||
lines = xtext->buffer->pagetop_line - xtext->buffer->pagetop_subline;
|
||
}
|
||
else if (line > xtext->buffer->pagetop_line - line)
|
||
{
|
||
/* move backwards from pagetop */
|
||
ent = xtext->buffer->pagetop_ent;
|
||
lines = xtext->buffer->pagetop_line - xtext->buffer->pagetop_subline;
|
||
while (1)
|
||
{
|
||
if (lines <= line)
|
||
{
|
||
*subline = line - lines;
|
||
return ent;
|
||
}
|
||
ent = ent->prev;
|
||
if (!ent)
|
||
break;
|
||
lines -= ent->subline_count;
|
||
}
|
||
return NULL;
|
||
}
|
||
}
|
||
/* -- end of optimization -- */
|
||
|
||
while (ent)
|
||
{
|
||
lines += ent->subline_count;
|
||
if (lines > line)
|
||
{
|
||
*subline = ent->subline_count - (lines - line);
|
||
return ent;
|
||
}
|
||
ent = ent->next;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* render enta (or an inclusive range enta->entb) */
|
||
|
||
static void
|
||
gtk_xtext_render_ents (GtkXText *xtext, textentry *enta, textentry *entb)
|
||
{
|
||
/* GTK coalesces selection, hover and append changes into the next frame. */
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
}
|
||
|
||
/* render a whole page/window, starting from 'startline' */
|
||
|
||
static void
|
||
gtk_xtext_render_page (GtkXText * xtext)
|
||
{
|
||
/*
|
||
* GTK3/Wayland is frame-driven. Drawing directly to a GdkWindow outside the
|
||
* widget's ::draw handler can result in the compositor never presenting the
|
||
* new buffer. Symptom: chat only updates after you move/resize the window.
|
||
*
|
||
* If we're not currently inside ::draw, xtext->draw_cr is NULL. In that case
|
||
* just request a redraw and let the normal GTK paint cycle do the work.
|
||
*/
|
||
if (xtext->draw_cr == NULL)
|
||
{
|
||
GtkWidget *w = GTK_WIDGET (xtext);
|
||
if (gtk_widget_get_realized (w))
|
||
gtk_widget_queue_draw (w);
|
||
return;
|
||
}
|
||
|
||
textentry *ent;
|
||
int line;
|
||
int lines_max;
|
||
int width;
|
||
int height;
|
||
int subline;
|
||
gdouble adj_value = gtk_adjustment_get_value (xtext->adj);
|
||
int startline = adj_value;
|
||
GdkWindow *window;
|
||
|
||
if(!gtk_widget_get_realized(GTK_WIDGET(xtext)))
|
||
return;
|
||
|
||
if (xtext->buffer->indent < MARGIN)
|
||
xtext->buffer->indent = MARGIN; /* 2 pixels is our left margin */
|
||
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return;
|
||
width = gdk_window_get_width (window);
|
||
height = gdk_window_get_height (window);
|
||
|
||
if (width < 34 || height < xtext->fontsize || width < xtext->buffer->indent + 32)
|
||
return;
|
||
|
||
xtext->pixel_offset = (adj_value - startline) *
|
||
xtext->fontsize;
|
||
|
||
subline = line = 0;
|
||
ent = xtext->buffer->text_first;
|
||
|
||
if (startline > 0)
|
||
ent = gtk_xtext_nth (xtext, startline, &subline);
|
||
|
||
xtext->buffer->pagetop_ent = ent;
|
||
xtext->buffer->pagetop_subline = subline;
|
||
xtext->buffer->pagetop_line = startline;
|
||
|
||
xtext->buffer->last_pixel_pos = adj_value * xtext->fontsize;
|
||
|
||
width -= MARGIN;
|
||
lines_max = ((height + xtext->pixel_offset) / xtext->fontsize) + 1;
|
||
|
||
while (ent)
|
||
{
|
||
gtk_xtext_reset (xtext, FALSE, TRUE);
|
||
line += gtk_xtext_render_line (xtext, ent, line, lines_max,
|
||
subline, width);
|
||
subline = 0;
|
||
|
||
if (line >= lines_max)
|
||
break;
|
||
|
||
ent = ent->next;
|
||
}
|
||
|
||
line = (xtext->fontsize * line) - xtext->pixel_offset;
|
||
/* fill any space below the last line with our background GC */
|
||
xtext_draw_bg (xtext, 0, line, width + MARGIN, height - line);
|
||
|
||
/* draw the separator line */
|
||
gtk_xtext_draw_sep (xtext, -1);
|
||
}
|
||
|
||
void
|
||
gtk_xtext_refresh (GtkXText * xtext)
|
||
{
|
||
if (gtk_widget_get_realized (GTK_WIDGET (xtext)))
|
||
{
|
||
gtk_xtext_render_page (xtext);
|
||
}
|
||
}
|
||
|
||
static int
|
||
gtk_xtext_kill_ent (xtext_buffer *buffer, textentry *ent)
|
||
{
|
||
int visible;
|
||
|
||
/* Set visible to TRUE if this is the current buffer */
|
||
/* and this ent shows up on the screen now */
|
||
visible = buffer->xtext->buffer == buffer &&
|
||
gtk_xtext_check_ent_visibility (buffer->xtext, ent, 0);
|
||
|
||
if (ent == buffer->pagetop_ent)
|
||
buffer->pagetop_ent = NULL;
|
||
|
||
if (ent == buffer->last_ent_start)
|
||
{
|
||
buffer->last_ent_start = ent->next;
|
||
buffer->last_offset_start = 0;
|
||
}
|
||
|
||
if (ent == buffer->last_ent_end)
|
||
{
|
||
buffer->last_ent_start = NULL;
|
||
buffer->last_ent_end = NULL;
|
||
}
|
||
|
||
if (buffer->marker_pos == ent)
|
||
{
|
||
/* Allow for "Marker line reset because exceeded scrollback limit. to appear. */
|
||
buffer->marker_pos = ent->next;
|
||
buffer->marker_state = MARKER_RESET_BY_KILL;
|
||
}
|
||
|
||
if (ent->marks)
|
||
{
|
||
gtk_xtext_search_textentry_del (buffer, ent);
|
||
}
|
||
|
||
gtk_xtext_entry_free (buffer->xtext, ent);
|
||
return visible;
|
||
}
|
||
|
||
/* remove the topline from the list */
|
||
|
||
static void
|
||
gtk_xtext_remove_top (xtext_buffer *buffer)
|
||
{
|
||
textentry *ent;
|
||
|
||
ent = buffer->text_first;
|
||
if (!ent)
|
||
return;
|
||
buffer->num_lines -= ent->subline_count;
|
||
buffer->pagetop_line -= ent->subline_count;
|
||
buffer->last_pixel_pos -= (ent->subline_count * buffer->xtext->fontsize);
|
||
buffer->text_first = ent->next;
|
||
if (buffer->text_first)
|
||
buffer->text_first->prev = NULL;
|
||
else
|
||
buffer->text_last = NULL;
|
||
|
||
buffer->old_value -= ent->subline_count;
|
||
if (buffer->xtext->buffer == buffer) /* is it the current buffer? */
|
||
{
|
||
gtk_adjustment_set_value (buffer->xtext->adj,
|
||
gtk_adjustment_get_value (buffer->xtext->adj) -
|
||
ent->subline_count);
|
||
buffer->xtext->select_start_adj -= ent->subline_count;
|
||
}
|
||
|
||
if (gtk_xtext_kill_ent (buffer, ent))
|
||
{
|
||
if (!buffer->xtext->add_io_tag)
|
||
{
|
||
/* remove scrolling events */
|
||
gtk_xtext_cancel_source (&buffer->xtext->io_tag);
|
||
buffer->xtext->force_render = TRUE;
|
||
buffer->xtext->add_io_tag = g_timeout_add (REFRESH_TIMEOUT * 2,
|
||
gtk_xtext_render_page_timeout,
|
||
buffer->xtext);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_remove_bottom (xtext_buffer *buffer)
|
||
{
|
||
textentry *ent;
|
||
|
||
ent = buffer->text_last;
|
||
if (!ent)
|
||
return;
|
||
buffer->num_lines -= ent->subline_count;
|
||
buffer->text_last = ent->prev;
|
||
if (buffer->text_last)
|
||
buffer->text_last->next = NULL;
|
||
else
|
||
buffer->text_first = NULL;
|
||
|
||
if (gtk_xtext_kill_ent (buffer, ent))
|
||
{
|
||
if (!buffer->xtext->add_io_tag)
|
||
{
|
||
/* remove scrolling events */
|
||
gtk_xtext_cancel_source (&buffer->xtext->io_tag);
|
||
buffer->xtext->force_render = TRUE;
|
||
buffer->xtext->add_io_tag = g_timeout_add (REFRESH_TIMEOUT * 2,
|
||
gtk_xtext_render_page_timeout,
|
||
buffer->xtext);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* If lines=0 => clear all */
|
||
|
||
void
|
||
gtk_xtext_clear (xtext_buffer *buf, int lines)
|
||
{
|
||
textentry *next;
|
||
int marker_reset = FALSE;
|
||
|
||
if (lines != 0)
|
||
{
|
||
if (lines < 0)
|
||
{
|
||
/* delete lines from bottom */
|
||
while (lines < 0 && buf->text_last)
|
||
{
|
||
if (buf->text_last == buf->marker_pos)
|
||
marker_reset = TRUE;
|
||
gtk_xtext_remove_bottom (buf);
|
||
lines++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* delete lines from top */
|
||
while (lines > 0 && buf->text_first)
|
||
{
|
||
if (buf->text_first == buf->marker_pos)
|
||
marker_reset = TRUE;
|
||
gtk_xtext_remove_top (buf);
|
||
lines--;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* delete all */
|
||
gtk_xtext_search_fini (buf);
|
||
if (buf->xtext->auto_indent)
|
||
buf->indent = MARGIN;
|
||
buf->scrollbar_down = TRUE;
|
||
buf->last_ent_start = NULL;
|
||
buf->last_ent_end = NULL;
|
||
buf->marker_pos = NULL;
|
||
if (buf->text_first)
|
||
marker_reset = TRUE;
|
||
dontscroll (buf);
|
||
|
||
while (buf->text_first)
|
||
{
|
||
next = buf->text_first->next;
|
||
gtk_xtext_entry_free (buf->xtext, buf->text_first);
|
||
buf->text_first = next;
|
||
}
|
||
buf->text_last = NULL;
|
||
buf->pagetop_ent = NULL;
|
||
buf->num_lines = 0;
|
||
buf->old_value = 0;
|
||
}
|
||
|
||
if (buf->xtext->buffer == buf)
|
||
{
|
||
gtk_xtext_calc_lines (buf, TRUE);
|
||
gtk_xtext_refresh (buf->xtext);
|
||
} else
|
||
{
|
||
gtk_xtext_calc_lines (buf, FALSE);
|
||
}
|
||
|
||
if (marker_reset)
|
||
buf->marker_state = MARKER_RESET_BY_CLEAR;
|
||
}
|
||
|
||
static gboolean
|
||
gtk_xtext_check_ent_visibility (GtkXText * xtext, textentry *find_ent, int add)
|
||
{
|
||
textentry *ent;
|
||
int lines;
|
||
xtext_buffer *buf = xtext->buffer;
|
||
int height;
|
||
GdkWindow *window;
|
||
|
||
if (find_ent == NULL)
|
||
{
|
||
return FALSE;
|
||
}
|
||
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return FALSE;
|
||
height = gdk_window_get_height (window);
|
||
|
||
ent = buf->pagetop_ent;
|
||
/* If top line not completely displayed return FALSE */
|
||
if (ent == find_ent && buf->pagetop_subline > 0)
|
||
{
|
||
return FALSE;
|
||
}
|
||
/* Loop through line positions looking for find_ent */
|
||
lines = ((height + xtext->pixel_offset) / xtext->fontsize) + buf->pagetop_subline + add;
|
||
while (ent)
|
||
{
|
||
lines -= ent->subline_count;
|
||
if (lines <= 0)
|
||
{
|
||
return FALSE;
|
||
}
|
||
if (ent == find_ent)
|
||
{
|
||
return TRUE;
|
||
}
|
||
ent = ent->next;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_check_marker_visibility (GtkXText * xtext)
|
||
{
|
||
if (gtk_xtext_check_ent_visibility (xtext, xtext->buffer->marker_pos, 1))
|
||
xtext->buffer->marker_seen = TRUE;
|
||
}
|
||
|
||
static void
|
||
gtk_xtext_unstrip_color (gint start, gint end, GSList *slp, GList **gl, gint maxo)
|
||
{
|
||
gint off1, off2;
|
||
GSList *cursl;
|
||
offsets_t marks;
|
||
offlen_t *meta;
|
||
|
||
off1 = 0;
|
||
cursl = slp;
|
||
while (cursl)
|
||
{
|
||
meta = cursl->data;
|
||
if (start < meta->len)
|
||
{
|
||
off1 = meta->off + start;
|
||
break;
|
||
}
|
||
start -= meta->len;
|
||
end -= meta->len;
|
||
cursl = g_slist_next (cursl);
|
||
}
|
||
|
||
off2 = off1;
|
||
while (cursl)
|
||
{
|
||
meta = cursl->data;
|
||
if (end < meta->len)
|
||
{
|
||
off2 = meta->off + end;
|
||
break;
|
||
}
|
||
end -= meta->len;
|
||
cursl = g_slist_next (cursl);
|
||
}
|
||
if (!cursl)
|
||
{
|
||
off2 = maxo;
|
||
}
|
||
|
||
marks.o.start = off1;
|
||
marks.o.end = off2;
|
||
*gl = g_list_append (*gl, GUINT_TO_POINTER (marks.u));
|
||
}
|
||
|
||
/* Case folding can change UTF-8 byte lengths (e.g. İ and ſ). Retain the
|
||
* original span for each folded byte so highlights and selections always
|
||
* land on whole characters in the original, color-stripped text. */
|
||
static char *
|
||
gtk_xtext_casefold_with_offsets (const char *text, int len, GArray **offsets)
|
||
{
|
||
GString *folded = g_string_sized_new (len);
|
||
const char *pos = text;
|
||
GArray *map = g_array_sized_new (FALSE, FALSE, sizeof (offsets_t), len);
|
||
while (pos < text + len)
|
||
{
|
||
int bytes = charlen (pos);
|
||
int before = folded->len;
|
||
offsets_t span;
|
||
span.o.start = pos - text;
|
||
span.o.end = span.o.start + bytes;
|
||
if ((guchar)*pos < 128)
|
||
g_string_append_c (folded, g_ascii_tolower (*pos));
|
||
else
|
||
{
|
||
char *part = g_utf8_casefold (pos, bytes);
|
||
g_string_append (folded, part);
|
||
g_free (part);
|
||
}
|
||
while (before++ < folded->len)
|
||
g_array_append_val (map, span);
|
||
pos += bytes;
|
||
}
|
||
*offsets = map;
|
||
return g_string_free (folded, FALSE);
|
||
}
|
||
|
||
/* Search a single textentry for occurrence(s) of search arg string */
|
||
static GList *
|
||
gtk_xtext_search_textentry (xtext_buffer *buf, textentry *ent)
|
||
{
|
||
gchar *str; /* text string to be searched */
|
||
GList *gl = NULL;
|
||
GSList *slp;
|
||
gint lstr;
|
||
|
||
if (buf->search_text == NULL)
|
||
{
|
||
return gl;
|
||
}
|
||
|
||
str = gtk_xtext_strip_color (ent->str, ent->str_len, buf->xtext->scratch_buffer,
|
||
&lstr, &slp, !buf->xtext->ignore_hidden);
|
||
|
||
/* Regular-expression matching --- */
|
||
if (buf->search_flags & regexp)
|
||
{
|
||
GMatchInfo *gmi;
|
||
gint start, end;
|
||
|
||
if (buf->search_re == NULL)
|
||
{
|
||
g_slist_free_full (slp, g_free);
|
||
return gl;
|
||
}
|
||
g_regex_match (buf->search_re, str, 0, &gmi);
|
||
while (g_match_info_matches (gmi))
|
||
{
|
||
g_match_info_fetch_pos (gmi, 0, &start, &end);
|
||
gtk_xtext_unstrip_color (start, end, slp, &gl, ent->str_len);
|
||
g_match_info_next (gmi, NULL);
|
||
}
|
||
g_match_info_free (gmi);
|
||
|
||
/* Non-regular-expression matching --- */
|
||
} else {
|
||
gchar *hay, *pos;
|
||
GArray *offsets = NULL;
|
||
gint lhay, off, len;
|
||
gint match = buf->search_flags & case_match;
|
||
|
||
hay = match ? g_strdup (str) : gtk_xtext_casefold_with_offsets (str, lstr, &offsets);
|
||
lhay = strlen (hay);
|
||
|
||
for (pos = hay, len = lhay; len;
|
||
off += buf->search_lnee, pos = hay + off, len = lhay - off)
|
||
{
|
||
str = g_strstr_len (pos, len, buf->search_nee);
|
||
if (str == NULL)
|
||
{
|
||
break;
|
||
}
|
||
off = str - hay;
|
||
int start = off, end = off + buf->search_lnee;
|
||
if (offsets)
|
||
{
|
||
start = g_array_index (offsets, offsets_t, start).o.start;
|
||
end = g_array_index (offsets, offsets_t, end - 1).o.end;
|
||
}
|
||
gtk_xtext_unstrip_color (start, end,
|
||
slp, &gl, ent->str_len);
|
||
}
|
||
|
||
g_free (hay);
|
||
g_clear_pointer (&offsets, g_array_unref);
|
||
}
|
||
|
||
/* Common processing --- */
|
||
g_slist_free_full (slp, g_free);
|
||
return gl;
|
||
}
|
||
|
||
/* Add a list of found search results to an entry, maybe NULL */
|
||
static void
|
||
gtk_xtext_search_textentry_add (xtext_buffer *buf, textentry *ent, GList *gl, gboolean pre)
|
||
{
|
||
if (ent->marks)
|
||
gtk_xtext_search_textentry_del (buf, ent);
|
||
ent->marks = gl;
|
||
if (gl)
|
||
{
|
||
buf->search_found = (pre? g_list_prepend: g_list_append) (buf->search_found, ent);
|
||
if (pre == FALSE && buf->hintsearch == NULL)
|
||
{
|
||
buf->hintsearch = ent;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Free all search information for a textentry */
|
||
static void
|
||
gtk_xtext_search_textentry_del (xtext_buffer *buf, textentry *ent)
|
||
{
|
||
g_list_free (ent->marks);
|
||
ent->marks = NULL;
|
||
if (buf->cursearch && buf->cursearch->data == ent)
|
||
{
|
||
buf->cursearch = NULL;
|
||
buf->curmark = NULL;
|
||
buf->curdata.u = 0;
|
||
}
|
||
if (buf->pagetop_ent == ent)
|
||
{
|
||
buf->pagetop_ent = NULL;
|
||
}
|
||
if (buf->hintsearch == ent)
|
||
{
|
||
buf->hintsearch = NULL;
|
||
}
|
||
buf->search_found = g_list_remove (buf->search_found, ent);
|
||
}
|
||
|
||
/* Used only by glist_foreach */
|
||
static void
|
||
gtk_xtext_search_textentry_fini (gpointer entp, gpointer dummy)
|
||
{
|
||
textentry *ent = entp;
|
||
|
||
g_list_free (ent->marks);
|
||
ent->marks = NULL;
|
||
}
|
||
|
||
/* Free all search information for all textentrys and the xtext_buffer */
|
||
static void
|
||
gtk_xtext_search_fini (xtext_buffer *buf)
|
||
{
|
||
g_list_foreach (buf->search_found, gtk_xtext_search_textentry_fini, 0);
|
||
g_list_free (buf->search_found);
|
||
buf->search_found = NULL;
|
||
g_free (buf->search_text);
|
||
buf->search_text = NULL;
|
||
g_free (buf->search_nee);
|
||
buf->search_nee = NULL;
|
||
buf->search_flags = 0;
|
||
buf->cursearch = NULL;
|
||
buf->curmark = NULL;
|
||
/* but leave buf->curdata.u alone! */
|
||
if (buf->search_re)
|
||
{
|
||
g_regex_unref (buf->search_re);
|
||
buf->search_re = NULL;
|
||
}
|
||
}
|
||
|
||
/* Returns TRUE if the base search information exists and is still okay to use */
|
||
static gboolean
|
||
gtk_xtext_search_init (xtext_buffer *buf, const gchar *text, gtk_xtext_search_flags flags, GError **perr)
|
||
{
|
||
/* Of the five flags, backward and highlight_all do not need a new search */
|
||
if (buf->search_found &&
|
||
strcmp (buf->search_text, text) == 0 &&
|
||
(buf->search_flags & case_match) == (flags & case_match) &&
|
||
(buf->search_flags & follow) == (flags & follow) &&
|
||
(buf->search_flags & regexp) == (flags & regexp))
|
||
{
|
||
return TRUE;
|
||
}
|
||
buf->hintsearch = buf->cursearch? buf->cursearch->data: NULL;
|
||
gtk_xtext_search_fini (buf);
|
||
buf->search_text = g_strdup (text);
|
||
if (flags & regexp)
|
||
{
|
||
buf->search_re = g_regex_new (text, (flags & case_match)? 0: G_REGEX_CASELESS, 0, perr);
|
||
if (perr && *perr)
|
||
{
|
||
return FALSE;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (flags & case_match)
|
||
{
|
||
buf->search_nee = g_strdup (text);
|
||
}
|
||
else
|
||
{
|
||
buf->search_nee = g_utf8_casefold (text, strlen (text));
|
||
}
|
||
buf->search_lnee = strlen (buf->search_nee);
|
||
}
|
||
buf->search_flags = flags;
|
||
buf->cursearch = NULL;
|
||
buf->curmark = NULL;
|
||
/* but leave buf->curdata.u alone! */
|
||
return FALSE;
|
||
}
|
||
|
||
#define BACKWARD (flags & backward)
|
||
#define FIRSTLAST(lp) (BACKWARD? g_list_last(lp): g_list_first(lp))
|
||
#define NEXTPREVIOUS(lp) (BACKWARD? g_list_previous(lp): g_list_next(lp))
|
||
textentry *
|
||
gtk_xtext_search (GtkXText * xtext, const gchar *text, gtk_xtext_search_flags flags, GError **perr)
|
||
{
|
||
textentry *ent = NULL;
|
||
xtext_buffer *buf = xtext->buffer;
|
||
GList *gl;
|
||
|
||
if (buf->text_first == NULL)
|
||
{
|
||
return NULL;
|
||
}
|
||
|
||
/* If the text arg is NULL, one of these has been toggled: highlight follow */
|
||
if (text == NULL) /* Here on highlight or follow toggle */
|
||
{
|
||
gint oldfollow = buf->search_flags & follow;
|
||
gint newfollow = flags & follow;
|
||
|
||
/* If "Follow" has just been checked, search possible new textentries --- */
|
||
if (newfollow && (newfollow != oldfollow))
|
||
{
|
||
gl = g_list_last (buf->search_found);
|
||
ent = gl ? ((textentry *)gl->data)->next : buf->text_first;
|
||
for (; ent; ent = ent->next)
|
||
{
|
||
GList *gl;
|
||
|
||
gl = gtk_xtext_search_textentry (buf, ent);
|
||
gtk_xtext_search_textentry_add (buf, ent, gl, FALSE);
|
||
}
|
||
}
|
||
buf->search_flags = flags;
|
||
ent = buf->pagetop_ent;
|
||
}
|
||
|
||
/* if the text arg is "", the reset button has been clicked or Control-Shift-F has been hit */
|
||
else if (text[0] == 0) /* Let a null string do a reset. */
|
||
{
|
||
gtk_xtext_search_fini (buf);
|
||
}
|
||
|
||
/* If the text arg is neither NULL nor "", it's the search string */
|
||
else
|
||
{
|
||
if (gtk_xtext_search_init (buf, text, flags, perr) == FALSE) /* If a new search: */
|
||
{
|
||
if (perr && *perr)
|
||
{
|
||
return NULL;
|
||
}
|
||
for (ent = buf->text_first; ent; ent = ent->next)
|
||
{
|
||
GList *gl;
|
||
|
||
gl = gtk_xtext_search_textentry (buf, ent);
|
||
gtk_xtext_search_textentry_add (buf, ent, gl, TRUE);
|
||
}
|
||
buf->search_found = g_list_reverse (buf->search_found);
|
||
}
|
||
|
||
/* Now base search results are in place. */
|
||
|
||
if (buf->search_found)
|
||
{
|
||
/* If we're in the midst of moving among found items */
|
||
if (buf->cursearch)
|
||
{
|
||
ent = buf->cursearch->data;
|
||
buf->curmark = NEXTPREVIOUS (buf->curmark);
|
||
if (buf->curmark == NULL)
|
||
{
|
||
/* We've returned all the matches for this textentry. */
|
||
buf->cursearch = NEXTPREVIOUS (buf->cursearch);
|
||
if (buf->cursearch)
|
||
{
|
||
ent = buf->cursearch->data;
|
||
buf->curmark = FIRSTLAST (ent->marks);
|
||
}
|
||
else /* We've returned all the matches for all textentries */
|
||
{
|
||
ent = NULL;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* If user changed the search, let's look starting where he was */
|
||
else if (buf->hintsearch)
|
||
{
|
||
GList *mark;
|
||
offsets_t last, this;
|
||
/*
|
||
* If we already have a 'current' item from the last search, and if
|
||
* the first character of an occurrence on this line for this new search
|
||
* is within that former item, use the occurrence as current.
|
||
*/
|
||
ent = buf->hintsearch;
|
||
last.u = buf->curdata.u;
|
||
for (mark = ent->marks; mark; mark = mark->next)
|
||
{
|
||
this.u = GPOINTER_TO_UINT (mark->data);
|
||
if (this.o.start >= last.o.start && this.o.start < last.o.end)
|
||
break;
|
||
}
|
||
if (mark == NULL)
|
||
{
|
||
for (ent = buf->hintsearch; ent; ent = BACKWARD? ent->prev: ent->next)
|
||
if (ent->marks)
|
||
break;
|
||
mark = ent? FIRSTLAST (ent->marks): NULL;
|
||
}
|
||
buf->cursearch = g_list_find (buf->search_found, ent);
|
||
buf->curmark = mark;
|
||
}
|
||
|
||
/* This is a fresh search */
|
||
else
|
||
{
|
||
buf->cursearch = FIRSTLAST (buf->search_found);
|
||
ent = buf->cursearch->data;
|
||
buf->curmark = FIRSTLAST (ent->marks);
|
||
}
|
||
buf->curdata.u = (buf->curmark)? GPOINTER_TO_UINT (buf->curmark->data): 0;
|
||
}
|
||
}
|
||
buf->hintsearch = ent;
|
||
|
||
if (!gtk_xtext_check_ent_visibility (xtext, ent, 1))
|
||
{
|
||
GtkAdjustment *adj = xtext->adj;
|
||
float value;
|
||
|
||
buf->pagetop_ent = NULL;
|
||
for (value = 0, ent = buf->text_first;
|
||
ent && ent != buf->hintsearch; ent = ent->next)
|
||
{
|
||
value += ent->subline_count;
|
||
}
|
||
if (value > gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj))
|
||
{
|
||
value = gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj);
|
||
}
|
||
else if ((flags & backward) && ent)
|
||
{
|
||
value -= gtk_adjustment_get_page_size (adj) -
|
||
ent->subline_count;
|
||
if (value < 0)
|
||
{
|
||
value = 0;
|
||
}
|
||
}
|
||
gtk_adjustment_set_value (adj, value);
|
||
}
|
||
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
|
||
return buf->hintsearch;
|
||
}
|
||
#undef BACKWARD
|
||
#undef FIRSTLAST
|
||
#undef NEXTPREVIOUS
|
||
|
||
static gboolean
|
||
gtk_xtext_render_page_timeout (gpointer data)
|
||
{
|
||
GtkXText *xtext = GTK_XTEXT (data);
|
||
GtkAdjustment *adj = xtext->adj;
|
||
|
||
xtext->add_io_tag = 0;
|
||
|
||
/* less than a complete page? */
|
||
if (xtext->buffer->num_lines <= gtk_adjustment_get_page_size (adj))
|
||
{
|
||
xtext->buffer->old_value = 0;
|
||
gtk_adjustment_set_value (adj, 0);
|
||
gtk_xtext_render_page (xtext);
|
||
} else if (xtext->buffer->scrollbar_down)
|
||
{
|
||
g_signal_handler_block (xtext->adj, xtext->vc_signal_tag);
|
||
gtk_xtext_adjustment_set (xtext->buffer, FALSE);
|
||
gtk_adjustment_set_value (adj,
|
||
gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj));
|
||
g_signal_handler_unblock (xtext->adj, xtext->vc_signal_tag);
|
||
xtext->buffer->old_value = gtk_adjustment_get_value (adj);
|
||
gtk_xtext_render_page (xtext);
|
||
} else
|
||
{
|
||
gtk_xtext_adjustment_set (xtext->buffer, TRUE);
|
||
if (xtext->force_render)
|
||
{
|
||
xtext->force_render = FALSE;
|
||
gtk_xtext_render_page (xtext);
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* append a textentry to our linked list */
|
||
|
||
static void
|
||
gtk_xtext_append_entry (xtext_buffer *buf, textentry * ent, time_t stamp)
|
||
{
|
||
int i;
|
||
|
||
/* we don't like tabs */
|
||
i = 0;
|
||
while (i < ent->str_len)
|
||
{
|
||
if (ent->str[i] == '\t')
|
||
ent->str[i] = ' ';
|
||
i++;
|
||
}
|
||
|
||
ent->stamp = stamp;
|
||
if (stamp == 0)
|
||
ent->stamp = time (0);
|
||
ent->slp = NULL;
|
||
ent->str_width = gtk_xtext_text_width_ent (buf->xtext, ent);
|
||
ent->mark_start = -1;
|
||
ent->mark_end = -1;
|
||
ent->next = NULL;
|
||
ent->marks = NULL;
|
||
ent->subline_count = 0;
|
||
ent->image = NULL;
|
||
ent->image_full = NULL;
|
||
ent->image_url = NULL;
|
||
|
||
if (ent->indent < MARGIN)
|
||
ent->indent = MARGIN; /* 2 pixels is the left margin */
|
||
|
||
/* append to our linked list */
|
||
if (buf->text_last)
|
||
buf->text_last->next = ent;
|
||
else
|
||
buf->text_first = ent;
|
||
ent->prev = buf->text_last;
|
||
buf->text_last = ent;
|
||
|
||
ent->sublines = NULL;
|
||
buf->num_lines += gtk_xtext_lines_taken (buf, ent);
|
||
|
||
if ((buf->marker_pos == NULL || buf->marker_seen) && (buf->xtext->buffer != buf ||
|
||
!gtk_window_has_toplevel_focus (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (buf->xtext))))))
|
||
{
|
||
buf->marker_pos = ent;
|
||
buf->marker_state = MARKER_IS_SET;
|
||
dontscroll (buf); /* force scrolling off */
|
||
buf->marker_seen = FALSE;
|
||
}
|
||
|
||
/* Search before trimming: an oversized sole entry may itself be removed. */
|
||
if (buf->search_flags & follow)
|
||
{
|
||
GList *gl;
|
||
|
||
gl = gtk_xtext_search_textentry (buf, ent);
|
||
gtk_xtext_search_textentry_add (buf, ent, gl, FALSE);
|
||
}
|
||
|
||
if (buf->xtext->max_lines > 2 && buf->xtext->max_lines < buf->num_lines)
|
||
{
|
||
gtk_xtext_remove_top (buf);
|
||
}
|
||
|
||
if (buf->xtext->buffer == buf)
|
||
{
|
||
/* this could be improved */
|
||
if ((buf->num_lines - 1) <= gtk_adjustment_get_page_size (buf->xtext->adj))
|
||
dontscroll (buf);
|
||
|
||
if (!buf->xtext->add_io_tag)
|
||
{
|
||
/* remove scrolling events */
|
||
gtk_xtext_cancel_source (&buf->xtext->io_tag);
|
||
/* When at the bottom of the buffer, render immediately so long
|
||
* scrollback doesn't delay newly-sent messages appearing.
|
||
* Otherwise, keep idle batching to avoid extra redraws while
|
||
* scrolling around old content. */
|
||
if (buf->scrollbar_down)
|
||
gtk_xtext_render_page_timeout (buf->xtext);
|
||
else
|
||
buf->xtext->add_io_tag = g_idle_add (gtk_xtext_render_page_timeout,
|
||
buf->xtext);
|
||
}
|
||
}
|
||
if (buf->scrollbar_down)
|
||
{
|
||
buf->old_value = buf->num_lines -
|
||
gtk_adjustment_get_page_size (buf->xtext->adj);
|
||
if (buf->old_value < 0)
|
||
buf->old_value = 0;
|
||
}
|
||
}
|
||
|
||
/* Keep the historical 4095-byte entry limit, but never cut a UTF-8 character
|
||
* or read beyond an explicitly supplied length. Invalid trailing text is
|
||
* discarded before Pango, selection and wrapping see it. */
|
||
static int
|
||
gtk_xtext_text_prefix (const guchar *text, int len, int limit)
|
||
{
|
||
const char *end;
|
||
int bounded;
|
||
|
||
/* Empty segments may have no backing string (PrintTextLine uses NULL/0). */
|
||
if (len == 0)
|
||
return 0;
|
||
if (len == -1)
|
||
len = strlen ((const char *)text);
|
||
bounded = MIN (len, limit);
|
||
if (!g_utf8_validate ((const char *)text, bounded, &end))
|
||
bounded = end - (const char *)text;
|
||
return bounded;
|
||
}
|
||
|
||
static textentry *
|
||
gtk_xtext_entry_new (int len)
|
||
{
|
||
textentry *ent = g_malloc0 (sizeof (textentry) + len + 1);
|
||
ent->str = (guchar *)(ent + 1);
|
||
ent->str_len = len;
|
||
return ent;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_append_indent (xtext_buffer *buf,
|
||
unsigned char *left_text, int left_len,
|
||
unsigned char *right_text, int right_len,
|
||
time_t stamp)
|
||
{
|
||
textentry *ent;
|
||
unsigned char *str;
|
||
int space;
|
||
int tempindent;
|
||
int prefix_width;
|
||
int min_indent;
|
||
|
||
g_return_if_fail (buf != NULL);
|
||
g_return_if_fail (left_len >= -1 && right_len >= -1);
|
||
g_return_if_fail (left_text != NULL || left_len == 0);
|
||
g_return_if_fail (right_text != NULL || right_len == 0);
|
||
|
||
if (right_len == -1)
|
||
right_len = strlen ((const char *)right_text);
|
||
if (right_len > 0 && right_text[right_len - 1] == '\n')
|
||
right_len--;
|
||
|
||
left_len = gtk_xtext_text_prefix (left_text, left_len,
|
||
sizeof (buf->xtext->scratch_buffer) - 2);
|
||
right_len = gtk_xtext_text_prefix (right_text, right_len,
|
||
sizeof (buf->xtext->scratch_buffer) - left_len - 2);
|
||
ent = gtk_xtext_entry_new (left_len + 1 + right_len);
|
||
str = (unsigned char *) ent + sizeof (textentry);
|
||
|
||
if (left_len)
|
||
memcpy (str, left_text, left_len);
|
||
str[left_len] = ' ';
|
||
if (right_len)
|
||
memcpy (str + left_len + 1, right_text, right_len);
|
||
str[left_len + 1 + right_len] = 0;
|
||
|
||
prefix_width = gtk_xtext_text_width (buf->xtext, str, left_len + 1);
|
||
|
||
ent->left_len = left_len;
|
||
ent->stamp_len = 0;
|
||
ent->str = str;
|
||
ent->str_len = left_len + 1 + right_len;
|
||
ent->indent = buf->indent - prefix_width;
|
||
|
||
/* This is copied into the scratch buffer later, double check math */
|
||
g_assert (ent->str_len < sizeof (buf->xtext->scratch_buffer));
|
||
|
||
if (buf->time_stamp)
|
||
space = buf->xtext->stamp_width;
|
||
else
|
||
space = 0;
|
||
|
||
min_indent = MARGIN + space;
|
||
|
||
/* do we need to auto adjust the separator position? */
|
||
if (buf->xtext->auto_indent &&
|
||
buf->indent < buf->xtext->max_auto_indent &&
|
||
ent->indent < min_indent)
|
||
{
|
||
tempindent = min_indent + prefix_width;
|
||
|
||
/* Ignore tiny one-pixel style nudges.
|
||
* They can trigger expensive full-width recalculations and are
|
||
* perceived as a slight delay when sending messages with indenting on. */
|
||
if (tempindent > buf->indent + buf->xtext->space_width)
|
||
buf->indent = tempindent;
|
||
|
||
if (buf->indent > buf->xtext->max_auto_indent)
|
||
buf->indent = buf->xtext->max_auto_indent;
|
||
|
||
if (buf->indent > ent->indent + prefix_width)
|
||
{
|
||
gtk_xtext_fix_indent (buf);
|
||
gtk_xtext_recalc_widths (buf, FALSE);
|
||
|
||
ent->indent = buf->indent - prefix_width;
|
||
buf->xtext->force_render = TRUE;
|
||
}
|
||
}
|
||
|
||
gtk_xtext_append_entry (buf, ent, stamp);
|
||
}
|
||
|
||
void
|
||
gtk_xtext_append (xtext_buffer *buf, unsigned char *text, int len, time_t stamp)
|
||
{
|
||
gtk_xtext_append_wrap (buf, text, len, stamp, 0);
|
||
}
|
||
|
||
/* stamp_len: how many bytes at the start of text are the timestamp */
|
||
|
||
void
|
||
gtk_xtext_append_wrap (xtext_buffer *buf, unsigned char *text, int len, time_t stamp, int stamp_len)
|
||
{
|
||
textentry *ent;
|
||
|
||
g_return_if_fail (buf != NULL && text != NULL);
|
||
g_return_if_fail (len >= -1);
|
||
|
||
if (len == -1)
|
||
len = strlen ((const char *)text);
|
||
if (len > 0 && text[len - 1] == '\n')
|
||
len--;
|
||
len = gtk_xtext_text_prefix (text, len, sizeof (buf->xtext->scratch_buffer) - 1);
|
||
|
||
ent = gtk_xtext_entry_new (len);
|
||
if (len)
|
||
memcpy (ent->str, text, len);
|
||
ent->indent = 0;
|
||
ent->left_len = -1;
|
||
ent->stamp_len = (stamp_len > 0 && stamp_len < ent->str_len) ? stamp_len : 0;
|
||
|
||
gtk_xtext_append_entry (buf, ent, stamp);
|
||
}
|
||
|
||
gboolean
|
||
gtk_xtext_is_empty (xtext_buffer *buf)
|
||
{
|
||
return buf->text_first == NULL;
|
||
}
|
||
|
||
|
||
int
|
||
gtk_xtext_lastlog (xtext_buffer *out, xtext_buffer *search_area)
|
||
{
|
||
textentry *ent;
|
||
int matches;
|
||
GList *gl;
|
||
|
||
ent = search_area->text_first;
|
||
matches = 0;
|
||
|
||
while (ent)
|
||
{
|
||
gl = gtk_xtext_search_textentry (out, ent);
|
||
if (gl)
|
||
{
|
||
matches++;
|
||
/* copy the text over */
|
||
if (search_area->xtext->auto_indent)
|
||
{
|
||
gtk_xtext_append_indent (out, ent->str, ent->left_len,
|
||
ent->str + ent->left_len + 1,
|
||
ent->str_len - ent->left_len - 1, 0);
|
||
}
|
||
else
|
||
{
|
||
gtk_xtext_append_wrap (out, ent->str, ent->str_len, 0, ent->stamp_len);
|
||
}
|
||
|
||
if (out->text_last)
|
||
{
|
||
out->text_last->stamp = ent->stamp;
|
||
gtk_xtext_search_textentry_add (out, out->text_last, gl, TRUE);
|
||
}
|
||
}
|
||
ent = ent->next;
|
||
}
|
||
out->search_found = g_list_reverse (out->search_found);
|
||
|
||
return matches;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_foreach (xtext_buffer *buf, GtkXTextForeach func, void *data)
|
||
{
|
||
textentry *ent = buf->text_first;
|
||
|
||
while (ent)
|
||
{
|
||
(*func) (buf->xtext, ent->str, data);
|
||
ent = ent->next;
|
||
}
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_indent (GtkXText *xtext, gboolean indent)
|
||
{
|
||
xtext->auto_indent = indent;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_max_indent (GtkXText *xtext, int max_auto_indent)
|
||
{
|
||
xtext->max_auto_indent = max_auto_indent;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_max_lines (GtkXText *xtext, int max_lines)
|
||
{
|
||
xtext->max_lines = max_lines;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_show_marker (GtkXText *xtext, gboolean show_marker)
|
||
{
|
||
xtext->marker = show_marker;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_show_separator (GtkXText *xtext, gboolean show_separator)
|
||
{
|
||
xtext->separator = show_separator;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_thin_separator (GtkXText *xtext, gboolean thin_separator)
|
||
{
|
||
xtext->thinline = thin_separator;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_time_stamp (xtext_buffer *buf, gboolean time_stamp)
|
||
{
|
||
buf->time_stamp = time_stamp;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_urlcheck_function (GtkXText *xtext, int (*urlcheck_function) (GtkWidget *, char *))
|
||
{
|
||
xtext->urlcheck_function = urlcheck_function;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_wordwrap (GtkXText *xtext, gboolean wordwrap)
|
||
{
|
||
xtext->wordwrap = wordwrap;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_set_marker_last (session *sess)
|
||
{
|
||
xtext_buffer *buf = sess->res->buffer;
|
||
|
||
buf->marker_pos = buf->text_last;
|
||
buf->marker_state = MARKER_IS_SET;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_reset_marker_pos (GtkXText *xtext)
|
||
{
|
||
if (xtext->buffer->marker_pos)
|
||
{
|
||
xtext->buffer->marker_pos = NULL;
|
||
dontscroll (xtext->buffer); /* force scrolling off */
|
||
gtk_xtext_render_page (xtext);
|
||
xtext->buffer->marker_state = MARKER_RESET_MANUALLY;
|
||
}
|
||
}
|
||
|
||
int
|
||
gtk_xtext_moveto_marker_pos (GtkXText *xtext)
|
||
{
|
||
gdouble value = 0;
|
||
xtext_buffer *buf = xtext->buffer;
|
||
textentry *ent = buf->text_first;
|
||
GtkAdjustment *adj = xtext->adj;
|
||
|
||
if (buf->marker_pos == NULL)
|
||
return buf->marker_state;
|
||
|
||
if (gtk_xtext_check_ent_visibility (xtext, buf->marker_pos, 1) == FALSE)
|
||
{
|
||
while (ent)
|
||
{
|
||
if (ent == buf->marker_pos)
|
||
break;
|
||
value += ent->subline_count;
|
||
ent = ent->next;
|
||
}
|
||
if (value >= gtk_adjustment_get_value (adj) &&
|
||
value < gtk_adjustment_get_value (adj) + gtk_adjustment_get_page_size (adj))
|
||
return MARKER_IS_SET;
|
||
value -= gtk_adjustment_get_page_size (adj) / 2;
|
||
if (value < 0)
|
||
value = 0;
|
||
if (value > gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj))
|
||
value = gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj);
|
||
gtk_adjustment_set_value (adj, value);
|
||
gtk_xtext_render_page (xtext);
|
||
}
|
||
|
||
/* If we previously lost marker position to scrollback limit -- */
|
||
if (buf->marker_pos == buf->text_first &&
|
||
buf->marker_state == MARKER_RESET_BY_KILL)
|
||
return MARKER_RESET_BY_KILL;
|
||
else
|
||
return MARKER_IS_SET;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_buffer_show (GtkXText *xtext, xtext_buffer *buf, int render)
|
||
{
|
||
int w, h;
|
||
GdkWindow *window;
|
||
|
||
buf->xtext = xtext;
|
||
|
||
if (xtext->buffer == buf)
|
||
return;
|
||
|
||
gtk_xtext_cancel_source (&xtext->add_io_tag);
|
||
|
||
gtk_xtext_cancel_source (&xtext->io_tag);
|
||
|
||
if (!gtk_widget_get_realized (GTK_WIDGET (xtext)))
|
||
gtk_widget_realize (GTK_WIDGET (xtext));
|
||
|
||
window = gtk_widget_get_window (GTK_WIDGET (xtext));
|
||
if (!window)
|
||
return;
|
||
h = gdk_window_get_height (window);
|
||
w = gdk_window_get_width (window);
|
||
|
||
/* after a font change */
|
||
if (buf->needs_recalc)
|
||
{
|
||
buf->needs_recalc = FALSE;
|
||
gtk_xtext_recalc_widths (buf, TRUE);
|
||
}
|
||
|
||
/* now change to the new buffer */
|
||
xtext->buffer = buf;
|
||
dontscroll (buf); /* force scrolling off */
|
||
{
|
||
gdouble lower = 0;
|
||
gdouble value = buf->old_value;
|
||
gdouble upper = buf->num_lines;
|
||
gdouble page_size = gtk_adjustment_get_page_size (xtext->adj);
|
||
|
||
/* if the scrollbar was down, keep it down */
|
||
if (xtext->buffer->scrollbar_down && value < upper - page_size)
|
||
value = upper - page_size;
|
||
|
||
if (upper == 0)
|
||
upper = 1;
|
||
/* sanity check */
|
||
else if (value > upper - page_size)
|
||
{
|
||
/*buf->pagetop_ent = NULL;*/
|
||
value = upper - page_size;
|
||
if (value < 0)
|
||
value = 0;
|
||
}
|
||
|
||
xtext_adjustment_apply (xtext->adj, lower, upper, value, page_size);
|
||
}
|
||
|
||
if (render)
|
||
{
|
||
/* did the window change size since this buffer was last shown? */
|
||
if (buf->window_width != w)
|
||
{
|
||
buf->window_width = w;
|
||
buf->window_height = h;
|
||
gtk_xtext_calc_lines (buf, FALSE);
|
||
if (buf->scrollbar_down)
|
||
gtk_adjustment_set_value (xtext->adj,
|
||
gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj));
|
||
} else if (buf->window_height != h)
|
||
{
|
||
buf->window_height = h;
|
||
buf->pagetop_ent = NULL;
|
||
if (buf->scrollbar_down)
|
||
gtk_adjustment_set_value (xtext->adj,
|
||
gtk_adjustment_get_upper (xtext->adj));
|
||
gtk_xtext_adjustment_set (buf, FALSE);
|
||
}
|
||
|
||
gtk_xtext_render_page (xtext);
|
||
{
|
||
GtkAllocation allocation;
|
||
gdouble lower = 0;
|
||
gdouble upper = buf->num_lines;
|
||
gdouble value = gtk_adjustment_get_value (xtext->adj);
|
||
gdouble page_size;
|
||
|
||
if (upper == 0)
|
||
upper = 1;
|
||
|
||
gtk_widget_get_allocation (GTK_WIDGET (xtext), &allocation);
|
||
page_size = allocation.height / xtext->fontsize;
|
||
|
||
if (value > upper - page_size)
|
||
{
|
||
buf->scrollbar_down = TRUE;
|
||
value = upper - page_size;
|
||
}
|
||
|
||
if (value < 0)
|
||
value = 0;
|
||
|
||
xtext_adjustment_apply (xtext->adj, lower, upper, value, page_size);
|
||
gtk_adjustment_set_page_increment (xtext->adj, page_size);
|
||
g_signal_emit_by_name (xtext->adj, "value-changed");
|
||
}
|
||
}
|
||
}
|
||
|
||
xtext_buffer *
|
||
gtk_xtext_buffer_new (GtkXText *xtext)
|
||
{
|
||
xtext_buffer *buf;
|
||
|
||
buf = g_new0 (xtext_buffer, 1);
|
||
buf->old_value = -1;
|
||
buf->xtext = xtext;
|
||
buf->scrollbar_down = TRUE;
|
||
buf->indent = xtext->space_width * 2;
|
||
dontscroll (buf);
|
||
|
||
return buf;
|
||
}
|
||
|
||
void
|
||
gtk_xtext_buffer_free (xtext_buffer *buf)
|
||
{
|
||
textentry *ent, *next;
|
||
|
||
if (buf->xtext->buffer == buf)
|
||
buf->xtext->buffer = buf == buf->xtext->orig_buffer ? NULL : buf->xtext->orig_buffer;
|
||
|
||
if (buf->xtext->selection_buffer == buf)
|
||
buf->xtext->selection_buffer = NULL;
|
||
|
||
gtk_xtext_search_fini (buf);
|
||
|
||
gtk_xtext_image_pending_forget (gtk_xtext_image_pending_match_buf, buf);
|
||
|
||
ent = buf->text_first;
|
||
while (ent)
|
||
{
|
||
next = ent->next;
|
||
gtk_xtext_entry_free (buf->xtext, ent);
|
||
ent = next;
|
||
}
|
||
|
||
g_free (buf);
|
||
}
|
||
|
||
/* ========================================= */
|
||
/* ========== INLINE IMAGE API ============= */
|
||
/* ========================================= */
|
||
|
||
/* the entry the last word_click signal was emitted for; only valid while
|
||
handling that signal */
|
||
|
||
textentry *
|
||
gtk_xtext_get_clicked_entry (GtkXText *xtext)
|
||
{
|
||
return xtext->clicked_ent;
|
||
}
|
||
|
||
/* the entry whose inline image the last word_click signal (with a NULL
|
||
word) landed on; only valid while handling that signal */
|
||
|
||
textentry *
|
||
gtk_xtext_get_clicked_image_entry (GtkXText *xtext)
|
||
{
|
||
return xtext->clicked_image_ent;
|
||
}
|
||
|
||
/* check that 'ent' is still an entry of the currently shown buffer; used to
|
||
validate entry pointers that were remembered across main loop iterations */
|
||
|
||
gboolean
|
||
gtk_xtext_buffer_contains (GtkXText *xtext, textentry *ent)
|
||
{
|
||
textentry *walk;
|
||
|
||
if (ent == NULL || xtext->buffer == NULL)
|
||
return FALSE;
|
||
|
||
for (walk = xtext->buffer->text_first; walk; walk = walk->next)
|
||
{
|
||
if (walk == ent)
|
||
return TRUE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
gboolean
|
||
gtk_xtext_entry_has_image (GtkXText *xtext, textentry *ent)
|
||
{
|
||
return gtk_xtext_buffer_contains (xtext, ent) && ent->image != NULL;
|
||
}
|
||
|
||
/* the URL of the entry's attached image, or NULL if it has none */
|
||
|
||
const char *
|
||
gtk_xtext_entry_get_image_url (GtkXText *xtext, textentry *ent)
|
||
{
|
||
return gtk_xtext_entry_has_image (xtext, ent) ? ent->image_url : NULL;
|
||
}
|
||
|
||
/* larger version of the entry's attached image, for the viewer */
|
||
|
||
GdkPixbuf *
|
||
gtk_xtext_entry_get_full_image (GtkXText *xtext, textentry *ent)
|
||
{
|
||
if (!gtk_xtext_buffer_contains (xtext, ent))
|
||
return NULL;
|
||
return ent->image_full ? ent->image_full : ent->image;
|
||
}
|
||
|
||
/* the entry's subline count changed (image attached or removed):
|
||
redo its layout, fix the scrollbar and redraw */
|
||
|
||
static void
|
||
gtk_xtext_image_relayout (GtkXText *xtext, xtext_buffer *buf, textentry *ent)
|
||
{
|
||
buf->num_lines -= ent->subline_count;
|
||
buf->num_lines += gtk_xtext_lines_taken (buf, ent);
|
||
buf->pagetop_ent = NULL;
|
||
dontscroll (buf);
|
||
|
||
if (xtext->buffer == buf)
|
||
{
|
||
gtk_xtext_adjustment_set (buf, TRUE);
|
||
if (buf->scrollbar_down)
|
||
gtk_adjustment_set_value (xtext->adj,
|
||
gtk_adjustment_get_upper (xtext->adj) -
|
||
gtk_adjustment_get_page_size (xtext->adj));
|
||
if (gtk_widget_get_realized (GTK_WIDGET (xtext)))
|
||
gtk_widget_queue_draw (GTK_WIDGET (xtext));
|
||
}
|
||
}
|
||
|
||
/* scale down (never up) to fit within max_width x max_height */
|
||
|
||
static GdkPixbuf *
|
||
gtk_xtext_image_scale_max (GdkPixbuf *pixbuf, int max_width, int max_height)
|
||
{
|
||
int width = gdk_pixbuf_get_width (pixbuf);
|
||
int height = gdk_pixbuf_get_height (pixbuf);
|
||
int new_width, new_height;
|
||
double scale;
|
||
|
||
if (width < 1 || height < 1)
|
||
return NULL;
|
||
|
||
if (width <= max_width && height <= max_height)
|
||
return g_object_ref (pixbuf);
|
||
|
||
scale = MIN ((double) max_width / width, (double) max_height / height);
|
||
new_width = MAX (1, (int) (width * scale));
|
||
new_height = MAX (1, (int) (height * scale));
|
||
|
||
return gdk_pixbuf_scale_simple (pixbuf, new_width, new_height,
|
||
GDK_INTERP_BILINEAR);
|
||
}
|
||
|
||
/* scale down (never up) to fit the usual text area */
|
||
|
||
static GdkPixbuf *
|
||
gtk_xtext_image_scale (xtext_buffer *buf, GdkPixbuf *pixbuf)
|
||
{
|
||
int max_width = XTEXT_IMAGE_MAX_WIDTH;
|
||
int avail;
|
||
|
||
avail = buf->window_width - buf->indent - MARGIN - XTEXT_IMAGE_PADDING * 2;
|
||
if (avail >= 64 && avail < max_width)
|
||
max_width = avail;
|
||
|
||
return gtk_xtext_image_scale_max (pixbuf, max_width, XTEXT_IMAGE_MAX_HEIGHT);
|
||
}
|
||
|
||
/* register an asynchronous image load for 'ent'. Returns a handle to pass
|
||
to gtk_xtext_image_load_finish(), or 0 if the entry already has an image
|
||
or a load underway. */
|
||
|
||
guint
|
||
gtk_xtext_image_load_begin (GtkXText *xtext, textentry *ent, const char *url)
|
||
{
|
||
xtext_image_pending *pending;
|
||
GSList *list;
|
||
|
||
if (!gtk_xtext_buffer_contains (xtext, ent) || ent->image != NULL)
|
||
return 0;
|
||
|
||
for (list = image_pending_list; list; list = list->next)
|
||
{
|
||
pending = list->data;
|
||
if (pending->ent == ent)
|
||
return 0;
|
||
}
|
||
|
||
pending = g_new0 (xtext_image_pending, 1);
|
||
pending->id = image_pending_next_id++;
|
||
if (image_pending_next_id == 0) /* skip 0 on wrap, it means failure */
|
||
image_pending_next_id = 1;
|
||
pending->xtext = xtext;
|
||
pending->buf = xtext->buffer;
|
||
pending->ent = ent;
|
||
pending->url = g_strdup (url);
|
||
image_pending_list = g_slist_prepend (image_pending_list, pending);
|
||
|
||
return pending->id;
|
||
}
|
||
|
||
/* complete a load started with gtk_xtext_image_load_begin(). pixbuf may be
|
||
NULL to abandon the load (e.g. download failed). Returns TRUE if the
|
||
image was attached, FALSE if it was dropped (failure, or the entry is
|
||
gone). The caller keeps ownership of the pixbuf. */
|
||
|
||
gboolean
|
||
gtk_xtext_image_load_finish (guint handle, GdkPixbuf *pixbuf)
|
||
{
|
||
xtext_image_pending *pending = NULL;
|
||
GSList *list;
|
||
GdkPixbuf *scaled;
|
||
GtkXText *xtext;
|
||
xtext_buffer *buf;
|
||
textentry *ent;
|
||
|
||
if (handle == 0)
|
||
return FALSE;
|
||
|
||
for (list = image_pending_list; list; list = list->next)
|
||
{
|
||
xtext_image_pending *walk = list->data;
|
||
if (walk->id == handle)
|
||
{
|
||
pending = walk;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (pending == NULL) /* entry, buffer or widget died meanwhile */
|
||
return FALSE;
|
||
|
||
xtext = pending->xtext;
|
||
buf = pending->buf;
|
||
ent = pending->ent;
|
||
image_pending_list = g_slist_remove (image_pending_list, pending);
|
||
|
||
if (pixbuf == NULL || ent->image != NULL)
|
||
{
|
||
g_free (pending->url);
|
||
g_free (pending);
|
||
return FALSE;
|
||
}
|
||
|
||
scaled = gtk_xtext_image_scale (buf, pixbuf);
|
||
if (scaled == NULL)
|
||
{
|
||
g_free (pending->url);
|
||
g_free (pending);
|
||
return FALSE;
|
||
}
|
||
|
||
ent->image = scaled;
|
||
ent->image_full = gtk_xtext_image_scale_max (pixbuf,
|
||
XTEXT_IMAGE_FULL_MAX_WIDTH, XTEXT_IMAGE_FULL_MAX_HEIGHT);
|
||
ent->image_url = pending->url; /* transferred */
|
||
g_free (pending);
|
||
gtk_xtext_image_relayout (xtext, buf, ent);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
/* remove an entry's inline image again. Returns FALSE if it had none. */
|
||
|
||
gboolean
|
||
gtk_xtext_image_remove (GtkXText *xtext, textentry *ent)
|
||
{
|
||
if (!gtk_xtext_entry_has_image (xtext, ent))
|
||
return FALSE;
|
||
|
||
g_object_unref (ent->image);
|
||
ent->image = NULL;
|
||
if (ent->image_full)
|
||
{
|
||
g_object_unref (ent->image_full);
|
||
ent->image_full = NULL;
|
||
}
|
||
g_free (ent->image_url);
|
||
ent->image_url = NULL;
|
||
gtk_xtext_image_relayout (xtext, xtext->buffer, ent);
|
||
|
||
return TRUE;
|
||
}
|