From 172ceffa9ec6722076074348c21d7069212a112e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Thu, 25 Aug 2022 06:06:19 +0200 Subject: X11: support copying text to CLIPBOARD Use the right mouse button. --- nncmpp.c | 217 ++++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 167 insertions(+), 50 deletions(-) diff --git a/nncmpp.c b/nncmpp.c index 84192ee..836f2ba 100644 --- a/nncmpp.c +++ b/nncmpp.c @@ -109,6 +109,7 @@ enum // Elementary port of the TUI to X11. #ifdef WITH_X11 +#include #include #include #include @@ -1152,6 +1153,9 @@ struct widget; /// Draw a widget on the window typedef void (*widget_render_fn) (struct widget *self); +/// Extract the contents of container widgets +typedef struct widget *(*widget_sublayout_fn) (struct widget *self); + /// A minimal abstraction appropriate for both TUI and GUI widgets. /// Units for the widget's region are frontend-specific. /// Having this as a linked list simplifies layouting and memory management. @@ -1165,6 +1169,7 @@ struct widget int height; ///< Height, initialized by UI methods widget_render_fn on_render; ///< Render callback + widget_sublayout_fn on_sublayout; ///< Optional sublayout callback chtype attrs; ///< Rendition, in Curses terms short id; ///< Post-layouting identification @@ -1362,6 +1367,7 @@ static struct app_context XftDraw *xft_draw; ///< Xft rendering context XftFont *xft_regular; ///< Regular font XftFont *xft_bold; ///< Bold font + char *x11_selection; ///< CLIPBOARD selection XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute XRenderColor x_bg[ATTRIBUTE_COUNT]; ///< Background per attribute @@ -1736,14 +1742,14 @@ app_invalidate (void) } static void -app_flush_layout (struct layout *l) +app_flush_layout_to (struct layout *l, int width, struct layout *dest) { hard_assert (l != NULL && l->head != NULL); - widget_redistribute (l->head, g.ui_width); + widget_redistribute (l->head, width); - struct widget *last = g.widgets.tail; + struct widget *last = dest->tail; if (!last) - g.widgets = *l; + *dest = *l; else { // Assuming there is no unclaimed vertical space. @@ -1752,10 +1758,16 @@ app_flush_layout (struct layout *l) last->next = l->head; l->head->prev = last; - g.widgets.tail = l->tail; + dest->tail = l->tail; } } +static void +app_flush_layout (struct layout *l) +{ + app_flush_layout_to (l, g.ui_width, &g.widgets); +} + static struct widget * app_push (struct layout *l, struct widget *w) { @@ -2041,7 +2053,7 @@ app_compute_scrollbar (struct tab *tab, long visible, long s) return (struct scrollbar) { length, offset }; } -static struct widget * +static struct layout app_layout_row (struct tab *tab, int item_index) { int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN); @@ -2075,6 +2087,30 @@ app_layout_row (struct tab *tab, int item_index) else *attrs |= row_attrs; } + return l; +} + +// XXX: This isn't a very clean design, in that part of layouting +// is done during the rendering stage. +static struct widget * +app_sublayout_list (struct widget *list) +{ + struct tab *tab = g.active_tab; + int to_show = MIN ((int) tab->item_count - tab->item_top, + list->height / g.ui_vunit); + + struct layout l = {}; + for (int row = 0; row < to_show; row++) + { + int item_index = tab->item_top + row; + struct layout subl = app_layout_row (tab, item_index); + app_flush_layout_to (&subl, list->width, &l); + } + LIST_FOR_EACH (struct widget, w, l.head) + { + w->x += list->x; + w->y += list->y; + } return l.head; } @@ -5124,28 +5160,10 @@ tui_make_scrollbar (chtype attrs) static void tui_render_list (struct widget *self) { - struct tab *tab = g.active_tab; - int to_show = - MIN (app_visible_items (), (int) tab->item_count - tab->item_top); - for (int row = 0; row < to_show; row++) + LIST_FOR_EACH (struct widget, w, self->on_sublayout (self)) { - int item_index = tab->item_top + row; - struct widget *head = app_layout_row (tab, item_index); - widget_redistribute (head, self->width); - - int x = self->x; - int y = self->y + row * g.ui_vunit; - LIST_FOR_EACH (struct widget, w, head) - { - w->x += x; - w->y += y; - } - - LIST_FOR_EACH (struct widget, w, head) - { - w->on_render (w); - free (w); - } + w->on_render (w); + free (w); } } @@ -5156,6 +5174,7 @@ tui_make_list (void) w->width = -1; w->height = g.active_tab->item_count; w->on_render = tui_render_list; + w->on_sublayout = app_sublayout_list; return w; } @@ -5749,28 +5768,10 @@ x11_render_list (struct widget *self) { x11_render_padding (self); - struct tab *tab = g.active_tab; - int to_show = - MIN (app_visible_items (), (int) tab->item_count - tab->item_top); - for (int row = 0; row < to_show; row++) + LIST_FOR_EACH (struct widget, w, self->on_sublayout (self)) { - int item_index = tab->item_top + row; - struct widget *head = app_layout_row (tab, item_index); - widget_redistribute (head, self->width); - - int x = self->x; - int y = self->y + row * g.ui_vunit; - LIST_FOR_EACH (struct widget, w, head) - { - w->x += x; - w->y += y; - } - - LIST_FOR_EACH (struct widget, w, head) - { - w->on_render (w); - free (w); - } + w->on_render (w); + free (w); } } @@ -5779,6 +5780,7 @@ x11_make_list (void) { struct widget *w = xcalloc (1, sizeof *w + 1); w->on_render = x11_render_list; + w->on_sublayout = app_sublayout_list; return w; } @@ -5862,6 +5864,7 @@ x11_destroy (void) XftDrawDestroy (g.xft_draw); XftFontClose (g.dpy, g.xft_regular); XftFontClose (g.dpy, g.xft_bold); + cstr_set (&g.x11_selection, NULL); poller_fd_reset (&g.x11_event); XCloseDisplay (g.dpy); @@ -6035,6 +6038,57 @@ x11_init_pixmap (void) = XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL); } +static char * +x11_find_text (struct widget *list, int x, int y) +{ + struct widget *target = NULL; + LIST_FOR_EACH (struct widget, w, list) + if (x >= w->x && x < w->x + w->width + && y >= w->y && y < w->y + w->height) + target = w; + if (!target) + return NULL; + + if (target->on_sublayout) + { + struct widget *sublist = target->on_sublayout (target); + char *result = x11_find_text (sublist, x, y); + LIST_FOR_EACH (struct widget, w, sublist) + free (w); + if (result) + return result; + } + return xstrdup (target->text); +} + +// TODO: OSC 52 exists for terminals, so make it possible to enable that there. +static bool +x11_process_press (int x, int y, int button, int modifiers) +{ + if (button != Button3) + goto out; + + char *text = x11_find_text (g.widgets.head, x, y); + if (!text || !*(cstr_strip_in_place (text, " \t"))) + { + free (text); + goto out; + } + + cstr_set (&g.x11_selection, text); + XSetSelectionOwner (g.dpy, XInternAtom (g.dpy, "CLIPBOARD", False), + g.x11_window, CurrentTime); + + cstr_set (&g.message, + xstrdup_printf ("Text copied to clipboard: %s", g.x11_selection)); + poller_timer_set (&g.message_timer, 5000); + app_invalidate (); + return true; + +out: + return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers); +} + static int x11_state_to_modifiers (unsigned int state) { @@ -6079,14 +6133,66 @@ on_x11_input_event (XEvent *ev) last_press_event = *ev; if (ev->type == ButtonPress) - return app_process_mouse - (TERMO_MOUSE_PRESS, x, y, button, modifiers); + return x11_process_press (x, y, button, modifiers); if (ev->type == ButtonRelease) return app_process_mouse (TERMO_MOUSE_RELEASE, x, y, button, modifiers); return false; } +static void +on_x11_selection_request (XSelectionRequestEvent *ev) +{ + Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False); + Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False); + Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False); + Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 }; + + bool ok = false; + Atom property = ev->property ? ev->property : ev->target; + if (!g.x11_selection) + goto out; + + XICCEncodingStyle style = 0; + if ((ok = ev->target == xa_targets)) + { + XChangeProperty (g.dpy, ev->requestor, property, + XA_ATOM, 32, PropModeReplace, + (const unsigned char *) targets, N_ELEMENTS (targets)); + goto out; + } + else if (ev->target == XA_STRING) + style = XStringStyle; + else if (ev->target == xa_compound_text) + style = XCompoundTextStyle; + else if (ev->target == xa_utf8) + style = XUTF8StringStyle; + else + goto out; + + // XXX: We let it crash us with BadLength, but we may, e.g., use INCR. + XTextProperty text = {}; + if ((ok = !Xutf8TextListToTextProperty + (g.dpy, &g.x11_selection, 1, style, &text))) + { + XChangeProperty (g.dpy, ev->requestor, property, + text.encoding, text.format, PropModeReplace, + text.value, text.nitems); + } + XFree (text.value); + +out: + XEvent response = {}; + response.xselection.type = SelectionNotify; + // XXX: We should check it against the event causing XSetSelectionOwner(). + response.xselection.time = ev->time; + response.xselection.requestor = ev->requestor; + response.xselection.selection = ev->selection; + response.xselection.target = ev->target; + response.xselection.property = ok ? property : None; + XSendEvent (g.dpy, ev->requestor, False, 0, &response); +} + static void on_x11_event (XEvent *ev) { @@ -6115,6 +6221,12 @@ on_x11_event (XEvent *ev) XftDrawChange (g.xft_draw, g.x11_pixmap); app_invalidate (); break; + case SelectionRequest: + on_x11_selection_request (&ev->xselectionrequest); + break; + case SelectionClear: + cstr_set (&g.x11_selection, NULL); + break; case UnmapNotify: app_quit (); break; @@ -6173,6 +6285,11 @@ on_x11_error (Display *dpy, XErrorEvent *event) || (event->error_code == BadDrawable && event->resourceid == g.x11_window)) return app_quit (), 0; + // XXX: The simplest possible way of discarding selection management errors. + // XCB would be a small win here, but it is a curse at the same time. + if (event->error_code == BadWindow && event->resourceid != g.x11_window) + return 0; + return x11_default_error_handler (dpy, event); } -- cgit v1.2.3