1 // Scintilla source code edit control
2 // PlatGTK.cxx - implementation of platform facilities on GTK+/Linux
3 // Copyright 1998-2004 by Neil Hodgson <neilh@scintilla.org>
4 // The License.txt file describes the conditions under which this software may be distributed.
5
6 #include <cstddef>
7 #include <cstdlib>
8 #include <cstring>
9 #include <cstdio>
10 #include <cmath>
11
12 #include <string>
13 #include <string_view>
14 #include <vector>
15 #include <map>
16 #include <algorithm>
17 #include <memory>
18 #include <sstream>
19
20 #include <glib.h>
21 #include <gmodule.h>
22 #include <gdk/gdk.h>
23 #include <gtk/gtk.h>
24 #include <gdk/gdkkeysyms.h>
25
26 #include "Platform.h"
27
28 #include "Scintilla.h"
29 #include "ScintillaWidget.h"
30 #include "StringCopy.h"
31 #include "IntegerRectangle.h"
32 #include "XPM.h"
33 #include "UniConversion.h"
34
35 #include "Converter.h"
36
37 #ifdef _MSC_VER
38 // Ignore unreferenced local functions in GTK+ headers
39 #pragma warning(disable: 4505)
40 #endif
41
42 using namespace Scintilla;
43
44 namespace {
45
46 constexpr double kPi = 3.14159265358979323846;
47
48 // The Pango version guard for pango_units_from_double and pango_units_to_double
49 // is more complex than simply implementing these here.
50
pangoUnitsFromDouble(double d)51 constexpr int pangoUnitsFromDouble(double d) noexcept {
52 return static_cast<int>(d * PANGO_SCALE + 0.5);
53 }
54
floatFromPangoUnits(int pu)55 constexpr float floatFromPangoUnits(int pu) noexcept {
56 return static_cast<float>(pu) / PANGO_SCALE;
57 }
58
CreateSimilarSurface(GdkWindow * window,cairo_content_t content,int width,int height)59 cairo_surface_t *CreateSimilarSurface(GdkWindow *window, cairo_content_t content, int width, int height) noexcept {
60 return gdk_window_create_similar_surface(window, content, width, height);
61 }
62
WindowFromWidget(GtkWidget * w)63 GdkWindow *WindowFromWidget(GtkWidget *w) noexcept {
64 return gtk_widget_get_window(w);
65 }
66
PWidget(WindowID wid)67 GtkWidget *PWidget(WindowID wid) noexcept {
68 return static_cast<GtkWidget *>(wid);
69 }
70
71 enum encodingType { singleByte, UTF8, dbcs };
72
73 // Holds a PangoFontDescription*.
74 class FontHandle {
75 public:
76 PangoFontDescription *pfd;
77 int characterSet;
FontHandle()78 FontHandle() noexcept : pfd(nullptr), characterSet(-1) {
79 }
FontHandle(PangoFontDescription * pfd_,int characterSet_)80 FontHandle(PangoFontDescription *pfd_, int characterSet_) noexcept {
81 pfd = pfd_;
82 characterSet = characterSet_;
83 }
84 // Deleted so FontHandle objects can not be copied.
85 FontHandle(const FontHandle &) = delete;
86 FontHandle(FontHandle &&) = delete;
87 FontHandle &operator=(const FontHandle &) = delete;
88 FontHandle &operator=(FontHandle &&) = delete;
~FontHandle()89 ~FontHandle() {
90 if (pfd)
91 pango_font_description_free(pfd);
92 pfd = nullptr;
93 }
94 static FontHandle *CreateNewFont(const FontParameters &fp);
95 };
96
CreateNewFont(const FontParameters & fp)97 FontHandle *FontHandle::CreateNewFont(const FontParameters &fp) {
98 PangoFontDescription *pfd = pango_font_description_new();
99 if (pfd) {
100 pango_font_description_set_family(pfd,
101 (fp.faceName[0] == '!') ? fp.faceName+1 : fp.faceName);
102 pango_font_description_set_size(pfd, pangoUnitsFromDouble(fp.size));
103 pango_font_description_set_weight(pfd, static_cast<PangoWeight>(fp.weight));
104 pango_font_description_set_style(pfd, fp.italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL);
105 return new FontHandle(pfd, fp.characterSet);
106 }
107
108 return nullptr;
109 }
110
111 // X has a 16 bit coordinate space, so stop drawing here to avoid wrapping
112 constexpr int maxCoordinate = 32000;
113
PFont(const Font & f)114 FontHandle *PFont(const Font &f) noexcept {
115 return static_cast<FontHandle *>(f.GetID());
116 }
117
118 }
119
Font()120 Font::Font() noexcept : fid(nullptr) {}
121
~Font()122 Font::~Font() {}
123
Create(const FontParameters & fp)124 void Font::Create(const FontParameters &fp) {
125 Release();
126 fid = FontHandle::CreateNewFont(fp);
127 }
128
Release()129 void Font::Release() {
130 if (fid)
131 delete static_cast<FontHandle *>(fid);
132 fid = nullptr;
133 }
134
135 // Required on OS X
136 namespace Scintilla {
137
138 // SurfaceID is a cairo_t*
139 class SurfaceImpl : public Surface {
140 encodingType et;
141 cairo_t *context;
142 cairo_surface_t *psurf;
143 int x;
144 int y;
145 bool inited;
146 bool createdGC;
147 PangoContext *pcontext;
148 PangoLayout *layout;
149 Converter conv;
150 int characterSet;
151 void SetConverter(int characterSet_);
152 public:
153 SurfaceImpl() noexcept;
154 // Deleted so SurfaceImpl objects can not be copied.
155 SurfaceImpl(const SurfaceImpl&) = delete;
156 SurfaceImpl(SurfaceImpl&&) = delete;
157 SurfaceImpl&operator=(const SurfaceImpl&) = delete;
158 SurfaceImpl&operator=(SurfaceImpl&&) = delete;
159 ~SurfaceImpl() override;
160
161 void Init(WindowID wid) override;
162 void Init(SurfaceID sid, WindowID wid) override;
163 void InitPixMap(int width, int height, Surface *surface_, WindowID wid) override;
164
165 void Clear() noexcept;
166 void Release() override;
167 bool Initialised() override;
168 void PenColour(ColourDesired fore) override;
169 int LogPixelsY() override;
170 int DeviceHeightFont(int points) override;
171 void MoveTo(int x_, int y_) override;
172 void LineTo(int x_, int y_) override;
173 void Polygon(Point *pts, size_t npts, ColourDesired fore, ColourDesired back) override;
174 void RectangleDraw(PRectangle rc, ColourDesired fore, ColourDesired back) override;
175 void FillRectangle(PRectangle rc, ColourDesired back) override;
176 void FillRectangle(PRectangle rc, Surface &surfacePattern) override;
177 void RoundedRectangle(PRectangle rc, ColourDesired fore, ColourDesired back) override;
178 void AlphaRectangle(PRectangle rc, int cornerSize, ColourDesired fill, int alphaFill,
179 ColourDesired outline, int alphaOutline, int flags) override;
180 void GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) override;
181 void DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) override;
182 void Ellipse(PRectangle rc, ColourDesired fore, ColourDesired back) override;
183 void Copy(PRectangle rc, Point from, Surface &surfaceSource) override;
184
185 std::unique_ptr<IScreenLineLayout> Layout(const IScreenLine *screenLine) override;
186
187 void DrawTextBase(PRectangle rc, const Font &font_, XYPOSITION ybase, std::string_view text, ColourDesired fore);
188 void DrawTextNoClip(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text, ColourDesired fore, ColourDesired back) override;
189 void DrawTextClipped(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text, ColourDesired fore, ColourDesired back) override;
190 void DrawTextTransparent(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text, ColourDesired fore) override;
191 void MeasureWidths(Font &font_, std::string_view text, XYPOSITION *positions) override;
192 XYPOSITION WidthText(Font &font_, std::string_view text) override;
193 XYPOSITION Ascent(Font &font_) override;
194 XYPOSITION Descent(Font &font_) override;
195 XYPOSITION InternalLeading(Font &font_) override;
196 XYPOSITION Height(Font &font_) override;
197 XYPOSITION AverageCharWidth(Font &font_) override;
198
199 void SetClip(PRectangle rc) override;
200 void FlushCachedState() override;
201
202 void SetUnicodeMode(bool unicodeMode_) override;
203 void SetDBCSMode(int codePage) override;
204 void SetBidiR2L(bool bidiR2L_) override;
205 };
206 }
207
CharacterSetID(int characterSet)208 const char *CharacterSetID(int characterSet) noexcept {
209 switch (characterSet) {
210 case SC_CHARSET_ANSI:
211 return "";
212 case SC_CHARSET_DEFAULT:
213 return "ISO-8859-1";
214 case SC_CHARSET_BALTIC:
215 return "ISO-8859-13";
216 case SC_CHARSET_CHINESEBIG5:
217 return "BIG-5";
218 case SC_CHARSET_EASTEUROPE:
219 return "ISO-8859-2";
220 case SC_CHARSET_GB2312:
221 return "CP936";
222 case SC_CHARSET_GREEK:
223 return "ISO-8859-7";
224 case SC_CHARSET_HANGUL:
225 return "CP949";
226 case SC_CHARSET_MAC:
227 return "MACINTOSH";
228 case SC_CHARSET_OEM:
229 return "ASCII";
230 case SC_CHARSET_RUSSIAN:
231 return "KOI8-R";
232 case SC_CHARSET_OEM866:
233 return "CP866";
234 case SC_CHARSET_CYRILLIC:
235 return "CP1251";
236 case SC_CHARSET_SHIFTJIS:
237 return "SHIFT-JIS";
238 case SC_CHARSET_SYMBOL:
239 return "";
240 case SC_CHARSET_TURKISH:
241 return "ISO-8859-9";
242 case SC_CHARSET_JOHAB:
243 return "CP1361";
244 case SC_CHARSET_HEBREW:
245 return "ISO-8859-8";
246 case SC_CHARSET_ARABIC:
247 return "ISO-8859-6";
248 case SC_CHARSET_VIETNAMESE:
249 return "";
250 case SC_CHARSET_THAI:
251 return "ISO-8859-11";
252 case SC_CHARSET_8859_15:
253 return "ISO-8859-15";
254 default:
255 return "";
256 }
257 }
258
SetConverter(int characterSet_)259 void SurfaceImpl::SetConverter(int characterSet_) {
260 if (characterSet != characterSet_) {
261 characterSet = characterSet_;
262 conv.Open("UTF-8", CharacterSetID(characterSet), false);
263 }
264 }
265
SurfaceImpl()266 SurfaceImpl::SurfaceImpl() noexcept : et(singleByte),
267 context(nullptr),
268 psurf(nullptr),
269 x(0), y(0), inited(false), createdGC(false),
270 pcontext(nullptr), layout(nullptr), characterSet(-1) {
271 }
272
~SurfaceImpl()273 SurfaceImpl::~SurfaceImpl() {
274 Clear();
275 }
276
Clear()277 void SurfaceImpl::Clear() noexcept {
278 et = singleByte;
279 if (createdGC) {
280 createdGC = false;
281 cairo_destroy(context);
282 }
283 context = nullptr;
284 if (psurf)
285 cairo_surface_destroy(psurf);
286 psurf = nullptr;
287 if (layout)
288 g_object_unref(layout);
289 layout = nullptr;
290 if (pcontext)
291 g_object_unref(pcontext);
292 pcontext = nullptr;
293 conv.Close();
294 characterSet = -1;
295 x = 0;
296 y = 0;
297 inited = false;
298 createdGC = false;
299 }
300
Release()301 void SurfaceImpl::Release() {
302 Clear();
303 }
304
Initialised()305 bool SurfaceImpl::Initialised() {
306 if (inited && context) {
307 if (cairo_status(context) == CAIRO_STATUS_SUCCESS) {
308 // Even when status is success, the target surface may have been
309 // finished which may cause an assertion to fail crashing the application.
310 // The cairo_surface_has_show_text_glyphs call checks the finished flag
311 // and when set, sets the status to CAIRO_STATUS_SURFACE_FINISHED
312 // which leads to warning messages instead of crashes.
313 // Performing the check in this method as it is called rarely and has no
314 // other side effects.
315 cairo_surface_t *psurfContext = cairo_get_target(context);
316 if (psurfContext) {
317 cairo_surface_has_show_text_glyphs(psurfContext);
318 }
319 }
320 return cairo_status(context) == CAIRO_STATUS_SUCCESS;
321 }
322 return inited;
323 }
324
Init(WindowID wid)325 void SurfaceImpl::Init(WindowID wid) {
326 Release();
327 PLATFORM_ASSERT(wid);
328 // if we are only created from a window ID, we can't perform drawing
329 psurf = nullptr;
330 context = nullptr;
331 createdGC = false;
332 pcontext = gtk_widget_create_pango_context(PWidget(wid));
333 PLATFORM_ASSERT(pcontext);
334 layout = pango_layout_new(pcontext);
335 PLATFORM_ASSERT(layout);
336 inited = true;
337 }
338
Init(SurfaceID sid,WindowID wid)339 void SurfaceImpl::Init(SurfaceID sid, WindowID wid) {
340 PLATFORM_ASSERT(sid);
341 Release();
342 PLATFORM_ASSERT(wid);
343 context = cairo_reference(static_cast<cairo_t *>(sid));
344 pcontext = gtk_widget_create_pango_context(PWidget(wid));
345 // update the Pango context in case sid isn't the widget's surface
346 pango_cairo_update_context(context, pcontext);
347 layout = pango_layout_new(pcontext);
348 cairo_set_line_width(context, 1);
349 createdGC = true;
350 inited = true;
351 }
352
InitPixMap(int width,int height,Surface * surface_,WindowID wid)353 void SurfaceImpl::InitPixMap(int width, int height, Surface *surface_, WindowID wid) {
354 PLATFORM_ASSERT(surface_);
355 Release();
356 SurfaceImpl *surfImpl = dynamic_cast<SurfaceImpl *>(surface_);
357 PLATFORM_ASSERT(surfImpl);
358 PLATFORM_ASSERT(wid);
359 context = cairo_reference(surfImpl->context);
360 pcontext = gtk_widget_create_pango_context(PWidget(wid));
361 // update the Pango context in case surface_ isn't the widget's surface
362 pango_cairo_update_context(context, pcontext);
363 PLATFORM_ASSERT(pcontext);
364 layout = pango_layout_new(pcontext);
365 PLATFORM_ASSERT(layout);
366 if (height > 0 && width > 0)
367 psurf = CreateSimilarSurface(
368 WindowFromWidget(PWidget(wid)),
369 CAIRO_CONTENT_COLOR_ALPHA, width, height);
370 cairo_destroy(context);
371 context = cairo_create(psurf);
372 cairo_rectangle(context, 0, 0, width, height);
373 cairo_set_source_rgb(context, 1.0, 0, 0);
374 cairo_fill(context);
375 // This produces sharp drawing more similar to GDK:
376 //cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE);
377 cairo_set_line_width(context, 1);
378 createdGC = true;
379 inited = true;
380 et = surfImpl->et;
381 }
382
PenColour(ColourDesired fore)383 void SurfaceImpl::PenColour(ColourDesired fore) {
384 if (context) {
385 const ColourDesired cdFore(fore.AsInteger());
386 cairo_set_source_rgb(context,
387 cdFore.GetRed() / 255.0,
388 cdFore.GetGreen() / 255.0,
389 cdFore.GetBlue() / 255.0);
390 }
391 }
392
LogPixelsY()393 int SurfaceImpl::LogPixelsY() {
394 return 72;
395 }
396
DeviceHeightFont(int points)397 int SurfaceImpl::DeviceHeightFont(int points) {
398 const int logPix = LogPixelsY();
399 return (points * logPix + logPix / 2) / 72;
400 }
401
MoveTo(int x_,int y_)402 void SurfaceImpl::MoveTo(int x_, int y_) {
403 x = x_;
404 y = y_;
405 }
406
Delta(int difference)407 static int Delta(int difference) noexcept {
408 if (difference < 0)
409 return -1;
410 else if (difference > 0)
411 return 1;
412 else
413 return 0;
414 }
415
LineTo(int x_,int y_)416 void SurfaceImpl::LineTo(int x_, int y_) {
417 // cairo_line_to draws the end position, unlike Win32 or GDK with GDK_CAP_NOT_LAST.
418 // For simple cases, move back one pixel from end.
419 if (context) {
420 const int xDiff = x_ - x;
421 const int xDelta = Delta(xDiff);
422 const int yDiff = y_ - y;
423 const int yDelta = Delta(yDiff);
424 if ((xDiff == 0) || (yDiff == 0)) {
425 // Horizontal or vertical lines can be more precisely drawn as a filled rectangle
426 const int xEnd = x_ - xDelta;
427 const int left = std::min(x, xEnd);
428 const int width = std::abs(x - xEnd) + 1;
429 const int yEnd = y_ - yDelta;
430 const int top = std::min(y, yEnd);
431 const int height = std::abs(y - yEnd) + 1;
432 cairo_rectangle(context, left, top, width, height);
433 cairo_fill(context);
434 } else if ((std::abs(xDiff) == std::abs(yDiff))) {
435 // 45 degree slope
436 cairo_move_to(context, x + 0.5, y + 0.5);
437 cairo_line_to(context, x_ + 0.5 - xDelta, y_ + 0.5 - yDelta);
438 } else {
439 // Line has a different slope so difficult to avoid last pixel
440 cairo_move_to(context, x + 0.5, y + 0.5);
441 cairo_line_to(context, x_ + 0.5, y_ + 0.5);
442 }
443 cairo_stroke(context);
444 }
445 x = x_;
446 y = y_;
447 }
448
Polygon(Point * pts,size_t npts,ColourDesired fore,ColourDesired back)449 void SurfaceImpl::Polygon(Point *pts, size_t npts, ColourDesired fore,
450 ColourDesired back) {
451 PLATFORM_ASSERT(context);
452 PenColour(back);
453 cairo_move_to(context, pts[0].x + 0.5, pts[0].y + 0.5);
454 for (size_t i = 1; i < npts; i++) {
455 cairo_line_to(context, pts[i].x + 0.5, pts[i].y + 0.5);
456 }
457 cairo_close_path(context);
458 cairo_fill_preserve(context);
459 PenColour(fore);
460 cairo_stroke(context);
461 }
462
RectangleDraw(PRectangle rc,ColourDesired fore,ColourDesired back)463 void SurfaceImpl::RectangleDraw(PRectangle rc, ColourDesired fore, ColourDesired back) {
464 if (context) {
465 cairo_rectangle(context, rc.left + 0.5, rc.top + 0.5,
466 rc.Width() - 1, rc.Height() - 1);
467 PenColour(back);
468 cairo_fill_preserve(context);
469 PenColour(fore);
470 cairo_stroke(context);
471 }
472 }
473
FillRectangle(PRectangle rc,ColourDesired back)474 void SurfaceImpl::FillRectangle(PRectangle rc, ColourDesired back) {
475 PenColour(back);
476 if (context && (rc.left < maxCoordinate)) { // Protect against out of range
477 rc.left = std::round(rc.left);
478 rc.right = std::round(rc.right);
479 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
480 cairo_fill(context);
481 }
482 }
483
FillRectangle(PRectangle rc,Surface & surfacePattern)484 void SurfaceImpl::FillRectangle(PRectangle rc, Surface &surfacePattern) {
485 SurfaceImpl &surfi = dynamic_cast<SurfaceImpl &>(surfacePattern);
486 if (context && surfi.psurf) {
487 // Tile pattern over rectangle
488 cairo_set_source_surface(context, surfi.psurf, rc.left, rc.top);
489 cairo_pattern_set_extend(cairo_get_source(context), CAIRO_EXTEND_REPEAT);
490 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
491 cairo_fill(context);
492 }
493 }
494
RoundedRectangle(PRectangle rc,ColourDesired fore,ColourDesired back)495 void SurfaceImpl::RoundedRectangle(PRectangle rc, ColourDesired fore, ColourDesired back) {
496 if (((rc.right - rc.left) > 4) && ((rc.bottom - rc.top) > 4)) {
497 // Approximate a round rect with some cut off corners
498 Point pts[] = {
499 Point(rc.left + 2, rc.top),
500 Point(rc.right - 2, rc.top),
501 Point(rc.right, rc.top + 2),
502 Point(rc.right, rc.bottom - 2),
503 Point(rc.right - 2, rc.bottom),
504 Point(rc.left + 2, rc.bottom),
505 Point(rc.left, rc.bottom - 2),
506 Point(rc.left, rc.top + 2),
507 };
508 Polygon(pts, std::size(pts), fore, back);
509 } else {
510 RectangleDraw(rc, fore, back);
511 }
512 }
513
PathRoundRectangle(cairo_t * context,double left,double top,double width,double height,double radius)514 static void PathRoundRectangle(cairo_t *context, double left, double top, double width, double height, double radius) noexcept {
515 constexpr double degrees = kPi / 180.0;
516
517 cairo_new_sub_path(context);
518 cairo_arc(context, left + width - radius, top + radius, radius, -90 * degrees, 0 * degrees);
519 cairo_arc(context, left + width - radius, top + height - radius, radius, 0 * degrees, 90 * degrees);
520 cairo_arc(context, left + radius, top + height - radius, radius, 90 * degrees, 180 * degrees);
521 cairo_arc(context, left + radius, top + radius, radius, 180 * degrees, 270 * degrees);
522 cairo_close_path(context);
523 }
524
AlphaRectangle(PRectangle rc,int cornerSize,ColourDesired fill,int alphaFill,ColourDesired outline,int alphaOutline,int)525 void SurfaceImpl::AlphaRectangle(PRectangle rc, int cornerSize, ColourDesired fill, int alphaFill,
526 ColourDesired outline, int alphaOutline, int /*flags*/) {
527 if (context && rc.Width() > 0) {
528 const ColourDesired cdFill(fill.AsInteger());
529 cairo_set_source_rgba(context,
530 cdFill.GetRed() / 255.0,
531 cdFill.GetGreen() / 255.0,
532 cdFill.GetBlue() / 255.0,
533 alphaFill / 255.0);
534 if (cornerSize > 0)
535 PathRoundRectangle(context, rc.left + 1.0, rc.top + 1.0, rc.Width() - 2.0, rc.Height() - 2.0, cornerSize);
536 else
537 cairo_rectangle(context, rc.left + 1.0, rc.top + 1.0, rc.Width() - 2.0, rc.Height() - 2.0);
538 cairo_fill(context);
539
540 const ColourDesired cdOutline(outline.AsInteger());
541 cairo_set_source_rgba(context,
542 cdOutline.GetRed() / 255.0,
543 cdOutline.GetGreen() / 255.0,
544 cdOutline.GetBlue() / 255.0,
545 alphaOutline / 255.0);
546 if (cornerSize > 0)
547 PathRoundRectangle(context, rc.left + 0.5, rc.top + 0.5, rc.Width() - 1, rc.Height() - 1, cornerSize);
548 else
549 cairo_rectangle(context, rc.left + 0.5, rc.top + 0.5, rc.Width() - 1, rc.Height() - 1);
550 cairo_stroke(context);
551 }
552 }
553
GradientRectangle(PRectangle rc,const std::vector<ColourStop> & stops,GradientOptions options)554 void SurfaceImpl::GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) {
555 if (context) {
556 cairo_pattern_t *pattern;
557 switch (options) {
558 case GradientOptions::leftToRight:
559 pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.right, rc.top);
560 break;
561 case GradientOptions::topToBottom:
562 default:
563 pattern = cairo_pattern_create_linear(rc.left, rc.top, rc.left, rc.bottom);
564 break;
565 }
566 for (const ColourStop &stop : stops) {
567 cairo_pattern_add_color_stop_rgba(pattern, stop.position,
568 stop.colour.GetRedComponent(),
569 stop.colour.GetGreenComponent(),
570 stop.colour.GetBlueComponent(),
571 stop.colour.GetAlphaComponent());
572 }
573 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
574 cairo_set_source(context, pattern);
575 cairo_fill(context);
576 cairo_pattern_destroy(pattern);
577 }
578 }
579
DrawRGBAImage(PRectangle rc,int width,int height,const unsigned char * pixelsImage)580 void SurfaceImpl::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) {
581 PLATFORM_ASSERT(context);
582 if (rc.Width() > width)
583 rc.left += (rc.Width() - width) / 2;
584 rc.right = rc.left + width;
585 if (rc.Height() > height)
586 rc.top += (rc.Height() - height) / 2;
587 rc.bottom = rc.top + height;
588
589 const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
590 const int ucs = stride * height;
591 std::vector<unsigned char> image(ucs);
592 for (ptrdiff_t iy=0; iy<height; iy++) {
593 unsigned char *pixel = &image[0] + iy*stride;
594 RGBAImage::BGRAFromRGBA(pixel, pixelsImage, width);
595 pixelsImage += RGBAImage::bytesPerPixel * width;
596 }
597
598 cairo_surface_t *psurfImage = cairo_image_surface_create_for_data(&image[0], CAIRO_FORMAT_ARGB32, width, height, stride);
599 cairo_set_source_surface(context, psurfImage, rc.left, rc.top);
600 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
601 cairo_fill(context);
602
603 cairo_surface_destroy(psurfImage);
604 }
605
Ellipse(PRectangle rc,ColourDesired fore,ColourDesired back)606 void SurfaceImpl::Ellipse(PRectangle rc, ColourDesired fore, ColourDesired back) {
607 PLATFORM_ASSERT(context);
608 PenColour(back);
609 cairo_arc(context, (rc.left + rc.right) / 2, (rc.top + rc.bottom) / 2,
610 std::min(rc.Width(), rc.Height()) / 2, 0, 2*kPi);
611 cairo_fill_preserve(context);
612 PenColour(fore);
613 cairo_stroke(context);
614 }
615
Copy(PRectangle rc,Point from,Surface & surfaceSource)616 void SurfaceImpl::Copy(PRectangle rc, Point from, Surface &surfaceSource) {
617 SurfaceImpl &surfi = static_cast<SurfaceImpl &>(surfaceSource);
618 const bool canDraw = surfi.psurf != nullptr;
619 if (canDraw) {
620 PLATFORM_ASSERT(context);
621 cairo_set_source_surface(context, surfi.psurf,
622 rc.left - from.x, rc.top - from.y);
623 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
624 cairo_fill(context);
625 }
626 }
627
Layout(const IScreenLine *)628 std::unique_ptr<IScreenLineLayout> SurfaceImpl::Layout(const IScreenLine *) {
629 return {};
630 }
631
UTF8FromLatin1(std::string_view text)632 std::string UTF8FromLatin1(std::string_view text) {
633 std::string utfForm(text.length()*2 + 1, '\0');
634 size_t lenU = 0;
635 for (const char ch : text) {
636 const unsigned char uch = ch;
637 if (uch < 0x80) {
638 utfForm[lenU++] = uch;
639 } else {
640 utfForm[lenU++] = static_cast<char>(0xC0 | (uch >> 6));
641 utfForm[lenU++] = static_cast<char>(0x80 | (uch & 0x3f));
642 }
643 }
644 utfForm.resize(lenU);
645 return utfForm;
646 }
647
648 namespace {
649
UTF8FromIconv(const Converter & conv,std::string_view text)650 std::string UTF8FromIconv(const Converter &conv, std::string_view text) {
651 if (conv) {
652 std::string utfForm(text.length()*3+1, '\0');
653 char *pin = const_cast<char *>(text.data());
654 gsize inLeft = text.length();
655 char *putf = &utfForm[0];
656 char *pout = putf;
657 gsize outLeft = text.length()*3+1;
658 const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft);
659 if (conversions != sizeFailure) {
660 *pout = '\0';
661 utfForm.resize(pout - putf);
662 return utfForm;
663 }
664 }
665 return std::string();
666 }
667
668 // Work out how many bytes are in a character by trying to convert using iconv,
669 // returning the first length that succeeds.
MultiByteLenFromIconv(const Converter & conv,const char * s,size_t len)670 size_t MultiByteLenFromIconv(const Converter &conv, const char *s, size_t len) noexcept {
671 for (size_t lenMB=1; (lenMB<4) && (lenMB <= len); lenMB++) {
672 char wcForm[2] {};
673 char *pin = const_cast<char *>(s);
674 gsize inLeft = lenMB;
675 char *pout = wcForm;
676 gsize outLeft = 2;
677 const gsize conversions = conv.Convert(&pin, &inLeft, &pout, &outLeft);
678 if (conversions != sizeFailure) {
679 return lenMB;
680 }
681 }
682 return 1;
683 }
684
685 }
686
DrawTextBase(PRectangle rc,const Font & font_,XYPOSITION ybase,std::string_view text,ColourDesired fore)687 void SurfaceImpl::DrawTextBase(PRectangle rc, const Font &font_, XYPOSITION ybase, std::string_view text,
688 ColourDesired fore) {
689 PenColour(fore);
690 if (context) {
691 const XYPOSITION xText = rc.left;
692 if (PFont(font_)->pfd) {
693 std::string utfForm;
694 if (et == UTF8) {
695 pango_layout_set_text(layout, text.data(), text.length());
696 } else {
697 SetConverter(PFont(font_)->characterSet);
698 utfForm = UTF8FromIconv(conv, text);
699 if (utfForm.empty()) { // iconv failed so treat as Latin1
700 utfForm = UTF8FromLatin1(text);
701 }
702 pango_layout_set_text(layout, utfForm.c_str(), utfForm.length());
703 }
704 pango_layout_set_font_description(layout, PFont(font_)->pfd);
705 pango_cairo_update_layout(context, layout);
706 PangoLayoutLine *pll = pango_layout_get_line_readonly(layout, 0);
707 cairo_move_to(context, xText, ybase);
708 pango_cairo_show_layout_line(context, pll);
709 }
710 }
711 }
712
DrawTextNoClip(PRectangle rc,Font & font_,XYPOSITION ybase,std::string_view text,ColourDesired fore,ColourDesired back)713 void SurfaceImpl::DrawTextNoClip(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text,
714 ColourDesired fore, ColourDesired back) {
715 FillRectangle(rc, back);
716 DrawTextBase(rc, font_, ybase, text, fore);
717 }
718
719 // On GTK+, exactly same as DrawTextNoClip
DrawTextClipped(PRectangle rc,Font & font_,XYPOSITION ybase,std::string_view text,ColourDesired fore,ColourDesired back)720 void SurfaceImpl::DrawTextClipped(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text,
721 ColourDesired fore, ColourDesired back) {
722 FillRectangle(rc, back);
723 DrawTextBase(rc, font_, ybase, text, fore);
724 }
725
DrawTextTransparent(PRectangle rc,Font & font_,XYPOSITION ybase,std::string_view text,ColourDesired fore)726 void SurfaceImpl::DrawTextTransparent(PRectangle rc, Font &font_, XYPOSITION ybase, std::string_view text,
727 ColourDesired fore) {
728 // Avoid drawing spaces in transparent mode
729 for (size_t i=0; i<text.length(); i++) {
730 if (text[i] != ' ') {
731 DrawTextBase(rc, font_, ybase, text, fore);
732 return;
733 }
734 }
735 }
736
737 class ClusterIterator {
738 PangoLayoutIter *iter;
739 PangoRectangle pos;
740 size_t lenPositions;
741 public:
742 bool finished;
743 XYPOSITION positionStart;
744 XYPOSITION position;
745 XYPOSITION distance;
746 int curIndex;
ClusterIterator(PangoLayout * layout,size_t len)747 ClusterIterator(PangoLayout *layout, size_t len) noexcept : lenPositions(len), finished(false),
748 positionStart(0), position(0), distance(0), curIndex(0) {
749 iter = pango_layout_get_iter(layout);
750 pango_layout_iter_get_cluster_extents(iter, nullptr, &pos);
751 }
752 // Deleted so ClusterIterator objects can not be copied.
753 ClusterIterator(const ClusterIterator&) = delete;
754 ClusterIterator(ClusterIterator&&) = delete;
755 ClusterIterator&operator=(const ClusterIterator&) = delete;
756 ClusterIterator&operator=(ClusterIterator&&) = delete;
757
~ClusterIterator()758 ~ClusterIterator() {
759 pango_layout_iter_free(iter);
760 }
761
Next()762 void Next() noexcept {
763 positionStart = position;
764 if (pango_layout_iter_next_cluster(iter)) {
765 pango_layout_iter_get_cluster_extents(iter, nullptr, &pos);
766 position = floatFromPangoUnits(pos.x);
767 curIndex = pango_layout_iter_get_index(iter);
768 } else {
769 finished = true;
770 position = floatFromPangoUnits(pos.x + pos.width);
771 curIndex = lenPositions;
772 }
773 distance = position - positionStart;
774 }
775 };
776
MeasureWidths(Font & font_,std::string_view text,XYPOSITION * positions)777 void SurfaceImpl::MeasureWidths(Font &font_, std::string_view text, XYPOSITION *positions) {
778 if (font_.GetID()) {
779 if (PFont(font_)->pfd) {
780 pango_layout_set_font_description(layout, PFont(font_)->pfd);
781 if (et == UTF8) {
782 // Simple and direct as UTF-8 is native Pango encoding
783 int i = 0;
784 pango_layout_set_text(layout, text.data(), text.length());
785 ClusterIterator iti(layout, text.length());
786 while (!iti.finished) {
787 iti.Next();
788 const int places = iti.curIndex - i;
789 while (i < iti.curIndex) {
790 // Evenly distribute space among bytes of this cluster.
791 // Would be better to find number of characters and then
792 // divide evenly between characters with each byte of a character
793 // being at the same position.
794 positions[i] = iti.position - (iti.curIndex - 1 - i) * iti.distance / places;
795 i++;
796 }
797 }
798 PLATFORM_ASSERT(static_cast<size_t>(i) == text.length());
799 } else {
800 int positionsCalculated = 0;
801 if (et == dbcs) {
802 SetConverter(PFont(font_)->characterSet);
803 std::string utfForm = UTF8FromIconv(conv, text);
804 if (!utfForm.empty()) {
805 // Convert to UTF-8 so can ask Pango for widths, then
806 // Loop through UTF-8 and DBCS forms, taking account of different
807 // character byte lengths.
808 Converter convMeasure("UCS-2", CharacterSetID(characterSet), false);
809 pango_layout_set_text(layout, utfForm.c_str(), strlen(utfForm.c_str()));
810 int i = 0;
811 int clusterStart = 0;
812 ClusterIterator iti(layout, strlen(utfForm.c_str()));
813 while (!iti.finished) {
814 iti.Next();
815 const int clusterEnd = iti.curIndex;
816 const int places = g_utf8_strlen(utfForm.c_str() + clusterStart, clusterEnd - clusterStart);
817 int place = 1;
818 while (clusterStart < clusterEnd) {
819 size_t lenChar = MultiByteLenFromIconv(convMeasure, text.data()+i, text.length()-i);
820 while (lenChar--) {
821 positions[i++] = iti.position - (places - place) * iti.distance / places;
822 positionsCalculated++;
823 }
824 clusterStart += UTF8BytesOfLead[static_cast<unsigned char>(utfForm[clusterStart])];
825 place++;
826 }
827 }
828 PLATFORM_ASSERT(static_cast<size_t>(i) == text.length());
829 }
830 }
831 if (positionsCalculated < 1) {
832 const size_t lenPositions = text.length();
833 // Either 8-bit or DBCS conversion failed so treat as 8-bit.
834 SetConverter(PFont(font_)->characterSet);
835 const bool rtlCheck = PFont(font_)->characterSet == SC_CHARSET_HEBREW ||
836 PFont(font_)->characterSet == SC_CHARSET_ARABIC;
837 std::string utfForm = UTF8FromIconv(conv, text);
838 if (utfForm.empty()) {
839 utfForm = UTF8FromLatin1(text);
840 }
841 pango_layout_set_text(layout, utfForm.c_str(), utfForm.length());
842 size_t i = 0;
843 int clusterStart = 0;
844 // Each 8-bit input character may take 1 or 2 bytes in UTF-8
845 // and groups of up to 3 may be represented as ligatures.
846 ClusterIterator iti(layout, utfForm.length());
847 while (!iti.finished) {
848 iti.Next();
849 const int clusterEnd = iti.curIndex;
850 const int ligatureLength = g_utf8_strlen(utfForm.c_str() + clusterStart, clusterEnd - clusterStart);
851 if (rtlCheck && ((clusterEnd <= clusterStart) || (ligatureLength == 0) || (ligatureLength > 3))) {
852 // Something has gone wrong: exit quickly but pretend all the characters are equally spaced:
853 int widthLayout = 0;
854 pango_layout_get_size(layout, &widthLayout, nullptr);
855 const XYPOSITION widthTotal = floatFromPangoUnits(widthLayout);
856 for (size_t bytePos=0; bytePos<lenPositions; bytePos++) {
857 positions[bytePos] = widthTotal / lenPositions * (bytePos + 1);
858 }
859 return;
860 }
861 PLATFORM_ASSERT(ligatureLength > 0 && ligatureLength <= 3);
862 for (int charInLig=0; charInLig<ligatureLength; charInLig++) {
863 positions[i++] = iti.position - (ligatureLength - 1 - charInLig) * iti.distance / ligatureLength;
864 }
865 clusterStart = clusterEnd;
866 }
867 while (i < lenPositions) {
868 // If something failed, fill in rest of the positions
869 positions[i++] = clusterStart;
870 }
871 PLATFORM_ASSERT(i == text.length());
872 }
873 }
874 }
875 } else {
876 // No font so return an ascending range of values
877 for (size_t i = 0; i < text.length(); i++) {
878 positions[i] = i + 1;
879 }
880 }
881 }
882
WidthText(Font & font_,std::string_view text)883 XYPOSITION SurfaceImpl::WidthText(Font &font_, std::string_view text) {
884 if (font_.GetID()) {
885 if (PFont(font_)->pfd) {
886 std::string utfForm;
887 pango_layout_set_font_description(layout, PFont(font_)->pfd);
888 if (et == UTF8) {
889 pango_layout_set_text(layout, text.data(), text.length());
890 } else {
891 SetConverter(PFont(font_)->characterSet);
892 utfForm = UTF8FromIconv(conv, text);
893 if (utfForm.empty()) { // iconv failed so treat as Latin1
894 utfForm = UTF8FromLatin1(text);
895 }
896 pango_layout_set_text(layout, utfForm.c_str(), utfForm.length());
897 }
898 PangoLayoutLine *pangoLine = pango_layout_get_line_readonly(layout, 0);
899 PangoRectangle pos {};
900 pango_layout_line_get_extents(pangoLine, nullptr, &pos);
901 return floatFromPangoUnits(pos.width);
902 }
903 return 1;
904 } else {
905 return 1;
906 }
907 }
908
909 // Ascent and descent determined by Pango font metrics.
910
Ascent(Font & font_)911 XYPOSITION SurfaceImpl::Ascent(Font &font_) {
912 if (!(font_.GetID()))
913 return 1;
914 XYPOSITION ascent = 0;
915 if (PFont(font_)->pfd) {
916 PangoFontMetrics *metrics = pango_context_get_metrics(pcontext,
917 PFont(font_)->pfd, pango_context_get_language(pcontext));
918 ascent = std::round(floatFromPangoUnits(
919 pango_font_metrics_get_ascent(metrics)));
920 pango_font_metrics_unref(metrics);
921 }
922 if (ascent == 0) {
923 ascent = 1;
924 }
925 return ascent;
926 }
927
Descent(Font & font_)928 XYPOSITION SurfaceImpl::Descent(Font &font_) {
929 if (!(font_.GetID()))
930 return 1;
931 if (PFont(font_)->pfd) {
932 PangoFontMetrics *metrics = pango_context_get_metrics(pcontext,
933 PFont(font_)->pfd, pango_context_get_language(pcontext));
934 const XYPOSITION descent = std::round(floatFromPangoUnits(
935 pango_font_metrics_get_descent(metrics)));
936 pango_font_metrics_unref(metrics);
937 return descent;
938 }
939 return 0;
940 }
941
InternalLeading(Font &)942 XYPOSITION SurfaceImpl::InternalLeading(Font &) {
943 return 0;
944 }
945
Height(Font & font_)946 XYPOSITION SurfaceImpl::Height(Font &font_) {
947 return Ascent(font_) + Descent(font_);
948 }
949
AverageCharWidth(Font & font_)950 XYPOSITION SurfaceImpl::AverageCharWidth(Font &font_) {
951 return WidthText(font_, "n");
952 }
953
SetClip(PRectangle rc)954 void SurfaceImpl::SetClip(PRectangle rc) {
955 PLATFORM_ASSERT(context);
956 cairo_rectangle(context, rc.left, rc.top, rc.Width(), rc.Height());
957 cairo_clip(context);
958 }
959
FlushCachedState()960 void SurfaceImpl::FlushCachedState() {}
961
SetUnicodeMode(bool unicodeMode_)962 void SurfaceImpl::SetUnicodeMode(bool unicodeMode_) {
963 if (unicodeMode_)
964 et = UTF8;
965 }
966
SetDBCSMode(int codePage)967 void SurfaceImpl::SetDBCSMode(int codePage) {
968 if (codePage && (codePage != SC_CP_UTF8))
969 et = dbcs;
970 }
971
SetBidiR2L(bool)972 void SurfaceImpl::SetBidiR2L(bool) {
973 }
974
Allocate(int)975 Surface *Surface::Allocate(int) {
976 return new SurfaceImpl();
977 }
978
~Window()979 Window::~Window() {}
980
Destroy()981 void Window::Destroy() {
982 if (wid) {
983 ListBox *listbox = dynamic_cast<ListBox *>(this);
984 if (listbox) {
985 gtk_widget_hide(GTK_WIDGET(wid));
986 // clear up window content
987 listbox->Clear();
988 // resize the window to the smallest possible size for it to adapt
989 // to future content
990 gtk_window_resize(GTK_WINDOW(wid), 1, 1);
991 } else {
992 gtk_widget_destroy(GTK_WIDGET(wid));
993 }
994 wid = nullptr;
995 }
996 }
997
GetPosition() const998 PRectangle Window::GetPosition() const {
999 // Before any size allocated pretend its 1000 wide so not scrolled
1000 PRectangle rc(0, 0, 1000, 1000);
1001 if (wid) {
1002 GtkAllocation allocation;
1003 gtk_widget_get_allocation(PWidget(wid), &allocation);
1004 rc.left = static_cast<XYPOSITION>(allocation.x);
1005 rc.top = static_cast<XYPOSITION>(allocation.y);
1006 if (allocation.width > 20) {
1007 rc.right = rc.left + allocation.width;
1008 rc.bottom = rc.top + allocation.height;
1009 }
1010 }
1011 return rc;
1012 }
1013
SetPosition(PRectangle rc)1014 void Window::SetPosition(PRectangle rc) {
1015 GtkAllocation alloc;
1016 alloc.x = static_cast<int>(rc.left);
1017 alloc.y = static_cast<int>(rc.top);
1018 alloc.width = static_cast<int>(rc.Width());
1019 alloc.height = static_cast<int>(rc.Height());
1020 gtk_widget_size_allocate(PWidget(wid), &alloc);
1021 }
1022
1023 namespace {
1024
MonitorRectangleForWidget(GtkWidget * wid)1025 GdkRectangle MonitorRectangleForWidget(GtkWidget *wid) noexcept {
1026 GdkWindow *wnd = WindowFromWidget(wid);
1027 GdkRectangle rcScreen = GdkRectangle();
1028 #if GTK_CHECK_VERSION(3,22,0)
1029 GdkDisplay *pdisplay = gtk_widget_get_display(wid);
1030 GdkMonitor *monitor = gdk_display_get_monitor_at_window(pdisplay, wnd);
1031 gdk_monitor_get_geometry(monitor, &rcScreen);
1032 #else
1033 GdkScreen *screen = gtk_widget_get_screen(wid);
1034 const gint monitor_num = gdk_screen_get_monitor_at_window(screen, wnd);
1035 gdk_screen_get_monitor_geometry(screen, monitor_num, &rcScreen);
1036 #endif
1037 return rcScreen;
1038 }
1039
1040 }
1041
SetPositionRelative(PRectangle rc,const Window * relativeTo)1042 void Window::SetPositionRelative(PRectangle rc, const Window *relativeTo) {
1043 const IntegerRectangle irc(rc);
1044 int ox = 0;
1045 int oy = 0;
1046 GdkWindow *wndRelativeTo = WindowFromWidget(PWidget(relativeTo->wid));
1047 gdk_window_get_origin(wndRelativeTo, &ox, &oy);
1048 ox += irc.left;
1049 oy += irc.top;
1050
1051 const GdkRectangle rcMonitor = MonitorRectangleForWidget(PWidget(relativeTo->wid));
1052
1053 /* do some corrections to fit into screen */
1054 const int sizex = irc.Width();
1055 const int sizey = irc.Height();
1056 if (sizex > rcMonitor.width || ox < rcMonitor.x)
1057 ox = rcMonitor.x; /* the best we can do */
1058 else if (ox + sizex > rcMonitor.x + rcMonitor.width)
1059 ox = rcMonitor.x + rcMonitor.width - sizex;
1060 if (sizey > rcMonitor.height || oy < rcMonitor.y)
1061 oy = rcMonitor.y;
1062 else if (oy + sizey > rcMonitor.y + rcMonitor.height)
1063 oy = rcMonitor.y + rcMonitor.height - sizey;
1064
1065 gtk_window_move(GTK_WINDOW(PWidget(wid)), ox, oy);
1066
1067 gtk_window_resize(GTK_WINDOW(wid), sizex, sizey);
1068 }
1069
GetClientPosition() const1070 PRectangle Window::GetClientPosition() const {
1071 // On GTK+, the client position is the window position
1072 return GetPosition();
1073 }
1074
Show(bool show)1075 void Window::Show(bool show) {
1076 if (show)
1077 gtk_widget_show(PWidget(wid));
1078 }
1079
InvalidateAll()1080 void Window::InvalidateAll() {
1081 if (wid) {
1082 gtk_widget_queue_draw(PWidget(wid));
1083 }
1084 }
1085
InvalidateRectangle(PRectangle rc)1086 void Window::InvalidateRectangle(PRectangle rc) {
1087 if (wid) {
1088 const IntegerRectangle irc(rc);
1089 gtk_widget_queue_draw_area(PWidget(wid),
1090 irc.left, irc.top,
1091 irc.Width(), irc.Height());
1092 }
1093 }
1094
SetFont(Font &)1095 void Window::SetFont(Font &) {
1096 // Can not be done generically but only needed for ListBox
1097 }
1098
SetCursor(Cursor curs)1099 void Window::SetCursor(Cursor curs) {
1100 // We don't set the cursor to same value numerous times under gtk because
1101 // it stores the cursor in the window once it's set
1102 if (curs == cursorLast)
1103 return;
1104
1105 cursorLast = curs;
1106 GdkDisplay *pdisplay = gtk_widget_get_display(PWidget(wid));
1107
1108 GdkCursor *gdkCurs;
1109 switch (curs) {
1110 case cursorText:
1111 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_XTERM);
1112 break;
1113 case cursorArrow:
1114 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_LEFT_PTR);
1115 break;
1116 case cursorUp:
1117 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_CENTER_PTR);
1118 break;
1119 case cursorWait:
1120 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_WATCH);
1121 break;
1122 case cursorHand:
1123 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_HAND2);
1124 break;
1125 case cursorReverseArrow:
1126 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_RIGHT_PTR);
1127 break;
1128 default:
1129 gdkCurs = gdk_cursor_new_for_display(pdisplay, GDK_LEFT_PTR);
1130 cursorLast = cursorArrow;
1131 break;
1132 }
1133
1134 if (WindowFromWidget(PWidget(wid)))
1135 gdk_window_set_cursor(WindowFromWidget(PWidget(wid)), gdkCurs);
1136 #if GTK_CHECK_VERSION(3,0,0)
1137 g_object_unref(gdkCurs);
1138 #else
1139 gdk_cursor_unref(gdkCurs);
1140 #endif
1141 }
1142
1143 /* Returns rectangle of monitor pt is on, both rect and pt are in Window's
1144 gdk window coordinates */
GetMonitorRect(Point pt)1145 PRectangle Window::GetMonitorRect(Point pt) {
1146 gint x_offset, y_offset;
1147
1148 gdk_window_get_origin(WindowFromWidget(PWidget(wid)), &x_offset, &y_offset);
1149
1150 GdkRectangle rect {};
1151
1152 #if GTK_CHECK_VERSION(3,22,0)
1153 GdkDisplay *pdisplay = gtk_widget_get_display(PWidget(wid));
1154 GdkMonitor *monitor = gdk_display_get_monitor_at_point(pdisplay,
1155 pt.x + x_offset, pt.y + y_offset);
1156 gdk_monitor_get_geometry(monitor, &rect);
1157 #else
1158 GdkScreen *screen = gtk_widget_get_screen(PWidget(wid));
1159 const gint monitor_num = gdk_screen_get_monitor_at_point(screen,
1160 pt.x + x_offset, pt.y + y_offset);
1161 gdk_screen_get_monitor_geometry(screen, monitor_num, &rect);
1162 #endif
1163 rect.x -= x_offset;
1164 rect.y -= y_offset;
1165 return PRectangle::FromInts(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
1166 }
1167
1168 typedef std::map<int, RGBAImage *> ImageMap;
1169
1170 struct ListImage {
1171 const RGBAImage *rgba_data;
1172 GdkPixbuf *pixbuf;
1173 };
1174
list_image_free(gpointer,gpointer value,gpointer)1175 static void list_image_free(gpointer, gpointer value, gpointer) noexcept {
1176 ListImage *list_image = static_cast<ListImage *>(value);
1177 if (list_image->pixbuf)
1178 g_object_unref(list_image->pixbuf);
1179 g_free(list_image);
1180 }
1181
ListBox()1182 ListBox::ListBox() noexcept {
1183 }
1184
~ListBox()1185 ListBox::~ListBox() {
1186 }
1187
1188 enum {
1189 PIXBUF_COLUMN,
1190 TEXT_COLUMN,
1191 N_COLUMNS
1192 };
1193
1194 class ListBoxX : public ListBox {
1195 WindowID widCached;
1196 WindowID frame;
1197 WindowID list;
1198 WindowID scroller;
1199 void *pixhash;
1200 GtkCellRenderer *pixbuf_renderer;
1201 GtkCellRenderer *renderer;
1202 RGBAImageSet images;
1203 int desiredVisibleRows;
1204 unsigned int maxItemCharacters;
1205 unsigned int aveCharWidth;
1206 #if GTK_CHECK_VERSION(3,0,0)
1207 GtkCssProvider *cssProvider;
1208 #endif
1209 public:
1210 IListBoxDelegate *delegate;
1211
ListBoxX()1212 ListBoxX() noexcept : widCached(nullptr), frame(nullptr), list(nullptr), scroller(nullptr),
1213 pixhash(nullptr), pixbuf_renderer(nullptr),
1214 renderer(nullptr),
1215 desiredVisibleRows(5), maxItemCharacters(0),
1216 aveCharWidth(1),
1217 #if GTK_CHECK_VERSION(3,0,0)
1218 cssProvider(nullptr),
1219 #endif
1220 delegate(nullptr) {
1221 }
1222 // Deleted so ListBoxX objects can not be copied.
1223 ListBoxX(const ListBoxX&) = delete;
1224 ListBoxX(ListBoxX&&) = delete;
1225 ListBoxX&operator=(const ListBoxX&) = delete;
1226 ListBoxX&operator=(ListBoxX&&) = delete;
~ListBoxX()1227 ~ListBoxX() override {
1228 if (pixhash) {
1229 g_hash_table_foreach((GHashTable *) pixhash, list_image_free, nullptr);
1230 g_hash_table_destroy((GHashTable *) pixhash);
1231 }
1232 if (widCached) {
1233 gtk_widget_destroy(GTK_WIDGET(widCached));
1234 wid = widCached = nullptr;
1235 }
1236 #if GTK_CHECK_VERSION(3,0,0)
1237 if (cssProvider) {
1238 g_object_unref(cssProvider);
1239 cssProvider = nullptr;
1240 }
1241 #endif
1242 }
1243 void SetFont(Font &font) override;
1244 void Create(Window &parent, int ctrlID, Point location_, int lineHeight_, bool unicodeMode_, int technology_) override;
1245 void SetAverageCharWidth(int width) override;
1246 void SetVisibleRows(int rows) override;
1247 int GetVisibleRows() const override;
1248 int GetRowHeight();
1249 PRectangle GetDesiredRect() override;
1250 int CaretFromEdge() override;
1251 void Clear() override;
1252 void Append(char *s, int type = -1) override;
1253 int Length() override;
1254 void Select(int n) override;
1255 int GetSelection() override;
1256 int Find(const char *prefix) override;
1257 void GetValue(int n, char *value, int len) override;
1258 void RegisterRGBA(int type, RGBAImage *image);
1259 void RegisterImage(int type, const char *xpm_data) override;
1260 void RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) override;
1261 void ClearRegisteredImages() override;
1262 void SetDelegate(IListBoxDelegate *lbDelegate) override;
1263 void SetList(const char *listText, char separator, char typesep) override;
1264 };
1265
Allocate()1266 ListBox *ListBox::Allocate() {
1267 ListBoxX *lb = new ListBoxX();
1268 return lb;
1269 }
1270
treeViewGetRowHeight(GtkTreeView * view)1271 static int treeViewGetRowHeight(GtkTreeView *view) {
1272 #if GTK_CHECK_VERSION(3,0,0)
1273 // This version sometimes reports erroneous results on GTK2, but the GTK2
1274 // version is inaccurate for GTK 3.14.
1275 GdkRectangle rect;
1276 GtkTreePath *path = gtk_tree_path_new_first();
1277 gtk_tree_view_get_background_area(view, path, nullptr, &rect);
1278 gtk_tree_path_free(path);
1279 return rect.height;
1280 #else
1281 int row_height=0;
1282 int vertical_separator=0;
1283 int expander_size=0;
1284 GtkTreeViewColumn *column = gtk_tree_view_get_column(view, 0);
1285 gtk_tree_view_column_cell_get_size(column, nullptr, nullptr, nullptr, nullptr, &row_height);
1286 gtk_widget_style_get(GTK_WIDGET(view),
1287 "vertical-separator", &vertical_separator,
1288 "expander-size", &expander_size, nullptr);
1289 row_height += vertical_separator;
1290 row_height = std::max(row_height, expander_size);
1291 return row_height;
1292 #endif
1293 }
1294
1295 // SmallScroller, a GtkScrolledWindow that can shrink very small, as
1296 // gtk_widget_set_size_request() cannot shrink widgets on GTK3
1297 typedef struct {
1298 GtkScrolledWindow parent;
1299 /* Workaround ABI issue with Windows GTK2 bundle and GCC > 3.
1300 See http://lists.geany.org/pipermail/devel/2015-April/thread.html#9379
1301
1302 GtkScrolledWindow contains a bitfield, and GCC 3.4 and 4.8 don't agree
1303 on the size of the structure (regardless of -mms-bitfields):
1304 - GCC 3.4 has sizeof(GtkScrolledWindow)=88
1305 - GCC 4.8 has sizeof(GtkScrolledWindow)=84
1306 As Windows GTK2 bundle is built with GCC 3, it requires types derived
1307 from GtkScrolledWindow to be at least 88 bytes, which means we need to
1308 add some fake padding to fill in the extra 4 bytes.
1309 There is however no other issue with the layout difference as we never
1310 access any GtkScrolledWindow fields ourselves. */
1311 int padding;
1312 } SmallScroller;
1313 typedef GtkScrolledWindowClass SmallScrollerClass;
1314
G_DEFINE_TYPE(SmallScroller,small_scroller,GTK_TYPE_SCROLLED_WINDOW)1315 G_DEFINE_TYPE(SmallScroller, small_scroller, GTK_TYPE_SCROLLED_WINDOW)
1316
1317 #if GTK_CHECK_VERSION(3,0,0)
1318 static void small_scroller_get_preferred_height(GtkWidget *widget, gint *min, gint *nat) {
1319 GtkWidget *child = gtk_bin_get_child(GTK_BIN(widget));
1320 if (GTK_IS_TREE_VIEW(child)) {
1321 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(child));
1322 int n_rows = gtk_tree_model_iter_n_children(model, nullptr);
1323 int row_height = treeViewGetRowHeight(GTK_TREE_VIEW(child));
1324
1325 *min = MAX(1, row_height);
1326 *nat = MAX(*min, n_rows * row_height);
1327 } else {
1328 GTK_WIDGET_CLASS(small_scroller_parent_class)->get_preferred_height(widget, min, nat);
1329 if (*min > 1)
1330 *min = 1;
1331 }
1332 }
1333 #else
1334 static void small_scroller_size_request(GtkWidget *widget, GtkRequisition *req) {
1335 GTK_WIDGET_CLASS(small_scroller_parent_class)->size_request(widget, req);
1336 req->height = 1;
1337 }
1338 #endif
1339
small_scroller_class_init(SmallScrollerClass * klass)1340 static void small_scroller_class_init(SmallScrollerClass *klass) {
1341 #if GTK_CHECK_VERSION(3,0,0)
1342 GTK_WIDGET_CLASS(klass)->get_preferred_height = small_scroller_get_preferred_height;
1343 #else
1344 GTK_WIDGET_CLASS(klass)->size_request = small_scroller_size_request;
1345 #endif
1346 }
1347
small_scroller_init(SmallScroller *)1348 static void small_scroller_init(SmallScroller *) {}
1349
ButtonPress(GtkWidget *,GdkEventButton * ev,gpointer p)1350 static gboolean ButtonPress(GtkWidget *, GdkEventButton *ev, gpointer p) {
1351 try {
1352 ListBoxX *lb = static_cast<ListBoxX *>(p);
1353 if (ev->type == GDK_2BUTTON_PRESS && lb->delegate) {
1354 ListBoxEvent event(ListBoxEvent::EventType::doubleClick);
1355 lb->delegate->ListNotify(&event);
1356 return TRUE;
1357 }
1358
1359 } catch (...) {
1360 // No pointer back to Scintilla to save status
1361 }
1362 return FALSE;
1363 }
1364
ButtonRelease(GtkWidget *,GdkEventButton * ev,gpointer p)1365 static gboolean ButtonRelease(GtkWidget *, GdkEventButton *ev, gpointer p) {
1366 try {
1367 ListBoxX *lb = static_cast<ListBoxX *>(p);
1368 if (ev->type != GDK_2BUTTON_PRESS && lb->delegate) {
1369 ListBoxEvent event(ListBoxEvent::EventType::selectionChange);
1370 lb->delegate->ListNotify(&event);
1371 return TRUE;
1372 }
1373 } catch (...) {
1374 // No pointer back to Scintilla to save status
1375 }
1376 return FALSE;
1377 }
1378
1379 /* Change the active colour to the selected colour so the listbox uses the colour
1380 scheme that it would use if it had the focus. */
StyleSet(GtkWidget * w,GtkStyle *,void *)1381 static void StyleSet(GtkWidget *w, GtkStyle *, void *) {
1382
1383 g_return_if_fail(w != nullptr);
1384
1385 /* Copy the selected colour to active. Note that the modify calls will cause
1386 recursive calls to this function after the value is updated and w->style to
1387 be set to a new object */
1388
1389 #if GTK_CHECK_VERSION(3,16,0)
1390 // On recent releases of GTK+, it does not appear necessary to set the list box colours.
1391 // This may be because of common themes and may be needed with other themes.
1392 // The *override* calls are deprecated now, so only call them for older versions of GTK+.
1393 #elif GTK_CHECK_VERSION(3,0,0)
1394 GtkStyleContext *styleContext = gtk_widget_get_style_context(w);
1395 if (styleContext == nullptr)
1396 return;
1397
1398 GdkRGBA colourForeSelected;
1399 gtk_style_context_get_color(styleContext, GTK_STATE_FLAG_SELECTED, &colourForeSelected);
1400 GdkRGBA colourForeActive;
1401 gtk_style_context_get_color(styleContext, GTK_STATE_FLAG_ACTIVE, &colourForeActive);
1402 if (!gdk_rgba_equal(&colourForeSelected, &colourForeActive))
1403 gtk_widget_override_color(w, GTK_STATE_FLAG_ACTIVE, &colourForeSelected);
1404
1405 styleContext = gtk_widget_get_style_context(w);
1406 if (styleContext == nullptr)
1407 return;
1408
1409 GdkRGBA colourBaseSelected;
1410 gtk_style_context_get_background_color(styleContext, GTK_STATE_FLAG_SELECTED, &colourBaseSelected);
1411 GdkRGBA colourBaseActive;
1412 gtk_style_context_get_background_color(styleContext, GTK_STATE_FLAG_ACTIVE, &colourBaseActive);
1413 if (!gdk_rgba_equal(&colourBaseSelected, &colourBaseActive))
1414 gtk_widget_override_background_color(w, GTK_STATE_FLAG_ACTIVE, &colourBaseSelected);
1415 #else
1416 GtkStyle *style = gtk_widget_get_style(w);
1417 if (style == nullptr)
1418 return;
1419 if (!gdk_color_equal(&style->base[GTK_STATE_SELECTED], &style->base[GTK_STATE_ACTIVE]))
1420 gtk_widget_modify_base(w, GTK_STATE_ACTIVE, &style->base[GTK_STATE_SELECTED]);
1421 style = gtk_widget_get_style(w);
1422 if (style == nullptr)
1423 return;
1424 if (!gdk_color_equal(&style->text[GTK_STATE_SELECTED], &style->text[GTK_STATE_ACTIVE]))
1425 gtk_widget_modify_text(w, GTK_STATE_ACTIVE, &style->text[GTK_STATE_SELECTED]);
1426 #endif
1427 }
1428
Create(Window & parent,int,Point,int,bool,int)1429 void ListBoxX::Create(Window &parent, int, Point, int, bool, int) {
1430 if (widCached != nullptr) {
1431 wid = widCached;
1432 return;
1433 }
1434
1435 #if GTK_CHECK_VERSION(3,0,0)
1436 if (!cssProvider) {
1437 cssProvider = gtk_css_provider_new();
1438 }
1439 #endif
1440
1441 wid = widCached = gtk_window_new(GTK_WINDOW_POPUP);
1442
1443 frame = gtk_frame_new(nullptr);
1444 gtk_widget_show(PWidget(frame));
1445 gtk_container_add(GTK_CONTAINER(GetID()), PWidget(frame));
1446 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_OUT);
1447 gtk_container_set_border_width(GTK_CONTAINER(frame), 0);
1448
1449 scroller = g_object_new(small_scroller_get_type(), nullptr);
1450 gtk_container_set_border_width(GTK_CONTAINER(scroller), 0);
1451 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller),
1452 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
1453 gtk_container_add(GTK_CONTAINER(frame), PWidget(scroller));
1454 gtk_widget_show(PWidget(scroller));
1455
1456 /* Tree and its model */
1457 GtkListStore *store =
1458 gtk_list_store_new(N_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING);
1459
1460 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
1461 g_signal_connect(G_OBJECT(list), "style-set", G_CALLBACK(StyleSet), nullptr);
1462
1463 #if GTK_CHECK_VERSION(3,0,0)
1464 GtkStyleContext *styleContext = gtk_widget_get_style_context(GTK_WIDGET(list));
1465 if (styleContext) {
1466 gtk_style_context_add_provider(styleContext, GTK_STYLE_PROVIDER(cssProvider),
1467 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
1468 }
1469 #endif
1470
1471 GtkTreeSelection *selection =
1472 gtk_tree_view_get_selection(GTK_TREE_VIEW(list));
1473 gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
1474 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
1475 gtk_tree_view_set_reorderable(GTK_TREE_VIEW(list), FALSE);
1476
1477 /* Columns */
1478 GtkTreeViewColumn *column = gtk_tree_view_column_new();
1479 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
1480 gtk_tree_view_column_set_title(column, "Autocomplete");
1481
1482 pixbuf_renderer = gtk_cell_renderer_pixbuf_new();
1483 gtk_cell_renderer_set_fixed_size(pixbuf_renderer, 0, -1);
1484 gtk_tree_view_column_pack_start(column, pixbuf_renderer, FALSE);
1485 gtk_tree_view_column_add_attribute(column, pixbuf_renderer,
1486 "pixbuf", PIXBUF_COLUMN);
1487
1488 renderer = gtk_cell_renderer_text_new();
1489 gtk_cell_renderer_text_set_fixed_height_from_font(GTK_CELL_RENDERER_TEXT(renderer), 1);
1490 gtk_tree_view_column_pack_start(column, renderer, TRUE);
1491 gtk_tree_view_column_add_attribute(column, renderer,
1492 "text", TEXT_COLUMN);
1493
1494 gtk_tree_view_append_column(GTK_TREE_VIEW(list), column);
1495 if (g_object_class_find_property(G_OBJECT_GET_CLASS(list), "fixed-height-mode"))
1496 g_object_set(G_OBJECT(list), "fixed-height-mode", TRUE, nullptr);
1497
1498 GtkWidget *widget = PWidget(list); // No code inside the G_OBJECT macro
1499 gtk_container_add(GTK_CONTAINER(PWidget(scroller)), widget);
1500 gtk_widget_show(widget);
1501 g_signal_connect(G_OBJECT(widget), "button_press_event",
1502 G_CALLBACK(ButtonPress), this);
1503 g_signal_connect(G_OBJECT(widget), "button_release_event",
1504 G_CALLBACK(ButtonRelease), this);
1505
1506 GtkWidget *top = gtk_widget_get_toplevel(static_cast<GtkWidget *>(parent.GetID()));
1507 gtk_window_set_transient_for(GTK_WINDOW(static_cast<GtkWidget *>(wid)),
1508 GTK_WINDOW(top));
1509 }
1510
SetFont(Font & font)1511 void ListBoxX::SetFont(Font &font) {
1512 // Only do for Pango font as there have been crashes for GDK fonts
1513 if (Created() && PFont(font)->pfd) {
1514 // Current font is Pango font
1515 #if GTK_CHECK_VERSION(3,0,0)
1516 if (cssProvider) {
1517 PangoFontDescription *pfd = PFont(font)->pfd;
1518 std::ostringstream ssFontSetting;
1519 ssFontSetting << "GtkTreeView, treeview { ";
1520 ssFontSetting << "font-family: " << pango_font_description_get_family(pfd) << "; ";
1521 ssFontSetting << "font-size:";
1522 ssFontSetting << static_cast<double>(pango_font_description_get_size(pfd)) / PANGO_SCALE;
1523 // On GTK < 3.21.0 the units are incorrectly parsed, so a font size in points
1524 // need to use the "px" unit. Normally we only get fonts in points here, so
1525 // don't bother to handle the case the font is actually in pixels on < 3.21.0.
1526 if (gtk_check_version(3, 21, 0) != nullptr || // on < 3.21.0
1527 pango_font_description_get_size_is_absolute(pfd)) {
1528 ssFontSetting << "px; ";
1529 } else {
1530 ssFontSetting << "pt; ";
1531 }
1532 ssFontSetting << "font-weight:"<< pango_font_description_get_weight(pfd) << "; ";
1533 ssFontSetting << "}";
1534 gtk_css_provider_load_from_data(GTK_CSS_PROVIDER(cssProvider),
1535 ssFontSetting.str().c_str(), -1, nullptr);
1536 }
1537 #else
1538 gtk_widget_modify_font(PWidget(list), PFont(font)->pfd);
1539 #endif
1540 gtk_cell_renderer_text_set_fixed_height_from_font(GTK_CELL_RENDERER_TEXT(renderer), -1);
1541 gtk_cell_renderer_text_set_fixed_height_from_font(GTK_CELL_RENDERER_TEXT(renderer), 1);
1542 }
1543 }
1544
SetAverageCharWidth(int width)1545 void ListBoxX::SetAverageCharWidth(int width) {
1546 aveCharWidth = width;
1547 }
1548
SetVisibleRows(int rows)1549 void ListBoxX::SetVisibleRows(int rows) {
1550 desiredVisibleRows = rows;
1551 }
1552
GetVisibleRows() const1553 int ListBoxX::GetVisibleRows() const {
1554 return desiredVisibleRows;
1555 }
1556
GetRowHeight()1557 int ListBoxX::GetRowHeight() {
1558 return treeViewGetRowHeight(GTK_TREE_VIEW(list));
1559 }
1560
GetDesiredRect()1561 PRectangle ListBoxX::GetDesiredRect() {
1562 // Before any size allocated pretend its 100 wide so not scrolled
1563 PRectangle rc(0, 0, 100, 100);
1564 if (wid) {
1565 int rows = Length();
1566 if ((rows == 0) || (rows > desiredVisibleRows))
1567 rows = desiredVisibleRows;
1568
1569 GtkRequisition req;
1570 // This, apparently unnecessary call, ensures gtk_tree_view_column_cell_get_size
1571 // returns reasonable values.
1572 #if GTK_CHECK_VERSION(3,0,0)
1573 gtk_widget_get_preferred_size(GTK_WIDGET(frame), nullptr, &req);
1574 #else
1575 gtk_widget_size_request(GTK_WIDGET(frame), &req);
1576 #endif
1577 int height;
1578
1579 // First calculate height of the clist for our desired visible
1580 // row count otherwise it tries to expand to the total # of rows
1581 // Get cell height
1582 const int row_height = GetRowHeight();
1583 #if GTK_CHECK_VERSION(3,0,0)
1584 GtkStyleContext *styleContextFrame = gtk_widget_get_style_context(PWidget(frame));
1585 GtkStateFlags stateFlagsFrame = gtk_style_context_get_state(styleContextFrame);
1586 GtkBorder padding, border, border_border = { 0, 0, 0, 0 };
1587 gtk_style_context_get_padding(styleContextFrame, stateFlagsFrame, &padding);
1588 gtk_style_context_get_border(styleContextFrame, stateFlagsFrame, &border);
1589
1590 # if GTK_CHECK_VERSION(3,20,0)
1591 // on GTK 3.20 the frame border is in a sub-node "border".
1592 // Unfortunately we need to be built against 3.20 to be able to support this, as it requires
1593 // new API.
1594 GtkStyleContext *styleContextFrameBorder = gtk_style_context_new();
1595 GtkWidgetPath *widget_path = gtk_widget_path_copy(gtk_style_context_get_path(styleContextFrame));
1596 gtk_widget_path_append_type(widget_path, GTK_TYPE_BORDER); // dummy type
1597 gtk_widget_path_iter_set_object_name(widget_path, -1, "border");
1598 gtk_style_context_set_path(styleContextFrameBorder, widget_path);
1599 gtk_widget_path_free(widget_path);
1600 gtk_style_context_get_border(styleContextFrameBorder, stateFlagsFrame, &border_border);
1601 g_object_unref(styleContextFrameBorder);
1602 # else // < 3.20
1603 if (gtk_check_version(3, 20, 0) == nullptr) {
1604 // default to 1px all around as it's likely what it is, and so we don't miss 2px height
1605 // on GTK 3.20 when built against an earlier version.
1606 border_border.top = border_border.bottom = border_border.left = border_border.right = 1;
1607 }
1608 # endif
1609
1610 height = (rows * row_height
1611 + padding.top + padding.bottom
1612 + border.top + border.bottom
1613 + border_border.top + border_border.bottom
1614 + 2 * gtk_container_get_border_width(GTK_CONTAINER(PWidget(list))));
1615 #else
1616 height = (rows * row_height
1617 + 2 * (PWidget(frame)->style->ythickness
1618 + GTK_CONTAINER(PWidget(list))->border_width));
1619 #endif
1620 rc.bottom = height;
1621
1622 int width = maxItemCharacters;
1623 if (width < 12)
1624 width = 12;
1625 rc.right = width * (aveCharWidth + aveCharWidth / 3);
1626 // Add horizontal padding and borders
1627 int horizontal_separator=0;
1628 gtk_widget_style_get(PWidget(list),
1629 "horizontal-separator", &horizontal_separator, nullptr);
1630 rc.right += horizontal_separator;
1631 #if GTK_CHECK_VERSION(3,0,0)
1632 rc.right += (padding.left + padding.right
1633 + border.left + border.right
1634 + border_border.left + border_border.right
1635 + 2 * gtk_container_get_border_width(GTK_CONTAINER(PWidget(list))));
1636 #else
1637 rc.right += 2 * (PWidget(frame)->style->xthickness
1638 + GTK_CONTAINER(PWidget(list))->border_width);
1639 #endif
1640 if (Length() > rows) {
1641 // Add the width of the scrollbar
1642 GtkWidget *vscrollbar =
1643 gtk_scrolled_window_get_vscrollbar(GTK_SCROLLED_WINDOW(scroller));
1644 #if GTK_CHECK_VERSION(3,0,0)
1645 gtk_widget_get_preferred_size(vscrollbar, nullptr, &req);
1646 #else
1647 gtk_widget_size_request(vscrollbar, &req);
1648 #endif
1649 rc.right += req.width;
1650 }
1651 }
1652 return rc;
1653 }
1654
CaretFromEdge()1655 int ListBoxX::CaretFromEdge() {
1656 gint renderer_width, renderer_height;
1657 gtk_cell_renderer_get_fixed_size(pixbuf_renderer, &renderer_width,
1658 &renderer_height);
1659 return 4 + renderer_width;
1660 }
1661
Clear()1662 void ListBoxX::Clear() {
1663 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1664 gtk_list_store_clear(GTK_LIST_STORE(model));
1665 maxItemCharacters = 0;
1666 }
1667
init_pixmap(ListImage * list_image)1668 static void init_pixmap(ListImage *list_image) {
1669 if (list_image->rgba_data) {
1670 // Drop any existing pixmap/bitmap as data may have changed
1671 if (list_image->pixbuf)
1672 g_object_unref(list_image->pixbuf);
1673 list_image->pixbuf =
1674 gdk_pixbuf_new_from_data(list_image->rgba_data->Pixels(),
1675 GDK_COLORSPACE_RGB,
1676 TRUE,
1677 8,
1678 list_image->rgba_data->GetWidth(),
1679 list_image->rgba_data->GetHeight(),
1680 list_image->rgba_data->GetWidth() * 4,
1681 nullptr,
1682 nullptr);
1683 }
1684 }
1685
1686 #define SPACING 5
1687
Append(char * s,int type)1688 void ListBoxX::Append(char *s, int type) {
1689 ListImage *list_image = nullptr;
1690 if ((type >= 0) && pixhash) {
1691 list_image = static_cast<ListImage *>(g_hash_table_lookup((GHashTable *) pixhash,
1692 GINT_TO_POINTER(type)));
1693 }
1694 GtkTreeIter iter {};
1695 GtkListStore *store =
1696 GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(list)));
1697 gtk_list_store_append(GTK_LIST_STORE(store), &iter);
1698 if (list_image) {
1699 if (nullptr == list_image->pixbuf)
1700 init_pixmap(list_image);
1701 if (list_image->pixbuf) {
1702 gtk_list_store_set(GTK_LIST_STORE(store), &iter,
1703 PIXBUF_COLUMN, list_image->pixbuf,
1704 TEXT_COLUMN, s, -1);
1705
1706 const gint pixbuf_width = gdk_pixbuf_get_width(list_image->pixbuf);
1707 gint renderer_height, renderer_width;
1708 gtk_cell_renderer_get_fixed_size(pixbuf_renderer,
1709 &renderer_width, &renderer_height);
1710 if (pixbuf_width > renderer_width)
1711 gtk_cell_renderer_set_fixed_size(pixbuf_renderer,
1712 pixbuf_width, -1);
1713 } else {
1714 gtk_list_store_set(GTK_LIST_STORE(store), &iter,
1715 TEXT_COLUMN, s, -1);
1716 }
1717 } else {
1718 gtk_list_store_set(GTK_LIST_STORE(store), &iter,
1719 TEXT_COLUMN, s, -1);
1720 }
1721 const size_t len = strlen(s);
1722 if (maxItemCharacters < len)
1723 maxItemCharacters = len;
1724 }
1725
Length()1726 int ListBoxX::Length() {
1727 if (wid)
1728 return gtk_tree_model_iter_n_children(gtk_tree_view_get_model
1729 (GTK_TREE_VIEW(list)), nullptr);
1730 return 0;
1731 }
1732
Select(int n)1733 void ListBoxX::Select(int n) {
1734 GtkTreeIter iter {};
1735 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1736 GtkTreeSelection *selection =
1737 gtk_tree_view_get_selection(GTK_TREE_VIEW(list));
1738
1739 if (n < 0) {
1740 gtk_tree_selection_unselect_all(selection);
1741 return;
1742 }
1743
1744 const bool valid = gtk_tree_model_iter_nth_child(model, &iter, nullptr, n) != FALSE;
1745 if (valid) {
1746 gtk_tree_selection_select_iter(selection, &iter);
1747
1748 // Move the scrollbar to show the selection.
1749 const int total = Length();
1750 #if GTK_CHECK_VERSION(3,0,0)
1751 GtkAdjustment *adj =
1752 gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(list));
1753 #else
1754 GtkAdjustment *adj =
1755 gtk_tree_view_get_vadjustment(GTK_TREE_VIEW(list));
1756 #endif
1757 gfloat value = (static_cast<gfloat>(n) / total) * (gtk_adjustment_get_upper(adj) - gtk_adjustment_get_lower(adj))
1758 + gtk_adjustment_get_lower(adj) - gtk_adjustment_get_page_size(adj) / 2;
1759 // Get cell height
1760 const int row_height = GetRowHeight();
1761
1762 int rows = Length();
1763 if ((rows == 0) || (rows > desiredVisibleRows))
1764 rows = desiredVisibleRows;
1765 if (rows & 0x1) {
1766 // Odd rows to display -- We are now in the middle.
1767 // Align it so that we don't chop off rows.
1768 value += static_cast<gfloat>(row_height) / 2.0f;
1769 }
1770 // Clamp it.
1771 value = (value < 0)? 0 : value;
1772 value = (value > (gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj)))?
1773 (gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj)) : value;
1774
1775 // Set it.
1776 gtk_adjustment_set_value(adj, value);
1777 } else {
1778 gtk_tree_selection_unselect_all(selection);
1779 }
1780
1781 if (delegate) {
1782 ListBoxEvent event(ListBoxEvent::EventType::selectionChange);
1783 delegate->ListNotify(&event);
1784 }
1785 }
1786
GetSelection()1787 int ListBoxX::GetSelection() {
1788 int index = -1;
1789 GtkTreeIter iter {};
1790 GtkTreeModel *model {};
1791 GtkTreeSelection *selection;
1792 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(list));
1793 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
1794 GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
1795 const int *indices = gtk_tree_path_get_indices(path);
1796 // Don't free indices.
1797 if (indices)
1798 index = indices[0];
1799 gtk_tree_path_free(path);
1800 }
1801 return index;
1802 }
1803
Find(const char * prefix)1804 int ListBoxX::Find(const char *prefix) {
1805 GtkTreeIter iter {};
1806 GtkTreeModel *model =
1807 gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1808 bool valid = gtk_tree_model_get_iter_first(model, &iter) != FALSE;
1809 int i = 0;
1810 while (valid) {
1811 gchar *s = nullptr;
1812 gtk_tree_model_get(model, &iter, TEXT_COLUMN, &s, -1);
1813 if (s && (0 == strncmp(prefix, s, strlen(prefix)))) {
1814 g_free(s);
1815 return i;
1816 }
1817 g_free(s);
1818 valid = gtk_tree_model_iter_next(model, &iter) != FALSE;
1819 i++;
1820 }
1821 return -1;
1822 }
1823
GetValue(int n,char * value,int len)1824 void ListBoxX::GetValue(int n, char *value, int len) {
1825 char *text = nullptr;
1826 GtkTreeIter iter {};
1827 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1828 const bool valid = gtk_tree_model_iter_nth_child(model, &iter, nullptr, n) != FALSE;
1829 if (valid) {
1830 gtk_tree_model_get(model, &iter, TEXT_COLUMN, &text, -1);
1831 }
1832 if (text && len > 0) {
1833 g_strlcpy(value, text, len);
1834 } else {
1835 value[0] = '\0';
1836 }
1837 g_free(text);
1838 }
1839
1840 // g_return_if_fail causes unnecessary compiler warning in release compile.
1841 #ifdef _MSC_VER
1842 #pragma warning(disable: 4127)
1843 #endif
1844
RegisterRGBA(int type,RGBAImage * image)1845 void ListBoxX::RegisterRGBA(int type, RGBAImage *image) {
1846 images.Add(type, image);
1847
1848 if (!pixhash) {
1849 pixhash = g_hash_table_new(g_direct_hash, g_direct_equal);
1850 }
1851 ListImage *list_image = static_cast<ListImage *>(g_hash_table_lookup((GHashTable *) pixhash,
1852 GINT_TO_POINTER(type)));
1853 if (list_image) {
1854 // Drop icon already registered
1855 if (list_image->pixbuf)
1856 g_object_unref(list_image->pixbuf);
1857 list_image->pixbuf = nullptr;
1858 list_image->rgba_data = image;
1859 } else {
1860 list_image = g_new0(ListImage, 1);
1861 list_image->rgba_data = image;
1862 g_hash_table_insert((GHashTable *) pixhash, GINT_TO_POINTER(type),
1863 (gpointer) list_image);
1864 }
1865 }
1866
RegisterImage(int type,const char * xpm_data)1867 void ListBoxX::RegisterImage(int type, const char *xpm_data) {
1868 g_return_if_fail(xpm_data);
1869 XPM xpmImage(xpm_data);
1870 RegisterRGBA(type, new RGBAImage(xpmImage));
1871 }
1872
RegisterRGBAImage(int type,int width,int height,const unsigned char * pixelsImage)1873 void ListBoxX::RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) {
1874 RegisterRGBA(type, new RGBAImage(width, height, 1.0, pixelsImage));
1875 }
1876
ClearRegisteredImages()1877 void ListBoxX::ClearRegisteredImages() {
1878 images.Clear();
1879 }
1880
SetDelegate(IListBoxDelegate * lbDelegate)1881 void ListBoxX::SetDelegate(IListBoxDelegate *lbDelegate) {
1882 delegate = lbDelegate;
1883 }
1884
SetList(const char * listText,char separator,char typesep)1885 void ListBoxX::SetList(const char *listText, char separator, char typesep) {
1886 Clear();
1887 const size_t count = strlen(listText) + 1;
1888 std::vector<char> words(listText, listText+count);
1889 char *startword = &words[0];
1890 char *numword = nullptr;
1891 int i = 0;
1892 for (; words[i]; i++) {
1893 if (words[i] == separator) {
1894 words[i] = '\0';
1895 if (numword)
1896 *numword = '\0';
1897 Append(startword, numword?atoi(numword + 1):-1);
1898 startword = &words[0] + i + 1;
1899 numword = nullptr;
1900 } else if (words[i] == typesep) {
1901 numword = &words[0] + i;
1902 }
1903 }
1904 if (startword) {
1905 if (numword)
1906 *numword = '\0';
1907 Append(startword, numword?atoi(numword + 1):-1);
1908 }
1909 }
1910
Menu()1911 Menu::Menu() noexcept : mid(nullptr) {}
1912
CreatePopUp()1913 void Menu::CreatePopUp() {
1914 Destroy();
1915 mid = gtk_menu_new();
1916 g_object_ref_sink(G_OBJECT(mid));
1917 }
1918
Destroy()1919 void Menu::Destroy() {
1920 if (mid)
1921 g_object_unref(G_OBJECT(mid));
1922 mid = nullptr;
1923 }
1924
1925 #if !GTK_CHECK_VERSION(3,22,0)
MenuPositionFunc(GtkMenu *,gint * x,gint * y,gboolean *,gpointer userData)1926 static void MenuPositionFunc(GtkMenu *, gint *x, gint *y, gboolean *, gpointer userData) noexcept {
1927 sptr_t intFromPointer = GPOINTER_TO_INT(userData);
1928 *x = intFromPointer & 0xffff;
1929 *y = intFromPointer >> 16;
1930 }
1931 #endif
1932
Show(Point pt,Window & w)1933 void Menu::Show(Point pt, Window &w) {
1934 GtkMenu *widget = static_cast<GtkMenu *>(mid);
1935 gtk_widget_show_all(GTK_WIDGET(widget));
1936 #if GTK_CHECK_VERSION(3,22,0)
1937 // Rely on GTK+ to do the right thing with positioning
1938 gtk_menu_popup_at_pointer(widget, nullptr);
1939 #else
1940 const GdkRectangle rcMonitor = MonitorRectangleForWidget(PWidget(w.GetID()));
1941 GtkRequisition requisition;
1942 #if GTK_CHECK_VERSION(3,0,0)
1943 gtk_widget_get_preferred_size(GTK_WIDGET(widget), nullptr, &requisition);
1944 #else
1945 gtk_widget_size_request(GTK_WIDGET(widget), &requisition);
1946 #endif
1947 if ((pt.x + requisition.width) > rcMonitor.x + rcMonitor.width) {
1948 pt.x = rcMonitor.x + rcMonitor.width - requisition.width;
1949 }
1950 if ((pt.y + requisition.height) > rcMonitor.y + rcMonitor.height) {
1951 pt.y = rcMonitor.y + rcMonitor.height - requisition.height;
1952 }
1953 gtk_menu_popup(widget, nullptr, nullptr, MenuPositionFunc,
1954 GINT_TO_POINTER((static_cast<int>(pt.y) << 16) | static_cast<int>(pt.x)), 0,
1955 gtk_get_current_event_time());
1956 #endif
1957 }
1958
1959 class DynamicLibraryImpl : public DynamicLibrary {
1960 protected:
1961 GModule *m;
1962 public:
DynamicLibraryImpl(const char * modulePath)1963 explicit DynamicLibraryImpl(const char *modulePath) noexcept {
1964 m = g_module_open(modulePath, G_MODULE_BIND_LAZY);
1965 }
1966 // Deleted so DynamicLibraryImpl objects can not be copied.
1967 DynamicLibraryImpl(const DynamicLibraryImpl&) = delete;
1968 DynamicLibraryImpl(DynamicLibraryImpl&&) = delete;
1969 DynamicLibraryImpl&operator=(const DynamicLibraryImpl&) = delete;
1970 DynamicLibraryImpl&operator=(DynamicLibraryImpl&&) = delete;
~DynamicLibraryImpl()1971 ~DynamicLibraryImpl() override {
1972 if (m != nullptr)
1973 g_module_close(m);
1974 }
1975
1976 // Use g_module_symbol to get a pointer to the relevant function.
FindFunction(const char * name)1977 Function FindFunction(const char *name) override {
1978 if (m != nullptr) {
1979 gpointer fn_address = nullptr;
1980 const gboolean status = g_module_symbol(m, name, &fn_address);
1981 if (status)
1982 return static_cast<Function>(fn_address);
1983 else
1984 return nullptr;
1985 } else {
1986 return nullptr;
1987 }
1988 }
1989
IsValid()1990 bool IsValid() override {
1991 return m != nullptr;
1992 }
1993 };
1994
Load(const char * modulePath)1995 DynamicLibrary *DynamicLibrary::Load(const char *modulePath) {
1996 return static_cast<DynamicLibrary *>(new DynamicLibraryImpl(modulePath));
1997 }
1998
Chrome()1999 ColourDesired Platform::Chrome() {
2000 return ColourDesired(0xe0, 0xe0, 0xe0);
2001 }
2002
ChromeHighlight()2003 ColourDesired Platform::ChromeHighlight() {
2004 return ColourDesired(0xff, 0xff, 0xff);
2005 }
2006
DefaultFont()2007 const char *Platform::DefaultFont() {
2008 #ifdef G_OS_WIN32
2009 return "Lucida Console";
2010 #else
2011 return "!Sans";
2012 #endif
2013 }
2014
DefaultFontSize()2015 int Platform::DefaultFontSize() {
2016 #ifdef G_OS_WIN32
2017 return 10;
2018 #else
2019 return 12;
2020 #endif
2021 }
2022
DoubleClickTime()2023 unsigned int Platform::DoubleClickTime() {
2024 return 500; // Half a second
2025 }
2026
DebugDisplay(const char * s)2027 void Platform::DebugDisplay(const char *s) {
2028 fprintf(stderr, "%s", s);
2029 }
2030
2031 //#define TRACE
2032
2033 #ifdef TRACE
DebugPrintf(const char * format,...)2034 void Platform::DebugPrintf(const char *format, ...) {
2035 char buffer[2000];
2036 va_list pArguments;
2037 va_start(pArguments, format);
2038 vsprintf(buffer, format, pArguments);
2039 va_end(pArguments);
2040 Platform::DebugDisplay(buffer);
2041 }
2042 #else
DebugPrintf(const char *,...)2043 void Platform::DebugPrintf(const char *, ...) {}
2044
2045 #endif
2046
2047 // Not supported for GTK+
2048 static bool assertionPopUps = true;
2049
ShowAssertionPopUps(bool assertionPopUps_)2050 bool Platform::ShowAssertionPopUps(bool assertionPopUps_) {
2051 const bool ret = assertionPopUps;
2052 assertionPopUps = assertionPopUps_;
2053 return ret;
2054 }
2055
Assert(const char * c,const char * file,int line)2056 void Platform::Assert(const char *c, const char *file, int line) {
2057 char buffer[2000];
2058 g_snprintf(buffer, sizeof(buffer), "Assertion [%s] failed at %s %d\r\n", c, file, line);
2059 Platform::DebugDisplay(buffer);
2060 abort();
2061 }
2062
Platform_Initialise()2063 void Platform_Initialise() {
2064 }
2065
Platform_Finalise()2066 void Platform_Finalise() {
2067 }
2068