/* Copyright (c) 2022, Anthony LaTorre * All rights reserved. * * 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 Free * Software Foundation, version 2. * * 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, see . */ #include #include #include #include /* Time in milliseconds it takes to write a character, i.e. if you hold down * the mouse button for this many ms it will write the character and move on to * the next character (or emoji). * * Initially had this set to 500 ms (half a second), but that was a bit short, * so I've updated it to one second. */ #define CLICK_TIME 1000 #define LEN(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) /* Returns the current time in seconds and milliseconds since the epoch. * Mostly useful for calculating time intervals. */ static void aeGetTime(long *seconds, long *milliseconds) { struct timeval tv; gettimeofday(&tv, NULL); *seconds = tv.tv_sec; *milliseconds = tv.tv_usec/1000; } GtkWidget *text_widget; GtkTextTag *tag; /* List of emojis. */ #define TELEVISION "\xf0\x9f\x93\xba" #define THOUGHT_BALLOON "\xf0\x9f\x92\xad" #define STOP "\xf0\x9f\x9b\x91" #define THANK_YOU "\xf0\x9f\x99\x8f" #define UNAMUSED "\xf0\x9f\x98\x92" #define SLEEPING "\xf0\x9f\x98\xb4" #define BED "\xf0\x9f\x9b\x8f" #define CHAIR "\xf0\x9f\xaa\x91" #define NURSE "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f" #define ROLLING_EYES "\xf0\x9f\x99\x84" #define OVERHEATED "\xf0\x9f\xa5\xb5" #define FREEZING "\xf0\x9f\xa5\xb6" #define NAUSEOUS "\xf0\x9f\xa4\xa2" #define THINKING "\xf0\x9f\xa4\x94" #define THUMBS_UP "\xf0\x9f\x91\x8d" #define THUMBS_DOWN "\xf0\x9f\x91\x8e" #define LEFT_ARROW "\xe2\x86\x90" #define CLEAR_SCREEN "\xe2\x8e\x9a" #define SOUND "\xf0\x9f\x94\x8a" #define DROPLET "\xf0\x9f\x92\xa7" #define FOOD "\xf0\x9f\x8d\xb2" #define TIRED "\xf0\x9f\x98\xab" #define HAPPY "\xf0\x9f\x98\x80" #define LAUGHING "\xf0\x9f\x98\x84" #define NEWLINE "\xe2\x86\xb5" typedef struct chars { const char **characters; int len; } chars; const char *emoji_array[] = { THINKING, THUMBS_UP, THUMBS_DOWN, STOP, HAPPY, LAUGHING, TIRED, FREEZING, OVERHEATED, NAUSEOUS, ROLLING_EYES, SLEEPING, UNAMUSED, THANK_YOU, NURSE, DROPLET, CHAIR, BED, THOUGHT_BALLOON, TELEVISION, //FOOD, LEFT_ARROW, CLEAR_SCREEN, }; const char *alphabet_array[] = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", LEFT_ARROW, CLEAR_SCREEN, SOUND, "_", NEWLINE, "!", "?", "." }; chars emoji = { emoji_array, LEN(emoji_array)}; chars alphabet = {alphabet_array, LEN(alphabet_array)}; chars *current_chars = &emoji; char buf[100000]; /* The current alphabetical letter. */ int current_letter = 0; /* The current position in the buffer. * * FIXME: What to do if we run out of buffer space?. */ int current_position = 0; /* Time the mouse was clicked (seconds) */ long pressed_time_sec = 0; /* Time the mouse was clicked (milliseconds) */ long pressed_time_msec = 0; /* Time the mouse was released (seconds) */ long released_time_sec = 0; /* Time the mouse was released (milliseconds) */ long released_time_msec = 0; /* Is the left mouse button currently pressed? */ int left_click_pressed = FALSE; /* Has the current character been processed? */ int processed = FALSE; void backspace(void) { GtkTextBuffer *text_buffer; GtkTextIter start, end; if (current_position > 0) { text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_widget)); gtk_text_buffer_set_text(text_buffer, buf, -1); gtk_text_buffer_get_iter_at_offset(text_buffer, &start, 0); gtk_text_buffer_get_iter_at_offset(text_buffer, &end, g_utf8_strlen(buf,-1)-2); current_position = strlen(gtk_text_buffer_get_slice(text_buffer,&start,&end,TRUE)); } } static void write_buf(void) { GtkTextBuffer *text_buffer; GtkTextIter start, end; text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_widget)); sprintf(buf+current_position,"%s",current_chars->characters[current_letter % current_chars->len]); gtk_text_buffer_set_text(text_buffer, buf, -1); gtk_text_buffer_get_iter_at_offset(text_buffer, &start, g_utf8_strlen(buf,-1)-1); gtk_text_buffer_get_iter_at_offset(text_buffer, &end, -1); gtk_text_buffer_apply_tag(text_buffer, tag, &start, &end); } static int right_click_pressed(GtkWidget *widget, gpointer data) { if (current_chars == &emoji) current_chars = &alphabet; else if (current_chars == &alphabet) current_chars = &emoji; current_letter = 0; write_buf(); return TRUE; } static int right_click_released(GtkWidget *widget, gpointer data) { return TRUE; } static int pressed(GtkWidget *widget, gpointer data) { aeGetTime(&pressed_time_sec,&pressed_time_msec); left_click_pressed = TRUE; processed = FALSE; return TRUE; } /* Timer function which runs every 10 ms. This function checks if the left * mouse button is clicked, and if so, checks if it's been clicked for more * than CLICK_TIME milliseconds. If so, it adds the current character to the * buffer and sets processed to TRUE, so that when the mouse button is released * it doesn't move to the next character. * * Always returns TRUE to make sure the function keeps running. */ gboolean timer(gpointer data) { const char *current_char; /* If the left mouse button isn't clicked, there is nothing to do. */ if (left_click_pressed == FALSE) return TRUE; aeGetTime(&released_time_sec,&released_time_msec); long long diff = (released_time_sec - pressed_time_sec)*1000 + (released_time_msec - pressed_time_msec); if (diff > 10000) { /* If the mouse button has been clicked for more than 10 seconds, clear * the screen. */ current_position = 0; current_letter = 0; write_buf(); processed = TRUE; aeGetTime(&pressed_time_sec,&pressed_time_msec); } else if ((processed == FALSE) && (diff > CLICK_TIME)) { /* If the mouse button has been clicked for more than CLICK_TIME * milliseconds, add the current character to the buffer. */ current_char = current_chars->characters[current_letter % current_chars->len]; if (!strcmp(current_char,LEFT_ARROW)) { backspace(); } else if (!strcmp(current_char,CLEAR_SCREEN)) { current_position = 0; current_letter = 0; } else if (!strcmp(current_char,SOUND)) { char cmd[1000000]; buf[current_position] = '\0'; sprintf(cmd, "gtts-cli \"%s\" --output moji.mp3", buf); if (!system(cmd)) { system("mpg123 moji.mp3"); system("rm moji.mp3"); } } else if (!strcmp(current_char,"_")) { sprintf(buf+current_position," "); current_position += strlen(current_char); } else if (!strcmp(current_char,NEWLINE)) { sprintf(buf+current_position,"\n"); current_position += 1; } else { sprintf(buf+current_position,"%s",current_char); current_position += strlen(current_char); } if (current_position > LEN(buf) - 1) { /* FIXME: better way to do this? */ current_position = 0; } /* Uncomment the next line to start over at the beginning of the * alphabet after a new letter is entered. But, I think it's better * to keep the current letter. The reason is that you might want to * delete a whole word and so when you navigate to the backspace you * just have to keep entering the same letter. */ current_letter = 0; write_buf(); aeGetTime(&pressed_time_sec,&pressed_time_msec); processed = TRUE; } return TRUE; } static int released(GtkWidget *widget, gpointer data) { left_click_pressed = FALSE; aeGetTime(&released_time_sec,&released_time_msec); long long diff = (released_time_sec - pressed_time_sec)*1000 + (released_time_msec - pressed_time_msec); if ((processed == FALSE) && (diff < CLICK_TIME)) { current_letter += 1; } write_buf(); return TRUE; } gboolean on_key_press(GtkEventControllerKey *, guint keyval, guint keycode, GdkModifierType mod, gpointer user_data) { switch (keyval) { case GDK_KEY_space: if (current_chars == &emoji) current_chars = &alphabet; else if (current_chars == &alphabet) current_chars = &emoji; current_letter = 0; write_buf(); break; case GDK_KEY_BackSpace: backspace(); write_buf(); break; default: return FALSE; } return FALSE; } static void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window; GtkTextBuffer *text_buffer; GtkCssProvider *provider; GtkStyleContext *context; GtkGesture *gesture, *right_click_gesture; GtkEventController *event_controller; window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Moji"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); event_controller = gtk_event_controller_key_new(); g_signal_connect(event_controller, "key-pressed", G_CALLBACK(on_key_press), NULL); gtk_widget_add_controller(window, event_controller); right_click_gesture = gtk_gesture_click_new(); gtk_gesture_single_set_button(GTK_GESTURE_SINGLE (right_click_gesture), 3); g_signal_connect(right_click_gesture, "pressed", G_CALLBACK(right_click_pressed), NULL); g_signal_connect(right_click_gesture, "released", G_CALLBACK(right_click_released), NULL); gtk_widget_add_controller(window, GTK_EVENT_CONTROLLER(right_click_gesture)); gesture = gtk_gesture_click_new(); g_signal_connect(gesture, "pressed", G_CALLBACK(pressed), NULL); g_signal_connect(gesture, "released", G_CALLBACK(released), NULL); gtk_widget_add_controller(window, GTK_EVENT_CONTROLLER(gesture)); text_widget = gtk_text_view_new(); gtk_text_view_set_editable((GtkTextView *) text_widget, FALSE); gtk_text_view_set_cursor_visible((GtkTextView *) text_widget, TRUE); gtk_text_view_set_wrap_mode((GtkTextView *) text_widget, GTK_WRAP_CHAR); gtk_widget_set_can_focus(text_widget, FALSE); gtk_widget_set_can_target(text_widget, FALSE); text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_widget)); /* Change default font and color throughout the widget */ provider = gtk_css_provider_new(); gtk_css_provider_load_from_data(provider, "textview {" " font: 144pt monospace;" " color: black;" "}", -1); context = gtk_widget_get_style_context(text_widget); gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); /* Use a tag to change the color for just one part of the widget */ tag = gtk_text_buffer_create_tag(text_buffer, "red_foreground", "foreground", "red", NULL); write_buf(); g_timeout_add(10, timer, NULL); gtk_window_set_child(GTK_WINDOW(window), text_widget); gtk_window_fullscreen(GTK_WINDOW(window)); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char **argv) { GtkApplication *app; int status; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }