1 /*
2  *  Copyright (c) 2008-2012 Mike Massonnet <mmassonnet@xfce.org>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
17  */
18 
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22 
23 #include <glib.h>
24 #include <glib-object.h>
25 #include <gtk/gtk.h>
26 #include <libxfce4util/libxfce4util.h>
27 
28 #include "common.h"
29 
30 #include "history.h"
31 
32 /*
33  * GObject declarations
34  */
35 
36 struct _ClipmanHistoryPrivate
37 {
38   GSList                       *items;
39   const ClipmanHistoryItem     *item_to_restore;
40   guint                         max_texts_in_history;
41   guint                         max_images_in_history;
42   gboolean                      save_on_quit;
43   gboolean                      reorder_items;
44 };
45 
46 G_DEFINE_TYPE_WITH_PRIVATE (ClipmanHistory, clipman_history, G_TYPE_OBJECT)
47 
48 enum
49 {
50   MAX_TEXTS_IN_HISTORY = 1,
51   MAX_IMAGES_IN_HISTORY,
52   SAVE_ON_QUIT,
53   REORDER_ITEMS,
54 };
55 
56 enum
57 {
58   ITEM_ADDED,
59   CLEAR,
60   LAST_SIGNAL,
61 };
62 static guint signals[LAST_SIGNAL];
63 
64 static void             clipman_history_finalize           (GObject *object);
65 static void             clipman_history_set_property       (GObject *object,
66                                                             guint property_id,
67                                                             const GValue *value,
68                                                             GParamSpec *pspec);
69 static void             clipman_history_get_property       (GObject *object,
70                                                             guint property_id,
71                                                             GValue *value,
72                                                             GParamSpec *pspec);
73 
74 /*
75  * Private methods declarations
76  */
77 
78 static void            _clipman_history_add_item           (ClipmanHistory *history,
79                                                             ClipmanHistoryItem *item);
80 
81 /*
82  * Misc functions declarations
83  */
84 
85 static void           __clipman_history_item_free          (ClipmanHistoryItem *item);
86 static gint           __g_slist_compare_texts              (gconstpointer a,
87                                                             gconstpointer b);
88 
89 
90 
91 /*
92  * Private methods
93  */
94 
95 static void
_clipman_history_add_item(ClipmanHistory * history,ClipmanHistoryItem * item)96 _clipman_history_add_item (ClipmanHistory *history,
97                            ClipmanHistoryItem *item)
98 {
99   GSList *list;
100   ClipmanHistoryItem *_item;
101   guint list_length;
102   guint n_texts = 0;
103   guint n_images = 0;
104 
105   /* Count initial items */
106   for (list = history->priv->items; list != NULL; list = list->next)
107     {
108       _item = list->data;
109       if (_item->type == CLIPMAN_HISTORY_TYPE_TEXT)
110         {
111           n_texts++;
112         }
113       else if (_item->type == CLIPMAN_HISTORY_TYPE_IMAGE)
114         {
115           n_images++;
116         }
117     }
118 
119   list_length = n_texts + n_images;
120 
121   /* First truncate history to max_items (max_texts stands for the size of the history) */
122   while (list_length > history->priv->max_texts_in_history)
123     {
124       DBG ("Delete oldest content from the history");
125       list = g_slist_last (history->priv->items);
126       _item = list->data;
127 
128       if (_item->type == CLIPMAN_HISTORY_TYPE_TEXT)
129         {
130           n_texts--;
131         }
132       else if (_item->type == CLIPMAN_HISTORY_TYPE_IMAGE)
133         {
134           n_images--;
135         }
136       list_length--;
137 
138       __clipman_history_item_free (_item);
139       history->priv->items = g_slist_remove (history->priv->items, _item);
140     }
141 
142   /* Free last image from history if max_images is reached, otherwise last item from history */
143   if (item->type == CLIPMAN_HISTORY_TYPE_IMAGE && n_images >= history->priv->max_images_in_history)
144     {
145       while (n_images >= history->priv->max_images_in_history)
146         {
147           guint i = 1;
148 
149           for (list = history->priv->items; list != NULL; list = list->next)
150             {
151               _item = list->data;
152 
153               if (_item->type != CLIPMAN_HISTORY_TYPE_IMAGE)
154                 continue;
155 
156               i++;
157 
158               if (i < n_images)
159                 continue;
160 
161               if (n_images >= history->priv->max_images_in_history)
162                 {
163                   __clipman_history_item_free (_item);
164                   history->priv->items = g_slist_remove (history->priv->items, _item);
165                 }
166               n_images--;
167 
168               break;
169             }
170         }
171     }
172   else if (list_length == history->priv->max_texts_in_history)
173     {
174       list = g_slist_last (history->priv->items);
175       _item = list->data;
176       __clipman_history_item_free (_item);
177       history->priv->items = g_slist_remove (history->priv->items, _item);
178     }
179 
180   /* Prepend item to start of the history */
181   history->priv->items = g_slist_prepend (history->priv->items, item);
182   history->priv->item_to_restore = item;
183 
184   /* Emit signal */
185   g_signal_emit (history, signals[ITEM_ADDED], 0);
186 }
187 
188 /*
189  * Misc functions
190  */
191 
192 static void
__clipman_history_item_free(ClipmanHistoryItem * item)193 __clipman_history_item_free (ClipmanHistoryItem *item)
194 {
195   switch (item->type)
196     {
197     case CLIPMAN_HISTORY_TYPE_TEXT:
198       DBG ("Delete text `%s'", item->content.text);
199       g_free (item->content.text);
200       g_free (item->preview.text);
201       break;
202 
203     case CLIPMAN_HISTORY_TYPE_IMAGE:
204       DBG ("Delete image (%p)", item->content.image);
205       g_object_unref (item->content.image);
206       g_object_unref (item->preview.image);
207       break;
208 
209     default:
210       g_assert_not_reached ();
211     }
212   g_slice_free (ClipmanHistoryItem, item);
213 }
214 
215 static gint
__g_slist_compare_texts(gconstpointer a,gconstpointer b)216 __g_slist_compare_texts (gconstpointer a,
217                          gconstpointer b)
218 {
219   const ClipmanHistoryItem *item = a;
220   const gchar *text = b;
221   if (item->type != CLIPMAN_HISTORY_TYPE_TEXT)
222     return -1;
223   return g_ascii_strcasecmp (item->content.text, text);
224 }
225 
226 /*
227  * Public methods
228  */
229 
230 /**
231  * clipman_history_add_text:
232  * @history:    a #ClipmanHistory
233  * @text:       the text to add to the history
234  *
235  * Stores a text inside the history.  If the history is growing over the
236  * maximum number of items, it will delete the oldest text.
237  */
238 void
clipman_history_add_text(ClipmanHistory * history,const gchar * text)239 clipman_history_add_text (ClipmanHistory *history,
240                           const gchar *text)
241 {
242   ClipmanHistoryItem *item;
243   gchar *tmp1, *tmp2;
244   const gchar *offset;
245   gint preview_length = 48;
246   GSList *list;
247 
248   /* Search for a previously existing content */
249   list = g_slist_find_custom (history->priv->items, text, (GCompareFunc)__g_slist_compare_texts);
250   if (list != NULL)
251     {
252       DBG ("Found a previous occurence for text `%s'", text);
253       item = list->data;
254       if (history->priv->reorder_items)
255         {
256           __clipman_history_item_free (item);
257           history->priv->items = g_slist_remove (history->priv->items, list->data);
258         }
259       else
260         {
261           history->priv->item_to_restore = item;
262           return;
263         }
264     }
265 
266   /* Store the text */
267   DBG ("Store text `%s')", text);
268 
269   item = g_slice_new0 (ClipmanHistoryItem);
270   item->type = CLIPMAN_HISTORY_TYPE_TEXT;
271   item->content.text = g_strdup (text);
272 
273   /* Strip white spaces for preview */
274   tmp1 = g_strchomp (g_strdup (text));
275 
276   tmp2 = tmp1;
277   while (tmp2)
278     {
279       tmp2 = g_strchug(++tmp2);
280       tmp2 = g_strstr_len (tmp1, preview_length, "  ");
281     }
282 
283   /* Shorten preview */
284   if (g_utf8_strlen (tmp1, -1) > preview_length)
285     {
286       offset = g_utf8_offset_to_pointer (tmp1, preview_length);
287       tmp2 = g_strndup (tmp1, offset - tmp1);
288       g_free (tmp1);
289       tmp1 = g_strconcat (tmp2, "...", NULL);
290       g_free (tmp2);
291     }
292 
293   /* Cleanup special characters from preview */
294   tmp1 = g_strdelimit (tmp1, "\n\r\t", ' ');
295 
296   /* Set preview */
297   item->preview.text = tmp1;
298 
299   _clipman_history_add_item (history, item);
300 }
301 
302 /**
303  * clipman_history_add_image:
304  * @history:    a #ClipmanHistory
305  * @image:      the image to add to the history
306  *
307  * Stores an image inside the history.  If the history is growing over the
308  * maximum number of items, it will delete the oldest image.
309  */
310 void
clipman_history_add_image(ClipmanHistory * history,const GdkPixbuf * image)311 clipman_history_add_image (ClipmanHistory *history,
312                            const GdkPixbuf *image)
313 {
314   ClipmanHistoryItem *item;
315 
316   if (history->priv->max_images_in_history == 0)
317     return;
318 
319   DBG ("Store image (%p)", image);
320 
321   item = g_slice_new0 (ClipmanHistoryItem);
322   item->type = CLIPMAN_HISTORY_TYPE_IMAGE;
323   item->content.image = gdk_pixbuf_copy (image);
324   item->preview.image = gdk_pixbuf_scale_simple (GDK_PIXBUF (image), 128, 128, GDK_INTERP_BILINEAR);
325 
326   DBG ("Copy of image (%p) is (%p)", image, item->content.image);
327 
328   _clipman_history_add_item (history, item);
329 }
330 
331 /**
332  * clipman_history_get_list:
333  * @history: a #ClipmanHistory
334  *
335  * Returns a unique list of the images and texts.
336  *
337  * Returns: a newly allocated #GSList that must be freed with g_slist_free()
338  */
339 GSList *
clipman_history_get_list(ClipmanHistory * history)340 clipman_history_get_list (ClipmanHistory *history)
341 {
342   return g_slist_copy (history->priv->items);
343 }
344 
345 /**
346  * clipman_history_get_max_texts_in_history:
347  * @history: a #ClipmanHistory
348  *
349  * Returns the most recent item that has been added to #ClipmanHistory.
350  *
351  * Returns: a #const #ClipmanHistoryItem
352  */
353 guint
clipman_history_get_max_texts_in_history(ClipmanHistory * history)354 clipman_history_get_max_texts_in_history (ClipmanHistory *history)
355 {
356   return history->priv->max_texts_in_history;
357 }
358 
359 /**
360  * clipman_history_get_item_to_restore:
361  * @history: a #ClipmanHistory
362  *
363  * Returns the most recent item that has been added to #ClipmanHistory.
364  *
365  * Returns: a #const #ClipmanHistoryItem
366  */
367 const ClipmanHistoryItem *
clipman_history_get_item_to_restore(ClipmanHistory * history)368 clipman_history_get_item_to_restore (ClipmanHistory *history)
369 {
370   return history->priv->item_to_restore;
371 }
372 
373 /**
374  * clipman_history_set_item_to_restore:
375  * @history: a #ClipmanHistory
376  * @item: a #ClipmanHistoryItem that must exist inside #ClipmanHistory
377  *
378  * This function is used as a hack around the images as they cannot be
379  * compared.  Before setting the clipboard with an image, the #ClipmanCollector
380  * must be set as being restored with clipman_collector_set_is_restoring(),
381  * than this function is called with the item that contains the image that is
382  * getting restored.
383  * Instead of being destroyed/recreated inside the history, it will remain at
384  * the same position in the history (unlike being pushed to the top) but will
385  * be marked as being the most recent item in the history.
386  */
387 void
clipman_history_set_item_to_restore(ClipmanHistory * history,const ClipmanHistoryItem * item)388 clipman_history_set_item_to_restore (ClipmanHistory *history,
389                                      const ClipmanHistoryItem *item)
390 {
391   /* TODO Verify that the item exists in the history */
392   history->priv->item_to_restore = item;
393 }
394 
395 /**
396  * clipman_history_clear:
397  * @history: a #ClipmanHistory
398  *
399  * Clears the lists containing the history of the texts and the images.
400  */
401 void
clipman_history_clear(ClipmanHistory * history)402 clipman_history_clear (ClipmanHistory *history)
403 {
404   GSList *list;
405 
406   DBG ("Clear the history");
407 
408   for (list = history->priv->items; list != NULL; list = list->next)
409     __clipman_history_item_free (list->data);
410 
411   g_slist_free (history->priv->items);
412   history->priv->items = NULL;
413   history->priv->item_to_restore = NULL;
414 
415   g_signal_emit (history, signals[CLEAR], 0);
416 }
417 
418 ClipmanHistory *
clipman_history_get(void)419 clipman_history_get (void)
420 {
421   static ClipmanHistory *singleton = NULL;
422 
423   if (singleton == NULL)
424     {
425       singleton = g_object_new (CLIPMAN_TYPE_HISTORY, NULL);
426       g_object_add_weak_pointer (G_OBJECT (singleton), (gpointer)&singleton);
427     }
428   else
429     g_object_ref (G_OBJECT (singleton));
430 
431   return singleton;
432 }
433 
434 /*
435  * GObject
436  */
437 
438 static void
clipman_history_class_init(ClipmanHistoryClass * klass)439 clipman_history_class_init (ClipmanHistoryClass *klass)
440 {
441   GObjectClass *object_class;
442 
443   clipman_history_parent_class = g_type_class_peek_parent (klass);
444 
445   object_class = G_OBJECT_CLASS (klass);
446   object_class->finalize = clipman_history_finalize;
447   object_class->set_property = clipman_history_set_property;
448   object_class->get_property = clipman_history_get_property;
449 
450   signals[ITEM_ADDED] =
451     g_signal_new ("item-added", G_TYPE_FROM_CLASS (klass),
452                   G_SIGNAL_RUN_LAST|G_SIGNAL_ACTION,
453                   G_STRUCT_OFFSET (ClipmanHistoryClass, item_added),
454                   0, NULL, g_cclosure_marshal_VOID__VOID,
455                   G_TYPE_NONE, 0);
456 
457   signals[CLEAR] =
458     g_signal_new ("clear", G_TYPE_FROM_CLASS (klass),
459                   G_SIGNAL_RUN_LAST|G_SIGNAL_ACTION,
460                   G_STRUCT_OFFSET (ClipmanHistoryClass, clear),
461                   0, NULL, g_cclosure_marshal_VOID__VOID,
462                   G_TYPE_NONE, 0);
463 
464   g_object_class_install_property (object_class,
465                                    MAX_TEXTS_IN_HISTORY,
466                                    g_param_spec_uint ("max-texts-in-history",
467                                                       "MaxTextsInHistory",
468                                                       "The number of maximum texts in history",
469                                                       5, 1000, DEFAULT_MAX_TEXTS_IN_HISTORY,
470                                                       G_PARAM_CONSTRUCT|G_PARAM_READWRITE));
471   g_object_class_install_property (object_class,
472                                    MAX_IMAGES_IN_HISTORY,
473                                    g_param_spec_uint ("max-images-in-history",
474                                                       "MaxImagesInHistory",
475                                                       "The number of maximum images in history",
476                                                       0, 5, DEFAULT_MAX_IMAGES_IN_HISTORY,
477                                                       G_PARAM_CONSTRUCT|G_PARAM_READWRITE));
478   g_object_class_install_property (object_class,
479                                    SAVE_ON_QUIT,
480                                    g_param_spec_boolean ("save-on-quit",
481                                                          "SaveOnQuit",
482                                                          "True if the history must be saved on quit",
483                                                          DEFAULT_SAVE_ON_QUIT,
484                                                          G_PARAM_CONSTRUCT|G_PARAM_READWRITE));
485 
486   g_object_class_install_property (object_class,
487                                    REORDER_ITEMS,
488                                    g_param_spec_boolean ("reorder-items",
489                                                          "ReorderItems",
490                                                          "Always push last clipboard content to the top of the history",
491                                                          DEFAULT_REORDER_ITEMS,
492                                                          G_PARAM_CONSTRUCT|G_PARAM_READWRITE));
493 }
494 
495 static void
clipman_history_init(ClipmanHistory * history)496 clipman_history_init (ClipmanHistory *history)
497 {
498   history->priv = clipman_history_get_instance_private (history);
499   history->priv->item_to_restore = NULL;
500 }
501 
502 static void
clipman_history_finalize(GObject * object)503 clipman_history_finalize (GObject *object)
504 {
505   clipman_history_clear (CLIPMAN_HISTORY (object));
506   G_OBJECT_CLASS (clipman_history_parent_class)->finalize (object);
507 }
508 
509 static void
clipman_history_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)510 clipman_history_set_property (GObject *object,
511                               guint property_id,
512                               const GValue *value,
513                               GParamSpec *pspec)
514 {
515   ClipmanHistoryPrivate *priv = CLIPMAN_HISTORY (object)->priv;
516 
517   switch (property_id)
518     {
519     case MAX_TEXTS_IN_HISTORY:
520       priv->max_texts_in_history = g_value_get_uint (value);
521       break;
522 
523     case MAX_IMAGES_IN_HISTORY:
524       priv->max_images_in_history = g_value_get_uint (value);
525       break;
526 
527     case SAVE_ON_QUIT:
528       priv->save_on_quit = g_value_get_boolean (value);
529       break;
530 
531     case REORDER_ITEMS:
532       priv->reorder_items = g_value_get_boolean (value);
533       break;
534 
535     default:
536       break;
537     }
538 }
539 
540 static void
clipman_history_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)541 clipman_history_get_property (GObject *object,
542                               guint property_id,
543                               GValue *value,
544                               GParamSpec *pspec)
545 {
546   ClipmanHistoryPrivate *priv = CLIPMAN_HISTORY (object)->priv;
547 
548   switch (property_id)
549     {
550     case MAX_TEXTS_IN_HISTORY:
551       g_value_set_uint (value, priv->max_texts_in_history);
552       break;
553 
554     case MAX_IMAGES_IN_HISTORY:
555       g_value_set_uint (value, priv->max_images_in_history);
556       break;
557 
558     case SAVE_ON_QUIT:
559       g_value_set_boolean (value, priv->save_on_quit);
560       break;
561 
562     case REORDER_ITEMS:
563       g_value_set_boolean (value, priv->reorder_items);
564       break;
565 
566     default:
567       break;
568     }
569 }
570