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