1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library 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 GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | /* |
19 | * Modified by the GTK+ Team and others 1997-2001. See the AUTHORS |
20 | * file for a list of people on the GTK+ Team. See the ChangeLog |
21 | * files for a list of changes. These files are distributed with |
22 | * GTK+ at ftp://ftp.gtk.org/pub/gtk/. |
23 | */ |
24 | |
25 | #include "config.h" |
26 | |
27 | #include "gtktextview.h" |
28 | #include "gtktextutil.h" |
29 | |
30 | #include "gtkcsscolorvalueprivate.h" |
31 | #include "gtkstylecontextprivate.h" |
32 | #include "gtktextbuffer.h" |
33 | #include "gtktextlayoutprivate.h" |
34 | #include "gtkintl.h" |
35 | #include "gtkwidgetprivate.h" |
36 | #include "gtkcssstyleprivate.h" |
37 | #include "gtkcsscolorvalueprivate.h" |
38 | |
39 | #define DRAG_ICON_MAX_WIDTH 250 |
40 | #define DRAG_ICON_MAX_HEIGHT 250 |
41 | #define DRAG_ICON_MAX_LINES 7 |
42 | #define ELLIPSIS_CHARACTER "\xe2\x80\xa6" |
43 | |
44 | static void |
45 | append_n_lines (GString *str, const char *text, GSList *lines, int n_lines) |
46 | { |
47 | PangoLayoutLine *line; |
48 | int i; |
49 | |
50 | for (i = 0; i < n_lines; i++) |
51 | { |
52 | line = lines->data; |
53 | g_string_append_len (string: str, |
54 | val: &text[pango_layout_line_get_start_index (line)], |
55 | len: pango_layout_line_get_length (line)); |
56 | lines = lines->next; |
57 | } |
58 | } |
59 | |
60 | static void |
61 | limit_layout_lines (PangoLayout *layout) |
62 | { |
63 | const char *text; |
64 | GString *str; |
65 | GSList *lines, *elem; |
66 | int n_lines; |
67 | |
68 | n_lines = pango_layout_get_line_count (layout); |
69 | |
70 | if (n_lines >= DRAG_ICON_MAX_LINES) |
71 | { |
72 | text = pango_layout_get_text (layout); |
73 | str = g_string_new (NULL); |
74 | lines = pango_layout_get_lines_readonly (layout); |
75 | |
76 | /* get first lines */ |
77 | elem = lines; |
78 | append_n_lines (str, text, lines: elem, |
79 | DRAG_ICON_MAX_LINES / 2); |
80 | |
81 | g_string_append (string: str, val: "\n" ELLIPSIS_CHARACTER "\n" ); |
82 | |
83 | /* get last lines */ |
84 | elem = g_slist_nth (list: lines, n: n_lines - DRAG_ICON_MAX_LINES / 2); |
85 | append_n_lines (str, text, lines: elem, |
86 | DRAG_ICON_MAX_LINES / 2); |
87 | |
88 | pango_layout_set_text (layout, text: str->str, length: -1); |
89 | g_string_free (string: str, TRUE); |
90 | } |
91 | } |
92 | |
93 | /** |
94 | * gtk_text_util_create_drag_icon: |
95 | * @widget: `GtkWidget` to extract the pango context |
96 | * @text: a #char to render the icon |
97 | * @len: length of @text, or -1 for NUL-terminated text |
98 | * |
99 | * Creates a drag and drop icon from @text. |
100 | * |
101 | * Returns: (transfer full): a `GdkPaintable` to use as DND icon |
102 | */ |
103 | GdkPaintable * |
104 | gtk_text_util_create_drag_icon (GtkWidget *widget, |
105 | char *text, |
106 | gssize len) |
107 | { |
108 | GtkCssStyle *style; |
109 | GtkSnapshot *snapshot; |
110 | PangoContext *context; |
111 | PangoLayout *layout; |
112 | GdkPaintable *paintable; |
113 | int layout_width; |
114 | int layout_height; |
115 | const GdkRGBA *color; |
116 | GdkDisplay *display; |
117 | |
118 | g_return_val_if_fail (widget != NULL, NULL); |
119 | g_return_val_if_fail (text != NULL, NULL); |
120 | |
121 | context = gtk_widget_get_pango_context (widget); |
122 | layout = pango_layout_new (context); |
123 | |
124 | pango_layout_set_text (layout, text, length: len); |
125 | pango_layout_set_wrap (layout, wrap: PANGO_WRAP_WORD_CHAR); |
126 | pango_layout_get_size (layout, width: &layout_width, NULL); |
127 | |
128 | layout_width = MIN (layout_width, DRAG_ICON_MAX_WIDTH * PANGO_SCALE); |
129 | pango_layout_set_width (layout, width: layout_width); |
130 | |
131 | limit_layout_lines (layout); |
132 | |
133 | snapshot = gtk_snapshot_new (); |
134 | |
135 | style = gtk_css_node_get_style (cssnode: gtk_widget_get_css_node (widget)); |
136 | color = gtk_css_color_value_get_rgba (color: style->core->color); |
137 | |
138 | display = gtk_widget_get_display (widget); |
139 | |
140 | if (!gdk_display_is_rgba (display) || |
141 | !gdk_display_is_composited (display)) |
142 | { |
143 | GtkWidget *bg_widget; |
144 | |
145 | if (GTK_IS_TEXT (widget)) |
146 | bg_widget = gtk_widget_get_parent (widget); |
147 | else |
148 | bg_widget = widget; |
149 | pango_layout_get_size (layout, width: &layout_width, height: &layout_height); |
150 | gtk_snapshot_render_background (snapshot, |
151 | context: gtk_widget_get_style_context (widget: bg_widget), |
152 | x: 0, y: 0, width: layout_width / PANGO_SCALE, |
153 | height: layout_height / PANGO_SCALE); |
154 | } |
155 | |
156 | gtk_snapshot_append_layout (snapshot, layout, color); |
157 | |
158 | paintable = gtk_snapshot_free_to_paintable (snapshot, NULL); |
159 | g_object_unref (object: layout); |
160 | |
161 | return paintable; |
162 | } |
163 | |
164 | static void |
165 | set_attributes_from_style (GtkWidget *widget, |
166 | GtkTextAttributes *values) |
167 | { |
168 | GtkCssStyle *style; |
169 | const GdkRGBA black = { 0, }; |
170 | |
171 | if (!values->appearance.bg_rgba) |
172 | values->appearance.bg_rgba = gdk_rgba_copy (rgba: &black); |
173 | if (!values->appearance.fg_rgba) |
174 | values->appearance.fg_rgba = gdk_rgba_copy (rgba: &black); |
175 | |
176 | style = gtk_css_node_get_style (cssnode: gtk_widget_get_css_node (widget)); |
177 | |
178 | *values->appearance.bg_rgba = *gtk_css_color_value_get_rgba (color: style->background->background_color); |
179 | *values->appearance.fg_rgba = *gtk_css_color_value_get_rgba (color: style->core->color); |
180 | |
181 | if (values->font) |
182 | pango_font_description_free (desc: values->font); |
183 | |
184 | values->font = gtk_css_style_get_pango_font (style); |
185 | } |
186 | |
187 | static int |
188 | get_border_window_size (GtkTextView *text_view, |
189 | GtkTextWindowType window_type) |
190 | { |
191 | GtkWidget *gutter; |
192 | |
193 | gutter = gtk_text_view_get_gutter (text_view, win: window_type); |
194 | if (gutter != NULL) |
195 | return gtk_widget_get_width (widget: gutter); |
196 | |
197 | return 0; |
198 | } |
199 | |
200 | GdkPaintable * |
201 | gtk_text_util_create_rich_drag_icon (GtkWidget *widget, |
202 | GtkTextBuffer *buffer, |
203 | GtkTextIter *start, |
204 | GtkTextIter *end) |
205 | { |
206 | GtkAllocation allocation; |
207 | GdkPaintable *paintable; |
208 | GtkSnapshot *snapshot; |
209 | int layout_width, layout_height; |
210 | GtkTextBuffer *new_buffer; |
211 | GtkTextLayout *layout; |
212 | GtkTextAttributes *style; |
213 | PangoContext *ltr_context, *rtl_context; |
214 | GtkTextIter iter; |
215 | GdkDisplay *display; |
216 | |
217 | g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); |
218 | g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL); |
219 | g_return_val_if_fail (start != NULL, NULL); |
220 | g_return_val_if_fail (end != NULL, NULL); |
221 | |
222 | new_buffer = gtk_text_buffer_new (table: gtk_text_buffer_get_tag_table (buffer)); |
223 | gtk_text_buffer_get_start_iter (buffer: new_buffer, iter: &iter); |
224 | |
225 | gtk_text_buffer_insert_range (buffer: new_buffer, iter: &iter, start, end); |
226 | |
227 | gtk_text_buffer_get_start_iter (buffer: new_buffer, iter: &iter); |
228 | |
229 | layout = gtk_text_layout_new (); |
230 | |
231 | ltr_context = gtk_widget_create_pango_context (widget); |
232 | pango_context_set_base_dir (context: ltr_context, direction: PANGO_DIRECTION_LTR); |
233 | rtl_context = gtk_widget_create_pango_context (widget); |
234 | pango_context_set_base_dir (context: rtl_context, direction: PANGO_DIRECTION_RTL); |
235 | |
236 | gtk_text_layout_set_contexts (layout, ltr_context, rtl_context); |
237 | |
238 | g_object_unref (object: ltr_context); |
239 | g_object_unref (object: rtl_context); |
240 | |
241 | style = gtk_text_attributes_new (); |
242 | |
243 | gtk_widget_get_allocation (widget, allocation: &allocation); |
244 | layout_width = allocation.width; |
245 | |
246 | set_attributes_from_style (widget, values: style); |
247 | |
248 | if (GTK_IS_TEXT_VIEW (widget)) |
249 | { |
250 | layout_width = layout_width |
251 | - get_border_window_size (GTK_TEXT_VIEW (widget), window_type: GTK_TEXT_WINDOW_LEFT) |
252 | - get_border_window_size (GTK_TEXT_VIEW (widget), window_type: GTK_TEXT_WINDOW_RIGHT); |
253 | } |
254 | |
255 | style->direction = gtk_widget_get_direction (widget); |
256 | style->wrap_mode = GTK_WRAP_WORD_CHAR; |
257 | |
258 | gtk_text_layout_set_default_style (layout, values: style); |
259 | gtk_text_attributes_unref (values: style); |
260 | |
261 | gtk_text_layout_set_buffer (layout, buffer: new_buffer); |
262 | gtk_text_layout_set_cursor_visible (layout, FALSE); |
263 | gtk_text_layout_set_screen_width (layout, width: layout_width); |
264 | |
265 | gtk_text_layout_validate (layout, DRAG_ICON_MAX_HEIGHT); |
266 | gtk_text_layout_get_size (layout, width: &layout_width, height: &layout_height); |
267 | |
268 | layout_width = MIN (layout_width, DRAG_ICON_MAX_WIDTH); |
269 | layout_height = MIN (layout_height, DRAG_ICON_MAX_HEIGHT); |
270 | |
271 | snapshot = gtk_snapshot_new (); |
272 | |
273 | display = gtk_widget_get_display (widget); |
274 | |
275 | if (!gdk_display_is_rgba (display) || |
276 | !gdk_display_is_composited (display)) |
277 | { |
278 | gtk_snapshot_render_background (snapshot, |
279 | context: gtk_widget_get_style_context (widget), |
280 | x: 0, y: 0, width: layout_width, height: layout_height); |
281 | } |
282 | |
283 | gtk_text_layout_snapshot (layout, widget, snapshot, clip: &(GdkRectangle) { 0, 0, layout_width, layout_height }, cursor_alpha: 1.0); |
284 | |
285 | g_object_unref (object: layout); |
286 | g_object_unref (object: new_buffer); |
287 | |
288 | paintable = gtk_snapshot_free_to_paintable (snapshot, size: &(graphene_size_t) { layout_width, layout_height }); |
289 | |
290 | return paintable; |
291 | } |
292 | |
293 | static int |
294 | layout_get_char_width (PangoLayout *layout) |
295 | { |
296 | int width; |
297 | PangoFontMetrics *metrics; |
298 | const PangoFontDescription *font_desc; |
299 | PangoContext *context = pango_layout_get_context (layout); |
300 | |
301 | font_desc = pango_layout_get_font_description (layout); |
302 | if (!font_desc) |
303 | font_desc = pango_context_get_font_description (context); |
304 | |
305 | metrics = pango_context_get_metrics (context, desc: font_desc, NULL); |
306 | width = pango_font_metrics_get_approximate_char_width (metrics); |
307 | pango_font_metrics_unref (metrics); |
308 | |
309 | return width; |
310 | } |
311 | |
312 | /* |
313 | * _gtk_text_util_get_block_cursor_location |
314 | * @layout: a `PangoLayout` |
315 | * @index: index at which cursor is located |
316 | * @pos: cursor location |
317 | * @at_line_end: whether cursor is drawn at line end, not over some |
318 | * character |
319 | * |
320 | * Returns: whether cursor should actually be drawn as a rectangle. |
321 | * It may not be the case if character at index is invisible. |
322 | */ |
323 | gboolean |
324 | _gtk_text_util_get_block_cursor_location (PangoLayout *layout, |
325 | int index, |
326 | PangoRectangle *pos, |
327 | gboolean *at_line_end) |
328 | { |
329 | PangoRectangle strong_pos, weak_pos; |
330 | PangoLayoutLine *layout_line; |
331 | gboolean rtl; |
332 | int line_no; |
333 | const char *text; |
334 | |
335 | g_return_val_if_fail (layout != NULL, FALSE); |
336 | g_return_val_if_fail (index >= 0, FALSE); |
337 | g_return_val_if_fail (pos != NULL, FALSE); |
338 | |
339 | pango_layout_index_to_pos (layout, index_: index, pos); |
340 | |
341 | if (pos->width != 0) |
342 | { |
343 | /* cursor is at some visible character, good */ |
344 | if (at_line_end) |
345 | *at_line_end = FALSE; |
346 | if (pos->width < 0) |
347 | { |
348 | pos->x += pos->width; |
349 | pos->width = -pos->width; |
350 | } |
351 | return TRUE; |
352 | } |
353 | |
354 | pango_layout_index_to_line_x (layout, index_: index, FALSE, line: &line_no, NULL); |
355 | layout_line = pango_layout_get_line_readonly (layout, line: line_no); |
356 | g_return_val_if_fail (layout_line != NULL, FALSE); |
357 | |
358 | text = pango_layout_get_text (layout); |
359 | |
360 | if (index < pango_layout_line_get_start_index (line: layout_line) + pango_layout_line_get_length (line: layout_line)) |
361 | { |
362 | /* this may be a zero-width character in the middle of the line, |
363 | * or it could be a character where line is wrapped, we do want |
364 | * block cursor in latter case */ |
365 | if (g_utf8_next_char (text + index) - text != |
366 | pango_layout_line_get_start_index (line: layout_line) + pango_layout_line_get_length (line: layout_line)) |
367 | { |
368 | /* zero-width character in the middle of the line, do not |
369 | * bother with block cursor */ |
370 | return FALSE; |
371 | } |
372 | } |
373 | |
374 | /* Cursor is at the line end. It may be an empty line, or it could |
375 | * be on the left or on the right depending on text direction, or it |
376 | * even could be in the middle of visual layout in bidi text. */ |
377 | |
378 | pango_layout_get_cursor_pos (layout, index_: index, strong_pos: &strong_pos, weak_pos: &weak_pos); |
379 | |
380 | if (strong_pos.x != weak_pos.x) |
381 | { |
382 | /* do not show block cursor in this case, since the character typed |
383 | * in may or may not appear at the cursor position */ |
384 | return FALSE; |
385 | } |
386 | |
387 | /* In case when index points to the end of line, pos->x is always most right |
388 | * pixel of the layout line, so we need to correct it for RTL text. */ |
389 | if (pango_layout_line_get_length (line: layout_line)) |
390 | { |
391 | if (pango_layout_line_get_resolved_direction (line: layout_line) == PANGO_DIRECTION_RTL) |
392 | { |
393 | PangoLayoutIter *iter; |
394 | PangoRectangle line_rect; |
395 | int i; |
396 | int left, right; |
397 | const char *p; |
398 | |
399 | p = g_utf8_prev_char (p: text + index); |
400 | |
401 | pango_layout_line_index_to_x (line: layout_line, index_: p - text, FALSE, x_pos: &left); |
402 | pango_layout_line_index_to_x (line: layout_line, index_: p - text, TRUE, x_pos: &right); |
403 | pos->x = MIN (left, right); |
404 | |
405 | iter = pango_layout_get_iter (layout); |
406 | for (i = 0; i < line_no; i++) |
407 | pango_layout_iter_next_line (iter); |
408 | pango_layout_iter_get_line_extents (iter, NULL, logical_rect: &line_rect); |
409 | pango_layout_iter_free (iter); |
410 | |
411 | rtl = TRUE; |
412 | pos->x += line_rect.x; |
413 | } |
414 | else |
415 | rtl = FALSE; |
416 | } |
417 | else |
418 | { |
419 | PangoContext *context = pango_layout_get_context (layout); |
420 | rtl = pango_context_get_base_dir (context) == PANGO_DIRECTION_RTL; |
421 | } |
422 | |
423 | pos->width = layout_get_char_width (layout); |
424 | |
425 | if (rtl) |
426 | pos->x -= pos->width - 1; |
427 | |
428 | if (at_line_end) |
429 | *at_line_end = TRUE; |
430 | |
431 | return pos->width != 0; |
432 | } |
433 | |