1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2002 CodeFactory AB
4  * Copyright (C) 2002 Mikael Hallendal <micke@imendio.com>
5  * Copyright (C) 2008 Imendio AB
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation; either version 2 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public
18  * License along with this program; if not, write to the
19  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20  * Boston, MA 02111-1307, USA.
21  */
22 
23 #include "config.h"
24 #include <gtk/gtk.h>
25 #include <string.h>
26 
27 #include "dh-link.h"
28 #include "dh-book.h"
29 #include "dh-keyword-model.h"
30 
31 struct _DhKeywordModelPriv {
32         DhBookManager *book_manager;
33 
34         GList *keyword_words;
35         gint   keyword_words_length;
36 
37         gint   stamp;
38 };
39 
40 #define G_LIST(x) ((GList *) x)
41 #define MAX_HITS 100
42 
43 static void dh_keyword_model_init            (DhKeywordModel      *list_store);
44 static void dh_keyword_model_class_init      (DhKeywordModelClass *class);
45 static void dh_keyword_model_tree_model_init (GtkTreeModelIface   *iface);
46 
47 G_DEFINE_TYPE_WITH_CODE (DhKeywordModel, dh_keyword_model, G_TYPE_OBJECT,
48                          G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL,
49                                                 dh_keyword_model_tree_model_init));
50 
51 static void
keyword_model_dispose(GObject * object)52 keyword_model_dispose (GObject *object)
53 {
54         DhKeywordModel     *model = DH_KEYWORD_MODEL (object);
55         DhKeywordModelPriv *priv = model->priv;
56 
57         if (priv->book_manager) {
58                 g_object_unref (priv->book_manager);
59                 priv->book_manager = NULL;
60         }
61 
62         G_OBJECT_CLASS (dh_keyword_model_parent_class)->dispose (object);
63 }
64 
65 static void
keyword_model_finalize(GObject * object)66 keyword_model_finalize (GObject *object)
67 {
68         DhKeywordModel     *model = DH_KEYWORD_MODEL (object);
69         DhKeywordModelPriv *priv = model->priv;
70 
71         g_list_free (priv->keyword_words);
72 
73         g_free (model->priv);
74 
75         G_OBJECT_CLASS (dh_keyword_model_parent_class)->finalize (object);
76 }
77 
78 static void
dh_keyword_model_class_init(DhKeywordModelClass * klass)79 dh_keyword_model_class_init (DhKeywordModelClass *klass)
80 {
81         GObjectClass *object_class = G_OBJECT_CLASS (klass);;
82 
83         object_class->finalize = keyword_model_finalize;
84         object_class->dispose = keyword_model_dispose;
85 }
86 
87 static void
dh_keyword_model_init(DhKeywordModel * model)88 dh_keyword_model_init (DhKeywordModel *model)
89 {
90         DhKeywordModelPriv *priv;
91 
92         priv = g_new0 (DhKeywordModelPriv, 1);
93         model->priv = priv;
94 
95         do {
96                 priv->stamp = g_random_int ();
97         } while (priv->stamp == 0);
98 }
99 
100 static GtkTreeModelFlags
keyword_model_get_flags(GtkTreeModel * tree_model)101 keyword_model_get_flags (GtkTreeModel *tree_model)
102 {
103         return GTK_TREE_MODEL_ITERS_PERSIST | GTK_TREE_MODEL_LIST_ONLY;
104 }
105 
106 static gint
keyword_model_get_n_columns(GtkTreeModel * tree_model)107 keyword_model_get_n_columns (GtkTreeModel *tree_model)
108 {
109         return DH_KEYWORD_MODEL_NUM_COLS;
110 }
111 
112 static GType
keyword_model_get_column_type(GtkTreeModel * tree_model,gint column)113 keyword_model_get_column_type (GtkTreeModel *tree_model,
114                                gint          column)
115 {
116         switch (column) {
117         case DH_KEYWORD_MODEL_COL_NAME:
118                 return G_TYPE_STRING;
119                 break;
120         case DH_KEYWORD_MODEL_COL_LINK:
121                 return G_TYPE_POINTER;
122         default:
123                 return G_TYPE_INVALID;
124         }
125 }
126 
127 static gboolean
keyword_model_get_iter(GtkTreeModel * tree_model,GtkTreeIter * iter,GtkTreePath * path)128 keyword_model_get_iter (GtkTreeModel *tree_model,
129                         GtkTreeIter  *iter,
130                         GtkTreePath  *path)
131 {
132         DhKeywordModel     *model;
133         DhKeywordModelPriv *priv;
134         GList              *node;
135         const gint         *indices;
136 
137         model = DH_KEYWORD_MODEL (tree_model);
138         priv = model->priv;
139 
140         indices = gtk_tree_path_get_indices (path);
141 
142         if (indices == NULL) {
143                 return FALSE;
144         }
145 
146         if (indices[0] >= priv->keyword_words_length) {
147                 return FALSE;
148         }
149 
150         node = g_list_nth (priv->keyword_words, indices[0]);
151 
152         iter->stamp     = priv->stamp;
153         iter->user_data = node;
154 
155         return TRUE;
156 }
157 
158 static GtkTreePath *
keyword_model_get_path(GtkTreeModel * tree_model,GtkTreeIter * iter)159 keyword_model_get_path (GtkTreeModel *tree_model,
160                         GtkTreeIter  *iter)
161 {
162         DhKeywordModel     *model = DH_KEYWORD_MODEL (tree_model);
163         DhKeywordModelPriv *priv;
164         GtkTreePath        *path;
165         gint                i = 0;
166 
167         g_return_val_if_fail (iter->stamp == model->priv->stamp, NULL);
168 
169         priv = model->priv;
170 
171         i = g_list_position (priv->keyword_words, iter->user_data);
172         if (i < 0) {
173                 return NULL;
174         }
175 
176         path = gtk_tree_path_new ();
177         gtk_tree_path_append_index (path, i);
178 
179         return path;
180 }
181 
182 static void
keyword_model_get_value(GtkTreeModel * tree_model,GtkTreeIter * iter,gint column,GValue * value)183 keyword_model_get_value (GtkTreeModel *tree_model,
184                          GtkTreeIter  *iter,
185                          gint          column,
186                          GValue       *value)
187 {
188         DhLink *link;
189 
190         link = G_LIST (iter->user_data)->data;
191 
192         switch (column) {
193         case DH_KEYWORD_MODEL_COL_NAME:
194                 g_value_init (value, G_TYPE_STRING);
195                 g_value_set_string (value, dh_link_get_name (link));
196                 break;
197         case DH_KEYWORD_MODEL_COL_LINK:
198                 g_value_init (value, G_TYPE_POINTER);
199                 g_value_set_pointer (value, link);
200                 break;
201         default:
202                 g_warning ("Bad column %d requested", column);
203         }
204 }
205 
206 static gboolean
keyword_model_iter_next(GtkTreeModel * tree_model,GtkTreeIter * iter)207 keyword_model_iter_next (GtkTreeModel *tree_model,
208                          GtkTreeIter  *iter)
209 {
210         DhKeywordModel *model = DH_KEYWORD_MODEL (tree_model);
211 
212         g_return_val_if_fail (model->priv->stamp == iter->stamp, FALSE);
213 
214         iter->user_data = G_LIST (iter->user_data)->next;
215 
216         return (iter->user_data != NULL);
217 }
218 
219 static gboolean
keyword_model_iter_children(GtkTreeModel * tree_model,GtkTreeIter * iter,GtkTreeIter * parent)220 keyword_model_iter_children (GtkTreeModel *tree_model,
221                              GtkTreeIter  *iter,
222                              GtkTreeIter  *parent)
223 {
224         DhKeywordModelPriv *priv;
225 
226         priv = DH_KEYWORD_MODEL (tree_model)->priv;
227 
228         /* This is a list, nodes have no children. */
229         if (parent) {
230                 return FALSE;
231         }
232 
233         /* But if parent == NULL we return the list itself as children of
234          * the "root".
235          */
236         if (priv->keyword_words) {
237                 iter->stamp = priv->stamp;
238                 iter->user_data = priv->keyword_words;
239                 return TRUE;
240         }
241 
242         return FALSE;
243 }
244 
245 static gboolean
keyword_model_iter_has_child(GtkTreeModel * tree_model,GtkTreeIter * iter)246 keyword_model_iter_has_child (GtkTreeModel *tree_model,
247                               GtkTreeIter  *iter)
248 {
249         return FALSE;
250 }
251 
252 static gint
keyword_model_iter_n_children(GtkTreeModel * tree_model,GtkTreeIter * iter)253 keyword_model_iter_n_children (GtkTreeModel *tree_model,
254                                GtkTreeIter  *iter)
255 {
256         DhKeywordModelPriv *priv;
257 
258         priv = DH_KEYWORD_MODEL (tree_model)->priv;
259 
260         if (iter == NULL) {
261                 return priv->keyword_words_length;
262         }
263 
264         g_return_val_if_fail (priv->stamp == iter->stamp, -1);
265 
266         return 0;
267 }
268 
269 static gboolean
keyword_model_iter_nth_child(GtkTreeModel * tree_model,GtkTreeIter * iter,GtkTreeIter * parent,gint n)270 keyword_model_iter_nth_child (GtkTreeModel *tree_model,
271                               GtkTreeIter  *iter,
272                               GtkTreeIter  *parent,
273                               gint          n)
274 {
275         DhKeywordModelPriv *priv;
276         GList              *child;
277 
278         priv = DH_KEYWORD_MODEL (tree_model)->priv;
279 
280         if (parent) {
281                 return FALSE;
282         }
283 
284         child = g_list_nth (priv->keyword_words, n);
285 
286         if (child) {
287                 iter->stamp = priv->stamp;
288                 iter->user_data = child;
289                 return TRUE;
290         }
291 
292         return FALSE;
293 }
294 
295 static gboolean
keyword_model_iter_parent(GtkTreeModel * tree_model,GtkTreeIter * iter,GtkTreeIter * child)296 keyword_model_iter_parent (GtkTreeModel *tree_model,
297                            GtkTreeIter  *iter,
298                            GtkTreeIter  *child)
299 {
300         return FALSE;
301 }
302 
303 static void
dh_keyword_model_tree_model_init(GtkTreeModelIface * iface)304 dh_keyword_model_tree_model_init (GtkTreeModelIface *iface)
305 {
306         iface->get_flags       = keyword_model_get_flags;
307         iface->get_n_columns   = keyword_model_get_n_columns;
308         iface->get_column_type = keyword_model_get_column_type;
309         iface->get_iter        = keyword_model_get_iter;
310         iface->get_path        = keyword_model_get_path;
311         iface->get_value       = keyword_model_get_value;
312         iface->iter_next       = keyword_model_iter_next;
313         iface->iter_children   = keyword_model_iter_children;
314         iface->iter_has_child  = keyword_model_iter_has_child;
315         iface->iter_n_children = keyword_model_iter_n_children;
316         iface->iter_nth_child  = keyword_model_iter_nth_child;
317         iface->iter_parent     = keyword_model_iter_parent;
318 }
319 
320 DhKeywordModel *
dh_keyword_model_new(void)321 dh_keyword_model_new (void)
322 {
323         DhKeywordModel *model;
324 
325         model = g_object_new (DH_TYPE_KEYWORD_MODEL, NULL);
326 
327         return model;
328 }
329 
330 void
dh_keyword_model_set_words(DhKeywordModel * model,DhBookManager * book_manager)331 dh_keyword_model_set_words (DhKeywordModel *model,
332                             DhBookManager  *book_manager)
333 {
334         g_return_if_fail (DH_IS_KEYWORD_MODEL (model));
335 
336         model->priv->book_manager = g_object_ref (book_manager);
337 }
338 
339 static GList *
keyword_model_search(DhKeywordModel * model,const gchar * string,gchar ** stringv,const gchar * book_id,gboolean case_sensitive,DhLink ** exact_link)340 keyword_model_search (DhKeywordModel  *model,
341                       const gchar     *string,
342                       gchar          **stringv,
343                       const gchar     *book_id,
344                       gboolean         case_sensitive,
345                       DhLink         **exact_link)
346 {
347         DhKeywordModelPriv *priv;
348         GList              *new_list = NULL, *b;
349         gint                hits = 0;
350         gchar              *page_id = NULL;
351         gchar              *page_filename_prefix = NULL;
352 
353         priv = model->priv;
354 
355         /* The search string may be prefixed by a page:foobar qualifier, it
356          * will be matched against the filenames of the hits to limit the
357          * search to pages whose filename is prefixed by "foobar.
358          */
359         if (stringv && g_str_has_prefix(stringv[0], "page:")) {
360                 page_id = stringv[0] + 5;
361                 page_filename_prefix = g_strdup_printf("%s.", page_id);
362                 stringv++;
363         }
364 
365         for (b = dh_book_manager_get_books (priv->book_manager);
366              b && hits < MAX_HITS;
367              b = g_list_next (b)) {
368                 DhBook *book;
369                 GList *l;
370 
371                 book = DH_BOOK (b->data);
372 
373                 for (l = dh_book_get_keywords (book);
374                      l && hits < MAX_HITS;
375                      l = g_list_next (l)) {
376                         DhLink   *link;
377                         gboolean  found;
378                         gchar    *name;
379 
380                         link = l->data;
381                         found = FALSE;
382 
383                         if (book_id &&
384                             dh_link_get_book_id (link) &&
385                             strcmp (dh_link_get_book_id (link), book_id) != 0) {
386                                 continue;
387                         }
388 
389                         if (page_id &&
390                             (dh_link_get_link_type (link) != DH_LINK_TYPE_PAGE &&
391                              !g_str_has_prefix (dh_link_get_file_name (link), page_filename_prefix))) {
392                                 continue;
393                         }
394 
395                         if (!case_sensitive) {
396                                 name = g_ascii_strdown (dh_link_get_name (link), -1);
397                         } else {
398                                 name = g_strdup (dh_link_get_name (link));
399                         }
400 
401                         if (!found) {
402                                 gint i;
403 
404                                 if (stringv[0] == NULL) {
405                                         /* means only a page was specified, no keyword */
406                                         if (g_strrstr (dh_link_get_name(link), page_id))
407                                                 found = TRUE;
408                                 } else {
409                                         found = TRUE;
410                                         for (i = 0; stringv[i] != NULL; i++) {
411                                                 if (!g_strrstr (name, stringv[i])) {
412                                                         found = FALSE;
413                                                         break;
414                                                 }
415                                         }
416                                 }
417                         }
418 
419                         g_free (name);
420 
421                         if (found) {
422                                 /* Include in the new list. */
423                                 new_list = g_list_prepend (new_list, link);
424                                 hits++;
425 
426                                 if (!*exact_link &&
427                                     dh_link_get_name (link) && (
428                                             (dh_link_get_link_type (link) == DH_LINK_TYPE_PAGE &&
429                                              page_id && strcmp (dh_link_get_name (link), page_id) == 0) ||
430                                             (strcmp (dh_link_get_name (link), string) == 0))) {
431                                         *exact_link = link;
432                                 }
433                         }
434                 }
435         }
436 
437         g_free (page_filename_prefix);
438 
439         return g_list_sort (new_list, dh_link_compare);
440 }
441 
442 DhLink *
dh_keyword_model_filter(DhKeywordModel * model,const gchar * string,const gchar * book_id)443 dh_keyword_model_filter (DhKeywordModel *model,
444                          const gchar    *string,
445                          const gchar    *book_id)
446 {
447         DhKeywordModelPriv  *priv;
448         GList               *new_list = NULL;
449         gint                 old_length;
450         DhLink              *exact_link = NULL;
451         gint                 hits;
452         gint                 i;
453         GtkTreePath         *path;
454         GtkTreeIter          iter;
455 
456         g_return_val_if_fail (DH_IS_KEYWORD_MODEL (model), NULL);
457         g_return_val_if_fail (string != NULL, NULL);
458 
459         priv = model->priv;
460 
461         /* Do the minimum amount of work: call update on all rows that are
462          * kept and remove the rest.
463          */
464         old_length = priv->keyword_words_length;
465         new_list = NULL;
466         hits = 0;
467 
468         if (string[0] != '\0') {
469                 gchar    **stringv;
470                 gboolean   case_sensitive;
471 
472                 stringv = g_strsplit (string, " ", -1);
473 
474                 case_sensitive = FALSE;
475 
476                 /* Search for any parameters and position search cursor to
477                  * the next element in the search string.
478                  */
479                 for (i = 0; stringv[i] != NULL; i++) {
480                         gchar *lower;
481 
482                         /* Searches are case sensitive when any uppercase
483                          * letter is used in the search terms, matching vim
484                          * smartcase behaviour.
485                          */
486                         lower = g_ascii_strdown (stringv[i], -1);
487                         if (strcmp (lower, stringv[i]) != 0) {
488                                 case_sensitive = TRUE;
489                                 g_free (lower);
490                                 break;
491                         }
492                         g_free (lower);
493                 }
494 
495                 new_list = keyword_model_search (model,
496                                                  string,
497                                                  stringv,
498                                                  book_id,
499                                                  case_sensitive,
500                                                  &exact_link);
501                 hits = g_list_length (new_list);
502 
503                 g_strfreev (stringv);
504         }
505 
506         /* Update the list of hits. */
507         g_list_free (priv->keyword_words);
508         priv->keyword_words = new_list;
509         priv->keyword_words_length = hits;
510 
511         /* Update model: rows 0 -> hits. */
512         for (i = 0; i < hits; ++i) {
513                 path = gtk_tree_path_new ();
514                 gtk_tree_path_append_index (path, i);
515                 keyword_model_get_iter (GTK_TREE_MODEL (model), &iter, path);
516                 gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
517                 gtk_tree_path_free (path);
518         }
519 
520         if (old_length > hits) {
521                 /* Update model: remove rows hits -> old_length. */
522                 for (i = old_length - 1; i >= hits; i--) {
523                         path = gtk_tree_path_new ();
524                         gtk_tree_path_append_index (path, i);
525                         gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
526                         gtk_tree_path_free (path);
527                 }
528         }
529         else if (old_length < hits) {
530                 /* Update model: add rows old_length -> hits. */
531                 for (i = old_length; i < hits; i++) {
532                         path = gtk_tree_path_new ();
533                         gtk_tree_path_append_index (path, i);
534                         keyword_model_get_iter (GTK_TREE_MODEL (model), &iter, path);
535                         gtk_tree_model_row_inserted (GTK_TREE_MODEL (model), path, &iter);
536                         gtk_tree_path_free (path);
537                 }
538         }
539 
540         if (hits == 1) {
541                 return priv->keyword_words->data;
542         }
543 
544         return exact_link;
545 }
546