Commits:
-
fcb31167
by Charlie Jiang
at 2022-09-05T12:11:13+08:00
[ftinspect] Add "Continuous View".
Most new features in the continuous view are included in the commit, except
the mouse left (details pane)/right (go to singular) click behaviour.
* src/ftinspect/panels/continuous.cpp, src/ftinspect/panels/continuous.hpp:
New files, the main continuous tab.
* src/ftinspect/glyphcomponents/glyphcontinuous.cpp,
src/ftinspect/glyphcomponents/glyphcontinuous.hpp:
New files, adding the `GlyphContinuous` as the actual canvas for
continuous rendering.
* src/ftinspect/engine/stringrenderer.cpp,
src/ftinspect/engine/stringrenderer.hpp:
New files, adding `StringRenderer` to layout the strings and produce
glyphs for the canvas to draw.
* src/ftinspect/widgets/charmapcombobox.cpp,
src/ftinspect/widgets/charmapcombobox.hpp:
New files, add the `CharMapComboBox` widget.
* src/ftinspect/engine/charmap.cpp,src/ftinspect/engine/charmap.hpp:
New files, adding `CharMapInfo` class.
* src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp:
Add necessary fields and getters for string rendering.
Retrieve charmap when loading fonts.
* src/ftinspect/maingui.cpp, src/ftinspect/maingui.hpp:
Add the continuous view to the main window.
Call `ContinuousTab::highlightGlyph` when switching from singular to
continuous view.
* src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
16 changed files:
Changes:
src/ftinspect/CMakeLists.txt
... |
... |
@@ -26,8 +26,11 @@ add_executable(ftinspect |
26
|
26
|
"engine/paletteinfo.cpp"
|
27
|
27
|
"engine/mmgx.cpp"
|
28
|
28
|
"engine/fontinfo.cpp"
|
|
29
|
+ "engine/stringrenderer.cpp"
|
|
30
|
+ "engine/charmap.cpp"
|
29
|
31
|
|
30
|
32
|
"glyphcomponents/glyphbitmap.cpp"
|
|
33
|
+ "glyphcomponents/glyphcontinuous.cpp"
|
31
|
34
|
"glyphcomponents/glyphoutline.cpp"
|
32
|
35
|
"glyphcomponents/glyphpointnumbers.cpp"
|
33
|
36
|
"glyphcomponents/glyphpoints.cpp"
|
... |
... |
@@ -38,12 +41,14 @@ add_executable(ftinspect |
38
|
41
|
"widgets/tripletselector.cpp"
|
39
|
42
|
"widgets/glyphindexselector.cpp"
|
40
|
43
|
"widgets/fontsizeselector.cpp"
|
|
44
|
+ "widgets/charmapcombobox.cpp"
|
41
|
45
|
|
42
|
46
|
"models/customcomboboxmodels.cpp"
|
43
|
47
|
|
44
|
48
|
"panels/settingpanel.cpp"
|
45
|
49
|
"panels/settingpanelmmgx.cpp"
|
46
|
50
|
"panels/singular.cpp"
|
|
51
|
+ "panels/continuous.cpp"
|
47
|
52
|
)
|
48
|
53
|
target_link_libraries(ftinspect
|
49
|
54
|
Qt5::Core Qt5::Widgets
|
src/ftinspect/engine/charmap.cpp
|
1
|
+// charmap.cpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#include "charmap.hpp"
|
|
6
|
+
|
|
7
|
+#include <QHash>
|
|
8
|
+#include <freetype/freetype.h>
|
|
9
|
+#include <freetype/tttables.h>
|
|
10
|
+
|
|
11
|
+namespace
|
|
12
|
+{
|
|
13
|
+QHash<FT_Encoding, QString>& encodingNames();
|
|
14
|
+}
|
|
15
|
+
|
|
16
|
+CharMapInfo::CharMapInfo(int index, FT_CharMap cmap)
|
|
17
|
+: index(index), ptr(cmap),
|
|
18
|
+ encoding(cmap->encoding),
|
|
19
|
+ platformID(cmap->platform_id),
|
|
20
|
+ encodingID(cmap->encoding_id),
|
|
21
|
+ formatID(FT_Get_CMap_Format(cmap)),
|
|
22
|
+ languageID(FT_Get_CMap_Language_ID(cmap)),
|
|
23
|
+ maxIndex(-1)
|
|
24
|
+{
|
|
25
|
+ auto& names = encodingNames();
|
|
26
|
+ auto it = names.find(encoding);
|
|
27
|
+ if (it == names.end())
|
|
28
|
+ encodingName = &names[static_cast<FT_Encoding>(FT_ENCODING_OTHER)];
|
|
29
|
+ else
|
|
30
|
+ encodingName = &it.value();
|
|
31
|
+
|
|
32
|
+ if (encoding != FT_ENCODING_OTHER)
|
|
33
|
+ maxIndex = computeMaxIndex();
|
|
34
|
+}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+QString
|
|
38
|
+CharMapInfo::stringifyIndex(int code, int idx)
|
|
39
|
+{
|
|
40
|
+ return QString("CharCode: %1 (glyph idx %2)")
|
|
41
|
+ .arg(stringifyIndexShort(code))
|
|
42
|
+ .arg(idx);
|
|
43
|
+}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+QString
|
|
47
|
+CharMapInfo::stringifyIndexShort(int code)
|
|
48
|
+{
|
|
49
|
+ return (encoding == FT_ENCODING_UNICODE ? "U+" : "0x")
|
|
50
|
+ + QString::number(code, 16).rightJustified(4, '0').toUpper();
|
|
51
|
+}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+int
|
|
55
|
+CharMapInfo::computeMaxIndex()
|
|
56
|
+{
|
|
57
|
+ int result;
|
|
58
|
+ switch (encoding)
|
|
59
|
+ {
|
|
60
|
+ case FT_ENCODING_UNICODE:
|
|
61
|
+ result = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
|
|
62
|
+ break;
|
|
63
|
+
|
|
64
|
+ case FT_ENCODING_ADOBE_LATIN_1:
|
|
65
|
+ case FT_ENCODING_ADOBE_STANDARD:
|
|
66
|
+ case FT_ENCODING_ADOBE_EXPERT:
|
|
67
|
+ case FT_ENCODING_ADOBE_CUSTOM:
|
|
68
|
+ case FT_ENCODING_APPLE_ROMAN:
|
|
69
|
+ result = 0x100;
|
|
70
|
+ break;
|
|
71
|
+
|
|
72
|
+ /* some fonts use range 0x00-0x100, others have 0xF000-0xF0FF */
|
|
73
|
+ case FT_ENCODING_MS_SYMBOL:
|
|
74
|
+ result = maxIndexForFaceAndCharMap(ptr, 0x10000) + 1;
|
|
75
|
+ break;
|
|
76
|
+
|
|
77
|
+ default:
|
|
78
|
+ // Some encodings can reach > 0x10000, e.g. GB 18030.
|
|
79
|
+ result = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
|
|
80
|
+ }
|
|
81
|
+ return result;
|
|
82
|
+}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+int
|
|
86
|
+CharMapInfo::maxIndexForFaceAndCharMap(FT_CharMap charMap,
|
|
87
|
+ unsigned maxIn)
|
|
88
|
+{
|
|
89
|
+ // code adopted from `ftcommon.c`
|
|
90
|
+ // This never overflows since no format here exceeds INT_MAX...
|
|
91
|
+ FT_ULong min = 0, max = maxIn;
|
|
92
|
+ FT_UInt glyphIndex;
|
|
93
|
+ FT_Face face = charMap->face;
|
|
94
|
+
|
|
95
|
+ if (FT_Set_Charmap(face, charMap))
|
|
96
|
+ return -1;
|
|
97
|
+
|
|
98
|
+ do
|
|
99
|
+ {
|
|
100
|
+ FT_ULong mid = (min + max) >> 1;
|
|
101
|
+ FT_ULong res = FT_Get_Next_Char(face, mid, &glyphIndex);
|
|
102
|
+
|
|
103
|
+ if (glyphIndex)
|
|
104
|
+ min = res;
|
|
105
|
+ else
|
|
106
|
+ {
|
|
107
|
+ max = mid;
|
|
108
|
+
|
|
109
|
+ // once moved, it helps to advance min through sparse regions
|
|
110
|
+ if (min)
|
|
111
|
+ {
|
|
112
|
+ res = FT_Get_Next_Char(face, min, &glyphIndex);
|
|
113
|
+
|
|
114
|
+ if (glyphIndex)
|
|
115
|
+ min = res;
|
|
116
|
+ else
|
|
117
|
+ max = min; // found it
|
|
118
|
+ }
|
|
119
|
+ }
|
|
120
|
+ } while (max > min);
|
|
121
|
+
|
|
122
|
+ return static_cast<int>(max);
|
|
123
|
+}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+namespace
|
|
127
|
+{
|
|
128
|
+// Mapping for `FT_Encoding` is placed here since it's only for the charmap.
|
|
129
|
+QHash<FT_Encoding, QString> encodingNamesCache;
|
|
130
|
+QHash<FT_Encoding, QString>&
|
|
131
|
+encodingNames()
|
|
132
|
+{
|
|
133
|
+ if (encodingNamesCache.empty())
|
|
134
|
+ {
|
|
135
|
+ encodingNamesCache[static_cast<FT_Encoding>(FT_ENCODING_OTHER)]
|
|
136
|
+ = "Unknown Encoding";
|
|
137
|
+ encodingNamesCache[FT_ENCODING_NONE] = "No Encoding";
|
|
138
|
+ encodingNamesCache[FT_ENCODING_MS_SYMBOL] = "MS Symbol (symb)";
|
|
139
|
+ encodingNamesCache[FT_ENCODING_UNICODE] = "Unicode (unic)";
|
|
140
|
+ encodingNamesCache[FT_ENCODING_SJIS] = "Shift JIS (sjis)";
|
|
141
|
+ encodingNamesCache[FT_ENCODING_PRC] = "PRC/GB 18030 (gb)";
|
|
142
|
+ encodingNamesCache[FT_ENCODING_BIG5] = "Big5 (big5)";
|
|
143
|
+ encodingNamesCache[FT_ENCODING_WANSUNG] = "Wansung (wans)";
|
|
144
|
+ encodingNamesCache[FT_ENCODING_JOHAB] = "Johab (joha)";
|
|
145
|
+ encodingNamesCache[FT_ENCODING_ADOBE_STANDARD] = "Adobe Standard (ADOB)";
|
|
146
|
+ encodingNamesCache[FT_ENCODING_ADOBE_EXPERT] = "Adobe Expert (ADBE)";
|
|
147
|
+ encodingNamesCache[FT_ENCODING_ADOBE_CUSTOM] = "Adobe Custom (ADBC)";
|
|
148
|
+ encodingNamesCache[FT_ENCODING_ADOBE_LATIN_1] = "Latin 1 (lat1)";
|
|
149
|
+ encodingNamesCache[FT_ENCODING_OLD_LATIN_2] = "Latin 2 (lat2)";
|
|
150
|
+ encodingNamesCache[FT_ENCODING_APPLE_ROMAN] = "Apple Roman (armn)";
|
|
151
|
+ }
|
|
152
|
+
|
|
153
|
+ return encodingNamesCache;
|
|
154
|
+}
|
|
155
|
+}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+// end of charmap.cpp |
src/ftinspect/engine/charmap.hpp
|
1
|
+// charmap.hpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#pragma once
|
|
6
|
+
|
|
7
|
+#include <QString>
|
|
8
|
+
|
|
9
|
+#include <ft2build.h>
|
|
10
|
+#include <freetype/freetype.h>
|
|
11
|
+
|
|
12
|
+class Engine;
|
|
13
|
+
|
|
14
|
+#define FT_ENCODING_OTHER 0xFFFE
|
|
15
|
+struct CharMapInfo
|
|
16
|
+{
|
|
17
|
+ int index;
|
|
18
|
+ FT_CharMap ptr;
|
|
19
|
+ FT_Encoding encoding;
|
|
20
|
+ unsigned short platformID;
|
|
21
|
+ unsigned short encodingID;
|
|
22
|
+ long formatID;
|
|
23
|
+ unsigned long languageID;
|
|
24
|
+ QString* encodingName;
|
|
25
|
+
|
|
26
|
+ // Actually this shouldn't go here, but for convenience...
|
|
27
|
+ int maxIndex;
|
|
28
|
+
|
|
29
|
+ CharMapInfo(int index, FT_CharMap cmap);
|
|
30
|
+
|
|
31
|
+ QString stringifyIndex(int code, int idx);
|
|
32
|
+ QString stringifyIndexShort(int code);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+ friend bool
|
|
36
|
+ operator==(const CharMapInfo& lhs, const CharMapInfo& rhs)
|
|
37
|
+ {
|
|
38
|
+ // omitting `ptr` by design!
|
|
39
|
+ return lhs.index == rhs.index
|
|
40
|
+ && lhs.encoding == rhs.encoding
|
|
41
|
+ && lhs.platformID == rhs.platformID
|
|
42
|
+ && lhs.encodingID == rhs.encodingID
|
|
43
|
+ && lhs.formatID == rhs.formatID
|
|
44
|
+ && lhs.languageID == rhs.languageID;
|
|
45
|
+ }
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+ friend bool
|
|
49
|
+ operator!=(const CharMapInfo& lhs, const CharMapInfo& rhs)
|
|
50
|
+ {
|
|
51
|
+ return !(lhs == rhs);
|
|
52
|
+ }
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+private:
|
|
56
|
+ int computeMaxIndex();
|
|
57
|
+ static int maxIndexForFaceAndCharMap(FT_CharMap charMap, unsigned max);
|
|
58
|
+};
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+// end of charmap.hpp |
src/ftinspect/engine/engine.cpp
... |
... |
@@ -152,6 +152,12 @@ Engine::Engine() |
152
|
152
|
// XXX error handling
|
153
|
153
|
}
|
154
|
154
|
|
|
155
|
+ error = FTC_CMapCache_New(cacheManager_, &cmapCache_);
|
|
156
|
+ if (error)
|
|
157
|
+ {
|
|
158
|
+ // XXX error handling
|
|
159
|
+ }
|
|
160
|
+
|
155
|
161
|
queryEngine();
|
156
|
162
|
renderingEngine_
|
157
|
163
|
= std::unique_ptr<RenderingEngine>(new RenderingEngine(this));
|
... |
... |
@@ -328,6 +334,7 @@ Engine::loadFont(int fontIndex, |
328
|
334
|
ftSize_ = NULL;
|
329
|
335
|
curFamilyName_ = QString();
|
330
|
336
|
curStyleName_ = QString();
|
|
337
|
+ curCharMaps_.clear();
|
331
|
338
|
curSFNTNames_.clear();
|
332
|
339
|
}
|
333
|
340
|
else
|
... |
... |
@@ -345,6 +352,11 @@ Engine::loadFont(int fontIndex, |
345
|
352
|
else
|
346
|
353
|
fontType_ = FontType_Other;
|
347
|
354
|
|
|
355
|
+ curCharMaps_.clear();
|
|
356
|
+ curCharMaps_.reserve(ftFallbackFace_->num_charmaps);
|
|
357
|
+ for (int i = 0; i < ftFallbackFace_->num_charmaps; i++)
|
|
358
|
+ curCharMaps_.emplace_back(i, ftFallbackFace_->charmaps[i]);
|
|
359
|
+
|
348
|
360
|
SFNTName::get(this, curSFNTNames_);
|
349
|
361
|
loadPaletteInfos();
|
350
|
362
|
curMMGXState_ = MMGXAxisInfo::get(this, curMMGXAxes_);
|
... |
... |
@@ -463,6 +475,58 @@ Engine::currentFontFixedSizes() |
463
|
475
|
}
|
464
|
476
|
|
465
|
477
|
|
|
478
|
+int
|
|
479
|
+Engine::currentFontFirstUnicodeCharMap()
|
|
480
|
+{
|
|
481
|
+ auto& charmaps = currentFontCharMaps();
|
|
482
|
+ for (auto& cmap : charmaps)
|
|
483
|
+ if (cmap.encoding == FT_ENCODING_UNICODE)
|
|
484
|
+ return cmap.index;
|
|
485
|
+ return -1;
|
|
486
|
+}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+unsigned
|
|
490
|
+Engine::glyphIndexFromCharCode(int code, int charMapIndex)
|
|
491
|
+{
|
|
492
|
+ if (charMapIndex < 0)
|
|
493
|
+ return code;
|
|
494
|
+ return FTC_CMapCache_Lookup(cmapCache_, scaler_.face_id, charMapIndex, code);
|
|
495
|
+}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+FT_Pos
|
|
499
|
+Engine::currentFontTrackingKerning(int degree)
|
|
500
|
+{
|
|
501
|
+ if (!ftSize_)
|
|
502
|
+ return 0;
|
|
503
|
+
|
|
504
|
+ FT_Pos result;
|
|
505
|
+ // this function needs and returns points, not pixels
|
|
506
|
+ if (!FT_Get_Track_Kerning(ftSize_->face,
|
|
507
|
+ static_cast<FT_Fixed>(scaler_.width) << 10,
|
|
508
|
+ -degree,
|
|
509
|
+ &result))
|
|
510
|
+ {
|
|
511
|
+ result = static_cast<FT_Pos>((result / 1024.0 * scaler_.x_res) / 72.0);
|
|
512
|
+ return result;
|
|
513
|
+ }
|
|
514
|
+ return 0;
|
|
515
|
+}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+FT_Vector
|
|
519
|
+Engine::currentFontKerning(int glyphIndex,
|
|
520
|
+ int prevIndex)
|
|
521
|
+{
|
|
522
|
+ FT_Vector kern = {0, 0};
|
|
523
|
+ FT_Get_Kerning(ftSize_->face,
|
|
524
|
+ prevIndex, glyphIndex,
|
|
525
|
+ FT_KERNING_UNFITTED, &kern);
|
|
526
|
+ return kern;
|
|
527
|
+}
|
|
528
|
+
|
|
529
|
+
|
466
|
530
|
std::pair<int, int>
|
467
|
531
|
Engine::currentSizeAscDescPx()
|
468
|
532
|
{
|
... |
... |
@@ -569,6 +633,19 @@ Engine::loadGlyphWithoutUpdate(int glyphIndex, |
569
|
633
|
}
|
570
|
634
|
|
571
|
635
|
|
|
636
|
+FT_Size_Metrics const&
|
|
637
|
+Engine::currentFontMetrics()
|
|
638
|
+{
|
|
639
|
+ return ftSize_->metrics;
|
|
640
|
+}
|
|
641
|
+
|
|
642
|
+FT_GlyphSlot
|
|
643
|
+Engine::currentFaceSlot()
|
|
644
|
+{
|
|
645
|
+ return ftSize_->face->glyph;
|
|
646
|
+}
|
|
647
|
+
|
|
648
|
+
|
572
|
649
|
bool
|
573
|
650
|
Engine::renderReady()
|
574
|
651
|
{
|
src/ftinspect/engine/engine.hpp
... |
... |
@@ -6,12 +6,13 @@ |
6
|
6
|
#pragma once
|
7
|
7
|
|
8
|
8
|
#include "fontfilemanager.hpp"
|
9
|
|
-
|
10
|
9
|
#include "paletteinfo.hpp"
|
11
|
10
|
#include "fontinfo.hpp"
|
12
|
11
|
#include "mmgx.hpp"
|
13
|
12
|
#include "rendering.hpp"
|
|
13
|
+#include "charmap.hpp"
|
14
|
14
|
|
|
15
|
+#include <vector>
|
15
|
16
|
#include <memory>
|
16
|
17
|
#include <utility>
|
17
|
18
|
#include <QString>
|
... |
... |
@@ -109,6 +110,9 @@ public: |
109
|
110
|
// (for current fonts)
|
110
|
111
|
FT_Face currentFallbackFtFace() { return ftFallbackFace_; }
|
111
|
112
|
FT_Size currentFtSize() { return ftSize_; }
|
|
113
|
+ FT_Size_Metrics const& currentFontMetrics();
|
|
114
|
+ FT_GlyphSlot currentFaceSlot();
|
|
115
|
+
|
112
|
116
|
bool renderReady(); // Can we render bitmaps? (implys `fontValid`)
|
113
|
117
|
bool fontValid(); // Is the current font valid (valid font may be unavailable
|
114
|
118
|
// to render, such as non-scalable font with invalid sizes)
|
... |
... |
@@ -124,6 +128,7 @@ public: |
124
|
128
|
MMGXState currentFontMMGXState() { return curMMGXState_; }
|
125
|
129
|
std::vector<MMGXAxisInfo>& currentFontMMGXAxes() { return curMMGXAxes_; }
|
126
|
130
|
std::vector<SFNTName>& currentFontSFNTNames() { return curSFNTNames_; }
|
|
131
|
+ std::vector<CharMapInfo>& currentFontCharMaps() { return curCharMaps_; }
|
127
|
132
|
|
128
|
133
|
QString glyphName(int glyphIndex);
|
129
|
134
|
long numberOfFaces(int fontIndex);
|
... |
... |
@@ -137,13 +142,21 @@ public: |
137
|
142
|
bool currentFontHasColorLayers();
|
138
|
143
|
std::vector<int> currentFontFixedSizes();
|
139
|
144
|
|
|
145
|
+ int currentFontFirstUnicodeCharMap();
|
|
146
|
+ // Note: the current font face must be properly set
|
|
147
|
+ unsigned glyphIndexFromCharCode(int code, int charMapIndex);
|
|
148
|
+ FT_Pos currentFontTrackingKerning(int degree);
|
|
149
|
+ FT_Vector currentFontKerning(int glyphIndex, int prevIndex);
|
140
|
150
|
std::pair<int, int> currentSizeAscDescPx();
|
141
|
151
|
|
142
|
152
|
// (settings)
|
143
|
153
|
int dpi() { return dpi_; }
|
|
154
|
+ double pointSize() { return pointSize_; }
|
144
|
155
|
FTC_ImageType imageType() { return &imageType_; }
|
145
|
156
|
bool antiAliasingEnabled() { return antiAliasingEnabled_; }
|
|
157
|
+ bool doHinting() { return doHinting_; }
|
146
|
158
|
bool embeddedBitmapEnabled() { return embeddedBitmap_; }
|
|
159
|
+ bool lcdUsingSubPixelPositioning() { return lcdSubPixelPositioning_; }
|
147
|
160
|
bool useColorLayer() { return useColorLayer_; }
|
148
|
161
|
int paletteIndex() { return paletteIndex_; }
|
149
|
162
|
FT_Render_Mode
|
... |
... |
@@ -179,8 +192,9 @@ public: |
179
|
192
|
void setEmbeddedBitmapEnabled(bool enabled) { embeddedBitmap_ = enabled; }
|
180
|
193
|
void setUseColorLayer(bool colorLayer) { useColorLayer_ = colorLayer; }
|
181
|
194
|
void setPaletteIndex(int index) { paletteIndex_ = index; }
|
|
195
|
+ void setLCDSubPixelPositioning(bool sp) { lcdSubPixelPositioning_ = sp; }
|
|
196
|
+
|
182
|
197
|
// (settings without backing fields)
|
183
|
|
-
|
184
|
198
|
// Note: These 3 functions now takes actual mode/version from FreeType,
|
185
|
199
|
// instead of values from enum in MainGUI!
|
186
|
200
|
void setLcdFilter(FT_LcdFilter filter);
|
... |
... |
@@ -210,6 +224,7 @@ private: |
210
|
224
|
QString curFamilyName_;
|
211
|
225
|
QString curStyleName_;
|
212
|
226
|
int curNumGlyphs_ = -1;
|
|
227
|
+ std::vector<CharMapInfo> curCharMaps_;
|
213
|
228
|
std::vector<PaletteInfo> curPaletteInfos_;
|
214
|
229
|
MMGXState curMMGXState_ = MMGXState::NoMMGX;
|
215
|
230
|
std::vector<MMGXAxisInfo> curMMGXAxes_;
|
... |
... |
@@ -220,6 +235,7 @@ private: |
220
|
235
|
FTC_Manager cacheManager_;
|
221
|
236
|
FTC_ImageCache imageCache_;
|
222
|
237
|
FTC_SBitCache sbitsCache_;
|
|
238
|
+ FTC_CMapCache cmapCache_;
|
223
|
239
|
EngineDefaultValues engineDefaults_;
|
224
|
240
|
|
225
|
241
|
// settings
|
... |
... |
@@ -249,6 +265,7 @@ private: |
249
|
265
|
bool useColorLayer_;
|
250
|
266
|
int paletteIndex_ = -1;
|
251
|
267
|
int antiAliasingTarget_;
|
|
268
|
+ bool lcdSubPixelPositioning_;
|
252
|
269
|
int renderMode_;
|
253
|
270
|
|
254
|
271
|
unsigned long loadFlags_;
|
src/ftinspect/engine/stringrenderer.cpp
|
1
|
+// stringrenderer.cpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#include "stringrenderer.hpp"
|
|
6
|
+
|
|
7
|
+#include "engine.hpp"
|
|
8
|
+
|
|
9
|
+#include <cmath>
|
|
10
|
+#include <QTextCodec>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+StringRenderer::StringRenderer(Engine* engine)
|
|
14
|
+: engine_(engine)
|
|
15
|
+{
|
|
16
|
+}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+StringRenderer::~StringRenderer()
|
|
20
|
+{
|
|
21
|
+ clearActive();
|
|
22
|
+}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+void
|
|
26
|
+StringRenderer::setCharMapIndex(int charMapIndex,
|
|
27
|
+ int limitIndex)
|
|
28
|
+{
|
|
29
|
+ auto& charMaps = engine_->currentFontCharMaps();
|
|
30
|
+ if (charMapIndex < 0
|
|
31
|
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
|
|
32
|
+ charMapIndex = -1;
|
|
33
|
+
|
|
34
|
+ charMapIndex_ = charMapIndex;
|
|
35
|
+ limitIndex_ = limitIndex;
|
|
36
|
+}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+void
|
|
40
|
+StringRenderer::setRotation(double rotation)
|
|
41
|
+{
|
|
42
|
+ rotation_ = rotation;
|
|
43
|
+
|
|
44
|
+ if (rotation <= -180)
|
|
45
|
+ rotation += 360;
|
|
46
|
+ if (rotation > 180)
|
|
47
|
+ rotation -= 360;
|
|
48
|
+
|
|
49
|
+ if (rotation == 0)
|
|
50
|
+ {
|
|
51
|
+ matrixEnabled_ = false;
|
|
52
|
+ return;
|
|
53
|
+ }
|
|
54
|
+
|
|
55
|
+ matrixEnabled_ = true;
|
|
56
|
+ double radian = rotation * 3.14159265 / 180.0;
|
|
57
|
+ auto cosinus = static_cast<FT_Fixed>(cos(radian) * 65536.0);
|
|
58
|
+ auto sinus = static_cast<FT_Fixed>(sin(radian) * 65536.0);
|
|
59
|
+
|
|
60
|
+ matrix_.xx = cosinus;
|
|
61
|
+ matrix_.yx = sinus;
|
|
62
|
+ matrix_.xy = -sinus;
|
|
63
|
+ matrix_.yy = cosinus;
|
|
64
|
+}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+void
|
|
68
|
+StringRenderer::setKerning(bool kerning)
|
|
69
|
+{
|
|
70
|
+ if (kerning)
|
|
71
|
+ {
|
|
72
|
+ kerningMode_ = KM_Smart;
|
|
73
|
+ kerningDegree_ = KD_Medium;
|
|
74
|
+ }
|
|
75
|
+ else
|
|
76
|
+ {
|
|
77
|
+ kerningMode_ = KM_None;
|
|
78
|
+ kerningDegree_ = KD_None;
|
|
79
|
+ }
|
|
80
|
+}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+void
|
|
84
|
+StringRenderer::reloadAll()
|
|
85
|
+{
|
|
86
|
+ clearActive(usingString_); // if "All Glyphs", then do a complete wipe
|
|
87
|
+ if (usingString_)
|
|
88
|
+ reloadGlyphIndices();
|
|
89
|
+}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+void
|
|
93
|
+StringRenderer::reloadGlyphs()
|
|
94
|
+{
|
|
95
|
+ clearActive(true);
|
|
96
|
+}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+void
|
|
100
|
+StringRenderer::setUseString(QString const& string)
|
|
101
|
+{
|
|
102
|
+ clearActive(); // clear existing
|
|
103
|
+ usingString_ = true;
|
|
104
|
+
|
|
105
|
+ long long totalCount = 0;
|
|
106
|
+ for (uint ch : string.toUcs4())
|
|
107
|
+ {
|
|
108
|
+ activeGlyphs_.emplace_back();
|
|
109
|
+ auto& it = activeGlyphs_.back();
|
|
110
|
+ it.charCodeUcs4 = it.charCode = static_cast<int>(ch);
|
|
111
|
+ it.glyphIndex = 0;
|
|
112
|
+ ++totalCount;
|
|
113
|
+ if (totalCount >= INT_MAX) // Prevent overflow
|
|
114
|
+ break;
|
|
115
|
+ }
|
|
116
|
+ reloadGlyphIndices();
|
|
117
|
+}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+void
|
|
121
|
+StringRenderer::setUseAllGlyphs()
|
|
122
|
+{
|
|
123
|
+ if (usingString_)
|
|
124
|
+ clearActive();
|
|
125
|
+ usingString_ = false;
|
|
126
|
+}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+void
|
|
130
|
+StringRenderer::reloadGlyphIndices()
|
|
131
|
+{
|
|
132
|
+ if (!usingString_)
|
|
133
|
+ return;
|
|
134
|
+ int charMapIndex = charMapIndex_;
|
|
135
|
+ auto& charMaps = engine_->currentFontCharMaps();
|
|
136
|
+ if (charMaps.empty())
|
|
137
|
+ return;
|
|
138
|
+ if (charMapIndex < 0
|
|
139
|
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
|
|
140
|
+ charMapIndex = engine_->currentFontFirstUnicodeCharMap();
|
|
141
|
+ if (charMapIndex < 0
|
|
142
|
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
|
|
143
|
+ charMapIndex = 0;
|
|
144
|
+ auto encoding = charMaps[charMapIndex].encoding;
|
|
145
|
+
|
|
146
|
+ if (charMapIndex < 0)
|
|
147
|
+ return;
|
|
148
|
+ for (auto& ctx : activeGlyphs_)
|
|
149
|
+ {
|
|
150
|
+ if (encoding != FT_ENCODING_UNICODE)
|
|
151
|
+ ctx.charCode = convertCharEncoding(ctx.charCodeUcs4, encoding);
|
|
152
|
+
|
|
153
|
+ auto index = engine_->glyphIndexFromCharCode(ctx.charCode, charMapIndex);
|
|
154
|
+ ctx.glyphIndex = static_cast<int>(index);
|
|
155
|
+ }
|
|
156
|
+}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+void
|
|
160
|
+StringRenderer::prepareRendering()
|
|
161
|
+{
|
|
162
|
+ engine_->reloadFont();
|
|
163
|
+ if (!engine_->renderReady())
|
|
164
|
+ return;
|
|
165
|
+ engine_->loadPalette();
|
|
166
|
+ if (kerningDegree_ != KD_None)
|
|
167
|
+ trackingKerning_ = engine_->currentFontTrackingKerning(kerningDegree_);
|
|
168
|
+ else
|
|
169
|
+ trackingKerning_ = 0;
|
|
170
|
+}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+void
|
|
174
|
+StringRenderer::loadSingleContext(GlyphContext* ctx,
|
|
175
|
+ GlyphContext* prev)
|
|
176
|
+{
|
|
177
|
+ if (ctx->cacheNode)
|
|
178
|
+ {
|
|
179
|
+ FTC_Node_Unref(ctx->cacheNode, engine_->cacheManager());
|
|
180
|
+ ctx->cacheNode = NULL;
|
|
181
|
+ }
|
|
182
|
+ else if (ctx->glyph)
|
|
183
|
+ FT_Done_Glyph(ctx->glyph); // when caching isn't used
|
|
184
|
+
|
|
185
|
+ // TODO use FTC?
|
|
186
|
+
|
|
187
|
+ // After `prepareRendering`, current size/face is properly set
|
|
188
|
+ FT_GlyphSlot slot = engine_->currentFaceSlot();
|
|
189
|
+ if (engine_->loadGlyphIntoSlotWithoutCache(ctx->glyphIndex) != 0)
|
|
190
|
+ {
|
|
191
|
+ ctx->glyph = NULL;
|
|
192
|
+ return;
|
|
193
|
+ }
|
|
194
|
+ if (FT_Get_Glyph(slot, &ctx->glyph) != 0)
|
|
195
|
+ {
|
|
196
|
+ ctx->glyph = NULL;
|
|
197
|
+ return;
|
|
198
|
+ }
|
|
199
|
+ auto& metrics = slot->metrics;
|
|
200
|
+ //ctx->glyph = engine_->loadGlyphWithoutUpdate(ctx->glyphIndex,
|
|
201
|
+ // &ctx->cacheNode);
|
|
202
|
+
|
|
203
|
+ if (!ctx->glyph)
|
|
204
|
+ return;
|
|
205
|
+
|
|
206
|
+ ctx->vvector.x = metrics.vertBearingX - metrics.horiBearingX;
|
|
207
|
+ ctx->vvector.y = -metrics.vertBearingY - metrics.horiBearingY;
|
|
208
|
+
|
|
209
|
+ ctx->vadvance.x = 0;
|
|
210
|
+ ctx->vadvance.y = -metrics.vertAdvance;
|
|
211
|
+
|
|
212
|
+ ctx->lsbDelta = slot->lsb_delta;
|
|
213
|
+ ctx->rsbDelta = slot->rsb_delta;
|
|
214
|
+
|
|
215
|
+ ctx->hadvance.x = metrics.horiAdvance;
|
|
216
|
+ ctx->hadvance.y = 0;
|
|
217
|
+
|
|
218
|
+ if (lsbRsbDeltaEnabled_ && engine_->lcdUsingSubPixelPositioning())
|
|
219
|
+ ctx->hadvance.x += ctx->lsbDelta - ctx->rsbDelta;
|
|
220
|
+ prev->hadvance.x += trackingKerning_;
|
|
221
|
+
|
|
222
|
+ if (kerningMode_ != KM_None)
|
|
223
|
+ {
|
|
224
|
+ FT_Vector kern = engine_->currentFontKerning(ctx->glyphIndex,
|
|
225
|
+ prev->glyphIndex);
|
|
226
|
+
|
|
227
|
+ prev->hadvance.x += kern.x;
|
|
228
|
+ prev->hadvance.y += kern.y;
|
|
229
|
+ }
|
|
230
|
+
|
|
231
|
+ if (!engine_->lcdUsingSubPixelPositioning()
|
|
232
|
+ && lsbRsbDeltaEnabled_)
|
|
233
|
+ {
|
|
234
|
+ if (prev->rsbDelta - ctx->lsbDelta > 32)
|
|
235
|
+ prev->hadvance.x -= 64;
|
|
236
|
+ else if (prev->rsbDelta - ctx->lsbDelta < -31)
|
|
237
|
+ prev->hadvance.x += 64;
|
|
238
|
+ }
|
|
239
|
+
|
|
240
|
+ if (!engine_->lcdUsingSubPixelPositioning() && engine_->doHinting())
|
|
241
|
+ {
|
|
242
|
+ prev->hadvance.x = (prev->hadvance.x + 32) & -64;
|
|
243
|
+ prev->hadvance.y = (prev->hadvance.y + 32) & -64;
|
|
244
|
+ }
|
|
245
|
+}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+void
|
|
249
|
+StringRenderer::loadStringGlyphs()
|
|
250
|
+{
|
|
251
|
+ if (!usingString_)
|
|
252
|
+ return;
|
|
253
|
+ GlyphContext* prev = &tempGlyphContext_; // = empty
|
|
254
|
+ tempGlyphContext_ = {};
|
|
255
|
+
|
|
256
|
+ for (auto& ctx : activeGlyphs_)
|
|
257
|
+ {
|
|
258
|
+ loadSingleContext(&ctx, prev);
|
|
259
|
+ prev = &ctx;
|
|
260
|
+ }
|
|
261
|
+
|
|
262
|
+ glyphCacheValid_ = true;
|
|
263
|
+}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+int
|
|
267
|
+StringRenderer::prepareLine(int offset,
|
|
268
|
+ int lineWidth,
|
|
269
|
+ FT_Vector& outActualLineWidth,
|
|
270
|
+ int nonSpacingPlaceholder,
|
|
271
|
+ bool handleMultiLine)
|
|
272
|
+{
|
|
273
|
+ int totalCount = 0;
|
|
274
|
+ outActualLineWidth = {0, 0};
|
|
275
|
+ if (!usingString_) // All glyphs
|
|
276
|
+ {
|
|
277
|
+ // The thing gets a little complicated when we're using "All Glyphs" mode
|
|
278
|
+ // The input sequence is actually infinite
|
|
279
|
+ // so we have to combine loading glyph into rendering, and can't preload
|
|
280
|
+ // all glyphs
|
|
281
|
+
|
|
282
|
+ // TODO: Low performance when the begin index is large.
|
|
283
|
+ // TODO: Optimize: use a sparse vector...!
|
|
284
|
+ // The problem is that when doing a `list::resize`, the ctor is called
|
|
285
|
+ // for unnecessarily many times.
|
|
286
|
+ tempGlyphContext_ = {};
|
|
287
|
+ for (unsigned n = offset; n < static_cast<unsigned>(limitIndex_);)
|
|
288
|
+ {
|
|
289
|
+ if (activeGlyphs_.capacity() <= n)
|
|
290
|
+ activeGlyphs_.reserve(static_cast<size_t>(n) * 2);
|
|
291
|
+ if (activeGlyphs_.size() <= n)
|
|
292
|
+ activeGlyphs_.resize(n + 1);
|
|
293
|
+
|
|
294
|
+ auto& ctx = activeGlyphs_[n];
|
|
295
|
+ ctx.charCode = static_cast<int>(n);
|
|
296
|
+ ctx.glyphIndex = static_cast<int>(
|
|
297
|
+ engine_->glyphIndexFromCharCode(static_cast<int>(n), charMapIndex_));
|
|
298
|
+
|
|
299
|
+ auto prev = n == 0 ? &tempGlyphContext_ : &activeGlyphs_[n - 1];
|
|
300
|
+ if (!ctx.glyph)
|
|
301
|
+ loadSingleContext(&ctx, prev);
|
|
302
|
+
|
|
303
|
+ // In All Glyphs mode, a red placeholder should be drawn for non-spacing
|
|
304
|
+ // glyphs (e.g. the stress mark)
|
|
305
|
+ auto actualAdvanceX = ctx.hadvance.x ? ctx.hadvance.x
|
|
306
|
+ : nonSpacingPlaceholder << 6;
|
|
307
|
+ if (outActualLineWidth.x + actualAdvanceX > lineWidth)
|
|
308
|
+ break;
|
|
309
|
+ outActualLineWidth.x += actualAdvanceX;
|
|
310
|
+ outActualLineWidth.y += ctx.hadvance.y;
|
|
311
|
+ ++n;
|
|
312
|
+ ++totalCount;
|
|
313
|
+ }
|
|
314
|
+ }
|
|
315
|
+ else // strings
|
|
316
|
+ {
|
|
317
|
+ if (!glyphCacheValid_)
|
|
318
|
+ {
|
|
319
|
+ clearActive(true);
|
|
320
|
+ loadStringGlyphs();
|
|
321
|
+ }
|
|
322
|
+
|
|
323
|
+ for (unsigned n = offset; n < activeGlyphs_.size();)
|
|
324
|
+ {
|
|
325
|
+ auto& ctx = activeGlyphs_[n];
|
|
326
|
+
|
|
327
|
+ if (handleMultiLine && ctx.charCode == '\n')
|
|
328
|
+ {
|
|
329
|
+ totalCount += 1; // Break here.
|
|
330
|
+ break;
|
|
331
|
+ }
|
|
332
|
+
|
|
333
|
+ if (repeated_) // if repeated, we must stop when we touch the end of line
|
|
334
|
+ {
|
|
335
|
+ if (outActualLineWidth.x + ctx.hadvance.x > lineWidth)
|
|
336
|
+ break;
|
|
337
|
+ outActualLineWidth.x += ctx.hadvance.x;
|
|
338
|
+ outActualLineWidth.y += ctx.hadvance.y;
|
|
339
|
+ ++n;
|
|
340
|
+ n %= static_cast<int>(activeGlyphs_.size()); // safe
|
|
341
|
+ }
|
|
342
|
+ else if (vertical_)
|
|
343
|
+ {
|
|
344
|
+ outActualLineWidth.x += ctx.vadvance.x;
|
|
345
|
+ outActualLineWidth.y += ctx.vadvance.y;
|
|
346
|
+ ++n;
|
|
347
|
+ }
|
|
348
|
+ else
|
|
349
|
+ {
|
|
350
|
+ outActualLineWidth.x += ctx.hadvance.x;
|
|
351
|
+ outActualLineWidth.y += ctx.hadvance.y;
|
|
352
|
+ ++n;
|
|
353
|
+ }
|
|
354
|
+ ++totalCount;
|
|
355
|
+ }
|
|
356
|
+ }
|
|
357
|
+ return totalCount;
|
|
358
|
+}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+int
|
|
362
|
+StringRenderer::render(int width,
|
|
363
|
+ int height,
|
|
364
|
+ int offset)
|
|
365
|
+{
|
|
366
|
+ engine_->reloadFont();
|
|
367
|
+
|
|
368
|
+ if (usingString_)
|
|
369
|
+ offset = 0;
|
|
370
|
+ if (!usingString_ && limitIndex_ <= 0)
|
|
371
|
+ return 0;
|
|
372
|
+ if (!engine_->fontValid())
|
|
373
|
+ return 0;
|
|
374
|
+
|
|
375
|
+ auto initialOffset = offset;
|
|
376
|
+
|
|
377
|
+ // Separated into 3 modes:
|
|
378
|
+ // Waterfall, fill the whole canvas and only single string.
|
|
379
|
+ if (waterfall_)
|
|
380
|
+ {
|
|
381
|
+ // Waterfall
|
|
382
|
+
|
|
383
|
+ vertical_ = false;
|
|
384
|
+ // They're only effective for non-bitmap-only (scalable) fonts!
|
|
385
|
+ auto originalSize = static_cast<int>(engine_->pointSize() * 64);
|
|
386
|
+ auto ptSize = originalSize;
|
|
387
|
+ auto ptHeight = 64 * 72 * height / engine_->dpi();
|
|
388
|
+ int step = 0;
|
|
389
|
+
|
|
390
|
+ auto bitmapOnly = engine_->currentFontBitmapOnly();
|
|
391
|
+ auto fixedSizes = engine_->currentFontFixedSizes();
|
|
392
|
+ std::sort(fixedSizes.begin(), fixedSizes.end());
|
|
393
|
+ auto fixedSizesIter = fixedSizes.begin();
|
|
394
|
+
|
|
395
|
+ if (waterfallStart_ <= 0)
|
|
396
|
+ {
|
|
397
|
+ // auto
|
|
398
|
+ step = (originalSize * originalSize / ptHeight + 64) & ~63;
|
|
399
|
+ ptSize = ptSize - step * (ptSize / step); // modulo
|
|
400
|
+ ptSize += step;
|
|
401
|
+ }
|
|
402
|
+ else if (!bitmapOnly)
|
|
403
|
+ {
|
|
404
|
+ ptSize = static_cast<int>(waterfallStart_ * 64.0) & ~31;
|
|
405
|
+ // we first get a ratio since height & ppem are near proportional...
|
|
406
|
+ // 64.0 is somewhat a magic reference number
|
|
407
|
+ engine_->setSizeByPoint(64.0);
|
|
408
|
+ engine_->reloadFont();
|
|
409
|
+ if (!engine_->renderReady())
|
|
410
|
+ return -1;
|
|
411
|
+ auto pixelActual = engine_->currentFontMetrics().height >> 6;
|
|
412
|
+
|
|
413
|
+ auto heightPt = height * 64.0 / pixelActual;
|
|
414
|
+
|
|
415
|
+ if (waterfallEnd_ < waterfallStart_)
|
|
416
|
+ waterfallEnd_ = waterfallStart_ + 1;
|
|
417
|
+
|
|
418
|
+ auto n = heightPt * 2 / (waterfallStart_ + waterfallEnd_);
|
|
419
|
+ auto stepTemp = (waterfallEnd_ - waterfallStart_) / (n + 1);
|
|
420
|
+ // rounding to 0.25
|
|
421
|
+ step = static_cast<int>(std::round(stepTemp * 4)) * 16 & ~15;
|
|
422
|
+ if (step == 0)
|
|
423
|
+ step = 16; // 0.25 pt
|
|
424
|
+ }
|
|
425
|
+
|
|
426
|
+ int y = 0;
|
|
427
|
+ // no position param in "All Glyphs" or repeated mode
|
|
428
|
+ int x = static_cast<int>((usingString_ && !repeated_) ? (width * position_)
|
|
429
|
+ : 0);
|
|
430
|
+ int count = 0;
|
|
431
|
+
|
|
432
|
+ while (true)
|
|
433
|
+ {
|
|
434
|
+ if (!bitmapOnly)
|
|
435
|
+ engine_->setSizeByPoint(ptSize / 64.0);
|
|
436
|
+ else
|
|
437
|
+ {
|
|
438
|
+ if (fixedSizesIter == fixedSizes.end())
|
|
439
|
+ break;
|
|
440
|
+ engine_->setSizeByPixel(*fixedSizesIter);
|
|
441
|
+ }
|
|
442
|
+ clearActive(true);
|
|
443
|
+ prepareRendering(); // set size/face for engine, so metrics are valid
|
|
444
|
+ auto& metrics = engine_->currentFontMetrics();
|
|
445
|
+
|
|
446
|
+ y += static_cast<int>(metrics.height >> 6) + 1;
|
|
447
|
+ if (y >= height && !bitmapOnly)
|
|
448
|
+ break;
|
|
449
|
+
|
|
450
|
+ loadStringGlyphs();
|
|
451
|
+ auto lcount = renderLine(x, y + static_cast<int>(metrics.descender >> 6),
|
|
452
|
+ width, height,
|
|
453
|
+ offset);
|
|
454
|
+ count = std::max(count, lcount);
|
|
455
|
+
|
|
456
|
+ if (!bitmapOnly)
|
|
457
|
+ {
|
|
458
|
+ if (step == 0)
|
|
459
|
+ break;
|
|
460
|
+ ptSize += step;
|
|
461
|
+ }
|
|
462
|
+ else
|
|
463
|
+ ++fixedSizesIter;
|
|
464
|
+ }
|
|
465
|
+ engine_->setSizeByPoint(originalSize / 64.0);
|
|
466
|
+
|
|
467
|
+ return count;
|
|
468
|
+ }
|
|
469
|
+ // end of waterfall
|
|
470
|
+
|
|
471
|
+ if (repeated_ || !usingString_)
|
|
472
|
+ {
|
|
473
|
+ // Fill the whole canvas (string repeated or all glyphs)
|
|
474
|
+
|
|
475
|
+ prepareRendering();
|
|
476
|
+ if (!engine_->renderReady())
|
|
477
|
+ return 0;
|
|
478
|
+ auto& metrics = engine_->currentFontMetrics();
|
|
479
|
+ auto y = 4 + static_cast<int>(metrics.ascender >> 6);
|
|
480
|
+ auto stepY = static_cast<int>(metrics.height >> 6) + 1;
|
|
481
|
+ auto limitY = height + static_cast<int>(metrics.descender >> 6);
|
|
482
|
+
|
|
483
|
+ // Only care about multiline when in string mode
|
|
484
|
+ for (; y < limitY; y += stepY)
|
|
485
|
+ {
|
|
486
|
+ offset = renderLine(0, y, width, height, offset, usingString_);
|
|
487
|
+ // For repeating
|
|
488
|
+ if (usingString_ && repeated_ && !activeGlyphs_.empty())
|
|
489
|
+ offset %= static_cast<int>(activeGlyphs_.size());
|
|
490
|
+ }
|
|
491
|
+ if (!usingString_) // only return count for All Glyphs mode.
|
|
492
|
+ return offset - initialOffset;
|
|
493
|
+ return 0;
|
|
494
|
+ }
|
|
495
|
+
|
|
496
|
+ // Single string
|
|
497
|
+ prepareRendering();
|
|
498
|
+ if (!engine_->renderReady())
|
|
499
|
+ return 0;
|
|
500
|
+
|
|
501
|
+ auto& metrics = engine_->currentFontMetrics();
|
|
502
|
+ auto x = static_cast<int>(width * position_);
|
|
503
|
+ // Anchor at top-left in vertical mode, at the center in horizontal mode
|
|
504
|
+ auto y = vertical_ ? 0 : (height / 2);
|
|
505
|
+ auto stepY = static_cast<int>(metrics.height >> 6) + 1;
|
|
506
|
+ y += 4 + static_cast<int>(metrics.ascender >> 6);
|
|
507
|
+
|
|
508
|
+ while (offset < static_cast<int>(activeGlyphs_.size()))
|
|
509
|
+ {
|
|
510
|
+ offset = renderLine(x, y, width, height, offset, true);
|
|
511
|
+ y += stepY;
|
|
512
|
+ }
|
|
513
|
+ return offset - initialOffset;
|
|
514
|
+}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+int
|
|
518
|
+StringRenderer::renderLine(int x,
|
|
519
|
+ int y,
|
|
520
|
+ int width,
|
|
521
|
+ int height,
|
|
522
|
+ int offset,
|
|
523
|
+ bool handleMultiLine)
|
|
524
|
+{
|
|
525
|
+ if (x < 0 || y < 0 || x > width || y > height)
|
|
526
|
+ return 0;
|
|
527
|
+
|
|
528
|
+ y = height - y; // change to Cartesian coordinates
|
|
529
|
+
|
|
530
|
+ FT_Vector pen = { 0, 0 };
|
|
531
|
+ FT_Vector advance;
|
|
532
|
+ auto nonSpacingPlaceholder = engine_->currentFontMetrics().y_ppem / 2 + 2;
|
|
533
|
+
|
|
534
|
+ // When in "All Glyphs" mode, no vertical support.
|
|
535
|
+ if (repeated_ || !usingString_)
|
|
536
|
+ vertical_ = false; // TODO: Support vertical + repeated
|
|
537
|
+
|
|
538
|
+ int lineLength = 64 * (vertical_ ? height : width);
|
|
539
|
+
|
|
540
|
+ // first prepare the line & determine the line length
|
|
541
|
+ int totalCount = prepareLine(offset, lineLength, pen,
|
|
542
|
+ nonSpacingPlaceholder, handleMultiLine);
|
|
543
|
+
|
|
544
|
+ // round to control initial pen position and preserve hinting...
|
|
545
|
+ // pen.x, y is the actual length now, and we multiple it by pos
|
|
546
|
+ auto centerFixed = static_cast<int>(0x10000 * position_);
|
|
547
|
+ if (!usingString_ || repeated_)
|
|
548
|
+ centerFixed = 0;
|
|
549
|
+ pen.x = FT_MulFix(pen.x, centerFixed) & ~63;
|
|
550
|
+ pen.y = FT_MulFix(pen.y, centerFixed) & ~63;
|
|
551
|
+
|
|
552
|
+ // ... unless rotating; XXX sbits
|
|
553
|
+ if (matrixEnabled_)
|
|
554
|
+ FT_Vector_Transform(&pen, &matrix_);
|
|
555
|
+
|
|
556
|
+ // get pen position: penPos = center - pos * width
|
|
557
|
+ pen.x = (x << 6) - pen.x;
|
|
558
|
+ pen.y = (y << 6) - pen.y;
|
|
559
|
+
|
|
560
|
+ // Need to transform the coord back to normal coord system
|
|
561
|
+ lineBeginCallback_({ (pen.x >> 6),
|
|
562
|
+ height - (pen.y >> 6) },
|
|
563
|
+ engine_->pointSize());
|
|
564
|
+
|
|
565
|
+ for (int i = offset; i < totalCount + offset; i++)
|
|
566
|
+ {
|
|
567
|
+ auto& ctx = activeGlyphs_[i % activeGlyphs_.size()];
|
|
568
|
+ if (handleMultiLine && ctx.charCode == '\n')
|
|
569
|
+ continue; // skip \n
|
|
570
|
+ FT_Glyph image = NULL; // Remember to clean up
|
|
571
|
+ FT_BBox bbox;
|
|
572
|
+
|
|
573
|
+ if (!ctx.glyph)
|
|
574
|
+ continue;
|
|
575
|
+
|
|
576
|
+ advance = vertical_ ? ctx.vadvance : ctx.hadvance;
|
|
577
|
+
|
|
578
|
+ QRect rect;
|
|
579
|
+ QImage* colorLayerImage
|
|
580
|
+ = engine_->renderingEngine()->tryDirectRenderColorLayers(ctx.glyphIndex,
|
|
581
|
+ &rect, true);
|
|
582
|
+
|
|
583
|
+ if (colorLayerImage)
|
|
584
|
+ {
|
|
585
|
+ FT_Vector penPos = { (pen.x >> 6), height - (pen.y >> 6) };
|
|
586
|
+ renderImageCallback_(colorLayerImage, rect, penPos, advance, ctx);
|
|
587
|
+ }
|
|
588
|
+ else
|
|
589
|
+ {
|
|
590
|
+ // copy the glyph because we're doing manipulation
|
|
591
|
+ auto error = FT_Glyph_Copy(ctx.glyph, &image);
|
|
592
|
+ if (error)
|
|
593
|
+ continue;
|
|
594
|
+
|
|
595
|
+ glyphPreprocessCallback_(&image);
|
|
596
|
+
|
|
597
|
+ if (image->format != FT_GLYPH_FORMAT_BITMAP)
|
|
598
|
+ {
|
|
599
|
+ if (vertical_)
|
|
600
|
+ error = FT_Glyph_Transform(image, NULL, &ctx.vvector);
|
|
601
|
+
|
|
602
|
+ if (!error)
|
|
603
|
+ {
|
|
604
|
+ if (matrixEnabled_)
|
|
605
|
+ error = FT_Glyph_Transform(image, &matrix_, NULL);
|
|
606
|
+ }
|
|
607
|
+
|
|
608
|
+ if (error)
|
|
609
|
+ {
|
|
610
|
+ FT_Done_Glyph(image);
|
|
611
|
+ continue;
|
|
612
|
+ }
|
|
613
|
+ }
|
|
614
|
+ else
|
|
615
|
+ {
|
|
616
|
+ auto bitmap = reinterpret_cast<FT_BitmapGlyph>(image);
|
|
617
|
+
|
|
618
|
+ if (vertical_)
|
|
619
|
+ {
|
|
620
|
+ bitmap->left += static_cast<int>(ctx.vvector.x) >> 6;
|
|
621
|
+ bitmap->top += static_cast<int>(ctx.vvector.y) >> 6;
|
|
622
|
+ }
|
|
623
|
+ }
|
|
624
|
+
|
|
625
|
+ if (matrixEnabled_)
|
|
626
|
+ FT_Vector_Transform(&advance, &matrix_);
|
|
627
|
+
|
|
628
|
+ FT_Glyph_Get_CBox(image, FT_GLYPH_BBOX_PIXELS, &bbox);
|
|
629
|
+
|
|
630
|
+ // Don't check for bounding box here.
|
|
631
|
+ FT_Vector penPos = { (pen.x >> 6), height - (pen.y >> 6) };
|
|
632
|
+ renderCallback_(image, penPos, ctx);
|
|
633
|
+
|
|
634
|
+ FT_Done_Glyph(image);
|
|
635
|
+ }
|
|
636
|
+
|
|
637
|
+ pen.x += advance.x;
|
|
638
|
+ pen.y += advance.y;
|
|
639
|
+
|
|
640
|
+ if (!advance.x && !usingString_) // add placeholder
|
|
641
|
+ pen.x += nonSpacingPlaceholder << 6;
|
|
642
|
+ }
|
|
643
|
+
|
|
644
|
+ return offset + totalCount;
|
|
645
|
+}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+void
|
|
649
|
+StringRenderer::clearActive(bool glyphOnly)
|
|
650
|
+{
|
|
651
|
+ for (auto& ctx : activeGlyphs_)
|
|
652
|
+ {
|
|
653
|
+ if (ctx.cacheNode)
|
|
654
|
+ FTC_Node_Unref(ctx.cacheNode, engine_->cacheManager());
|
|
655
|
+ else if (ctx.glyph)
|
|
656
|
+ FT_Done_Glyph(ctx.glyph); // when caching isn't used
|
|
657
|
+ ctx.cacheNode = NULL;
|
|
658
|
+ ctx.glyph = NULL;
|
|
659
|
+ }
|
|
660
|
+ if (!glyphOnly)
|
|
661
|
+ activeGlyphs_.clear();
|
|
662
|
+
|
|
663
|
+ glyphCacheValid_ = false;
|
|
664
|
+}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+int
|
|
668
|
+StringRenderer::convertCharEncoding(int charUcs4, FT_Encoding encoding)
|
|
669
|
+{
|
|
670
|
+ switch (encoding)
|
|
671
|
+ {
|
|
672
|
+ case FT_ENCODING_MS_SYMBOL:
|
|
673
|
+ case FT_ENCODING_UNICODE:
|
|
674
|
+ case FT_ENCODING_ADOBE_STANDARD: // These may be problematic...
|
|
675
|
+ case FT_ENCODING_ADOBE_EXPERT:
|
|
676
|
+ case FT_ENCODING_ADOBE_CUSTOM:
|
|
677
|
+ case FT_ENCODING_ADOBE_LATIN_1:
|
|
678
|
+ return charUcs4;
|
|
679
|
+ }
|
|
680
|
+
|
|
681
|
+ auto mib = -1;
|
|
682
|
+ switch (encoding)
|
|
683
|
+ {
|
|
684
|
+ case FT_ENCODING_SJIS:
|
|
685
|
+ mib = 17; // Shift_JIS
|
|
686
|
+ break;
|
|
687
|
+ case FT_ENCODING_PRC:
|
|
688
|
+ mib = 114; // GB 18030
|
|
689
|
+ break;
|
|
690
|
+ case FT_ENCODING_BIG5:
|
|
691
|
+ mib = 2026; // Big5
|
|
692
|
+ break;
|
|
693
|
+ case FT_ENCODING_WANSUNG:
|
|
694
|
+ mib = -949; // KS C 5601:1987, this is a fake mib value
|
|
695
|
+ break;
|
|
696
|
+ case FT_ENCODING_JOHAB:
|
|
697
|
+ mib = 38; // KS C 5601:1992 / EUC-KR
|
|
698
|
+ break;
|
|
699
|
+ case FT_ENCODING_APPLE_ROMAN:
|
|
700
|
+ mib = 2027;
|
|
701
|
+ break;
|
|
702
|
+ }
|
|
703
|
+
|
|
704
|
+ if (mib == -1)
|
|
705
|
+ return charUcs4; // unsupported charmap
|
|
706
|
+ auto codec = QTextCodec::codecForMib(mib);
|
|
707
|
+ if (!codec)
|
|
708
|
+ return charUcs4; // unsupported
|
|
709
|
+
|
|
710
|
+ auto res = codec->fromUnicode(
|
|
711
|
+ QString::fromUcs4(reinterpret_cast<uint*>(&charUcs4), 1));
|
|
712
|
+ if (res.size() == 0)
|
|
713
|
+ return charUcs4;
|
|
714
|
+ if (res.size() == 1)
|
|
715
|
+ return res[0];
|
|
716
|
+ return ((static_cast<int>(res[0]) & 0xFF) << 8)
|
|
717
|
+ | (static_cast<int>(res[1]) & 0xFF);
|
|
718
|
+}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+// end of stringrenderer.cpp |
src/ftinspect/engine/stringrenderer.hpp
|
1
|
+// stringrenderer.hpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#pragma once
|
|
6
|
+
|
|
7
|
+#include <vector>
|
|
8
|
+#include <functional>
|
|
9
|
+
|
|
10
|
+#include <QString>
|
|
11
|
+
|
|
12
|
+#include <ft2build.h>
|
|
13
|
+#include <qslider.h>
|
|
14
|
+#include <freetype/freetype.h>
|
|
15
|
+#include <freetype/ftcache.h>
|
|
16
|
+#include <freetype/ftglyph.h>
|
|
17
|
+
|
|
18
|
+// adopted from `ftcommon.h`
|
|
19
|
+
|
|
20
|
+class Engine;
|
|
21
|
+struct GlyphContext
|
|
22
|
+{
|
|
23
|
+ int charCode = 0;
|
|
24
|
+ int charCodeUcs4 = 0;
|
|
25
|
+ int glyphIndex = 0;
|
|
26
|
+ FT_Glyph glyph = NULL;
|
|
27
|
+ FTC_Node cacheNode = NULL;
|
|
28
|
+
|
|
29
|
+ FT_Pos lsbDelta = 0; // delta caused by hinting
|
|
30
|
+ FT_Pos rsbDelta = 0; // delta caused by hinting
|
|
31
|
+ FT_Vector hadvance = { 0, 0 }; // kerned horizontal advance
|
|
32
|
+
|
|
33
|
+ FT_Vector vvector = { 0, 0 }; // vert. origin => hori. origin
|
|
34
|
+ FT_Vector vadvance = { 0, 0 }; // vertical advance
|
|
35
|
+};
|
|
36
|
+
|
|
37
|
+// Class to populate chars to render, to load and properly position glyphs.
|
|
38
|
+// Use callbacks to receive characters and lines. You should save the result
|
|
39
|
+// from the callbacks to a cache.
|
|
40
|
+class StringRenderer
|
|
41
|
+{
|
|
42
|
+public:
|
|
43
|
+ StringRenderer(Engine* engine);
|
|
44
|
+ ~StringRenderer();
|
|
45
|
+
|
|
46
|
+ enum KerningDegree // XXX: Not honored actually
|
|
47
|
+ {
|
|
48
|
+ KD_None = 0,
|
|
49
|
+ KD_Light,
|
|
50
|
+ KD_Medium,
|
|
51
|
+ KD_Tight
|
|
52
|
+ };
|
|
53
|
+
|
|
54
|
+ enum KerningMode
|
|
55
|
+ {
|
|
56
|
+ KM_None = 0,
|
|
57
|
+ KM_Normal,
|
|
58
|
+ KM_Smart
|
|
59
|
+ };
|
|
60
|
+
|
|
61
|
+ /*
|
|
62
|
+ * Called when outputting a glyph. The receiver is reponsible for rendering
|
|
63
|
+ * the glyph to bitmap.
|
|
64
|
+ *
|
|
65
|
+ * Need to pass the pen position because sometimes the outline vector
|
|
66
|
+ * contains no points, and thus can't be translated to the desired pen
|
|
67
|
+ * position.
|
|
68
|
+ */
|
|
69
|
+ using RenderCallback = std::function<void(FT_Glyph, // glyph
|
|
70
|
+ FT_Vector, // penPos
|
|
71
|
+ GlyphContext&)>;
|
|
72
|
+ /*
|
|
73
|
+ * Called when outputtng a glyph with bitmap pre-rendered.
|
|
74
|
+ * The receiver can simply use the bitmap, mainly for color layered fonts.
|
|
75
|
+ *
|
|
76
|
+ * TODO: Remove `RenderCallback` and do QImage creation in this class?
|
|
77
|
+ * The receiver is responsible for deleteing the `QImage`
|
|
78
|
+ * (ownership transfered).
|
|
79
|
+ */
|
|
80
|
+ using RenderImageCallback = std::function<void(QImage*, // bitmap
|
|
81
|
+ QRect, // bbox
|
|
82
|
+ FT_Vector, // penPos
|
|
83
|
+ FT_Vector, // advance
|
|
84
|
+ GlyphContext&)>;
|
|
85
|
+ /*
|
|
86
|
+ * Called right after the glyph is obtained from the font, before any other
|
|
87
|
+ * operation is done. The receiver can do pre-processing like slanting and
|
|
88
|
+ * emboldening in this function.
|
|
89
|
+ *
|
|
90
|
+ * The glyph pointer may be replaced. In that case, ownership is transfered
|
|
91
|
+ * to the renderer, and the new glyph will be eventually freed by
|
|
92
|
+ * the renderer. The callback is responsible to free the old glyph.
|
|
93
|
+ * This allows you to do the following:
|
|
94
|
+ * void callback(FT_Glyph* ptr) {
|
|
95
|
+ * ....
|
|
96
|
+ * auto oldPtr = *ptr;
|
|
97
|
+ * *ptr = ....;
|
|
98
|
+ * FT_Done_Glyph(olPtr);
|
|
99
|
+ * }
|
|
100
|
+ */
|
|
101
|
+ using PreprocessCallback = std::function<void(FT_Glyph*)>;
|
|
102
|
+ /*
|
|
103
|
+ * Called when a new line begins.
|
|
104
|
+ */
|
|
105
|
+ using LineBeginCallback = std::function<void(FT_Vector, // initial penPos
|
|
106
|
+ double)>; // size (points)
|
|
107
|
+
|
|
108
|
+ //////// Getters
|
|
109
|
+ bool isWaterfall() { return waterfall_; }
|
|
110
|
+ double position(){ return position_; }
|
|
111
|
+ int charMapIndex() { return charMapIndex_; }
|
|
112
|
+
|
|
113
|
+ //////// Callbacks
|
|
114
|
+ void
|
|
115
|
+ setCallback(RenderCallback cb)
|
|
116
|
+ {
|
|
117
|
+ renderCallback_ = std::move(cb);
|
|
118
|
+ }
|
|
119
|
+ void
|
|
120
|
+ setImageCallback(RenderImageCallback cb)
|
|
121
|
+ {
|
|
122
|
+ renderImageCallback_ = std::move(cb);
|
|
123
|
+ }
|
|
124
|
+ void
|
|
125
|
+ setPreprocessCallback(PreprocessCallback cb)
|
|
126
|
+ {
|
|
127
|
+ glyphPreprocessCallback_ = std::move(cb);
|
|
128
|
+ }
|
|
129
|
+ void
|
|
130
|
+ setLineBeginCallback(LineBeginCallback cb)
|
|
131
|
+ {
|
|
132
|
+ lineBeginCallback_ = std::move(cb);
|
|
133
|
+ }
|
|
134
|
+
|
|
135
|
+ //////// Setters for options
|
|
136
|
+ void setCharMapIndex(int charMapIndex, int limitIndex);
|
|
137
|
+ void setRepeated(bool repeated) { repeated_ = repeated; }
|
|
138
|
+ void setVertical(bool vertical) { vertical_ = vertical; }
|
|
139
|
+ void setRotation(double rotation);
|
|
140
|
+ void setWaterfall(bool waterfall) { waterfall_ = waterfall; }
|
|
141
|
+ void setWaterfallParameters(double start, double end)
|
|
142
|
+ {
|
|
143
|
+ waterfallStart_ = start;
|
|
144
|
+ waterfallEnd_ = end;
|
|
145
|
+ }
|
|
146
|
+ void setPosition(double pos) { position_ = pos; }
|
|
147
|
+ void setLsbRsbDelta(bool enabled) { lsbRsbDeltaEnabled_ = enabled; }
|
|
148
|
+ void setKerning(bool kerning);
|
|
149
|
+
|
|
150
|
+ // Need to be called when font or charMap changes
|
|
151
|
+ void setUseString(QString const& string);
|
|
152
|
+ void setUseAllGlyphs();
|
|
153
|
+
|
|
154
|
+ //////// Actions
|
|
155
|
+ int render(int width,
|
|
156
|
+ int height,
|
|
157
|
+ int offset);
|
|
158
|
+ int renderLine(int x, int y,
|
|
159
|
+ int width, int height,
|
|
160
|
+ int offset,
|
|
161
|
+ bool handleMultiLine = false);
|
|
162
|
+
|
|
163
|
+ void reloadAll(); // text/font/charmap changes, will call
|
|
164
|
+ // `reloadGlyphs`
|
|
165
|
+ void reloadGlyphs(); // any other parameter changes
|
|
166
|
+
|
|
167
|
+private:
|
|
168
|
+ Engine* engine_;
|
|
169
|
+
|
|
170
|
+ // Generally, rendering has those steps:
|
|
171
|
+ // 1. If in string mode, the string is load into `activeGlyphs_`
|
|
172
|
+ // (in `updateString`)
|
|
173
|
+ // 2. The char codes in contexts are converted to glyph indices
|
|
174
|
+ // (in `reloadGlyphIndices`)
|
|
175
|
+ // 3. If in string mode, glyphs are loaded into contexts.
|
|
176
|
+ // (in `loadStringGlyphs`)
|
|
177
|
+ // 4. In `render` function, according to mode, `renderLine` is called line
|
|
178
|
+ // by line (as well as `prepareRendering`).
|
|
179
|
+ // 5. In `renderLine`, if in all glyphs mode, glyphs from the begin index
|
|
180
|
+ // are loaded until the line is full (if the glyph already exists, it will
|
|
181
|
+ // be reused). If in string mode, it will directly use the prepared glyphs.
|
|
182
|
+ // Preprocessing is done within this step, such as emboldening or stroking.
|
|
183
|
+ // Eventually the `FT_Glyph` pointer is passed to the callback.
|
|
184
|
+
|
|
185
|
+ GlyphContext tempGlyphContext_;
|
|
186
|
+ // This vector stores all active glyphs for rendering. When rendering strings,
|
|
187
|
+ // this is the container for chars, so DO NOT directly clear it to flush
|
|
188
|
+ // cache, you should clean glyph objects only. However when rendering all
|
|
189
|
+ // glyphs, it's generally to directly wipe the vector because it's dynamically
|
|
190
|
+ // generated in `render` function (see above).
|
|
191
|
+ //
|
|
192
|
+ // Note: Because of kerning, this list must be ordered and allow duplicate
|
|
193
|
+ // characters.
|
|
194
|
+ //
|
|
195
|
+ // Actually this means 3 parts of storage: string charcode, glyph indices and
|
|
196
|
+ // glyph (+ all related info). Different parameter changes will trigger
|
|
197
|
+ // different levels of flushing.
|
|
198
|
+ std::vector<GlyphContext> activeGlyphs_;
|
|
199
|
+ bool glyphCacheValid_ = false;
|
|
200
|
+
|
|
201
|
+ int charMapIndex_ = 0;
|
|
202
|
+ int limitIndex_ = 0;
|
|
203
|
+ bool usingString_ = false;
|
|
204
|
+ bool repeated_ = false;
|
|
205
|
+ bool vertical_ = false;
|
|
206
|
+ double position_ = 0.5;
|
|
207
|
+ double rotation_ = 0;
|
|
208
|
+ int kerningDegree_ = KD_None;
|
|
209
|
+ KerningMode kerningMode_ = KM_None;
|
|
210
|
+ FT_Pos trackingKerning_ = 0;
|
|
211
|
+ FT_Matrix matrix_ = {};
|
|
212
|
+ bool matrixEnabled_ = false;
|
|
213
|
+ bool lsbRsbDeltaEnabled_ = true;
|
|
214
|
+
|
|
215
|
+ bool waterfall_ = false;
|
|
216
|
+ double waterfallStart_ = -1;
|
|
217
|
+ double waterfallEnd_ = -1; // -1 = Auto
|
|
218
|
+
|
|
219
|
+ RenderCallback renderCallback_;
|
|
220
|
+ RenderImageCallback renderImageCallback_;
|
|
221
|
+ PreprocessCallback glyphPreprocessCallback_;
|
|
222
|
+ LineBeginCallback lineBeginCallback_;
|
|
223
|
+
|
|
224
|
+ void reloadGlyphIndices(); // for string rendering
|
|
225
|
+ void prepareRendering();
|
|
226
|
+ void loadSingleContext(GlyphContext* ctx, GlyphContext* prev);
|
|
227
|
+ // Need to be called when font, charMap or size changes;
|
|
228
|
+ void loadStringGlyphs();
|
|
229
|
+ // Returns total line count
|
|
230
|
+ int prepareLine(int offset,
|
|
231
|
+ int lineWidth,
|
|
232
|
+ FT_Vector& outActualLineWidth,
|
|
233
|
+ int nonSpacingPlaceholder,
|
|
234
|
+ bool handleMultiLine = false);
|
|
235
|
+ void clearActive(bool glyphOnly = false);
|
|
236
|
+
|
|
237
|
+ int convertCharEncoding(int charUcs4, FT_Encoding encoding);
|
|
238
|
+};
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+// end of stringrenderer.hpp |
src/ftinspect/glyphcomponents/glyphcontinuous.cpp
|
1
|
+// glyphcontinuous.cpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#include "glyphcontinuous.hpp"
|
|
6
|
+
|
|
7
|
+#include "../engine/engine.hpp"
|
|
8
|
+
|
|
9
|
+#include <QPainter>
|
|
10
|
+#include <QWheelEvent>
|
|
11
|
+
|
|
12
|
+#include <freetype/ftbitmap.h>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+GlyphCacheEntry::~GlyphCacheEntry()
|
|
16
|
+{
|
|
17
|
+ delete image;
|
|
18
|
+}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+GlyphCacheEntry::GlyphCacheEntry(GlyphCacheEntry&& other) noexcept
|
|
22
|
+{
|
|
23
|
+ *this = std::move(other);
|
|
24
|
+}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+GlyphCacheEntry&
|
|
28
|
+GlyphCacheEntry::operator=(GlyphCacheEntry&& other) noexcept
|
|
29
|
+{
|
|
30
|
+ if (this == &other)
|
|
31
|
+ return *this;
|
|
32
|
+
|
|
33
|
+ auto oldImage = image;
|
|
34
|
+ image = other.image;
|
|
35
|
+ basePosition = other.basePosition;
|
|
36
|
+ penPos = other.penPos;
|
|
37
|
+ charCode = other.charCode;
|
|
38
|
+ glyphIndex = other.glyphIndex;
|
|
39
|
+ nonSpacingPlaceholder = other.nonSpacingPlaceholder;
|
|
40
|
+ advance = other.advance;
|
|
41
|
+ other.image = oldImage;
|
|
42
|
+ return *this;
|
|
43
|
+}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+GlyphContinuous::GlyphContinuous(QWidget* parent, Engine* engine)
|
|
47
|
+: QWidget(parent),
|
|
48
|
+ engine_(engine),
|
|
49
|
+ stringRenderer_(engine)
|
|
50
|
+{
|
|
51
|
+ setAcceptDrops(false);
|
|
52
|
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
53
|
+
|
|
54
|
+ flashTimer_ = new QTimer(this);
|
|
55
|
+ flashTimer_->setInterval(FlashIntervalMs);
|
|
56
|
+ connect(flashTimer_, &QTimer::timeout,
|
|
57
|
+ this, &GlyphContinuous::flashTimerFired);
|
|
58
|
+
|
|
59
|
+ FT_Stroker_New(engine_->ftLibrary(), &stroker_);
|
|
60
|
+}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+GlyphContinuous::~GlyphContinuous()
|
|
64
|
+{
|
|
65
|
+ FT_Stroker_Done(stroker_);
|
|
66
|
+}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+void
|
|
70
|
+GlyphContinuous::setSource(Source source)
|
|
71
|
+{
|
|
72
|
+ source_ = source;
|
|
73
|
+ switch (source)
|
|
74
|
+ {
|
|
75
|
+ case SRC_AllGlyphs:
|
|
76
|
+ stringRenderer_.setUseAllGlyphs();
|
|
77
|
+ positionDelta_ = {};
|
|
78
|
+ break;
|
|
79
|
+
|
|
80
|
+ case SRC_TextStringRepeated:
|
|
81
|
+ positionDelta_ = {};
|
|
82
|
+ /* fall through */
|
|
83
|
+ case SRC_TextString:
|
|
84
|
+ updateRendererText();
|
|
85
|
+ break;
|
|
86
|
+ }
|
|
87
|
+}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+void
|
|
91
|
+GlyphContinuous::setSourceText(QString text)
|
|
92
|
+{
|
|
93
|
+ text_ = std::move(text);
|
|
94
|
+ updateRendererText();
|
|
95
|
+}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+void
|
|
99
|
+GlyphContinuous::flashOnGlyph(int glyphIndex)
|
|
100
|
+{
|
|
101
|
+ flashTimer_->stop();
|
|
102
|
+
|
|
103
|
+ flashGlyphIndex_ = glyphIndex;
|
|
104
|
+ flashRemainingCount_ = FlashDurationMs / FlashIntervalMs;
|
|
105
|
+ flashTimer_->start();
|
|
106
|
+}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+void
|
|
110
|
+GlyphContinuous::stopFlashing()
|
|
111
|
+{
|
|
112
|
+ flashGlyphIndex_ = -1;
|
|
113
|
+ flashTimer_->stop();
|
|
114
|
+}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+void
|
|
118
|
+GlyphContinuous::purgeCache()
|
|
119
|
+{
|
|
120
|
+ glyphCache_.clear();
|
|
121
|
+ backgroundColorCache_ = engine_->renderingEngine()->background();
|
|
122
|
+ currentWritingLine_ = NULL;
|
|
123
|
+}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+void
|
|
127
|
+GlyphContinuous::resetPositionDelta()
|
|
128
|
+{
|
|
129
|
+ positionDelta_ = {};
|
|
130
|
+ repaint();
|
|
131
|
+}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+void
|
|
135
|
+GlyphContinuous::paintEvent(QPaintEvent* event)
|
|
136
|
+{
|
|
137
|
+ QPainter painter(this);
|
|
138
|
+ painter.fillRect(rect(), backgroundColorCache_);
|
|
139
|
+ painter.scale(scale_, scale_);
|
|
140
|
+
|
|
141
|
+ if (glyphCache_.empty())
|
|
142
|
+ fillCache();
|
|
143
|
+ paintCache(&painter);
|
|
144
|
+}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+void
|
|
148
|
+GlyphContinuous::wheelEvent(QWheelEvent* event)
|
|
149
|
+{
|
|
150
|
+ int numSteps = event->angleDelta().y() / 120;
|
|
151
|
+ if (event->modifiers() & Qt::ShiftModifier)
|
|
152
|
+ emit wheelResize(numSteps);
|
|
153
|
+ else if (event->modifiers() & Qt::ControlModifier)
|
|
154
|
+ emit wheelZoom(numSteps);
|
|
155
|
+ else if (event->modifiers() == 0)
|
|
156
|
+ emit wheelNavigate(-numSteps);
|
|
157
|
+}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+void
|
|
161
|
+GlyphContinuous::resizeEvent(QResizeEvent* event)
|
|
162
|
+{
|
|
163
|
+ purgeCache();
|
|
164
|
+ QWidget::resizeEvent(event);
|
|
165
|
+}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+void
|
|
169
|
+GlyphContinuous::mousePressEvent(QMouseEvent* event)
|
|
170
|
+{
|
|
171
|
+ if (!mouseOperationEnabled_)
|
|
172
|
+ return;
|
|
173
|
+ if (event->button() == Qt::LeftButton)
|
|
174
|
+ {
|
|
175
|
+ prevPositionDelta_ = positionDelta_;
|
|
176
|
+ mouseDownPostition_ = event->pos();
|
|
177
|
+ prevHoriPosition_ = stringRenderer_.position();
|
|
178
|
+ prevIndex_ = beginIndex_;
|
|
179
|
+ // We need to precalculate this value because after the first change of
|
|
180
|
+ // the begin index, the average line count would change. If we don't use the
|
|
181
|
+ // old value, then moving up/down for the same distance would not return
|
|
182
|
+ // to the original index which is confusing.
|
|
183
|
+ averageLineCount_ = calculateAverageLineCount();
|
|
184
|
+ }
|
|
185
|
+}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+void
|
|
189
|
+GlyphContinuous::mouseMoveEvent(QMouseEvent* event)
|
|
190
|
+{
|
|
191
|
+ if (!mouseOperationEnabled_)
|
|
192
|
+ return;
|
|
193
|
+ if (event->buttons() != Qt::LeftButton)
|
|
194
|
+ return;
|
|
195
|
+ auto delta = event->pos() - mouseDownPostition_;
|
|
196
|
+ delta /= scale_;
|
|
197
|
+ if (source_ == SRC_AllGlyphs)
|
|
198
|
+ {
|
|
199
|
+ auto deltaIndex = -delta.x() / HorizontalUnitLength
|
|
200
|
+ - delta.y() / VerticalUnitLength * averageLineCount_;
|
|
201
|
+ if (prevIndex_ + deltaIndex != beginIndex_)
|
|
202
|
+ emit beginIndexChangeRequest(beginIndex_ + deltaIndex);
|
|
203
|
+ }
|
|
204
|
+ else if (source_ == SRC_TextString)
|
|
205
|
+ {
|
|
206
|
+ positionDelta_ = prevPositionDelta_ + delta;
|
|
207
|
+ positionDelta_.setX(0); // Don't move horizontally
|
|
208
|
+
|
|
209
|
+ // but use the renderer
|
|
210
|
+ // purpose for two scale_: one for undoing the `delta /= scale_`
|
|
211
|
+ // the other for effectively dividing width by the scale
|
|
212
|
+ auto horiPos = delta.x() * scale_ * scale_ / static_cast<double>(width());
|
|
213
|
+ horiPos += prevHoriPosition_;
|
|
214
|
+ horiPos = qBound(0.0, horiPos, 1.0);
|
|
215
|
+ stringRenderer_.setPosition(horiPos);
|
|
216
|
+
|
|
217
|
+ purgeCache();
|
|
218
|
+ repaint();
|
|
219
|
+ }
|
|
220
|
+}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+void
|
|
224
|
+GlyphContinuous::paintByRenderer()
|
|
225
|
+{
|
|
226
|
+ purgeCache();
|
|
227
|
+
|
|
228
|
+ stringRenderer_.setRepeated(source_ == SRC_TextStringRepeated);
|
|
229
|
+ stringRenderer_.setCallback(
|
|
230
|
+ [&](FT_Glyph glyph, FT_Vector penPos, GlyphContext& ctx)
|
|
231
|
+ {
|
|
232
|
+ saveSingleGlyph(glyph, penPos, ctx);
|
|
233
|
+ });
|
|
234
|
+ stringRenderer_.setImageCallback(
|
|
235
|
+ [&](QImage* image,
|
|
236
|
+ QRect pos,
|
|
237
|
+ FT_Vector penPos, FT_Vector advance,
|
|
238
|
+ GlyphContext& ctx)
|
|
239
|
+ {
|
|
240
|
+ saveSingleGlyphImage(image, pos, penPos, advance, ctx);
|
|
241
|
+ });
|
|
242
|
+ stringRenderer_.setPreprocessCallback(
|
|
243
|
+ [&](FT_Glyph* ptr)
|
|
244
|
+ {
|
|
245
|
+ preprocessGlyph(ptr);
|
|
246
|
+ });
|
|
247
|
+ stringRenderer_.setLineBeginCallback(
|
|
248
|
+ [&](FT_Vector pos, double size)
|
|
249
|
+ {
|
|
250
|
+ beginSaveLine(pos, size);
|
|
251
|
+ });
|
|
252
|
+ auto count = stringRenderer_.render(static_cast<int>(width() / scale_),
|
|
253
|
+ static_cast<int>(height() / scale_),
|
|
254
|
+ beginIndex_);
|
|
255
|
+ if (source_ == SRC_AllGlyphs)
|
|
256
|
+ displayingCount_ = count;
|
|
257
|
+ else
|
|
258
|
+ displayingCount_ = 0;
|
|
259
|
+}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+void
|
|
263
|
+GlyphContinuous::transformGlyphFancy(FT_Glyph glyph)
|
|
264
|
+{
|
|
265
|
+ auto& metrics = engine_->currentFontMetrics();
|
|
266
|
+ auto emboldeningX = (FT_Pos)(metrics.y_ppem * 64 * boldX_);
|
|
267
|
+ auto emboldeningY = (FT_Pos)(metrics.y_ppem * 64 * boldY_);
|
|
268
|
+ // adopted from ftview.c:289
|
|
269
|
+ if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
|
|
270
|
+ {
|
|
271
|
+ auto outline = reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
|
|
272
|
+ FT_Glyph_Transform(glyph, &shearMatrix_, NULL);
|
|
273
|
+ if (FT_Outline_EmboldenXY(&outline, emboldeningX, emboldeningY))
|
|
274
|
+ {
|
|
275
|
+ // XXX error handling?
|
|
276
|
+ return;
|
|
277
|
+ }
|
|
278
|
+
|
|
279
|
+ if (glyph->advance.x)
|
|
280
|
+ glyph->advance.x += emboldeningX;
|
|
281
|
+
|
|
282
|
+ if (glyph->advance.y)
|
|
283
|
+ glyph->advance.y += emboldeningY;
|
|
284
|
+ }
|
|
285
|
+ else if (glyph->format == FT_GLYPH_FORMAT_BITMAP)
|
|
286
|
+ {
|
|
287
|
+ auto xstr = emboldeningX & ~63;
|
|
288
|
+ auto ystr = emboldeningY & ~63;
|
|
289
|
+
|
|
290
|
+ auto bitmap = &reinterpret_cast<FT_BitmapGlyph>(glyph)->bitmap;
|
|
291
|
+ // No shearing support for bitmap
|
|
292
|
+ FT_Bitmap_Embolden(engine_->ftLibrary(), bitmap,
|
|
293
|
+ xstr, ystr);
|
|
294
|
+ }
|
|
295
|
+ else
|
|
296
|
+ return; // XXX no support for SVG
|
|
297
|
+}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+FT_Glyph
|
|
301
|
+GlyphContinuous::transformGlyphStroked(FT_Glyph glyph)
|
|
302
|
+{
|
|
303
|
+ // Well, here only outline glyph is supported.
|
|
304
|
+ if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
|
|
305
|
+ return NULL;
|
|
306
|
+ auto error = FT_Glyph_Stroke(&glyph, stroker_, 0);
|
|
307
|
+ if (error)
|
|
308
|
+ return NULL;
|
|
309
|
+ return glyph;
|
|
310
|
+}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+void
|
|
314
|
+GlyphContinuous::paintCache(QPainter* painter)
|
|
315
|
+{
|
|
316
|
+ bool flashFlipFlop = false;
|
|
317
|
+ if (flashRemainingCount_ >= 0)
|
|
318
|
+ {
|
|
319
|
+ if (flashGlyphIndex_ >= 0) // only flash when the glyph index valid
|
|
320
|
+ flashFlipFlop = flashRemainingCount_ % 2 == 1;
|
|
321
|
+ else
|
|
322
|
+ {
|
|
323
|
+ flashTimer_->stop();
|
|
324
|
+ flashRemainingCount_ = 0;
|
|
325
|
+ }
|
|
326
|
+ flashRemainingCount_--;
|
|
327
|
+ }
|
|
328
|
+ else if (flashGlyphIndex_ >= 0)
|
|
329
|
+ {
|
|
330
|
+ flashGlyphIndex_ = -1;
|
|
331
|
+ flashTimer_->stop();
|
|
332
|
+ }
|
|
333
|
+
|
|
334
|
+ if (stringRenderer_.isWaterfall())
|
|
335
|
+ positionDelta_.setY(0);
|
|
336
|
+ for (auto& line : glyphCache_)
|
|
337
|
+ {
|
|
338
|
+ beginDrawCacheLine(painter, line);
|
|
339
|
+ for (auto& glyph : line.entries)
|
|
340
|
+ {
|
|
341
|
+ if (glyph.glyphIndex == flashGlyphIndex_ && flashFlipFlop)
|
|
342
|
+ drawCacheGlyph(painter, glyph, true);
|
|
343
|
+ else
|
|
344
|
+ drawCacheGlyph(painter, glyph);
|
|
345
|
+ }
|
|
346
|
+ }
|
|
347
|
+}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+void
|
|
351
|
+GlyphContinuous::fillCache()
|
|
352
|
+{
|
|
353
|
+ prePaint();
|
|
354
|
+ paintByRenderer();
|
|
355
|
+ emit displayingCountUpdated(displayingCount_);
|
|
356
|
+}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+void
|
|
360
|
+GlyphContinuous::prePaint()
|
|
361
|
+{
|
|
362
|
+ displayingCount_ = 0;
|
|
363
|
+
|
|
364
|
+ // Used by fancy:
|
|
365
|
+ // adopted from ftview.c:289
|
|
366
|
+ /***************************************************************/
|
|
367
|
+ /* */
|
|
368
|
+ /* 2*2 affine transformation matrix, 16.16 fixed float format */
|
|
369
|
+ /* */
|
|
370
|
+ /* Shear matrix: */
|
|
371
|
+ /* */
|
|
372
|
+ /* | x' | | 1 k | | x | x' = x + ky */
|
|
373
|
+ /* | | = | | * | | <==> */
|
|
374
|
+ /* | y' | | 0 1 | | y | y' = y */
|
|
375
|
+ /* */
|
|
376
|
+ /* outline' shear outline */
|
|
377
|
+ /* */
|
|
378
|
+ /***************************************************************/
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+ shearMatrix_.xx = 1 << 16;
|
|
382
|
+ shearMatrix_.xy = static_cast<FT_Fixed>(slant_ * (1 << 16));
|
|
383
|
+ shearMatrix_.yx = 0;
|
|
384
|
+ shearMatrix_.yy = 1 << 16;
|
|
385
|
+}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+void
|
|
389
|
+GlyphContinuous::updateStroke()
|
|
390
|
+{
|
|
391
|
+ if (mode_ != M_Stroked || !engine_->renderReady())
|
|
392
|
+ return;
|
|
393
|
+
|
|
394
|
+ auto& metrics = engine_->currentFontMetrics();
|
|
395
|
+ auto radius = static_cast<FT_Fixed>(metrics.y_ppem * 64 * strokeRadius_);
|
|
396
|
+ strokeRadiusForSize_ = radius;
|
|
397
|
+ FT_Stroker_Set(stroker_, radius,
|
|
398
|
+ FT_STROKER_LINECAP_ROUND,
|
|
399
|
+ FT_STROKER_LINEJOIN_ROUND,
|
|
400
|
+ 0);
|
|
401
|
+}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+void
|
|
405
|
+GlyphContinuous::updateRendererText()
|
|
406
|
+{
|
|
407
|
+ stringRenderer_.setUseString(text_); // TODO this need to be called when font,
|
|
408
|
+ // size or charmap change
|
|
409
|
+}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+void
|
|
413
|
+GlyphContinuous::preprocessGlyph(FT_Glyph* glyphPtr)
|
|
414
|
+{
|
|
415
|
+ auto glyph = *glyphPtr;
|
|
416
|
+ switch (mode_)
|
|
417
|
+ {
|
|
418
|
+ case M_Fancy:
|
|
419
|
+ transformGlyphFancy(glyph);
|
|
420
|
+ break;
|
|
421
|
+ case M_Stroked:
|
|
422
|
+ {
|
|
423
|
+ auto stroked = transformGlyphStroked(glyph);
|
|
424
|
+ if (stroked)
|
|
425
|
+ {
|
|
426
|
+ FT_Done_Glyph(glyph);
|
|
427
|
+ *glyphPtr = stroked;
|
|
428
|
+ }
|
|
429
|
+ }
|
|
430
|
+ break;
|
|
431
|
+ default:; // Nothing for M_NORMAL.
|
|
432
|
+ }
|
|
433
|
+}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+void
|
|
437
|
+GlyphContinuous::beginSaveLine(FT_Vector pos,
|
|
438
|
+ double sizePoint)
|
|
439
|
+{
|
|
440
|
+ glyphCache_.emplace_back();
|
|
441
|
+ currentWritingLine_ = &glyphCache_.back();
|
|
442
|
+ currentWritingLine_->nonSpacingPlaceholder
|
|
443
|
+ = engine_->currentFontMetrics().y_ppem / 2;
|
|
444
|
+ currentWritingLine_->sizePoint = sizePoint;
|
|
445
|
+ currentWritingLine_->basePosition = { static_cast<int>(pos.x),
|
|
446
|
+ static_cast<int>(pos.y) };
|
|
447
|
+}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+void
|
|
451
|
+GlyphContinuous::saveSingleGlyph(FT_Glyph glyph,
|
|
452
|
+ FT_Vector penPos,
|
|
453
|
+ GlyphContext gctx)
|
|
454
|
+{
|
|
455
|
+ if (!currentWritingLine_)
|
|
456
|
+ return;
|
|
457
|
+
|
|
458
|
+ QRect rect;
|
|
459
|
+ QImage* image = engine_->renderingEngine()->convertGlyphToQImage(glyph,
|
|
460
|
+ &rect,
|
|
461
|
+ true);
|
|
462
|
+ saveSingleGlyphImage(image, rect, penPos, glyph->advance, gctx);
|
|
463
|
+}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+void
|
|
467
|
+GlyphContinuous::saveSingleGlyphImage(QImage* image,
|
|
468
|
+ QRect rect,
|
|
469
|
+ FT_Vector penPos,
|
|
470
|
+ FT_Vector advance,
|
|
471
|
+ GlyphContext gctx)
|
|
472
|
+{
|
|
473
|
+ if (!currentWritingLine_)
|
|
474
|
+ return;
|
|
475
|
+
|
|
476
|
+ currentWritingLine_->entries.emplace_back();
|
|
477
|
+ auto& entry = currentWritingLine_->entries.back();
|
|
478
|
+
|
|
479
|
+ QPoint penPosPoint = { static_cast<int>(penPos.x),
|
|
480
|
+ static_cast<int>(penPos.y) };
|
|
481
|
+
|
|
482
|
+ rect.translate(penPosPoint);
|
|
483
|
+
|
|
484
|
+ entry.image = image;
|
|
485
|
+ entry.basePosition = rect;
|
|
486
|
+ entry.charCode = gctx.charCode;
|
|
487
|
+ entry.glyphIndex = gctx.glyphIndex;
|
|
488
|
+ entry.advance = advance;
|
|
489
|
+ entry.penPos = penPosPoint;
|
|
490
|
+ entry.nonSpacingPlaceholder = currentWritingLine_->nonSpacingPlaceholder;
|
|
491
|
+}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+void
|
|
495
|
+GlyphContinuous::beginDrawCacheLine(QPainter* painter,
|
|
496
|
+ GlyphCacheLine& line)
|
|
497
|
+{
|
|
498
|
+ // Now only used by waterfall mode to draw a size indicator.
|
|
499
|
+ if (!stringRenderer_.isWaterfall())
|
|
500
|
+ {
|
|
501
|
+ sizeIndicatorOffset_ = 0;
|
|
502
|
+ return;
|
|
503
|
+ }
|
|
504
|
+
|
|
505
|
+ auto oldFont = painter->font();
|
|
506
|
+ oldFont.setPointSizeF(line.sizePoint);
|
|
507
|
+ painter->setFont(oldFont);
|
|
508
|
+ auto metrics = painter->fontMetrics();
|
|
509
|
+
|
|
510
|
+ auto printSize = line.sizePoint;
|
|
511
|
+ if (engine_->currentFontBitmapOnly())
|
|
512
|
+ printSize = printSize * engine_->dpi() / 72.0; // convert back
|
|
513
|
+ auto sizePrefix = QString("%1: ").arg(printSize);
|
|
514
|
+ painter->drawText(line.basePosition, sizePrefix);
|
|
515
|
+
|
|
516
|
+ sizeIndicatorOffset_ = metrics.horizontalAdvance(sizePrefix);
|
|
517
|
+ line.sizeIndicatorOffset = sizeIndicatorOffset_;
|
|
518
|
+}
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+void
|
|
522
|
+GlyphContinuous::drawCacheGlyph(QPainter* painter,
|
|
523
|
+ const GlyphCacheEntry& entry,
|
|
524
|
+ bool colorInverted)
|
|
525
|
+{
|
|
526
|
+ // ftview.c:557
|
|
527
|
+ // Well, metrics is also part of the cache...
|
|
528
|
+ int width = entry.advance.x ? entry.advance.x >> 16
|
|
529
|
+ : entry.nonSpacingPlaceholder;
|
|
530
|
+
|
|
531
|
+ if (entry.advance.x == 0
|
|
532
|
+ && !stringRenderer_.isWaterfall()
|
|
533
|
+ && source_ == SRC_AllGlyphs)
|
|
534
|
+ {
|
|
535
|
+ // Draw a red square to indicate non-spacing glyphs
|
|
536
|
+ auto squarePoint = entry.penPos;
|
|
537
|
+ squarePoint.setY(squarePoint.y() - width);
|
|
538
|
+ auto rect = QRect(squarePoint, QSize(width, width));
|
|
539
|
+ painter->fillRect(rect, Qt::red);
|
|
540
|
+ }
|
|
541
|
+
|
|
542
|
+ QRect rect = entry.basePosition;
|
|
543
|
+ rect.moveLeft(rect.x() + sizeIndicatorOffset_);
|
|
544
|
+ rect.translate(positionDelta_);
|
|
545
|
+
|
|
546
|
+ if (colorInverted)
|
|
547
|
+ {
|
|
548
|
+ auto inverted = entry.image->copy();
|
|
549
|
+ inverted.invertPixels();
|
|
550
|
+ painter->drawImage(rect.topLeft(), inverted);
|
|
551
|
+ }
|
|
552
|
+ else
|
|
553
|
+ painter->drawImage(rect.topLeft(), *entry.image);
|
|
554
|
+}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+GlyphCacheEntry*
|
|
558
|
+GlyphContinuous::findGlyphByMouse(QPoint position,
|
|
559
|
+ double* outSizePoint)
|
|
560
|
+{
|
|
561
|
+ position -= positionDelta_;
|
|
562
|
+ position /= scale_;
|
|
563
|
+ for (auto& line : glyphCache_)
|
|
564
|
+ for (auto& entry : line.entries)
|
|
565
|
+ {
|
|
566
|
+ auto rect = entry.basePosition;
|
|
567
|
+ rect.moveLeft(rect.x() + line.sizeIndicatorOffset);
|
|
568
|
+ if (rect.contains(position))
|
|
569
|
+ {
|
|
570
|
+ if (outSizePoint)
|
|
571
|
+ *outSizePoint = line.sizePoint;
|
|
572
|
+ return &entry;
|
|
573
|
+ }
|
|
574
|
+ }
|
|
575
|
+ return NULL;
|
|
576
|
+}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+int
|
|
580
|
+GlyphContinuous::calculateAverageLineCount()
|
|
581
|
+{
|
|
582
|
+ int averageLineCount = 0;
|
|
583
|
+ for (auto& line : glyphCache_)
|
|
584
|
+ {
|
|
585
|
+ // line.entries.size must < INT_MAX because the total glyph count in
|
|
586
|
+ // the renderer is below that
|
|
587
|
+ averageLineCount += static_cast<int>(line.entries.size());
|
|
588
|
+ }
|
|
589
|
+ if (!glyphCache_.empty())
|
|
590
|
+ averageLineCount /= static_cast<int>(glyphCache_.size());
|
|
591
|
+ return averageLineCount;
|
|
592
|
+}
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+void
|
|
596
|
+GlyphContinuous::flashTimerFired()
|
|
597
|
+{
|
|
598
|
+ repaint();
|
|
599
|
+}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+// end of glyphcontinuous.cpp |
src/ftinspect/glyphcomponents/glyphcontinuous.hpp
|
1
|
+// glyphcontinuous.hpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#pragma once
|
|
6
|
+
|
|
7
|
+#include "graphicsdefault.hpp"
|
|
8
|
+#include "../engine/stringrenderer.hpp"
|
|
9
|
+
|
|
10
|
+#include <utility>
|
|
11
|
+#include <vector>
|
|
12
|
+
|
|
13
|
+#include <QWidget>
|
|
14
|
+#include <QImage>
|
|
15
|
+#include <QTimer>
|
|
16
|
+
|
|
17
|
+#include <freetype/freetype.h>
|
|
18
|
+#include <freetype/ftglyph.h>
|
|
19
|
+#include <freetype/ftoutln.h>
|
|
20
|
+#include <freetype/ftstroke.h>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+// We store images in the cache so we don't need to render all glyphs every time
|
|
24
|
+// when repainting the widget.
|
|
25
|
+struct GlyphCacheEntry
|
|
26
|
+{
|
|
27
|
+ QImage* image = NULL;
|
|
28
|
+ QRect basePosition = {};
|
|
29
|
+ QPoint penPos = {};
|
|
30
|
+ int charCode = -1;
|
|
31
|
+ int glyphIndex = -1;
|
|
32
|
+ unsigned nonSpacingPlaceholder = 0;
|
|
33
|
+
|
|
34
|
+ FT_Vector advance = {};
|
|
35
|
+
|
|
36
|
+ GlyphCacheEntry() {}
|
|
37
|
+ ~GlyphCacheEntry();
|
|
38
|
+ GlyphCacheEntry(const GlyphCacheEntry& other) = delete;
|
|
39
|
+ GlyphCacheEntry& operator=(const GlyphCacheEntry& other) = delete;
|
|
40
|
+ GlyphCacheEntry(GlyphCacheEntry&& other) noexcept;
|
|
41
|
+ GlyphCacheEntry& operator=(GlyphCacheEntry&& other) noexcept;
|
|
42
|
+};
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+struct GlyphCacheLine
|
|
46
|
+{
|
|
47
|
+ QPoint basePosition = {};
|
|
48
|
+ double sizePoint = 0.0;
|
|
49
|
+ int sizeIndicatorOffset;
|
|
50
|
+ unsigned short nonSpacingPlaceholder;
|
|
51
|
+ std::vector<GlyphCacheEntry> entries;
|
|
52
|
+};
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+class Engine;
|
|
56
|
+class GlyphContinuous
|
|
57
|
+: public QWidget
|
|
58
|
+{
|
|
59
|
+ Q_OBJECT
|
|
60
|
+public:
|
|
61
|
+ GlyphContinuous(QWidget* parent, Engine* engine);
|
|
62
|
+ ~GlyphContinuous() override;
|
|
63
|
+
|
|
64
|
+ enum Source : int
|
|
65
|
+ {
|
|
66
|
+ SRC_AllGlyphs,
|
|
67
|
+ SRC_TextString,
|
|
68
|
+ SRC_TextStringRepeated
|
|
69
|
+ };
|
|
70
|
+
|
|
71
|
+ enum Mode : int
|
|
72
|
+ {
|
|
73
|
+ M_Normal,
|
|
74
|
+ M_Fancy,
|
|
75
|
+ M_Stroked
|
|
76
|
+ };
|
|
77
|
+
|
|
78
|
+ int displayingCount() { return displayingCount_; }
|
|
79
|
+ StringRenderer& stringRenderer() { return stringRenderer_; }
|
|
80
|
+
|
|
81
|
+ // all those setters don't trigger repaint.
|
|
82
|
+ void setBeginIndex(int index) { beginIndex_ = index; }
|
|
83
|
+ void setSource(Source source);
|
|
84
|
+ void setMode(Mode mode) { mode_ = mode; }
|
|
85
|
+ void setScale(double scale) { scale_ = scale; }
|
|
86
|
+ void setFancyParams(double boldX, double boldY, double slant)
|
|
87
|
+ {
|
|
88
|
+ boldX_ = boldX;
|
|
89
|
+ boldY_ = boldY;
|
|
90
|
+ slant_ = slant;
|
|
91
|
+ }
|
|
92
|
+ void setStrokeRadius(double radius) { strokeRadius_ = radius; }
|
|
93
|
+ void setSourceText(QString text);
|
|
94
|
+ void setMouseOperationEnabled(bool enabled)
|
|
95
|
+ {
|
|
96
|
+ mouseOperationEnabled_ = enabled;
|
|
97
|
+ }
|
|
98
|
+
|
|
99
|
+ void flashOnGlyph(int glyphIndex);
|
|
100
|
+ void stopFlashing();
|
|
101
|
+ void purgeCache();
|
|
102
|
+ void resetPositionDelta();
|
|
103
|
+
|
|
104
|
+signals:
|
|
105
|
+ void wheelNavigate(int steps);
|
|
106
|
+ void wheelResize(int steps);
|
|
107
|
+ void wheelZoom(int steps);
|
|
108
|
+ void beginIndexChangeRequest(int newIndex);
|
|
109
|
+ void displayingCountUpdated(int newCount);
|
|
110
|
+
|
|
111
|
+protected:
|
|
112
|
+ void paintEvent(QPaintEvent* event) override;
|
|
113
|
+ void wheelEvent(QWheelEvent* event) override;
|
|
114
|
+ void resizeEvent(QResizeEvent* event) override;
|
|
115
|
+ void mousePressEvent(QMouseEvent* event) override;
|
|
116
|
+ void mouseMoveEvent(QMouseEvent* event) override;
|
|
117
|
+
|
|
118
|
+private:
|
|
119
|
+ Engine* engine_;
|
|
120
|
+ StringRenderer stringRenderer_;
|
|
121
|
+
|
|
122
|
+ QTimer* flashTimer_;
|
|
123
|
+ int flashRemainingCount_ = 0;
|
|
124
|
+ int flashGlyphIndex_ = -1;
|
|
125
|
+
|
|
126
|
+ Source source_ = SRC_AllGlyphs;
|
|
127
|
+ Mode mode_ = M_Normal;
|
|
128
|
+ int beginIndex_;
|
|
129
|
+ double boldX_, boldY_, slant_;
|
|
130
|
+ double strokeRadius_;
|
|
131
|
+ QString text_;
|
|
132
|
+ int sizeIndicatorOffset_ = 0; // For Waterfall Rendering...
|
|
133
|
+
|
|
134
|
+ bool mouseOperationEnabled_ = true;
|
|
135
|
+ int displayingCount_ = 0;
|
|
136
|
+ FT_Fixed strokeRadiusForSize_ = 0;
|
|
137
|
+ double scale_ = 1.0;
|
|
138
|
+ FT_Matrix shearMatrix_;
|
|
139
|
+
|
|
140
|
+ FT_Stroker stroker_;
|
|
141
|
+
|
|
142
|
+ std::vector<GlyphCacheLine> glyphCache_;
|
|
143
|
+ QColor backgroundColorCache_;
|
|
144
|
+ GlyphCacheLine* currentWritingLine_ = NULL;
|
|
145
|
+
|
|
146
|
+ // Mouse operation related fields
|
|
147
|
+ QPoint positionDelta_; // For dragging on the text to move
|
|
148
|
+ double prevHoriPosition_;
|
|
149
|
+ QPoint prevPositionDelta_ = { 0, 0 };
|
|
150
|
+ QPoint mouseDownPostition_ = { 0, 0 };
|
|
151
|
+ int prevIndex_ = -1;
|
|
152
|
+ int averageLineCount_ = 0;
|
|
153
|
+
|
|
154
|
+ void paintByRenderer();
|
|
155
|
+
|
|
156
|
+ // These two assume ownership of glyphs, but don't free them.
|
|
157
|
+ // However, remember to free the glyph returned from `transformGlyphStroked`
|
|
158
|
+ void transformGlyphFancy(FT_Glyph glyph);
|
|
159
|
+ FT_Glyph transformGlyphStroked(FT_Glyph glyph);
|
|
160
|
+
|
|
161
|
+ void paintCache(QPainter* painter);
|
|
162
|
+ void fillCache();
|
|
163
|
+ void prePaint();
|
|
164
|
+ void updateStroke();
|
|
165
|
+ void updateRendererText();
|
|
166
|
+ void preprocessGlyph(FT_Glyph* glyphPtr);
|
|
167
|
+ // Callbacks
|
|
168
|
+ void beginSaveLine(FT_Vector pos,
|
|
169
|
+ double sizePoint);
|
|
170
|
+ void saveSingleGlyph(FT_Glyph glyph,
|
|
171
|
+ FT_Vector penPos,
|
|
172
|
+ GlyphContext gctx);
|
|
173
|
+ void saveSingleGlyphImage(QImage* image,
|
|
174
|
+ QRect rect,
|
|
175
|
+ FT_Vector penPos,
|
|
176
|
+ FT_Vector advance,
|
|
177
|
+ GlyphContext gctx);
|
|
178
|
+
|
|
179
|
+ // Funcs drawing from the cache
|
|
180
|
+ void beginDrawCacheLine(QPainter* painter,
|
|
181
|
+ GlyphCacheLine& line);
|
|
182
|
+ void drawCacheGlyph(QPainter* painter,
|
|
183
|
+ const GlyphCacheEntry& entry,
|
|
184
|
+ bool colorInverted = false);
|
|
185
|
+
|
|
186
|
+ // Mouse operations
|
|
187
|
+ GlyphCacheEntry* findGlyphByMouse(QPoint position,
|
|
188
|
+ double* outSizePoint);
|
|
189
|
+ int calculateAverageLineCount();
|
|
190
|
+
|
|
191
|
+ void flashTimerFired();
|
|
192
|
+
|
|
193
|
+ // Mouse constants
|
|
194
|
+ constexpr static int ClickDragThreshold = 10;
|
|
195
|
+ constexpr static int HorizontalUnitLength = 100;
|
|
196
|
+ constexpr static int VerticalUnitLength = 150;
|
|
197
|
+
|
|
198
|
+ // Flash Timer constants
|
|
199
|
+ constexpr static int FlashIntervalMs = 250;
|
|
200
|
+ constexpr static int FlashDurationMs = 3000;
|
|
201
|
+};
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+// end of glyphcontinuous.hpp |
src/ftinspect/maingui.cpp
... |
... |
@@ -164,8 +164,14 @@ MainGUI::onTripletChanged() |
164
|
164
|
void
|
165
|
165
|
MainGUI::switchTab()
|
166
|
166
|
{
|
|
167
|
+ auto current = tabWidget_->currentWidget();
|
167
|
168
|
reloadCurrentTabFont();
|
168
|
|
- lastTab_ = tabWidget_->currentWidget();
|
|
169
|
+
|
|
170
|
+ if (current == continuousTab_ && lastTab_ == singularTab_
|
|
171
|
+ && singularTab_->currentGlyph() >= 0)
|
|
172
|
+ continuousTab_->highlightGlyph(singularTab_->currentGlyph());
|
|
173
|
+
|
|
174
|
+ lastTab_ = current;
|
169
|
175
|
}
|
170
|
176
|
|
171
|
177
|
|
... |
... |
@@ -215,6 +221,7 @@ MainGUI::createLayout() |
215
|
221
|
|
216
|
222
|
// right side
|
217
|
223
|
singularTab_ = new SingularTab(this, engine_);
|
|
224
|
+ continuousTab_ = new ContinuousTab(this, engine_);
|
218
|
225
|
|
219
|
226
|
tabWidget_ = new QTabWidget(this);
|
220
|
227
|
tabWidget_->setObjectName("mainTab"); // for stylesheet
|
... |
... |
@@ -222,10 +229,15 @@ MainGUI::createLayout() |
222
|
229
|
// Note those two list must be in sync
|
223
|
230
|
tabs_.push_back(singularTab_);
|
224
|
231
|
tabWidget_->addTab(singularTab_, tr("Singular Grid View"));
|
|
232
|
+ tabs_.push_back(continuousTab_);
|
|
233
|
+ tabWidget_->addTab(continuousTab_, tr("Continuous View"));
|
225
|
234
|
lastTab_ = singularTab_;
|
226
|
235
|
|
227
|
236
|
tabWidget_->setTabToolTip(0, tr("View single glyph in grid view.\n"
|
228
|
237
|
"For pixelwise inspection of the glyphs."));
|
|
238
|
+ tabWidget_->setTabToolTip(1, tr("View a string of glyphs continuously.\n"
|
|
239
|
+ "Show all glyphs in the font or render "
|
|
240
|
+ "strings."));
|
229
|
241
|
tripletSelector_ = new TripletSelector(this, engine_);
|
230
|
242
|
|
231
|
243
|
rightLayout_ = new QVBoxLayout;
|
src/ftinspect/maingui.hpp
... |
... |
@@ -10,6 +10,7 @@ |
10
|
10
|
#include "panels/settingpanel.hpp"
|
11
|
11
|
#include "panels/abstracttab.hpp"
|
12
|
12
|
#include "panels/singular.hpp"
|
|
13
|
+#include "panels/continuous.hpp"
|
13
|
14
|
|
14
|
15
|
#include <vector>
|
15
|
16
|
#include <QAction>
|
... |
... |
@@ -88,6 +89,7 @@ private: |
88
|
89
|
QTabWidget* tabWidget_;
|
89
|
90
|
std::vector<AbstractTab*> tabs_;
|
90
|
91
|
SingularTab* singularTab_;
|
|
92
|
+ ContinuousTab* continuousTab_;
|
91
|
93
|
QWidget* lastTab_ = NULL;
|
92
|
94
|
|
93
|
95
|
void openFonts(QStringList const& fileNames);
|
src/ftinspect/meson.build
... |
... |
@@ -23,28 +23,40 @@ if qt5_dep.found() |
23
|
23
|
sources = files([
|
24
|
24
|
'engine/engine.cpp',
|
25
|
25
|
'engine/fontfilemanager.cpp',
|
26
|
|
- 'engine/rendering.cpp',
|
|
26
|
+ 'engine/charmap.cpp',
|
27
|
27
|
'engine/paletteinfo.cpp',
|
28
|
|
- 'engine/mmgx.cpp',
|
|
28
|
+ 'engine/stringrenderer.cpp',
|
29
|
29
|
'engine/fontinfo.cpp',
|
|
30
|
+ 'engine/fontinfonamesmapping.cpp',
|
|
31
|
+ 'engine/mmgx.cpp',
|
|
32
|
+ 'engine/rendering.cpp',
|
|
33
|
+ 'engine/renderutils.cpp',
|
|
34
|
+ 'engine/charmap.cpp',
|
30
|
35
|
|
31
|
36
|
'glyphcomponents/glyphbitmap.cpp',
|
32
|
37
|
'glyphcomponents/glyphoutline.cpp',
|
33
|
38
|
'glyphcomponents/glyphpointnumbers.cpp',
|
34
|
39
|
'glyphcomponents/glyphpoints.cpp',
|
|
40
|
+ 'glyphcomponents/glyphcontinuous.cpp',
|
35
|
41
|
'glyphcomponents/grid.cpp',
|
36
|
42
|
'glyphcomponents/graphicsdefault.cpp',
|
37
|
43
|
|
38
|
44
|
'widgets/customwidgets.cpp',
|
39
|
|
- 'widgets/tripletselector.cpp',
|
40
|
45
|
'widgets/glyphindexselector.cpp',
|
41
|
46
|
'widgets/fontsizeselector.cpp',
|
|
47
|
+ 'widgets/tripletselector.cpp',
|
|
48
|
+ 'widgets/charmapcombobox.cpp',
|
42
|
49
|
|
43
|
50
|
'models/customcomboboxmodels.cpp',
|
|
51
|
+ 'models/fontinfomodels.cpp',
|
44
|
52
|
|
45
|
53
|
'panels/settingpanel.cpp',
|
46
|
54
|
'panels/settingpanelmmgx.cpp',
|
47
|
55
|
'panels/singular.cpp',
|
|
56
|
+ 'panels/continuous.cpp',
|
|
57
|
+ 'panels/comparator.cpp',
|
|
58
|
+ 'panels/glyphdetails.cpp',
|
|
59
|
+ 'panels/info.cpp',
|
48
|
60
|
|
49
|
61
|
'ftinspect.cpp',
|
50
|
62
|
'maingui.cpp',
|
... |
... |
@@ -55,14 +67,22 @@ if qt5_dep.found() |
55
|
67
|
moc_headers: [
|
56
|
68
|
'engine/fontfilemanager.hpp',
|
57
|
69
|
'widgets/customwidgets.hpp',
|
58
|
|
- 'widgets/tripletselector.hpp',
|
59
|
70
|
'widgets/glyphindexselector.hpp',
|
60
|
71
|
'widgets/fontsizeselector.hpp',
|
61
|
|
- 'maingui.hpp',
|
|
72
|
+ 'widgets/tripletselector.hpp',
|
|
73
|
+ 'widgets/charmapcombobox.hpp',
|
|
74
|
+ 'glyphcomponents/glyphbitmap.hpp',
|
|
75
|
+ 'glyphcomponents/glyphcontinuous.hpp',
|
62
|
76
|
'models/customcomboboxmodels.hpp',
|
|
77
|
+ 'models/fontinfomodels.hpp',
|
63
|
78
|
'panels/settingpanel.hpp',
|
64
|
79
|
'panels/settingpanelmmgx.hpp',
|
65
|
80
|
'panels/singular.hpp',
|
|
81
|
+ 'panels/continuous.hpp',
|
|
82
|
+ 'panels/comparator.hpp',
|
|
83
|
+ 'panels/glyphdetails.hpp',
|
|
84
|
+ 'panels/info.hpp',
|
|
85
|
+ 'maingui.hpp',
|
66
|
86
|
],
|
67
|
87
|
dependencies: qt5_dep)
|
68
|
88
|
|
src/ftinspect/panels/continuous.cpp
|
1
|
+// continuous.cpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#include "continuous.hpp"
|
|
6
|
+
|
|
7
|
+#include "../uihelper.hpp"
|
|
8
|
+
|
|
9
|
+#include <climits>
|
|
10
|
+#include <QToolTip>
|
|
11
|
+#include <QVariant>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+ContinuousTab::ContinuousTab(QWidget* parent,
|
|
15
|
+ Engine* engine)
|
|
16
|
+: QWidget(parent),
|
|
17
|
+ engine_(engine)
|
|
18
|
+{
|
|
19
|
+ createLayout();
|
|
20
|
+
|
|
21
|
+ std::vector<CharMapInfo> tempCharMaps;
|
|
22
|
+ charMapSelector_->repopulate(tempCharMaps); // pass in an empty one
|
|
23
|
+
|
|
24
|
+ checkModeSource();
|
|
25
|
+ setDefaults();
|
|
26
|
+
|
|
27
|
+ createConnections();
|
|
28
|
+}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+void
|
|
32
|
+ContinuousTab::repaintGlyph()
|
|
33
|
+{
|
|
34
|
+ sizeSelector_->applyToEngine(engine_);
|
|
35
|
+
|
|
36
|
+ applySettings();
|
|
37
|
+ canvas_->stopFlashing();
|
|
38
|
+ canvas_->purgeCache();
|
|
39
|
+ canvas_->repaint();
|
|
40
|
+}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+void
|
|
44
|
+ContinuousTab::reloadFont()
|
|
45
|
+{
|
|
46
|
+ currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
|
|
47
|
+ {
|
|
48
|
+ QSignalBlocker blocker(sizeSelector_);
|
|
49
|
+ sizeSelector_->reloadFromFont(engine_);
|
|
50
|
+ }
|
|
51
|
+ setGlyphCount(qBound(0, currentGlyphCount_, INT_MAX));
|
|
52
|
+ checkModeSource();
|
|
53
|
+
|
|
54
|
+ charMapSelector_->repopulate();
|
|
55
|
+ canvas_->stopFlashing();
|
|
56
|
+ canvas_->stringRenderer().reloadAll();
|
|
57
|
+ canvas_->purgeCache();
|
|
58
|
+ repaintGlyph();
|
|
59
|
+}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+void
|
|
63
|
+ContinuousTab::applySettings()
|
|
64
|
+{
|
|
65
|
+ auto mode = static_cast<GlyphContinuous::Mode>(modeSelector_->currentIndex());
|
|
66
|
+ auto src
|
|
67
|
+ = static_cast<GlyphContinuous::Source>(sourceSelector_->currentIndex());
|
|
68
|
+ canvas_->setMode(mode);
|
|
69
|
+ canvas_->setSource(src);
|
|
70
|
+ canvas_->setBeginIndex(indexSelector_->currentIndex());
|
|
71
|
+ canvas_->setScale(sizeSelector_->zoomFactor());
|
|
72
|
+ auto& sr = canvas_->stringRenderer();
|
|
73
|
+ sr.setWaterfall(waterfallCheckBox_->isChecked());
|
|
74
|
+ sr.setVertical(verticalCheckBox_->isChecked());
|
|
75
|
+ sr.setKerning(kerningCheckBox_->isChecked());
|
|
76
|
+ sr.setRotation(rotationSpinBox_->value());
|
|
77
|
+
|
|
78
|
+ // -1: Glyph order, otherwise the char map index in the original list
|
|
79
|
+ sr.setCharMapIndex(charMapSelector_->currentCharMapIndex(), glyphLimitIndex_);
|
|
80
|
+
|
|
81
|
+ if (sr.isWaterfall())
|
|
82
|
+ sr.setWaterfallParameters(wfConfigDialog_->startSize(),
|
|
83
|
+ wfConfigDialog_->endSize());
|
|
84
|
+
|
|
85
|
+ canvas_->setFancyParams(xEmboldeningSpinBox_->value(),
|
|
86
|
+ yEmboldeningSpinBox_->value(),
|
|
87
|
+ slantSpinBox_->value());
|
|
88
|
+ canvas_->setStrokeRadius(strokeRadiusSpinBox_->value());
|
|
89
|
+}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+void
|
|
93
|
+ContinuousTab::highlightGlyph(int index)
|
|
94
|
+{
|
|
95
|
+ canvas_->flashOnGlyph(index);
|
|
96
|
+}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+void
|
|
100
|
+ContinuousTab::setGlyphCount(int count)
|
|
101
|
+{
|
|
102
|
+ currentGlyphCount_ = count;
|
|
103
|
+ updateLimitIndex();
|
|
104
|
+}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+void
|
|
108
|
+ContinuousTab::setGlyphBeginindex(int index)
|
|
109
|
+{
|
|
110
|
+ indexSelector_->setCurrentIndex(index);
|
|
111
|
+}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+void
|
|
115
|
+ContinuousTab::updateLimitIndex()
|
|
116
|
+{
|
|
117
|
+ auto cMap = charMapSelector_->currentCharMapIndex();
|
|
118
|
+ if (cMap < 0)
|
|
119
|
+ glyphLimitIndex_ = currentGlyphCount_;
|
|
120
|
+ else
|
|
121
|
+ glyphLimitIndex_ = charMapSelector_->charMaps()[cMap].maxIndex + 1;
|
|
122
|
+ indexSelector_->setMinMax(0, glyphLimitIndex_ - 1);
|
|
123
|
+}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+void
|
|
127
|
+ContinuousTab::checkModeSource()
|
|
128
|
+{
|
|
129
|
+ auto isFancy = modeSelector_->currentIndex() == GlyphContinuous::M_Fancy;
|
|
130
|
+ auto isStroked = modeSelector_->currentIndex() == GlyphContinuous::M_Stroked;
|
|
131
|
+ xEmboldeningSpinBox_->setEnabled(isFancy);
|
|
132
|
+ yEmboldeningSpinBox_->setEnabled(isFancy);
|
|
133
|
+ slantSpinBox_->setEnabled(isFancy);
|
|
134
|
+ strokeRadiusSpinBox_->setEnabled(isStroked);
|
|
135
|
+
|
|
136
|
+ auto src
|
|
137
|
+ = static_cast<GlyphContinuous::Source>(sourceSelector_->currentIndex());
|
|
138
|
+ auto isTextStrict = src == GlyphContinuous::SRC_TextString;
|
|
139
|
+ auto isText = src == GlyphContinuous::SRC_TextString
|
|
140
|
+ || src == GlyphContinuous::SRC_TextStringRepeated;
|
|
141
|
+ indexSelector_->setEnabled(src == GlyphContinuous::SRC_AllGlyphs);
|
|
142
|
+ sourceTextEdit_->setEnabled(isText);
|
|
143
|
+ sampleStringSelector_->setEnabled(isText);
|
|
144
|
+
|
|
145
|
+ {
|
|
146
|
+ QSignalBlocker blocker(kerningCheckBox_);
|
|
147
|
+ kerningCheckBox_->setEnabled(isText);
|
|
148
|
+ if (!isText)
|
|
149
|
+ kerningCheckBox_->setChecked(false);
|
|
150
|
+ }
|
|
151
|
+
|
|
152
|
+ canvas_->setSource(src);
|
|
153
|
+
|
|
154
|
+ {
|
|
155
|
+ auto wf = waterfallCheckBox_->isChecked();
|
|
156
|
+ QSignalBlocker blocker(verticalCheckBox_);
|
|
157
|
+ if (wf || !isTextStrict)
|
|
158
|
+ verticalCheckBox_->setChecked(false);
|
|
159
|
+ verticalCheckBox_->setEnabled(!wf && isTextStrict);
|
|
160
|
+ }
|
|
161
|
+
|
|
162
|
+ {
|
|
163
|
+ auto vert = verticalCheckBox_->isChecked();
|
|
164
|
+ QSignalBlocker blocker(waterfallCheckBox_);
|
|
165
|
+ if (vert)
|
|
166
|
+ waterfallCheckBox_->setChecked(false);
|
|
167
|
+ waterfallCheckBox_->setEnabled(!vert);
|
|
168
|
+ }
|
|
169
|
+
|
|
170
|
+ waterfallConfigButton_->setEnabled(waterfallCheckBox_->isChecked()
|
|
171
|
+ && !engine_->currentFontBitmapOnly());
|
|
172
|
+}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+void
|
|
176
|
+ContinuousTab::checkModeSourceAndRepaint()
|
|
177
|
+{
|
|
178
|
+ checkModeSource();
|
|
179
|
+ repaintGlyph();
|
|
180
|
+}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+void
|
|
184
|
+ContinuousTab::charMapChanged()
|
|
185
|
+{
|
|
186
|
+ int newIndex = charMapSelector_->currentCharMapIndex();
|
|
187
|
+ if (newIndex != lastCharMapIndex_)
|
|
188
|
+ setGlyphBeginindex(charMapSelector_->defaultFirstGlyphIndex());
|
|
189
|
+ updateLimitIndex();
|
|
190
|
+
|
|
191
|
+ applySettings();
|
|
192
|
+ canvas_->stringRenderer().reloadAll();
|
|
193
|
+ repaintGlyph();
|
|
194
|
+ lastCharMapIndex_ = newIndex;
|
|
195
|
+}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+void
|
|
199
|
+ContinuousTab::sourceTextChanged()
|
|
200
|
+{
|
|
201
|
+ canvas_->setSourceText(sourceTextEdit_->toPlainText());
|
|
202
|
+ repaintGlyph();
|
|
203
|
+}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+void
|
|
207
|
+ContinuousTab::presetStringSelected()
|
|
208
|
+{
|
|
209
|
+ auto index = sampleStringSelector_->currentIndex();
|
|
210
|
+ if (index < 0)
|
|
211
|
+ return;
|
|
212
|
+
|
|
213
|
+ auto var = sampleStringSelector_->currentData();
|
|
214
|
+ if (var.isValid() && var.canConvert<QString>())
|
|
215
|
+ {
|
|
216
|
+ auto str = var.toString();
|
|
217
|
+ if (!str.isEmpty())
|
|
218
|
+ sourceTextEdit_->setPlainText(str);
|
|
219
|
+ }
|
|
220
|
+}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+void
|
|
224
|
+ContinuousTab::reloadGlyphsAndRepaint()
|
|
225
|
+{
|
|
226
|
+ canvas_->stringRenderer().reloadGlyphs();
|
|
227
|
+ repaintGlyph();
|
|
228
|
+}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+void
|
|
232
|
+ContinuousTab::openWaterfallConfig()
|
|
233
|
+{
|
|
234
|
+ wfConfigDialog_->setVisible(true); // no `exec`: modalless
|
|
235
|
+}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+void
|
|
239
|
+ContinuousTab::showToolTip()
|
|
240
|
+{
|
|
241
|
+ QToolTip::showText(mapToGlobal(helpButton_->pos()),
|
|
242
|
+ tr(
|
|
243
|
+R"(Shift + Scroll: Adjust Font Size
|
|
244
|
+Ctrl + Scroll: Adjust Zoom Factor
|
|
245
|
+Shift + Plus/Minus: Adjust Font Size
|
|
246
|
+Shift + 0: Reset Font Size to Default
|
|
247
|
+Left Click: Show Glyph Details Info
|
|
248
|
+Right Click: Inspect Glyph in Singular Grid View
|
|
249
|
+
|
|
250
|
+<All Glyphs Source>
|
|
251
|
+ Drag: Adjust Begin Index
|
|
252
|
+<Text String Source>
|
|
253
|
+ Drag: Move String Position)"),
|
|
254
|
+ helpButton_);
|
|
255
|
+}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+bool
|
|
259
|
+ContinuousTab::eventFilter(QObject* watched,
|
|
260
|
+ QEvent* event)
|
|
261
|
+{
|
|
262
|
+ if (event->type() == QEvent::KeyPress)
|
|
263
|
+ {
|
|
264
|
+ auto keyEvent = dynamic_cast<QKeyEvent*>(event);
|
|
265
|
+ if (sizeSelector_->handleKeyEvent(keyEvent))
|
|
266
|
+ return true;
|
|
267
|
+ }
|
|
268
|
+ return false;
|
|
269
|
+}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+void
|
|
273
|
+ContinuousTab::wheelNavigate(int steps)
|
|
274
|
+{
|
|
275
|
+ if (sourceSelector_->currentIndex() == GlyphContinuous::SRC_AllGlyphs)
|
|
276
|
+ setGlyphBeginindex(indexSelector_->currentIndex() + steps);
|
|
277
|
+}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+void
|
|
281
|
+ContinuousTab::wheelZoom(int steps)
|
|
282
|
+{
|
|
283
|
+ sizeSelector_->handleWheelZoomBySteps(steps);
|
|
284
|
+}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+void
|
|
288
|
+ContinuousTab::wheelResize(int steps)
|
|
289
|
+{
|
|
290
|
+ sizeSelector_->handleWheelResizeBySteps(steps);
|
|
291
|
+}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+void
|
|
295
|
+ContinuousTab::createLayout()
|
|
296
|
+{
|
|
297
|
+ canvasFrame_ = new QFrame(this);
|
|
298
|
+ canvasFrame_->setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
|
|
299
|
+
|
|
300
|
+ canvas_ = new GlyphContinuous(canvasFrame_, engine_);
|
|
301
|
+ sizeSelector_ = new FontSizeSelector(this, false, true);
|
|
302
|
+
|
|
303
|
+ indexSelector_ = new GlyphIndexSelector(this);
|
|
304
|
+ indexSelector_->setSingleMode(false);
|
|
305
|
+ indexSelector_->setNumberRenderer([this](int index)
|
|
306
|
+ { return formatIndex(index); });
|
|
307
|
+ sourceTextEdit_ = new QPlainTextEdit(
|
|
308
|
+ tr("The quick brown fox jumps over the lazy dog."), this);
|
|
309
|
+
|
|
310
|
+ modeSelector_ = new QComboBox(this);
|
|
311
|
+ charMapSelector_ = new CharMapComboBox(this, engine_);
|
|
312
|
+ sourceSelector_ = new QComboBox(this);
|
|
313
|
+ sampleStringSelector_ = new QComboBox(this);
|
|
314
|
+
|
|
315
|
+ charMapSelector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
|
316
|
+
|
|
317
|
+ // Note: in sync with the enum!!
|
|
318
|
+ modeSelector_->insertItem(GlyphContinuous::M_Normal, tr("Normal"));
|
|
319
|
+ modeSelector_->insertItem(GlyphContinuous::M_Fancy, tr("Fancy"));
|
|
320
|
+ modeSelector_->insertItem(GlyphContinuous::M_Stroked, tr("Stroked"));
|
|
321
|
+ modeSelector_->setCurrentIndex(GlyphContinuous::M_Normal);
|
|
322
|
+
|
|
323
|
+ // Note: in sync with the enum!!
|
|
324
|
+ sourceSelector_->insertItem(GlyphContinuous::SRC_AllGlyphs,
|
|
325
|
+ tr("All Glyphs"));
|
|
326
|
+ sourceSelector_->insertItem(GlyphContinuous::SRC_TextString,
|
|
327
|
+ tr("Text String"));
|
|
328
|
+ sourceSelector_->insertItem(GlyphContinuous::SRC_TextStringRepeated,
|
|
329
|
+ tr("Text String (Repeated)"));
|
|
330
|
+
|
|
331
|
+ verticalCheckBox_ = new QCheckBox(tr("Vertical"), this);
|
|
332
|
+ waterfallCheckBox_ = new QCheckBox(tr("Waterfall"), this);
|
|
333
|
+ kerningCheckBox_ = new QCheckBox(tr("Kerning"), this);
|
|
334
|
+
|
|
335
|
+ modeLabel_ = new QLabel(tr("Mode:"), this);
|
|
336
|
+ sourceLabel_ = new QLabel(tr("Text Source:"), this);
|
|
337
|
+ charMapLabel_ = new QLabel(tr("Char Map:"), this);
|
|
338
|
+ xEmboldeningLabel_ = new QLabel(tr("Horz. Emb.:"), this);
|
|
339
|
+ yEmboldeningLabel_ = new QLabel(tr("Vert. Emb.:"), this);
|
|
340
|
+ slantLabel_ = new QLabel(tr("Slanting:"), this);
|
|
341
|
+ strokeRadiusLabel_ = new QLabel(tr("Stroke Radius:"), this);
|
|
342
|
+ rotationLabel_ = new QLabel(tr("Rotation:"), this);
|
|
343
|
+
|
|
344
|
+ resetPositionButton_ = new QPushButton(tr("Reset Pos"), this);
|
|
345
|
+ waterfallConfigButton_ = new QPushButton(tr("WF Config"), this);
|
|
346
|
+ helpButton_ = new QPushButton(this);
|
|
347
|
+ helpButton_->setText(tr("?"));
|
|
348
|
+
|
|
349
|
+ xEmboldeningSpinBox_ = new QDoubleSpinBox(this);
|
|
350
|
+ yEmboldeningSpinBox_ = new QDoubleSpinBox(this);
|
|
351
|
+ slantSpinBox_ = new QDoubleSpinBox(this);
|
|
352
|
+ strokeRadiusSpinBox_ = new QDoubleSpinBox(this);
|
|
353
|
+ rotationSpinBox_ = new QDoubleSpinBox(this);
|
|
354
|
+
|
|
355
|
+ xEmboldeningSpinBox_->setSingleStep(0.005);
|
|
356
|
+ xEmboldeningSpinBox_->setMinimum(-0.1);
|
|
357
|
+ xEmboldeningSpinBox_->setMaximum(0.1);
|
|
358
|
+ yEmboldeningSpinBox_->setSingleStep(0.005);
|
|
359
|
+ yEmboldeningSpinBox_->setMinimum(-0.1);
|
|
360
|
+ yEmboldeningSpinBox_->setMaximum(0.1);
|
|
361
|
+ slantSpinBox_->setSingleStep(0.02);
|
|
362
|
+ slantSpinBox_->setMinimum(-1);
|
|
363
|
+ slantSpinBox_->setMaximum(1);
|
|
364
|
+ strokeRadiusSpinBox_->setSingleStep(0.005);
|
|
365
|
+ strokeRadiusSpinBox_->setMinimum(0);
|
|
366
|
+ strokeRadiusSpinBox_->setMaximum(0.05);
|
|
367
|
+ rotationSpinBox_->setSingleStep(5);
|
|
368
|
+ rotationSpinBox_->setMinimum(-180);
|
|
369
|
+ rotationSpinBox_->setMaximum(180);
|
|
370
|
+
|
|
371
|
+ wfConfigDialog_ = new WaterfallConfigDialog(this);
|
|
372
|
+
|
|
373
|
+ // Tooltips
|
|
374
|
+ sourceSelector_->setToolTip(tr("Choose what to display as the text source."));
|
|
375
|
+ modeSelector_->setToolTip(
|
|
376
|
+ tr("Choose the special effect in which the text is displayed."));
|
|
377
|
+ strokeRadiusSpinBox_->setToolTip(
|
|
378
|
+ tr("Stroke corner radius (only available when mode set to Stroked)"));
|
|
379
|
+ rotationSpinBox_->setToolTip(tr("Rotation, in degrees"));
|
|
380
|
+ xEmboldeningSpinBox_->setToolTip(
|
|
381
|
+ tr("Horizontal Emboldening (only available when mode set to Fancy)"));
|
|
382
|
+ yEmboldeningSpinBox_->setToolTip(
|
|
383
|
+ tr("Vertical Emboldening (only available when mode set to Fancy)"));
|
|
384
|
+ slantSpinBox_->setToolTip(
|
|
385
|
+ tr("Slanting (only available when mode set to Fancy)"));
|
|
386
|
+ sourceTextEdit_->setToolTip(
|
|
387
|
+ tr("Source string (only available when source set to Text String)"));
|
|
388
|
+ waterfallConfigButton_->setToolTip(tr(
|
|
389
|
+ "Set waterfall start and end size. Not available when the font is not\n"
|
|
390
|
+ "scalable because in such case all available sizes would be displayed."));
|
|
391
|
+ sampleStringSelector_->setToolTip(
|
|
392
|
+ tr("Select preset sample strings (only available when source set to\nText "
|
|
393
|
+ "String)"));
|
|
394
|
+ resetPositionButton_->setToolTip(tr("Reset the position to the center (only "
|
|
395
|
+ "available when source set to\nText "
|
|
396
|
+ "String)"));
|
|
397
|
+ waterfallCheckBox_->setToolTip(tr(
|
|
398
|
+ "Enable waterfall mode: show the font output in different sizes.\nWill "
|
|
399
|
+ "show all available sizes when the font is not scalable."));
|
|
400
|
+ verticalCheckBox_->setToolTip(tr("Enable vertical rendering (only available\n"
|
|
401
|
+ "when source set to Text String)"));
|
|
402
|
+ kerningCheckBox_->setToolTip(tr("Enable kerning (GPOS table unsupported)"));
|
|
403
|
+ helpButton_->setToolTip(tr("Get mouse helps"));
|
|
404
|
+
|
|
405
|
+ // Layouting
|
|
406
|
+ canvasFrameLayout_ = new QHBoxLayout;
|
|
407
|
+ canvasFrameLayout_->addWidget(canvas_);
|
|
408
|
+ canvasFrame_->setLayout(canvasFrameLayout_);
|
|
409
|
+ canvasFrameLayout_->setContentsMargins(2, 2, 2, 2);
|
|
410
|
+ canvasFrame_->setContentsMargins(2, 2, 2, 2);
|
|
411
|
+
|
|
412
|
+ sizeHelpLayout_ = new QHBoxLayout;
|
|
413
|
+ sizeHelpLayout_->addWidget(sizeSelector_, 1, Qt::AlignVCenter);
|
|
414
|
+ sizeHelpLayout_->addWidget(helpButton_, 0);
|
|
415
|
+
|
|
416
|
+ bottomLayout_ = new QGridLayout;
|
|
417
|
+ bottomLayout_->addWidget(sourceLabel_, 0, 0);
|
|
418
|
+ bottomLayout_->addWidget(modeLabel_, 1, 0);
|
|
419
|
+ bottomLayout_->addWidget(charMapLabel_, 2, 0);
|
|
420
|
+ bottomLayout_->addWidget(sourceSelector_, 0, 1);
|
|
421
|
+ bottomLayout_->addWidget(modeSelector_, 1, 1);
|
|
422
|
+ bottomLayout_->addWidget(charMapSelector_, 2, 1);
|
|
423
|
+
|
|
424
|
+ bottomLayout_->addWidget(xEmboldeningLabel_, 1, 2);
|
|
425
|
+ bottomLayout_->addWidget(yEmboldeningLabel_, 2, 2);
|
|
426
|
+ bottomLayout_->addWidget(slantLabel_, 3, 2);
|
|
427
|
+ bottomLayout_->addWidget(strokeRadiusLabel_, 3, 0);
|
|
428
|
+ bottomLayout_->addWidget(rotationLabel_, 0, 2);
|
|
429
|
+
|
|
430
|
+ bottomLayout_->addWidget(xEmboldeningSpinBox_, 1, 3);
|
|
431
|
+ bottomLayout_->addWidget(yEmboldeningSpinBox_, 2, 3);
|
|
432
|
+ bottomLayout_->addWidget(slantSpinBox_, 3, 3);
|
|
433
|
+ bottomLayout_->addWidget(strokeRadiusSpinBox_, 3, 1);
|
|
434
|
+ bottomLayout_->addWidget(rotationSpinBox_, 0, 3);
|
|
435
|
+
|
|
436
|
+ bottomLayout_->addWidget(indexSelector_, 0, 4, 1, 2);
|
|
437
|
+ bottomLayout_->addWidget(sourceTextEdit_, 1, 4, 3, 1);
|
|
438
|
+ bottomLayout_->addWidget(resetPositionButton_, 0, 6);
|
|
439
|
+ bottomLayout_->addWidget(waterfallCheckBox_, 1, 6);
|
|
440
|
+ bottomLayout_->addWidget(verticalCheckBox_, 2, 6);
|
|
441
|
+ bottomLayout_->addWidget(kerningCheckBox_, 3, 6);
|
|
442
|
+ bottomLayout_->addWidget(waterfallConfigButton_, 1, 5);
|
|
443
|
+ bottomLayout_->addWidget(sampleStringSelector_, 2, 5);
|
|
444
|
+
|
|
445
|
+ bottomLayout_->setColumnStretch(4, 1);
|
|
446
|
+
|
|
447
|
+ mainLayout_ = new QVBoxLayout;
|
|
448
|
+ mainLayout_->addWidget(canvasFrame_);
|
|
449
|
+ mainLayout_->addLayout(sizeHelpLayout_);
|
|
450
|
+ mainLayout_->addLayout(bottomLayout_);
|
|
451
|
+
|
|
452
|
+ setLayout(mainLayout_);
|
|
453
|
+}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+void
|
|
457
|
+ContinuousTab::createConnections()
|
|
458
|
+{
|
|
459
|
+ connect(sizeSelector_, &FontSizeSelector::valueChanged,
|
|
460
|
+ this, &ContinuousTab::reloadGlyphsAndRepaint);
|
|
461
|
+
|
|
462
|
+ connect(canvas_, &GlyphContinuous::wheelResize,
|
|
463
|
+ this, &ContinuousTab::wheelResize);
|
|
464
|
+ connect(canvas_, &GlyphContinuous::wheelNavigate,
|
|
465
|
+ this, &ContinuousTab::wheelNavigate);
|
|
466
|
+ connect(canvas_, &GlyphContinuous::wheelZoom,
|
|
467
|
+ this, &ContinuousTab::wheelZoom);
|
|
468
|
+ connect(canvas_, &GlyphContinuous::displayingCountUpdated,
|
|
469
|
+ indexSelector_, &GlyphIndexSelector::setShowingCount);
|
|
470
|
+ connect(canvas_, &GlyphContinuous::beginIndexChangeRequest,
|
|
471
|
+ this, &ContinuousTab::setGlyphBeginindex);
|
|
472
|
+
|
|
473
|
+ connect(indexSelector_, &GlyphIndexSelector::currentIndexChanged,
|
|
474
|
+ this, &ContinuousTab::repaintGlyph);
|
|
475
|
+ connect(modeSelector_, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
476
|
+ this, &ContinuousTab::checkModeSourceAndRepaint);
|
|
477
|
+ connect(charMapSelector_,
|
|
478
|
+ QOverload<int>::of(&CharMapComboBox::currentIndexChanged),
|
|
479
|
+ this, &ContinuousTab::charMapChanged);
|
|
480
|
+ connect(charMapSelector_, &CharMapComboBox::forceUpdateLimitIndex,
|
|
481
|
+ this, &ContinuousTab::updateLimitIndex);
|
|
482
|
+ connect(sourceSelector_, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
483
|
+ this, &ContinuousTab::checkModeSourceAndRepaint);
|
|
484
|
+
|
|
485
|
+ connect(resetPositionButton_, &QPushButton::clicked,
|
|
486
|
+ canvas_, &GlyphContinuous::resetPositionDelta);
|
|
487
|
+ connect(waterfallConfigButton_, &QPushButton::clicked,
|
|
488
|
+ this, &ContinuousTab::openWaterfallConfig);
|
|
489
|
+ connect(helpButton_, &QPushButton::clicked,
|
|
490
|
+ this, &ContinuousTab::showToolTip);
|
|
491
|
+ connect(wfConfigDialog_, &WaterfallConfigDialog::sizeUpdated,
|
|
492
|
+ this, &ContinuousTab::repaintGlyph);
|
|
493
|
+
|
|
494
|
+ connect(xEmboldeningSpinBox_,
|
|
495
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
496
|
+ this, &ContinuousTab::repaintGlyph);
|
|
497
|
+ connect(yEmboldeningSpinBox_,
|
|
498
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
499
|
+ this, &ContinuousTab::repaintGlyph);
|
|
500
|
+ connect(slantSpinBox_,
|
|
501
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
502
|
+ this, &ContinuousTab::repaintGlyph);
|
|
503
|
+ connect(strokeRadiusSpinBox_,
|
|
504
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
505
|
+ this, &ContinuousTab::repaintGlyph);
|
|
506
|
+ connect(rotationSpinBox_,
|
|
507
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
508
|
+ this, &ContinuousTab::repaintGlyph);
|
|
509
|
+
|
|
510
|
+ connect(waterfallCheckBox_, &QCheckBox::clicked,
|
|
511
|
+ this, &ContinuousTab::checkModeSourceAndRepaint);
|
|
512
|
+ connect(verticalCheckBox_, &QCheckBox::clicked,
|
|
513
|
+ this, &ContinuousTab::checkModeSourceAndRepaint);
|
|
514
|
+ connect(kerningCheckBox_, &QCheckBox::clicked,
|
|
515
|
+ this, &ContinuousTab::reloadGlyphsAndRepaint);
|
|
516
|
+ connect(sourceTextEdit_, &QPlainTextEdit::textChanged,
|
|
517
|
+ this, &ContinuousTab::sourceTextChanged);
|
|
518
|
+ connect(sampleStringSelector_,
|
|
519
|
+ QOverload<int>::of(&QComboBox::currentIndexChanged),
|
|
520
|
+ this, &ContinuousTab::presetStringSelected);
|
|
521
|
+
|
|
522
|
+ sizeSelector_->installEventFilterForWidget(canvas_);
|
|
523
|
+ sizeSelector_->installEventFilterForWidget(this);
|
|
524
|
+}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+extern const char* StringSamples[];
|
|
528
|
+
|
|
529
|
+void
|
|
530
|
+ContinuousTab::setDefaults()
|
|
531
|
+{
|
|
532
|
+ xEmboldeningSpinBox_->setValue(0.04);
|
|
533
|
+ yEmboldeningSpinBox_->setValue(0.04);
|
|
534
|
+ slantSpinBox_->setValue(0.22);
|
|
535
|
+ strokeRadiusSpinBox_->setValue(0.02);
|
|
536
|
+ rotationSpinBox_->setValue(0);
|
|
537
|
+
|
|
538
|
+ canvas_->setSourceText(sourceTextEdit_->toPlainText());
|
|
539
|
+ canvas_->setSource(GlyphContinuous::SRC_AllGlyphs);
|
|
540
|
+
|
|
541
|
+ sampleStringSelector_->addItem(tr("<Sample>"));
|
|
542
|
+ sampleStringSelector_->addItem(tr("English"), QString(StringSamples[0]));
|
|
543
|
+ sampleStringSelector_->addItem(tr("Latin"), QString(StringSamples[1]));
|
|
544
|
+ sampleStringSelector_->addItem(tr("Greek"), QString(StringSamples[2]));
|
|
545
|
+ sampleStringSelector_->addItem(tr("Cyrillic"), QString(StringSamples[3]));
|
|
546
|
+ sampleStringSelector_->addItem(tr("Chinese"), QString(StringSamples[4]));
|
|
547
|
+ sampleStringSelector_->addItem(tr("Japanese"), QString(StringSamples[5]));
|
|
548
|
+ sampleStringSelector_->addItem(tr("Korean"), QString(StringSamples[6]));
|
|
549
|
+}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+QString
|
|
553
|
+ContinuousTab::formatIndex(int index)
|
|
554
|
+{
|
|
555
|
+ auto idx = charMapSelector_->currentCharMapIndex();
|
|
556
|
+ if (idx < 0) // glyph order
|
|
557
|
+ return QString::number(index);
|
|
558
|
+ return charMapSelector_->charMaps()[idx].stringifyIndexShort(index);
|
|
559
|
+}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+WaterfallConfigDialog::WaterfallConfigDialog(QWidget* parent)
|
|
563
|
+: QDialog(parent)
|
|
564
|
+{
|
|
565
|
+ setModal(false);
|
|
566
|
+ setWindowTitle(tr("Waterfall Config"));
|
|
567
|
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
568
|
+
|
|
569
|
+ createLayout();
|
|
570
|
+ checkAutoStatus();
|
|
571
|
+ createConnections();
|
|
572
|
+}
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+double
|
|
576
|
+WaterfallConfigDialog::startSize()
|
|
577
|
+{
|
|
578
|
+ if (autoBox_->isChecked())
|
|
579
|
+ return -1.0;
|
|
580
|
+ return startSpinBox_->value();
|
|
581
|
+}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+double
|
|
585
|
+WaterfallConfigDialog::endSize()
|
|
586
|
+{
|
|
587
|
+ if (autoBox_->isChecked())
|
|
588
|
+ return -1.0;
|
|
589
|
+ return endSpinBox_->value();
|
|
590
|
+}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+void
|
|
594
|
+WaterfallConfigDialog::createLayout()
|
|
595
|
+{
|
|
596
|
+ startLabel_ = new QLabel(tr("Start Size (pt):"), this);
|
|
597
|
+ endLabel_ = new QLabel(tr("End Size (pt):"), this);
|
|
598
|
+
|
|
599
|
+ startSpinBox_ = new QDoubleSpinBox(this);
|
|
600
|
+ endSpinBox_ = new QDoubleSpinBox(this);
|
|
601
|
+
|
|
602
|
+ startSpinBox_->setSingleStep(0.5);
|
|
603
|
+ startSpinBox_->setMinimum(0.5);
|
|
604
|
+ startSpinBox_->setValue(1);
|
|
605
|
+
|
|
606
|
+ endSpinBox_->setSingleStep(0.5);
|
|
607
|
+ endSpinBox_->setMinimum(0.5);
|
|
608
|
+ endSpinBox_->setValue(1);
|
|
609
|
+
|
|
610
|
+ autoBox_ = new QCheckBox(tr("Auto"), this);
|
|
611
|
+ autoBox_->setChecked(true);
|
|
612
|
+
|
|
613
|
+ // Tooltips
|
|
614
|
+ autoBox_->setToolTip(tr(
|
|
615
|
+ "Use the default value which will try to start from near zero and place "
|
|
616
|
+ "in the middle of the screen the size selected in the selector."));
|
|
617
|
+ startSpinBox_->setToolTip(tr("Start size, will be always guaranteed."));
|
|
618
|
+ endSpinBox_->setToolTip(tr(
|
|
619
|
+ "End size, may not be guaranteed due to rounding and precision issues."));
|
|
620
|
+
|
|
621
|
+ // Layouting
|
|
622
|
+ layout_ = new QGridLayout;
|
|
623
|
+ gridLayout2ColAddWidget(layout_, autoBox_);
|
|
624
|
+ gridLayout2ColAddWidget(layout_, startLabel_, startSpinBox_);
|
|
625
|
+ gridLayout2ColAddWidget(layout_, endLabel_, endSpinBox_);
|
|
626
|
+
|
|
627
|
+ setLayout(layout_);
|
|
628
|
+}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+void
|
|
632
|
+WaterfallConfigDialog::createConnections()
|
|
633
|
+{
|
|
634
|
+ connect(autoBox_, &QCheckBox::clicked,
|
|
635
|
+ this, &WaterfallConfigDialog::checkAutoStatus);
|
|
636
|
+ connect(startSpinBox_,
|
|
637
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
638
|
+ this, &WaterfallConfigDialog::sizeUpdated);
|
|
639
|
+ connect(endSpinBox_,
|
|
640
|
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
|
641
|
+ this, &WaterfallConfigDialog::sizeUpdated);
|
|
642
|
+}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+void
|
|
646
|
+WaterfallConfigDialog::checkAutoStatus()
|
|
647
|
+{
|
|
648
|
+ startSpinBox_->setEnabled(!autoBox_->isChecked());
|
|
649
|
+ endSpinBox_->setEnabled(!autoBox_->isChecked());
|
|
650
|
+
|
|
651
|
+ emit sizeUpdated();
|
|
652
|
+}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+const char* StringSamples[] = {
|
|
656
|
+ "The quick brown fox jumps over the lazy dog",
|
|
657
|
+
|
|
658
|
+ /* Luís argüia à Júlia que «brações, fé, chá, óxido, pôr, zângão» */
|
|
659
|
+ /* eram palavras do português */
|
|
660
|
+ "Lu\u00EDs arg\u00FCia \u00E0 J\u00FAlia que \u00ABbra\u00E7\u00F5es, "
|
|
661
|
+ "f\u00E9, ch\u00E1, \u00F3xido, p\u00F4r, z\u00E2ng\u00E3o\u00BB eram "
|
|
662
|
+ "palavras do portugu\u00EAs",
|
|
663
|
+
|
|
664
|
+ /* Ο καλύμνιος σφουγγαράς ψιθύρισε πως θα βουτήξει χωρίς να διστάζει */
|
|
665
|
+ "\u039F \u03BA\u03B1\u03BB\u03CD\u03BC\u03BD\u03B9\u03BF\u03C2 \u03C3"
|
|
666
|
+ "\u03C6\u03BF\u03C5\u03B3\u03B3\u03B1\u03C1\u03AC\u03C2 \u03C8\u03B9"
|
|
667
|
+ "\u03B8\u03CD\u03C1\u03B9\u03C3\u03B5 \u03C0\u03C9\u03C2 \u03B8\u03B1 "
|
|
668
|
+ "\u03B2\u03BF\u03C5\u03C4\u03AE\u03BE\u03B5\u03B9 \u03C7\u03C9\u03C1"
|
|
669
|
+ "\u03AF\u03C2 \u03BD\u03B1 \u03B4\u03B9\u03C3\u03C4\u03AC\u03B6\u03B5"
|
|
670
|
+ "\u03B9",
|
|
671
|
+
|
|
672
|
+ /* Съешь ещё этих мягких французских булок да выпей же чаю */
|
|
673
|
+ "\u0421\u044A\u0435\u0448\u044C \u0435\u0449\u0451 \u044D\u0442\u0438"
|
|
674
|
+ "\u0445 \u043C\u044F\u0433\u043A\u0438\u0445 \u0444\u0440\u0430\u043D"
|
|
675
|
+ "\u0446\u0443\u0437\u0441\u043A\u0438\u0445 \u0431\u0443\u043B\u043E"
|
|
676
|
+ "\u043A \u0434\u0430 \u0432\u044B\u043F\u0435\u0439 \u0436\u0435 "
|
|
677
|
+ "\u0447\u0430\u044E",
|
|
678
|
+
|
|
679
|
+ /* 天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。寒來暑往,秋收冬藏。*/
|
|
680
|
+ "\u5929\u5730\u7384\u9EC3\uFF0C\u5B87\u5B99\u6D2A\u8352\u3002\u65E5"
|
|
681
|
+ "\u6708\u76C8\u6603\uFF0C\u8FB0\u5BBF\u5217\u5F35\u3002\u5BD2\u4F86"
|
|
682
|
+ "\u6691\u5F80\uFF0C\u79CB\u6536\u51AC\u85CF\u3002",
|
|
683
|
+
|
|
684
|
+ /* いろはにほへと ちりぬるを わかよたれそ つねならむ */
|
|
685
|
+ /* うゐのおくやま けふこえて あさきゆめみし ゑひもせす */
|
|
686
|
+ "\u3044\u308D\u306F\u306B\u307B\u3078\u3068 \u3061\u308A\u306C\u308B"
|
|
687
|
+ "\u3092 \u308F\u304B\u3088\u305F\u308C\u305D \u3064\u306D\u306A\u3089"
|
|
688
|
+ "\u3080 \u3046\u3090\u306E\u304A\u304F\u3084\u307E \u3051\u3075\u3053"
|
|
689
|
+ "\u3048\u3066 \u3042\u3055\u304D\u3086\u3081\u307F\u3057 \u3091\u3072"
|
|
690
|
+ "\u3082\u305B\u3059",
|
|
691
|
+
|
|
692
|
+ /* 키스의 고유조건은 입술끼리 만나야 하고 특별한 기술은 필요치 않다 */
|
|
693
|
+ "\uD0A4\uC2A4\uC758 \uACE0\uC720\uC870\uAC74\uC740 \uC785\uC220\uB07C"
|
|
694
|
+ "\uB9AC \uB9CC\uB098\uC57C \uD558\uACE0 \uD2B9\uBCC4\uD55C \uAE30"
|
|
695
|
+ "\uC220\uC740 \uD544\uC694\uCE58 \uC54A\uB2E4"
|
|
696
|
+};
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+// end of continuous.cpp |
src/ftinspect/panels/continuous.hpp
|
1
|
+// continuous.hpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#pragma once
|
|
6
|
+
|
|
7
|
+#include "abstracttab.hpp"
|
|
8
|
+#include "../widgets/customwidgets.hpp"
|
|
9
|
+#include "../widgets/glyphindexselector.hpp"
|
|
10
|
+#include "../widgets/fontsizeselector.hpp"
|
|
11
|
+#include "../widgets/charmapcombobox.hpp"
|
|
12
|
+#include "../glyphcomponents/graphicsdefault.hpp"
|
|
13
|
+#include "../glyphcomponents/glyphcontinuous.hpp"
|
|
14
|
+#include "../engine/engine.hpp"
|
|
15
|
+
|
|
16
|
+#include <vector>
|
|
17
|
+#include <QWidget>
|
|
18
|
+#include <QDialog>
|
|
19
|
+#include <QFrame>
|
|
20
|
+#include <QLabel>
|
|
21
|
+#include <QComboBox>
|
|
22
|
+#include <QGridLayout>
|
|
23
|
+#include <QBoxLayout>
|
|
24
|
+#include <QPlainTextEdit>
|
|
25
|
+#include <QCheckBox>
|
|
26
|
+
|
|
27
|
+class WaterfallConfigDialog;
|
|
28
|
+class ContinuousTab
|
|
29
|
+: public QWidget, public AbstractTab
|
|
30
|
+{
|
|
31
|
+ Q_OBJECT
|
|
32
|
+public:
|
|
33
|
+ ContinuousTab(QWidget* parent, Engine* engine);
|
|
34
|
+ ~ContinuousTab() override = default;
|
|
35
|
+
|
|
36
|
+ void repaintGlyph() override;
|
|
37
|
+ void reloadFont() override;
|
|
38
|
+ void highlightGlyph(int index);
|
|
39
|
+ void applySettings();
|
|
40
|
+
|
|
41
|
+protected:
|
|
42
|
+ bool eventFilter(QObject* watched, QEvent* event) override;
|
|
43
|
+
|
|
44
|
+private:
|
|
45
|
+ Engine* engine_;
|
|
46
|
+
|
|
47
|
+ int currentGlyphCount_;
|
|
48
|
+ int lastCharMapIndex_ = 0;
|
|
49
|
+ int glyphLimitIndex_ = 0;
|
|
50
|
+
|
|
51
|
+ GlyphContinuous* canvas_;
|
|
52
|
+ QFrame* canvasFrame_;
|
|
53
|
+ FontSizeSelector* sizeSelector_;
|
|
54
|
+
|
|
55
|
+ QComboBox* modeSelector_;
|
|
56
|
+ QComboBox* sourceSelector_;
|
|
57
|
+ CharMapComboBox* charMapSelector_ = NULL;
|
|
58
|
+ QComboBox* sampleStringSelector_;
|
|
59
|
+
|
|
60
|
+ QPushButton* resetPositionButton_;
|
|
61
|
+ QPushButton* waterfallConfigButton_;
|
|
62
|
+ QPushButton* helpButton_;
|
|
63
|
+
|
|
64
|
+ QLabel* modeLabel_;
|
|
65
|
+ QLabel* sourceLabel_;
|
|
66
|
+ QLabel* charMapLabel_;
|
|
67
|
+ QLabel* xEmboldeningLabel_;
|
|
68
|
+ QLabel* yEmboldeningLabel_;
|
|
69
|
+ QLabel* slantLabel_;
|
|
70
|
+ QLabel* strokeRadiusLabel_;
|
|
71
|
+ QLabel* rotationLabel_;
|
|
72
|
+
|
|
73
|
+ QDoubleSpinBox* xEmboldeningSpinBox_;
|
|
74
|
+ QDoubleSpinBox* yEmboldeningSpinBox_;
|
|
75
|
+ QDoubleSpinBox* slantSpinBox_;
|
|
76
|
+ QDoubleSpinBox* strokeRadiusSpinBox_;
|
|
77
|
+ QDoubleSpinBox* rotationSpinBox_;
|
|
78
|
+
|
|
79
|
+ QCheckBox* verticalCheckBox_;
|
|
80
|
+ QCheckBox* waterfallCheckBox_;
|
|
81
|
+ QCheckBox* kerningCheckBox_;
|
|
82
|
+
|
|
83
|
+ GlyphIndexSelector* indexSelector_;
|
|
84
|
+ QPlainTextEdit* sourceTextEdit_;
|
|
85
|
+
|
|
86
|
+ QHBoxLayout* canvasFrameLayout_;
|
|
87
|
+ QHBoxLayout* sizeHelpLayout_;
|
|
88
|
+ QGridLayout* bottomLayout_;
|
|
89
|
+ QVBoxLayout* mainLayout_;
|
|
90
|
+
|
|
91
|
+ WaterfallConfigDialog* wfConfigDialog_;
|
|
92
|
+
|
|
93
|
+ void createLayout();
|
|
94
|
+ void createConnections();
|
|
95
|
+
|
|
96
|
+ void updateLimitIndex();
|
|
97
|
+ void checkModeSource();
|
|
98
|
+
|
|
99
|
+ // This doesn't trigger immediate repaint
|
|
100
|
+ void setGlyphCount(int count);
|
|
101
|
+
|
|
102
|
+ // But they do
|
|
103
|
+ void setGlyphBeginindex(int index);
|
|
104
|
+ void checkModeSourceAndRepaint();
|
|
105
|
+ void charMapChanged();
|
|
106
|
+ void sourceTextChanged();
|
|
107
|
+ void presetStringSelected();
|
|
108
|
+ void reloadGlyphsAndRepaint();
|
|
109
|
+ void openWaterfallConfig();
|
|
110
|
+ void showToolTip();
|
|
111
|
+
|
|
112
|
+ void wheelNavigate(int steps);
|
|
113
|
+ void wheelZoom(int steps);
|
|
114
|
+ void wheelResize(int steps);
|
|
115
|
+
|
|
116
|
+ void setDefaults();
|
|
117
|
+ QString formatIndex(int index);
|
|
118
|
+};
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+class WaterfallConfigDialog
|
|
122
|
+: public QDialog
|
|
123
|
+{
|
|
124
|
+ Q_OBJECT
|
|
125
|
+
|
|
126
|
+public:
|
|
127
|
+ WaterfallConfigDialog(QWidget* parent);
|
|
128
|
+
|
|
129
|
+ double startSize();
|
|
130
|
+ double endSize();
|
|
131
|
+
|
|
132
|
+signals:
|
|
133
|
+ void sizeUpdated();
|
|
134
|
+
|
|
135
|
+private:
|
|
136
|
+ QLabel* startLabel_;
|
|
137
|
+ QLabel* endLabel_;
|
|
138
|
+
|
|
139
|
+ QDoubleSpinBox* startSpinBox_;
|
|
140
|
+ QDoubleSpinBox* endSpinBox_;
|
|
141
|
+
|
|
142
|
+ QCheckBox* autoBox_;
|
|
143
|
+
|
|
144
|
+ QGridLayout* layout_;
|
|
145
|
+
|
|
146
|
+ void createLayout();
|
|
147
|
+ void createConnections();
|
|
148
|
+
|
|
149
|
+ void checkAutoStatus();
|
|
150
|
+};
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+// end of continuous.hpp |
src/ftinspect/widgets/charmapcombobox.cpp
|
1
|
+// charmapcombobox.cpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#include "charmapcombobox.hpp"
|
|
6
|
+
|
|
7
|
+#include "../engine/engine.hpp"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+CharMapComboBox::CharMapComboBox(QWidget* parent,
|
|
11
|
+ Engine* engine,
|
|
12
|
+ bool haveGlyphOrder)
|
|
13
|
+: QComboBox(parent),
|
|
14
|
+ haveGlyphOrder_(haveGlyphOrder),
|
|
15
|
+ engine_(engine)
|
|
16
|
+{
|
|
17
|
+ setToolTip("Set current charmap.");
|
|
18
|
+ connect(this, QOverload<int>::of(&CharMapComboBox::currentIndexChanged),
|
|
19
|
+ this, &CharMapComboBox::updateToolTip);
|
|
20
|
+}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+int
|
|
24
|
+CharMapComboBox::currentCharMapIndex()
|
|
25
|
+{
|
|
26
|
+ auto index = haveGlyphOrder_ ? currentIndex() - 1 : currentIndex();
|
|
27
|
+ if (index < 0 || charMaps_.size() <= static_cast<unsigned>(index))
|
|
28
|
+ return -1;
|
|
29
|
+ return index;
|
|
30
|
+}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+int
|
|
34
|
+CharMapComboBox::defaultFirstGlyphIndex()
|
|
35
|
+{
|
|
36
|
+ auto newIndex = currentCharMapIndex();
|
|
37
|
+ if (newIndex < 0)
|
|
38
|
+ return 0;
|
|
39
|
+ if (charMaps_[newIndex].maxIndex <= 20)
|
|
40
|
+ return charMaps_[newIndex].maxIndex - 1;
|
|
41
|
+ return 0x20;
|
|
42
|
+}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+void
|
|
46
|
+CharMapComboBox::repopulate()
|
|
47
|
+{
|
|
48
|
+ repopulate(engine_->currentFontCharMaps());
|
|
49
|
+}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+#define EncodingRole (Qt::UserRole + 10)
|
|
53
|
+void
|
|
54
|
+CharMapComboBox::repopulate(std::vector<CharMapInfo>& charMaps)
|
|
55
|
+{
|
|
56
|
+ if (charMaps_ == charMaps)
|
|
57
|
+ {
|
|
58
|
+ charMaps_ = charMaps; // Still need to substitute because ptr may differ
|
|
59
|
+ return;
|
|
60
|
+ }
|
|
61
|
+ charMaps_ = charMaps;
|
|
62
|
+ int oldIndex = currentIndex();
|
|
63
|
+ unsigned oldEncoding = 0u;
|
|
64
|
+
|
|
65
|
+ // Using additional UserRole to store encoding id
|
|
66
|
+ auto oldEncodingV = itemData(oldIndex, EncodingRole);
|
|
67
|
+ if (oldEncodingV.isValid() && oldEncodingV.canConvert<unsigned>())
|
|
68
|
+ oldEncoding = oldEncodingV.value<unsigned>();
|
|
69
|
+
|
|
70
|
+ { // This brace isn't for the `if` statement!
|
|
71
|
+ // suppress events during updating
|
|
72
|
+ QSignalBlocker selectorBlocker(this);
|
|
73
|
+
|
|
74
|
+ clear();
|
|
75
|
+ if (haveGlyphOrder_)
|
|
76
|
+ {
|
|
77
|
+ addItem(tr("Glyph Order"));
|
|
78
|
+ setItemData(0, 0u, EncodingRole);
|
|
79
|
+ }
|
|
80
|
+
|
|
81
|
+ int i = 0;
|
|
82
|
+ int newIndex = 0;
|
|
83
|
+ for (auto& map : charMaps)
|
|
84
|
+ {
|
|
85
|
+ addItem(tr("%1: %2 (platform %3, encoding %4)")
|
|
86
|
+ .arg(i)
|
|
87
|
+ .arg(*map.encodingName)
|
|
88
|
+ .arg(map.platformID)
|
|
89
|
+ .arg(map.encodingID));
|
|
90
|
+ auto encoding = static_cast<unsigned>(map.encoding);
|
|
91
|
+ setItemData(haveGlyphOrder_ ? i + 1 : i, encoding, EncodingRole);
|
|
92
|
+
|
|
93
|
+ if (encoding == oldEncoding && i == oldIndex)
|
|
94
|
+ newIndex = i;
|
|
95
|
+
|
|
96
|
+ i++;
|
|
97
|
+ }
|
|
98
|
+
|
|
99
|
+ // this shouldn't emit any event either, because force repainting
|
|
100
|
+ // will happen later, so embrace it into blocker block
|
|
101
|
+ setCurrentIndex(newIndex);
|
|
102
|
+ updateToolTip();
|
|
103
|
+ }
|
|
104
|
+
|
|
105
|
+ emit forceUpdateLimitIndex();
|
|
106
|
+}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+void
|
|
110
|
+CharMapComboBox::updateToolTip()
|
|
111
|
+{
|
|
112
|
+ auto index = currentIndex();
|
|
113
|
+ if (index >= 0 && index < count())
|
|
114
|
+ setToolTip(this->currentText());
|
|
115
|
+}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+// end of charmapcombobox.cpp |
src/ftinspect/widgets/charmapcombobox.hpp
|
1
|
+// charmapcombobox.hpp
|
|
2
|
+
|
|
3
|
+// Copyright (C) 2022 by Charlie Jiang.
|
|
4
|
+
|
|
5
|
+#pragma once
|
|
6
|
+
|
|
7
|
+#include "../engine/charmap.hpp"
|
|
8
|
+
|
|
9
|
+#include <vector>
|
|
10
|
+#include <QComboBox>
|
|
11
|
+
|
|
12
|
+class Engine;
|
|
13
|
+class CharMapComboBox
|
|
14
|
+: public QComboBox
|
|
15
|
+{
|
|
16
|
+ Q_OBJECT
|
|
17
|
+public:
|
|
18
|
+ CharMapComboBox(QWidget* parent, Engine* engine, bool haveGlyphOrder = true);
|
|
19
|
+ ~CharMapComboBox() override = default;
|
|
20
|
+
|
|
21
|
+ bool haveGlyphOrder_;
|
|
22
|
+
|
|
23
|
+ std::vector<CharMapInfo>& charMaps() { return charMaps_; }
|
|
24
|
+ int currentCharMapIndex();
|
|
25
|
+ int defaultFirstGlyphIndex();
|
|
26
|
+ void repopulate();
|
|
27
|
+ void repopulate(std::vector<CharMapInfo>& charMaps);
|
|
28
|
+
|
|
29
|
+signals:
|
|
30
|
+ void forceUpdateLimitIndex();
|
|
31
|
+
|
|
32
|
+private:
|
|
33
|
+ Engine* engine_;
|
|
34
|
+ std::vector<CharMapInfo> charMaps_;
|
|
35
|
+
|
|
36
|
+ void updateToolTip();
|
|
37
|
+};
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+// charmapcombobox.hpp |
|