gucharmap-search-dialog.c 29.4 KB
Newer Older
Noah Levitt's avatar
Noah Levitt committed
1
/*
Christian Persch's avatar
Christian Persch committed
2
 * Copyright © 2004 Noah Levitt
Noah Levitt's avatar
Noah Levitt committed
3 4 5
 *
 * 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
Christian Persch's avatar
Christian Persch committed
6
 * Free Software Foundation; either version 3 of the License, or (at your
Noah Levitt's avatar
Noah Levitt committed
7 8 9 10 11 12 13 14 15
 * 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.,
16
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA
Noah Levitt's avatar
Noah Levitt committed
17 18
 */

Christian Persch's avatar
Christian Persch committed
19
#include <config.h>
20
#include <glib/gi18n-lib.h>
Noah Levitt's avatar
Noah Levitt committed
21 22 23 24 25 26 27 28
#include <gtk/gtk.h>
#include <stdlib.h>
#include <string.h>
#include "gucharmap-search-dialog.h"
#include "gucharmap-window.h"

#define GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), gucharmap_search_dialog_get_type (), GucharmapSearchDialogPrivate))

29 30
#define I_(string) g_intern_static_string (string)

Noah Levitt's avatar
Noah Levitt committed
31 32 33 34 35 36 37
enum
{
  SEARCH_START,
  SEARCH_FINISH,
  NUM_SIGNALS
};

38
static guint gucharmap_search_dialog_signals[NUM_SIGNALS];
Noah Levitt's avatar
Noah Levitt committed
39 40 41 42 43 44 45 46 47 48 49 50 51

enum
{
  GUCHARMAP_RESPONSE_PREVIOUS,
  GUCHARMAP_RESPONSE_NEXT
};

typedef struct _GucharmapSearchDialogPrivate GucharmapSearchDialogPrivate;
typedef struct _GucharmapSearchState GucharmapSearchState;

struct _GucharmapSearchState
{
  GucharmapCodepointList *list;
Noah Levitt's avatar
Noah Levitt committed
52
  gchar                  *search_string;
53 54 55 56 57 58 59 60
  gchar                  *search_string_nfd_temp;
  gchar                  *search_string_nfd;  /* points into search_string_nfd_temp */
  gint                    search_string_nfd_len;
  gint                    search_index_nfd;
  gchar                  *search_string_nfc;
  gint                    search_string_nfc_len;
  gint                    search_index_nfc;
  gint                    search_string_value;
Noah Levitt's avatar
Noah Levitt committed
61 62 63 64
  gint                    start_index;
  gint                    curr_index;
  GucharmapDirection      increment;
  gboolean                whole_word;
65
  gboolean                annotations;
Noah Levitt's avatar
Noah Levitt committed
66 67 68 69
  gint                    found_index;       /* index of the found character */
  /* true if there are known to be no matches, or there is known to be
   * exactly one match and it has been found */
  gboolean                dont_search;
70
  gboolean                did_before_checks;
Noah Levitt's avatar
Noah Levitt committed
71 72
  gpointer                saved_data;        /* holds some data to pass back to the caller */
  gint                    list_num_chars;    /* last_index + 1 */
Noah Levitt's avatar
Noah Levitt committed
73 74
  gboolean                searching;
  gint                    strings_checked;
Noah Levitt's avatar
Noah Levitt committed
75 76 77 78 79 80
};

struct _GucharmapSearchDialogPrivate
{
  GucharmapWindow       *guw;
  GtkWidget             *entry;
81
  GtkWidget             *whole_word_option;
82
  GtkWidget             *annotations_option;
Noah Levitt's avatar
Noah Levitt committed
83 84 85 86 87
  GucharmapSearchState  *search_state;
  GtkWidget             *prev_button;
  GtkWidget             *next_button;
};

88 89 90 91
static void gucharmap_search_dialog_class_init (GucharmapSearchDialogClass *klass);
static void gucharmap_search_dialog_init       (GucharmapSearchDialog *dialog);

G_DEFINE_TYPE (GucharmapSearchDialog, gucharmap_search_dialog, GTK_TYPE_DIALOG)
92

Noah Levitt's avatar
Noah Levitt committed
93 94
static const gchar *
utf8_strcasestr (const gchar *haystack, 
95 96
                 const gchar *needle,
		 const gboolean whole_word)
Noah Levitt's avatar
Noah Levitt committed
97 98 99 100 101 102 103
{
  gint needle_len = strlen (needle);
  gint haystack_len = strlen (haystack);
  const gchar *p, *q, *r;

  for (p = haystack;  p + needle_len <= haystack + haystack_len;  p = g_utf8_next_char (p))
    {
104 105 106
      if (whole_word && !(p == haystack || g_unichar_isspace(p[-1])))
	goto next;

Noah Levitt's avatar
Noah Levitt committed
107 108 109 110 111 112 113
      for (q = needle, r = p;  *q && *r;  q = g_utf8_next_char (q), r = g_utf8_next_char (r))
        {
          gunichar lc0 = g_unichar_tolower (g_utf8_get_char (r));
          gunichar lc1 = g_unichar_tolower (g_utf8_get_char (q));
          if (lc0 != lc1)
            goto next;
        }
114 115 116 117

      if (whole_word && !(r[0] == '\0' || g_unichar_isspace(r[0])))
	goto next;

Noah Levitt's avatar
Noah Levitt committed
118 119
      return p;

120
      next:
Noah Levitt's avatar
Noah Levitt committed
121 122 123 124 125 126 127
        ;
    }

  return NULL;
}

static gboolean
Noah Levitt's avatar
Noah Levitt committed
128 129
matches (GucharmapSearchDialog *search_dialog,
         gunichar               wc,
130 131
         const gchar           *search_string_nfd,
         const gboolean         annotations)
Noah Levitt's avatar
Noah Levitt committed
132
{
Noah Levitt's avatar
Noah Levitt committed
133
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
Noah Levitt's avatar
Noah Levitt committed
134
  const gchar *haystack; 
135
  const gchar **haystack_arr; 
Noah Levitt's avatar
Noah Levitt committed
136
  gchar *haystack_nfd;
137 138
  gboolean matched = FALSE;
  gint i;
Noah Levitt's avatar
Noah Levitt committed
139

Noah Levitt's avatar
Noah Levitt committed
140
  haystack = gucharmap_get_unicode_data_name (wc);
Noah Levitt's avatar
Noah Levitt committed
141 142
  if (haystack)
    {
Noah Levitt's avatar
Noah Levitt committed
143 144
      priv->search_state->strings_checked++;

Noah Levitt's avatar
Noah Levitt committed
145
      /* character names are ascii, so are nfd */
Noah Levitt's avatar
Noah Levitt committed
146
      haystack_nfd = (gchar *) haystack;
147
      matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
Noah Levitt's avatar
Noah Levitt committed
148 149
    }

150
  if (annotations)
Noah Levitt's avatar
Noah Levitt committed
151
    {
152 153 154 155 156 157 158 159 160
      haystack = gucharmap_get_unicode_kDefinition (wc);
      if (haystack)
	{
	  priv->search_state->strings_checked++;

	  haystack_nfd = g_utf8_normalize (haystack, -1, G_NORMALIZE_NFD);
	  matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
	  g_free (haystack_nfd);
	}
161 162 163

      if (matched)
	return TRUE;
Noah Levitt's avatar
Noah Levitt committed
164

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
      haystack_arr = gucharmap_get_nameslist_equals (wc);
      if (haystack_arr)
	{
	  for (i = 0; haystack_arr[i] != NULL; i++)
	    {
	      priv->search_state->strings_checked++;

	      haystack_nfd = g_utf8_normalize (haystack_arr[i], -1, G_NORMALIZE_NFD);
	      matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
	      g_free (haystack_nfd);
	      if (matched)
		break;
	    }
	  g_free (haystack_arr);
	}
Noah Levitt's avatar
Noah Levitt committed
180

181 182 183
      if (matched)
	return TRUE;

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
      haystack_arr = gucharmap_get_nameslist_stars (wc);
      if (haystack_arr)
	{
	  for (i = 0; haystack_arr[i] != NULL; i++)
	    {
	      priv->search_state->strings_checked++;

	      haystack_nfd = g_utf8_normalize (haystack_arr[i], -1, G_NORMALIZE_NFD);
	      matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
	      g_free (haystack_nfd);
	      if (matched)
		break;
	    }
	  g_free (haystack_arr);
	}

200 201 202
      if (matched)
	return TRUE;

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
      haystack_arr = gucharmap_get_nameslist_colons (wc);
      if (haystack_arr)
	{
	  for (i = 0; haystack_arr[i] != NULL; i++)
	    {
	      priv->search_state->strings_checked++;

	      haystack_nfd = g_utf8_normalize (haystack_arr[i], -1, G_NORMALIZE_NFD);
	      matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
	      g_free (haystack_nfd);
	      if (matched)
		break;
	    }
	  g_free (haystack_arr);
	}

219 220 221
      if (matched)
	return TRUE;

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
      haystack_arr = gucharmap_get_nameslist_pounds (wc);
      if (haystack_arr)
	{
	  for (i = 0; haystack_arr[i] != NULL; i++)
	    {
	      priv->search_state->strings_checked++;

	      haystack_nfd = g_utf8_normalize (haystack_arr[i], -1, G_NORMALIZE_NFD);
	      matched = utf8_strcasestr (haystack_nfd, search_string_nfd, priv->search_state->whole_word) != NULL;
	      g_free (haystack_nfd);
	      if (matched)
		break;
	    }
	  g_free (haystack_arr);
	}
237 238 239

      if (matched)
	return TRUE;
240
    }
Noah Levitt's avatar
Noah Levitt committed
241

242 243
  /* XXX: other strings */

244
  return matched;
Noah Levitt's avatar
Noah Levitt committed
245 246 247 248 249 250 251 252 253
}

/* string should have no leading spaces */
static gint
check_for_explicit_codepoint (const GucharmapCodepointList *list,
                              const gchar                  *string)
{
  const gchar *nptr;
  gchar *endptr;
254
  gunichar wc;
Noah Levitt's avatar
Noah Levitt committed
255 256 257 258 259 260 261 262 263 264

  /* check for explicit decimal codepoint */
  nptr = string;
  if (g_ascii_strncasecmp (string, "&#", 2) == 0)
    nptr = string + 2;
  else if (*string == '#')
    nptr = string + 1;

  if (nptr != string)
    {
265
      wc = strtoul (nptr, &endptr, 10);
Noah Levitt's avatar
Noah Levitt committed
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
      if (endptr != nptr)
        {
          gint index = gucharmap_codepoint_list_get_index ((GucharmapCodepointList *) list, wc);
          if (index != -1)
            return index;
        }
    }

  /* check for explicit hex code point */
  nptr = string;
  if (g_ascii_strncasecmp (string, "&#x", 3) == 0)
    nptr = string + 3;
  else if (g_ascii_strncasecmp (string, "U+", 2) == 0 || g_ascii_strncasecmp (string, "0x", 2) == 0)
    nptr = string + 2;

  if (nptr != string)
    {
283
      wc = strtoul (nptr, &endptr, 16);
Noah Levitt's avatar
Noah Levitt committed
284 285 286 287 288 289 290 291
      if (endptr != nptr)
        {
          gint index = gucharmap_codepoint_list_get_index ((GucharmapCodepointList *) list, wc);
          if (index != -1)
            return index;
        }
    }

292 293 294 295 296 297 298 299 300 301 302 303
  /* check for hex codepoint without any prefix */
  /* as unicode standard assigns numerical codes to characters, its very usual
   * to search with character code without any prefix. so moved it to here.
   */
  wc = strtoul (string, &endptr, 16);
  if (endptr-3 >= string)
    {
      gint index = gucharmap_codepoint_list_get_index ((GucharmapCodepointList *) list, wc);
      if (index != -1)
	return index;
    }

Noah Levitt's avatar
Noah Levitt committed
304 305 306 307
  return -1;
}

static gboolean
Noah Levitt's avatar
Noah Levitt committed
308
quick_checks_before (GucharmapSearchState *search_state)
Noah Levitt's avatar
Noah Levitt committed
309 310 311 312
{
  if (search_state->dont_search)
    return TRUE;

313 314 315 316
  if (search_state->did_before_checks)
    return FALSE;
  search_state->did_before_checks = TRUE;

317 318
  g_return_val_if_fail (search_state->search_string_nfd != NULL, FALSE);
  g_return_val_if_fail (search_state->search_string_nfc != NULL, FALSE);
319

Noah Levitt's avatar
Noah Levitt committed
320
  /* caller should check for empty string */
Noah Levitt's avatar
Noah Levitt committed
321
  if (search_state->search_string_nfd[0] == '\0')
Noah Levitt's avatar
Noah Levitt committed
322 323 324 325 326
    {
      search_state->dont_search = TRUE;
      return TRUE;
    }

327
  if (!search_state->whole_word)
Noah Levitt's avatar
Noah Levitt committed
328 329
    {

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
      /* if NFD of the search string is a single character, jump to that */
      if (search_state->search_string_nfd_len == 1)
	{
	  if (search_state->search_index_nfd != -1)
	    {
	      search_state->found_index = search_state->curr_index = search_state->search_index_nfd;
	      search_state->dont_search = TRUE;
	      return TRUE;
	    }
	}

      /* if NFC of the search string is a single character, jump to that */
      if (search_state->search_string_nfc_len == 1)
	{
	  if (search_state->search_index_nfc != -1)
	    {
	      search_state->found_index = search_state->curr_index = search_state->search_index_nfc;
	      search_state->dont_search = TRUE;
	      return TRUE;
	    }
	}
Noah Levitt's avatar
Noah Levitt committed
351

352
    }
353

Noah Levitt's avatar
Noah Levitt committed
354 355 356
  return FALSE;
}

Noah Levitt's avatar
Noah Levitt committed
357 358 359 360
static gboolean
quick_checks_after (GucharmapSearchState *search_state)
{
  /* jump to the first nonspace character unless it’s plain ascii */
361 362 363 364 365 366 367 368 369 370 371
  if (!search_state->whole_word)
    if (search_state->search_string_nfd[0] < 0x20 || search_state->search_string_nfd[0] > 0x7e)
      {
	gint index = gucharmap_codepoint_list_get_index (search_state->list, g_utf8_get_char (search_state->search_string_nfd));
	if (index != -1)
	  {
	    search_state->found_index = index;
	    search_state->dont_search = TRUE;
	    return TRUE;
	  }
      }
Noah Levitt's avatar
Noah Levitt committed
372 373 374 375

  return FALSE;
}

Noah Levitt's avatar
Noah Levitt committed
376 377 378 379 380
static gboolean
idle_search (GucharmapSearchDialog *search_dialog)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
  gunichar wc;
Christian Persch's avatar
Christian Persch committed
381
  GTimer *timer;
Noah Levitt's avatar
Noah Levitt committed
382

383 384 385
  /* search without leading and tailing spaces */
  /* with "match whole word" option, there's no need for leading and tailing spaces */

Noah Levitt's avatar
Noah Levitt committed
386
  if (quick_checks_before (priv->search_state))
Noah Levitt's avatar
Noah Levitt committed
387 388
    return FALSE;

Christian Persch's avatar
Christian Persch committed
389 390
  timer = g_timer_new ();

Noah Levitt's avatar
Noah Levitt committed
391 392 393 394 395 396 397 398 399
  do
    {
      priv->search_state->curr_index = (priv->search_state->curr_index + priv->search_state->increment + priv->search_state->list_num_chars) % priv->search_state->list_num_chars;
      wc = gucharmap_codepoint_list_get_char (priv->search_state->list, priv->search_state->curr_index);

      if (!gucharmap_unichar_validate (wc) || !gucharmap_unichar_isdefined (wc))
        continue;


400 401 402 403
      /* check for explicit codepoint */
      if (priv->search_state->search_string_value != -1 && priv->search_state->curr_index == priv->search_state->search_string_value)
	{
	  priv->search_state->found_index = priv->search_state->curr_index;
Christian Persch's avatar
Christian Persch committed
404
          g_timer_destroy (timer);
405 406
	  return FALSE;
	}
Noah Levitt's avatar
Noah Levitt committed
407

408
      /* check for other matches */
409
      if (matches (search_dialog, wc, priv->search_state->search_string_nfd, priv->search_state->annotations))
410 411
        {
          priv->search_state->found_index = priv->search_state->curr_index;
Christian Persch's avatar
Christian Persch committed
412
          g_timer_destroy (timer);
Noah Levitt's avatar
Noah Levitt committed
413 414 415 416
          return FALSE;
        }

      if (g_timer_elapsed (timer, NULL) > 0.050)
Christian Persch's avatar
Christian Persch committed
417 418 419 420
        {
          g_timer_destroy (timer);
          return TRUE;
        }
Noah Levitt's avatar
Noah Levitt committed
421 422 423
    }
  while (priv->search_state->curr_index != priv->search_state->start_index);

Christian Persch's avatar
Christian Persch committed
424 425
  g_timer_destroy (timer);

Noah Levitt's avatar
Noah Levitt committed
426 427
  if (quick_checks_after (priv->search_state))
    return FALSE;
Noah Levitt's avatar
Noah Levitt committed
428 429

  priv->search_state->dont_search = TRUE;
430

Noah Levitt's avatar
Noah Levitt committed
431 432 433 434 435 436 437 438
  return FALSE;
}

/**
 * gucharmap_search_state_get_found_char:
 * @search_state: 
 * Return value: 
 **/
Noah Levitt's avatar
Noah Levitt committed
439
static gunichar
Noah Levitt's avatar
Noah Levitt committed
440 441 442 443 444 445 446 447 448 449 450 451
gucharmap_search_state_get_found_char (GucharmapSearchState *search_state)
{
  if (search_state->found_index > 0)
    return gucharmap_codepoint_list_get_char (search_state->list, search_state->found_index);
  else
    return (gunichar)(-1);
}

/**
 * gucharmap_search_state_free:
 * @search_state: 
 **/
Noah Levitt's avatar
Noah Levitt committed
452
static void
Noah Levitt's avatar
Noah Levitt committed
453 454
gucharmap_search_state_free (GucharmapSearchState *search_state)
{
455
  g_object_unref (search_state->list);
456 457
  g_free (search_state->search_string_nfd_temp);
  g_free (search_state->search_string_nfc);
458
  g_slice_free (GucharmapSearchState, search_state);
Noah Levitt's avatar
Noah Levitt committed
459 460 461 462 463 464 465 466 467
}

/**
 * gucharmap_search_state_new:
 * @list: a #GucharmapCodepointList to be searched
 * @search_string: the text to search for
 * @start_index: the starting point within @list
 * @direction: forward or backward
 * @whole_word: %TRUE if it should match whole words
468
 * @annotations: %TRUE if it should search in character's annotations
Noah Levitt's avatar
Noah Levitt committed
469 470 471 472 473 474
 *
 * Initializes a #GucharmapSearchState to search for the next character in
 * the codepoint list that matches @search_string. Assumes input is valid.
 *
 * Return value: the new #GucharmapSearchState.
 **/
Noah Levitt's avatar
Noah Levitt committed
475
static GucharmapSearchState * 
476
gucharmap_search_state_new (GucharmapCodepointList       *list,
Noah Levitt's avatar
Noah Levitt committed
477 478 479
                            const gchar                  *search_string, 
                            gint                          start_index, 
                            GucharmapDirection            direction, 
480 481
                            gboolean                      whole_word,
                            gboolean                      annotations)
Noah Levitt's avatar
Noah Levitt committed
482 483
{
  GucharmapSearchState *search_state;
484
  gchar *p, *q, *r;
Noah Levitt's avatar
Noah Levitt committed
485 486 487

  g_assert (direction == GUCHARMAP_DIRECTION_BACKWARD || direction == GUCHARMAP_DIRECTION_FORWARD);

488
  search_state = g_slice_new (GucharmapSearchState);
Noah Levitt's avatar
Noah Levitt committed
489

490
  search_state->list = g_object_ref (list);
Noah Levitt's avatar
Noah Levitt committed
491 492
  search_state->list_num_chars = gucharmap_codepoint_list_get_last_index (search_state->list) + 1;

Noah Levitt's avatar
Noah Levitt committed
493
  search_state->search_string = g_strdup (search_string);
494
  search_state->search_string_nfd_temp = g_utf8_normalize (search_string, -1, G_NORMALIZE_NFD);
Noah Levitt's avatar
Noah Levitt committed
495 496 497

  search_state->increment = direction;
  search_state->whole_word = whole_word;
498
  search_state->annotations = annotations;
499
  search_state->did_before_checks = FALSE;
Noah Levitt's avatar
Noah Levitt committed
500 501 502 503 504 505 506

  search_state->found_index = -1;
  search_state->dont_search = FALSE;

  search_state->start_index = start_index;
  search_state->curr_index = start_index;

507
  /* set end of search string to last non-space character */
508
  for (p = q = r = search_state->search_string_nfd_temp;
509 510 511 512 513 514 515 516
       p[0] != '\0';
       q = p, p = g_utf8_next_char (p))
    if (g_unichar_isspace (g_utf8_get_char (p)) && !g_unichar_isspace (g_utf8_get_char (q)))
	r = p;
  if (!g_unichar_isspace (g_utf8_get_char (q)))
      r = p;
  r[0] = '\0';

517 518 519
  /* caller should check not to search for empty string */
  g_return_val_if_fail (r != search_state->search_string_nfd_temp, FALSE);

520
  /* NFD */
Noah Levitt's avatar
Noah Levitt committed
521
  /* set pointer to first non-space character in the search string */
522 523 524 525 526 527 528 529 530 531
  for (search_state->search_string_nfd = search_state->search_string_nfd_temp;
       *search_state->search_string_nfd != '\0'
       && g_unichar_isspace (g_utf8_get_char (search_state->search_string_nfd));
       search_state->search_string_nfd = g_utf8_next_char (search_state->search_string_nfd))
    ;
  search_state->search_string_nfd_len = g_utf8_strlen (search_state->search_string_nfd, -1);
  if (search_state->search_string_nfd_len == 1)
    search_state->search_index_nfd = gucharmap_codepoint_list_get_index (search_state->list, g_utf8_get_char (search_state->search_string_nfd));
  else
    search_state->search_index_nfd = -1;
Noah Levitt's avatar
Noah Levitt committed
532

533 534 535 536 537 538 539 540 541 542
  /* NFC */
  search_state->search_string_nfc = g_utf8_normalize (search_state->search_string_nfd, -1, G_NORMALIZE_NFC);
  search_state->search_string_nfc_len = g_utf8_strlen (search_state->search_string_nfc, -1);
  if (search_state->search_string_nfc_len == 1)
    search_state->search_index_nfc = gucharmap_codepoint_list_get_index (search_state->list, g_utf8_get_char (search_state->search_string_nfc));
  else
    search_state->search_index_nfc = -1;

  /* INDEX */
  search_state->search_string_value = check_for_explicit_codepoint (search_state->list, search_state->search_string_nfd);
Noah Levitt's avatar
Noah Levitt committed
543

544
  search_state->searching = FALSE;
Noah Levitt's avatar
Noah Levitt committed
545 546 547 548 549 550 551
  return search_state;
}

static void
information_dialog (GucharmapSearchDialog *search_dialog,
                    const gchar           *message)
{
Noah Levitt's avatar
Noah Levitt committed
552
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
553 554
  GtkWidget *dialog;

555
  dialog = gtk_message_dialog_new (gtk_widget_get_visible (GTK_WIDGET (search_dialog)) ?
556 557 558 559 560 561
                                     GTK_WINDOW (search_dialog) :
                                     GTK_WINDOW (priv->guw),
                                   GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
                                   GTK_MESSAGE_INFO,
                                   GTK_BUTTONS_OK,
                                   "%s", message);
Noah Levitt's avatar
Noah Levitt committed
562
  gtk_window_set_title (GTK_WINDOW (dialog), _("Information"));
563
  gtk_window_set_icon_name (GTK_WINDOW (dialog), GUCHARMAP_ICON_NAME);
Noah Levitt's avatar
Noah Levitt committed
564 565 566
  gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);

567
  gtk_window_present (GTK_WINDOW (dialog));
Noah Levitt's avatar
Noah Levitt committed
568 569 570 571 572 573 574 575
}

static void
search_completed (GucharmapSearchDialog *search_dialog)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
  gunichar found_char = gucharmap_search_state_get_found_char (priv->search_state);

Noah Levitt's avatar
Noah Levitt committed
576 577
  priv->search_state->searching = FALSE;

Noah Levitt's avatar
Noah Levitt committed
578 579 580
  g_signal_emit (search_dialog, gucharmap_search_dialog_signals[SEARCH_FINISH], 0, found_char);

  if (found_char == (gunichar)(-1))
581
    {
582 583 584
      information_dialog (search_dialog, _("Not found."));
      gtk_widget_set_sensitive (priv->prev_button, FALSE);
      gtk_widget_set_sensitive (priv->next_button, FALSE);
585
    }
Noah Levitt's avatar
Noah Levitt committed
586

587 588
  if (gtk_widget_get_realized (GTK_WIDGET (search_dialog)))
      gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (search_dialog)), NULL);
Noah Levitt's avatar
Noah Levitt committed
589 590
}

591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
static gboolean
_entry_is_empty (GtkEntry *entry)
{
  const gchar *text = gtk_entry_get_text (entry);
  const gchar *p;	/* points into text */

  for (p = text;
       p[0] != '\0' && g_unichar_isspace (g_utf8_get_char (p));
       p = g_utf8_next_char (p))
    ;
  return p[0] == '\0';
}

static void
_gucharmap_search_dialog_fire_search (GucharmapSearchDialog *search_dialog,
Noah Levitt's avatar
Noah Levitt committed
606 607 608 609 610 611
                                      GucharmapDirection     direction)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
  GucharmapCodepointList *list;
  gunichar start_char;
  gint start_index;
Christian Persch's avatar
Christian Persch committed
612
  GdkCursor *cursor;
Noah Levitt's avatar
Noah Levitt committed
613

614 615 616
  if (priv->search_state && priv->search_state->searching) /* Already searching */
    return;

617 618 619 620 621
  if (gtk_widget_get_realized (GTK_WIDGET (search_dialog))) {
    cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (search_dialog)), GDK_WATCH);
    gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (search_dialog)), cursor);
    g_object_unref (cursor);
  }
Noah Levitt's avatar
Noah Levitt committed
622

623
  list = gucharmap_charmap_get_book_codepoint_list (priv->guw->charmap);
624 625
  if (!list)
    return;
Noah Levitt's avatar
Noah Levitt committed
626 627 628

  if (priv->search_state == NULL
      || list != priv->search_state->list
629
      || strcmp (priv->search_state->search_string, gtk_entry_get_text (GTK_ENTRY (priv->entry))) != 0
630 631
      || priv->search_state->whole_word != gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->whole_word_option))
      || priv->search_state->annotations != gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->annotations_option)) )
Noah Levitt's avatar
Noah Levitt committed
632 633 634 635
    {
      if (priv->search_state)
        gucharmap_search_state_free (priv->search_state);

636
      start_char = gucharmap_charmap_get_active_character (priv->guw->charmap);
Noah Levitt's avatar
Noah Levitt committed
637
      start_index = gucharmap_codepoint_list_get_index (list, start_char);
638 639 640 641
      priv->search_state = gucharmap_search_state_new (list, gtk_entry_get_text (GTK_ENTRY (priv->entry)),
			      start_index, direction,
			      gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->whole_word_option)),
			      gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->annotations_option)) );
Noah Levitt's avatar
Noah Levitt committed
642 643 644
    }
  else
    {
645
      start_char = gucharmap_charmap_get_active_character (priv->guw->charmap);
Noah Levitt's avatar
Noah Levitt committed
646 647 648 649
      priv->search_state->start_index = gucharmap_codepoint_list_get_index (list, start_char);
      priv->search_state->curr_index = priv->search_state->start_index;
      priv->search_state->increment = direction;
    }
Noah Levitt's avatar
Noah Levitt committed
650

Noah Levitt's avatar
Noah Levitt committed
651 652
  priv->search_state->searching = TRUE;
  priv->search_state->strings_checked = 0;
Noah Levitt's avatar
Noah Levitt committed
653 654

  g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, (GSourceFunc) idle_search, search_dialog, (GDestroyNotify) search_completed);
Noah Levitt's avatar
Noah Levitt committed
655
  g_signal_emit (search_dialog, gucharmap_search_dialog_signals[SEARCH_START], 0);
656 657

  g_object_unref (list);
Noah Levitt's avatar
Noah Levitt committed
658 659
}

660 661 662 663 664 665
void
gucharmap_search_dialog_start_search (GucharmapSearchDialog *search_dialog,
                                      GucharmapDirection     direction)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);

666
  if (!_entry_is_empty (GTK_ENTRY (priv->entry)))
667 668 669 670 671
    _gucharmap_search_dialog_fire_search (search_dialog, direction);
  else
    gtk_window_present (GTK_WINDOW (search_dialog));
}

Noah Levitt's avatar
Noah Levitt committed
672 673 674 675 676
static void
search_find_response (GtkDialog *dialog,
                      gint       response)
{
  GucharmapSearchDialog *search_dialog = GUCHARMAP_SEARCH_DIALOG (dialog);
Noah Levitt's avatar
Noah Levitt committed
677
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
Noah Levitt's avatar
Noah Levitt committed
678 679 680 681

  switch (response)
    {
      case GUCHARMAP_RESPONSE_PREVIOUS:
682
        _gucharmap_search_dialog_fire_search (search_dialog, GUCHARMAP_DIRECTION_BACKWARD);
Noah Levitt's avatar
Noah Levitt committed
683 684 685
        break;

      case GUCHARMAP_RESPONSE_NEXT:
686
        _gucharmap_search_dialog_fire_search (search_dialog, GUCHARMAP_DIRECTION_FORWARD);
Noah Levitt's avatar
Noah Levitt committed
687 688 689 690 691 692
        break;

      default:
        gtk_widget_hide (GTK_WIDGET (search_dialog));
        break;
    }
Noah Levitt's avatar
Noah Levitt committed
693 694

  gtk_editable_select_region (GTK_EDITABLE (priv->entry), 0, -1);
Noah Levitt's avatar
Noah Levitt committed
695 696 697
}

static void
698
entry_changed (GObject               *object,
Noah Levitt's avatar
Noah Levitt committed
699 700 701
               GucharmapSearchDialog *search_dialog)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
Christian Persch's avatar
Christian Persch committed
702
  gboolean is_empty;
Noah Levitt's avatar
Noah Levitt committed
703

Christian Persch's avatar
Christian Persch committed
704 705 706 707
  is_empty = _entry_is_empty (GTK_ENTRY (priv->entry));
      
  gtk_widget_set_sensitive (priv->prev_button, !is_empty);
  gtk_widget_set_sensitive (priv->next_button, !is_empty);
Noah Levitt's avatar
Noah Levitt committed
708 709
}

Noah Levitt's avatar
Noah Levitt committed
710 711 712 713 714
static void 
set_button_stock_image_and_label (GtkButton *button,
                                  gchar     *stock_id,
                                  gchar     *mnemonic)
{
715
  GtkWidget *image;
Noah Levitt's avatar
Noah Levitt committed
716 717

  image = gtk_image_new_from_stock (stock_id, GTK_ICON_SIZE_BUTTON);
718 719 720
  gtk_button_set_image (button, image);
  gtk_button_set_label (button, mnemonic);
  gtk_button_set_use_underline (button, TRUE);
Noah Levitt's avatar
Noah Levitt committed
721 722
}

Noah Levitt's avatar
Noah Levitt committed
723 724 725 726
static void
gucharmap_search_dialog_init (GucharmapSearchDialog *search_dialog)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
727
  GtkWidget *grid, *label, *content_area;
Christian Persch's avatar
Christian Persch committed
728 729

  content_area = gtk_dialog_get_content_area (GTK_DIALOG (search_dialog));
Noah Levitt's avatar
Noah Levitt committed
730 731 732 733 734

  /* follow hig guidelines */
  gtk_window_set_title (GTK_WINDOW (search_dialog), _("Find"));
  gtk_container_set_border_width (GTK_CONTAINER (search_dialog), 6);
  gtk_window_set_destroy_with_parent (GTK_WINDOW (search_dialog), TRUE);
Christian Persch's avatar
Christian Persch committed
735
  gtk_box_set_spacing (GTK_BOX (content_area), 12);
Noah Levitt's avatar
Noah Levitt committed
736 737
  gtk_window_set_resizable (GTK_WINDOW (search_dialog), FALSE);

Noah Levitt's avatar
Noah Levitt committed
738
  g_signal_connect (search_dialog, "delete-event", G_CALLBACK (gtk_widget_hide_on_delete), NULL);
Noah Levitt's avatar
Noah Levitt committed
739 740

  /* add buttons */
741
  gtk_dialog_add_button (GTK_DIALOG (search_dialog), GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE);
Noah Levitt's avatar
Noah Levitt committed
742 743

  priv->prev_button = gtk_button_new ();
744
  gtk_widget_set_can_default (priv->prev_button, TRUE);
Noah Levitt's avatar
Noah Levitt committed
745 746 747 748 749
  set_button_stock_image_and_label (GTK_BUTTON (priv->prev_button), GTK_STOCK_GO_BACK, _("_Previous"));
  gtk_dialog_add_action_widget (GTK_DIALOG (search_dialog), priv->prev_button, GUCHARMAP_RESPONSE_PREVIOUS);
  gtk_widget_show (priv->prev_button);

  priv->next_button = gtk_button_new ();
Daiki Ueno's avatar
Daiki Ueno committed
750
  gtk_widget_set_can_default (priv->next_button, TRUE);
Noah Levitt's avatar
Noah Levitt committed
751 752 753 754
  gtk_widget_show (priv->next_button);
  set_button_stock_image_and_label (GTK_BUTTON (priv->next_button), GTK_STOCK_GO_FORWARD, _("_Next"));
  gtk_dialog_add_action_widget (GTK_DIALOG (search_dialog), priv->next_button, GUCHARMAP_RESPONSE_NEXT);

Noah Levitt's avatar
Noah Levitt committed
755
  gtk_dialog_set_default_response (GTK_DIALOG (search_dialog), GUCHARMAP_RESPONSE_NEXT);
756 757 758 759 760
  gtk_dialog_set_alternative_button_order (GTK_DIALOG (search_dialog),
                                           GUCHARMAP_RESPONSE_PREVIOUS,
                                           GUCHARMAP_RESPONSE_NEXT,
                                           GTK_RESPONSE_CLOSE,
                                           -1);
Noah Levitt's avatar
Noah Levitt committed
761

762 763 764 765 766
  grid = gtk_grid_new ();
  gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
  gtk_widget_show (grid);
  gtk_container_set_border_width (GTK_CONTAINER (grid), 6);
  gtk_box_pack_start (GTK_BOX (content_area), grid, FALSE, FALSE, 0);
Noah Levitt's avatar
Noah Levitt committed
767 768 769

  label = gtk_label_new_with_mnemonic (_("_Search:"));
  gtk_widget_show (label);
770
  gtk_grid_attach (GTK_GRID (grid), label, 0, 0, 1, 1);
Noah Levitt's avatar
Noah Levitt committed
771 772 773

  priv->entry = gtk_entry_new ();
  gtk_widget_show (priv->entry);
774
  gtk_widget_set_hexpand (priv->entry, TRUE);
Noah Levitt's avatar
Noah Levitt committed
775
  gtk_entry_set_activates_default (GTK_ENTRY (priv->entry), TRUE);
776
  gtk_grid_attach (GTK_GRID (grid), priv->entry, 1, 0, 1, 1);
Noah Levitt's avatar
Noah Levitt committed
777 778
  g_signal_connect (priv->entry, "changed", G_CALLBACK (entry_changed), search_dialog);

779
  priv->whole_word_option = gtk_check_button_new_with_mnemonic (_("Match _whole word"));
780
  gtk_widget_show (priv->whole_word_option);
Christian Persch's avatar
Christian Persch committed
781
  gtk_box_pack_start (GTK_BOX (content_area), priv->whole_word_option, FALSE, FALSE, 0);
782
  g_signal_connect (priv->whole_word_option, "toggled", G_CALLBACK (entry_changed), search_dialog);
783

784 785
  priv->annotations_option = gtk_check_button_new_with_mnemonic (_("Search in character _details"));
  gtk_widget_show (priv->annotations_option);
Christian Persch's avatar
Christian Persch committed
786
  gtk_box_pack_start (GTK_BOX (content_area), priv->annotations_option, FALSE, FALSE, 0);
787
  g_signal_connect (priv->annotations_option, "toggled", G_CALLBACK (entry_changed), search_dialog);
788

Noah Levitt's avatar
Noah Levitt committed
789 790
  gtk_label_set_mnemonic_widget (GTK_LABEL (label), priv->entry);

Noah Levitt's avatar
Noah Levitt committed
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808
  /* since the entry is empty */
  gtk_widget_set_sensitive (priv->prev_button, FALSE);
  gtk_widget_set_sensitive (priv->next_button, FALSE);

  priv->search_state = NULL;
  priv->guw = NULL;

  g_signal_connect (GTK_DIALOG (search_dialog), "response", G_CALLBACK (search_find_response), NULL);
}

static void 
gucharmap_search_dialog_finalize (GObject *object)
{
  GucharmapSearchDialog *search_dialog = GUCHARMAP_SEARCH_DIALOG (object);
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);

  if (priv->search_state)
    gucharmap_search_state_free (priv->search_state);
809

810
  G_OBJECT_CLASS (gucharmap_search_dialog_parent_class)->finalize (object);
Noah Levitt's avatar
Noah Levitt committed
811 812 813 814 815 816 817 818 819 820 821 822 823
}

static void
gucharmap_search_dialog_class_init (GucharmapSearchDialogClass *clazz)
{
  g_type_class_add_private (clazz, sizeof (GucharmapSearchDialogPrivate));

  G_OBJECT_CLASS (clazz)->finalize = gucharmap_search_dialog_finalize;

  clazz->search_start = NULL;
  clazz->search_finish = NULL;

  gucharmap_search_dialog_signals[SEARCH_START] =
824
      g_signal_new (I_("search-start"), gucharmap_search_dialog_get_type (), G_SIGNAL_RUN_FIRST,
Noah Levitt's avatar
Noah Levitt committed
825
                    G_STRUCT_OFFSET (GucharmapSearchDialogClass, search_start), NULL, NULL, 
826
                    g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
Noah Levitt's avatar
Noah Levitt committed
827
  gucharmap_search_dialog_signals[SEARCH_FINISH] =
828
      g_signal_new (I_("search-finish"), gucharmap_search_dialog_get_type (), G_SIGNAL_RUN_FIRST, 
Noah Levitt's avatar
Noah Levitt committed
829
                    G_STRUCT_OFFSET (GucharmapSearchDialogClass, search_finish), NULL, NULL, 
830
                    g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT);
Noah Levitt's avatar
Noah Levitt committed
831 832
}

833
GtkWidget *
Noah Levitt's avatar
Noah Levitt committed
834 835 836 837 838 839 840 841 842 843 844 845 846 847
gucharmap_search_dialog_new (GucharmapWindow *guw)
{
  GucharmapSearchDialog *search_dialog = g_object_new (gucharmap_search_dialog_get_type (), NULL);
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);

  priv->guw = guw;

  gtk_window_set_transient_for (GTK_WINDOW (search_dialog), GTK_WINDOW (guw));

  if (guw)
    gtk_window_set_icon (GTK_WINDOW (search_dialog), gtk_window_get_icon (GTK_WINDOW (guw)));

  return GTK_WIDGET (search_dialog);
}
Noah Levitt's avatar
Noah Levitt committed
848

849 850 851 852 853 854 855
void
gucharmap_search_dialog_present (GucharmapSearchDialog *search_dialog)
{
  gtk_widget_grab_focus (GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog)->entry);
  gtk_window_present (GTK_WINDOW (search_dialog));
}

856 857 858 859 860 861 862 863 864 865 866 867
void
gucharmap_search_dialog_set_search (GucharmapSearchDialog *search_dialog,
                                    const char            *search_string)
{
  GucharmapSearchDialogPrivate *priv;
  g_return_if_fail (GUCHARMAP_IS_SEARCH_DIALOG (search_dialog));
  g_return_if_fail (search_string != NULL);

  priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);
  gtk_entry_set_text (GTK_ENTRY (priv->entry), search_string);
}

Noah Levitt's avatar
Noah Levitt committed
868 869 870 871 872 873 874 875 876 877 878 879 880
gdouble
gucharmap_search_dialog_get_completed (GucharmapSearchDialog *search_dialog)
{
  GucharmapSearchDialogPrivate *priv = GUCHARMAP_SEARCH_DIALOG_GET_PRIVATE (search_dialog);

  if (priv->search_state == NULL || !priv->search_state->searching)
    return -1.0;
  else
    {
      gdouble total = gucharmap_get_unicode_data_name_count () + gucharmap_get_unihan_count ();
      return (gdouble) priv->search_state->strings_checked / total;
    }
}