1 /* Copyright (C) 2016-2017 Shengyu Zhang <i@silverrainz.me>
2  *
3  * This file is part of Srain.
4  *
5  * Srain is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 /**
20  * @file sui_buffer.c
21  * @brief
22  * @author Shengyu Zhang <i@silverrainz.me>
23  * @version 0.06.2
24  * @date 2016-03-01
25  */
26 
27 
28 #include <gtk/gtk.h>
29 #include <assert.h>
30 #include <string.h>
31 
32 #include "core/core.h"
33 #include "sui/sui.h"
34 
35 #include "sui_common.h"
36 #include "sui_event_hdr.h"
37 #include "sui_buffer.h"
38 #include "sui_recv_message.h"
39 
40 #include "log.h"
41 #include "i18n.h"
42 #include "utils.h"
43 
44 static GtkListStore* real_completion_func(SuiBuffer *self, const char *context);
45 static bool push_input_history(SuiBuffer *self, char *msg);
46 static void start_browse_input(SuiBuffer *self);
47 static void reset_browse_input(SuiBuffer *self);
48 
49 static void sui_buffer_set_ctx(SuiBuffer *self, void *ctx);
50 static void sui_buffer_set_events(SuiBuffer *self, SuiBufferEvents *events);
51 
52 static void topic_menu_item_on_toggled(GtkWidget* widget, gpointer user_data);
53 
54 /*****************************************************************************
55  * GObject functions
56  *****************************************************************************/
57 
58 enum
59 {
60   // 0 for PROP_NOME
61   PROP_CTX = 1,
62   PROP_EVENTS,
63   PROP_CONFIG,
64   PROP_NAME,
65   PROP_REMARK,
66   N_PROPERTIES
67 };
68 
69 G_DEFINE_TYPE(SuiBuffer, sui_buffer, GTK_TYPE_BOX);
70 
71 static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
72 
sui_buffer_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)73 static void sui_buffer_set_property(GObject *object, guint property_id,
74         const GValue *value, GParamSpec *pspec){
75   SuiBuffer *self = SUI_BUFFER(object);
76 
77   switch (property_id){
78       case PROP_CTX:
79           sui_buffer_set_ctx(self, g_value_get_pointer(value));
80           break;
81       case PROP_EVENTS:
82           sui_buffer_set_events(self, g_value_get_pointer(value));
83           break;
84       case PROP_CONFIG:
85           sui_buffer_set_config(self, g_value_get_pointer(value));
86           break;
87       default:
88           /* We don't have any other property... */
89           G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
90           break;
91   }
92 }
93 
sui_buffer_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)94 static void sui_buffer_get_property(GObject *object, guint property_id,
95         GValue *value, GParamSpec *pspec){
96     SuiBuffer *self = SUI_BUFFER(object);
97 
98     switch (property_id){
99         case PROP_CTX:
100             g_value_set_pointer(value, sui_buffer_get_ctx(self));
101             break;
102         case PROP_EVENTS:
103             g_value_set_pointer(value, sui_buffer_get_events(self));
104             break;
105         case PROP_CONFIG:
106             g_value_set_pointer(value, sui_buffer_get_config(self));
107             break;
108         case PROP_NAME:
109             g_value_set_string(value, sui_buffer_get_name(self));
110             break;
111         case PROP_REMARK:
112             g_value_set_string(value, sui_buffer_get_remark(self));
113             break;
114         default:
115             /* We don't have any other property... */
116             G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
117             break;
118     }
119 }
120 
sui_buffer_init(SuiBuffer * self)121 static void sui_buffer_init(SuiBuffer *self){
122     GtkBuilder *builder;
123 
124     gtk_widget_init_template(GTK_WIDGET(self));
125 
126     /* Init menus */
127     builder = gtk_builder_new_from_resource("/im/srain/Srain/buffer_menu.glade");
128     self->topic_menu_item =
129         (GtkCheckMenuItem *)gtk_builder_get_object(builder, "topic_menu_item");
130     gtk_menu_shell_append(
131             GTK_MENU_SHELL(self->menu),
132             GTK_WIDGET(self->topic_menu_item));
133     g_object_unref(builder);
134 
135     /* Init msg list */
136     self->msg_list = sui_message_list_new();
137     gtk_box_pack_start(self->msg_list_box, GTK_WIDGET(self->msg_list),
138             TRUE, TRUE, 0);
139     gtk_widget_show(GTK_WIDGET(self->msg_list));
140 
141     /* Setup completion */
142     self->completion = sui_completion_new(self->input_text_buffer);
143 
144     g_signal_connect(self->topic_label, "activate-link",
145             G_CALLBACK(sui_common_activate_gtk_label_link), self);
146     g_signal_connect(self->topic_menu_item, "toggled",
147             G_CALLBACK(topic_menu_item_on_toggled), self);
148 }
149 
sui_buffer_constructed(GObject * object)150 static void sui_buffer_constructed(GObject *object){
151     SuiBuffer *self;
152 
153     self = SUI_BUFFER(object);
154 
155     sui_buffer_show_topic(self, self->cfg->show_topic);
156 
157     G_OBJECT_CLASS(sui_buffer_parent_class)->constructed(object);
158 }
159 
sui_buffer_finalize(GObject * object)160 static void sui_buffer_finalize(GObject *object){
161     G_OBJECT_CLASS(sui_buffer_parent_class)->finalize(object);
162 }
163 
sui_buffer_class_init(SuiBufferClass * class)164 static void sui_buffer_class_init(SuiBufferClass *class){
165     GObjectClass *object_class;
166     GtkWidgetClass *widget_class;
167 
168     /* Overwrite callbacks */
169     object_class = G_OBJECT_CLASS(class);
170     object_class->constructed = sui_buffer_constructed;
171     object_class->finalize = sui_buffer_finalize;
172     object_class->set_property = sui_buffer_set_property;
173     object_class->get_property = sui_buffer_get_property;
174 
175     /* Install properties */
176     obj_properties[PROP_NAME] =
177         g_param_spec_string("name",
178                 "Name",
179                 "Name of buffer.",
180                 NULL  /* default value */,
181                 G_PARAM_READABLE);
182 
183     obj_properties[PROP_REMARK] =
184         g_param_spec_string("remark",
185                 "Remark",
186                 "Remark of buffer.",
187                 NULL  /* default value */,
188                 G_PARAM_READABLE);
189 
190     obj_properties[PROP_CTX] =
191         g_param_spec_pointer("context",
192                 "Context",
193                 "Context of buffer.",
194                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
195 
196     obj_properties[PROP_EVENTS] =
197         g_param_spec_pointer("events",
198                 "Events",
199                 "Event callbacks of buffer.",
200                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
201 
202     obj_properties[PROP_CONFIG] =
203         g_param_spec_pointer("config",
204                 "Config",
205                 "Configuration of buffer.",
206                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
207 
208     g_object_class_install_properties(object_class,
209             N_PROPERTIES,
210             obj_properties);
211 
212     widget_class = GTK_WIDGET_CLASS(class);
213 
214     /* Bind child */
215     gtk_widget_class_set_template_from_resource(
216             widget_class, "/im/srain/Srain/buffer.glade");
217 
218     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, menu);
219     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, topic_revealer);
220     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, topic_label);
221     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, user_list_revealer);
222     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, msg_list_box);
223     gtk_widget_class_bind_template_child(widget_class, SuiBuffer, input_text_buffer);
224 
225     class->completion_func = real_completion_func;
226 }
227 
228 
229 /*****************************************************************************
230  * Expored functions
231  *****************************************************************************/
232 
sui_buffer_new(void * ctx,SuiBufferEvents * events,SuiBufferConfig * cfg)233 SuiBuffer* sui_buffer_new(void *ctx, SuiBufferEvents *events, SuiBufferConfig *cfg){
234     return g_object_new(SUI_TYPE_BUFFER,
235             "context", ctx,
236             "events", events,
237             "config", cfg,
238             NULL);
239 }
240 
sui_buffer_insert_text(SuiBuffer * self,const char * text,int line,int offset)241 void sui_buffer_insert_text(SuiBuffer *self, const char *text, int line, int offset){
242     GtkTextMark *insert;
243     GtkTextIter iter;
244 
245     g_return_if_fail(SUI_IS_BUFFER(self));
246 
247     insert = gtk_text_buffer_get_insert(self->input_text_buffer);
248     gtk_text_buffer_get_iter_at_mark(self->input_text_buffer, &iter, insert);
249     if (line == -1){ // Current line
250         line = gtk_text_iter_get_line(&iter);
251     }
252     if (offset == -1) {
253         offset = gtk_text_iter_get_line_offset(&iter);
254     }
255 
256     gtk_text_buffer_get_iter_at_line_offset(self->input_text_buffer, &iter, line, offset);
257     gtk_text_buffer_insert(self->input_text_buffer, &iter, text, -1);
258 }
259 
sui_buffer_show_topic(SuiBuffer * self,bool isshow)260 void sui_buffer_show_topic(SuiBuffer *self, bool isshow){
261     g_return_if_fail(SUI_IS_BUFFER(self));
262 
263     gtk_check_menu_item_set_active(self->topic_menu_item, isshow);
264 }
265 
266 /**
267  * @brief sui_buffer_complete completes the contents of the input text
268  * buffer of SuiBuffer
269  *
270  * @param self
271  */
sui_buffer_complete(SuiBuffer * self)272 void sui_buffer_complete(SuiBuffer *self){
273     sui_completion_complete(self->completion, sui_buffer_completion_func, self);
274 }
275 
sui_buffer_completion_func(const char * context,void * user_data)276 GtkTreeModel* sui_buffer_completion_func(const char *context, void *user_data) {
277     SuiBuffer *self;
278     SuiBufferClass *class;
279 
280     self = user_data;
281     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
282     class = SUI_BUFFER_GET_CLASS(self);
283     g_return_val_if_fail(class->completion_func, NULL);
284 
285     return GTK_TREE_MODEL(class->completion_func(self, context));
286 }
287 
288 /**
289  * @brief sui_buffer_send_input Send a single line of input message via
290  * SuiBufferEvent
291  *
292  * @param self
293  *
294  * @return TRUE if send successfully
295  */
sui_buffer_send_input(SuiBuffer * self)296 bool sui_buffer_send_input(SuiBuffer *self){
297     int nline;
298     char *line;
299     GVariantDict *params;
300     GtkTextBuffer *buf;
301     GtkTextIter start;
302     GtkTextIter end;
303     SrnRet ret;
304 
305     buf = self->input_text_buffer;
306 
307     nline = gtk_text_buffer_get_line_count(buf);
308     gtk_text_buffer_get_iter_at_line(buf, &start, 0);
309     gtk_text_buffer_get_iter_at_line(buf, &end, 1);
310     line = gtk_text_buffer_get_text(buf, &start, &end, FALSE);
311 
312     if (g_str_has_suffix(line, "\n")){
313         line[strlen(line)-1] = '\0'; // Remove the trailing newline
314     }
315     if (strlen(line) == 0){
316         g_free(line);
317 
318         if (nline <= 1){
319             // Text buffer is empty
320             return FALSE;
321         } else {
322             // Only a newline, skip it
323             gtk_text_buffer_delete(buf, &start, &end);
324             return TRUE;
325         }
326     }
327 
328     params = g_variant_dict_new(NULL);
329     g_variant_dict_insert(params, "message", SUI_EVENT_PARAM_STRING, line);
330     ret = sui_buffer_event_hdr(self, SUI_EVENT_SEND, params);
331     g_variant_dict_unref(params);
332 
333     // Push history. Ownership of line will transferred into input history list
334     // if this function call successes.
335     if (!push_input_history(self, line)){
336         g_free(line);
337     }
338     // Delete the sent line from text buffer
339     gtk_text_buffer_delete(buf, &start, &end);
340 
341     return RET_IS_OK(ret);
342 }
343 
sui_buffer_browse_prev_input(SuiBuffer * self)344 void sui_buffer_browse_prev_input(SuiBuffer *self){
345     if (!self->input_history_iter){
346         DBG_FR("No input history");
347         return; // No history to browse
348     }
349 
350     if (!self->input_stage) { // Start browsing history
351         start_browse_input(self);
352     } else {
353         if (!g_list_previous(self->input_history_iter)){
354             DBG_FR("No previous input history");
355             return;
356         }
357         // Browse previous history
358         self->input_history_iter = g_list_previous(self->input_history_iter);
359     }
360 
361     /* Whether last input history available? */
362     gtk_text_buffer_set_text(self->input_text_buffer,
363             self->input_history_iter->data, -1);
364 }
365 
sui_buffer_browse_next_input(SuiBuffer * self)366 void sui_buffer_browse_next_input(SuiBuffer *self){
367     if (!self->input_stage) {
368         DBG_FR("Not in browsing history");
369         return;
370     }
371     g_return_if_fail(self->input_history_iter);
372 
373     if (!g_list_next(self->input_history_iter)){
374         // Reached the newest history(a.k.a stage), end browsing
375         DBG_FR("No next input history");
376         reset_browse_input(self);
377         return;
378     }
379     // Browse next history
380     self->input_history_iter = g_list_next(self->input_history_iter);
381 
382     gtk_text_buffer_set_text(self->input_text_buffer,
383             self->input_history_iter->data, -1);
384 }
385 
sui_buffer_get_name(SuiBuffer * self)386 const char* sui_buffer_get_name(SuiBuffer *self){
387     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
388 
389     return self->ctx->name;
390 }
391 
sui_buffer_get_remark(SuiBuffer * self)392 const char* sui_buffer_get_remark(SuiBuffer *self){
393     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
394 
395     return self->ctx->srv->name;
396 }
397 
398 // sui_buffer_set_events is static
399 
sui_buffer_get_events(SuiBuffer * self)400 SuiBufferEvents* sui_buffer_get_events(SuiBuffer *self){
401     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
402 
403     return self->events;
404 }
405 
sui_buffer_set_config(SuiBuffer * self,SuiBufferConfig * cfg)406 void sui_buffer_set_config(SuiBuffer *self, SuiBufferConfig *cfg){
407     g_return_if_fail(SUI_IS_BUFFER(self));
408 
409     self->cfg = cfg;
410     sui_buffer_show_topic(self, self->cfg->show_topic);
411 }
412 
sui_buffer_get_config(SuiBuffer * self)413 SuiBufferConfig* sui_buffer_get_config(SuiBuffer *self){
414     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
415 
416     return self->cfg;
417 }
418 
419 // sui_buffer_set_ctx() is static
420 
sui_buffer_get_ctx(SuiBuffer * self)421 void* sui_buffer_get_ctx(SuiBuffer *self){
422     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
423 
424     return self->ctx;
425 }
426 
sui_buffer_set_topic(SuiBuffer * self,const char * topic)427 void sui_buffer_set_topic(SuiBuffer *self, const char *topic){
428     g_return_if_fail(SUI_IS_BUFFER(self));
429 
430     gtk_label_set_markup(self->topic_label, topic);
431     gtk_check_menu_item_toggled(self->topic_menu_item);
432 }
433 
sui_buffer_set_topic_setter(SuiBuffer * self,const char * setter)434 void sui_buffer_set_topic_setter(SuiBuffer *self, const char *setter){
435     g_return_if_fail(SUI_IS_BUFFER(self));
436 
437     gtk_widget_set_tooltip_text(GTK_WIDGET(self->topic_label), setter);
438 }
439 
sui_buffer_get_message_list(SuiBuffer * self)440 SuiMessageList* sui_buffer_get_message_list(SuiBuffer *self){
441     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
442 
443     return self->msg_list;
444 }
445 
sui_buffer_get_menu(SuiBuffer * self)446 GtkMenu* sui_buffer_get_menu(SuiBuffer *self){
447     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
448 
449     return self->menu;
450 }
451 
sui_buffer_get_input_text_buffer(SuiBuffer * self)452 GtkTextBuffer* sui_buffer_get_input_text_buffer(SuiBuffer *self){
453     g_return_val_if_fail(SUI_IS_BUFFER(self), NULL);
454 
455     return self->input_text_buffer;
456 }
457 
458 /*****************************************************************************
459  * Static functions
460  *****************************************************************************/
461 
sui_buffer_set_ctx(SuiBuffer * self,void * ctx)462 static void sui_buffer_set_ctx(SuiBuffer *self, void *ctx){
463     self->ctx = ctx;
464 }
465 
sui_buffer_set_events(SuiBuffer * self,SuiBufferEvents * events)466 static void sui_buffer_set_events(SuiBuffer *self, SuiBufferEvents *events){
467     self->events = events;
468 }
469 
topic_menu_item_on_toggled(GtkWidget * widget,gpointer user_data)470 static void topic_menu_item_on_toggled(GtkWidget* widget, gpointer user_data){
471     bool active;
472     SuiBuffer *self = SUI_BUFFER(user_data);
473     GtkCheckMenuItem *item = GTK_CHECK_MENU_ITEM(widget);
474 
475     active = gtk_check_menu_item_get_active(item);
476     gtk_revealer_set_reveal_child(self->topic_revealer, active);
477 
478     // If topic is empty, do not show it anyway
479     if (strlen(gtk_label_get_text(self->topic_label)) != 0){
480         gtk_widget_show(GTK_WIDGET(self->topic_label));
481     } else {
482         gtk_widget_hide(GTK_WIDGET(self->topic_label));
483     }
484 }
485 
real_completion_func(SuiBuffer * self,const char * context)486 static GtkListStore* real_completion_func(SuiBuffer *self, const char *context){
487     const char *prev;
488     const char *prefix;
489     GList *msgs;
490     GList *cmds;
491     GtkListStore *store;
492     SrnChat *ctx;
493 
494     store = gtk_list_store_new(SUI_COMPLETION_N_COLUMNS,
495             G_TYPE_STRING,
496             G_TYPE_STRING,
497             G_TYPE_STRING);
498 
499     /* Get command completions */
500     ctx = sui_buffer_get_ctx(self);
501     cmds = srn_chat_complete_command(ctx, context);
502     for (GList *lst = cmds; lst; lst = g_list_next(lst)){
503         const char *cmd;
504         GtkTreeIter iter;
505 
506         cmd = lst->data;
507         gtk_list_store_append(store, &iter);
508         gtk_list_store_set(store, &iter,
509                 SUI_COMPLETION_COLUMN_PREFIX, context,
510                 SUI_COMPLETION_COLUMN_SUFFIX, cmd + strlen(context),
511                 -1);
512     }
513     g_list_free_full(cmds, g_free);
514 
515     /* Get most recent message senders */
516     prev = context + strlen(context);
517     do {
518         /* Get longest valid prefix */
519         prefix = prev;
520         prev = g_utf8_find_prev_char(context, prefix);
521         if (!prev) {
522             break;
523         }
524     } while (!g_unichar_isspace(g_utf8_get_char(prev)));
525 
526     if (!prefix) {
527         prefix = "";
528     }
529 
530     msgs = sui_message_list_get_recent_messages(self->msg_list, 10);
531     for (GList *lst = msgs; lst; lst = g_list_next(lst)){
532         const char *user;
533         GtkTreeIter iter;
534         SuiRecvMessage *rmsg;
535 
536         if (!SUI_IS_RECV_MESSAGE(lst->data)){
537             continue;
538         }
539         rmsg = SUI_RECV_MESSAGE(lst->data);
540         user = gtk_label_get_text(rmsg->sender_label);
541         if (g_str_has_prefix(user, prefix)){
542             gtk_list_store_append(store, &iter);
543             gtk_list_store_set(store, &iter,
544                     SUI_COMPLETION_COLUMN_PREFIX, prefix,
545                     SUI_COMPLETION_COLUMN_SUFFIX, user + strlen(prefix),
546                     -1);
547         }
548     }
549     g_list_free(msgs);
550 
551     return store;
552 }
553 
push_input_history(SuiBuffer * self,char * msg)554 static bool push_input_history(SuiBuffer *self, char *msg){
555     reset_browse_input(self);
556 
557     DBG_FR("Push input history");
558 
559     if (self->input_history_iter){ // Iter should be the newest history
560         if (g_strcmp0(self->input_history_iter->data, msg) == 0) {
561             DBG_FR("Duplicated input history");
562             return FALSE;
563         }
564     }
565 
566     self->input_history = g_list_append(self->input_history, msg);
567     if (g_list_length(self->input_history) > 10){
568         GList *head;
569 
570         head = g_list_first(self->input_history);
571         g_free(head->data);
572         self->input_history = g_list_delete_link(self->input_history, head);
573     }
574     self->input_history_iter = g_list_last(self->input_history);
575 
576     return TRUE;
577 }
578 
start_browse_input(SuiBuffer * self)579 static void start_browse_input(SuiBuffer *self){
580     char *stage;
581     GtkTextIter start;
582     GtkTextIter end;
583 
584     DBG_FR("Start browsing input history");
585 
586     /* Save current input text to input stage */
587     gtk_text_buffer_get_start_iter(self->input_text_buffer, &start);
588     gtk_text_buffer_get_end_iter(self->input_text_buffer, &end);
589     stage = gtk_text_buffer_get_text(self->input_text_buffer, &start, &end, FALSE);
590 
591     self->input_history = g_list_append(self->input_history, stage);
592     self->input_stage = g_list_last(self->input_history);
593 }
594 
reset_browse_input(SuiBuffer * self)595 static void reset_browse_input(SuiBuffer *self){
596     DBG_FR("Reset browsing input history");
597 
598     if (self->input_stage) {
599         g_free(self->input_stage->data);
600         self->input_history = g_list_delete_link(self->input_history, self->input_stage);
601         self->input_stage = NULL;
602     }
603     self->input_history_iter = g_list_last(self->input_history);
604 }
605