diff options
Diffstat (limited to 'src/ui/translation.c')
-rw-r--r-- | src/ui/translation.c | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/src/ui/translation.c b/src/ui/translation.c new file mode 100644 index 00000000..cfb6eb79 --- /dev/null +++ b/src/ui/translation.c | |||
@@ -0,0 +1,360 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "translation.h" | ||
24 | |||
25 | #include "app.h" | ||
26 | #include "gmdocument.h" | ||
27 | #include "ui/command.h" | ||
28 | #include "ui/documentwidget.h" | ||
29 | #include "ui/labelwidget.h" | ||
30 | #include "ui/paint.h" | ||
31 | #include "ui/util.h" | ||
32 | |||
33 | #include <the_Foundation/regexp.h> | ||
34 | #include <SDL_timer.h> | ||
35 | #include <math.h> | ||
36 | |||
37 | /*----------------------------------------------------------------------------------------------*/ | ||
38 | |||
39 | iDeclareWidgetClass(TranslationProgressWidget) | ||
40 | iDeclareObjectConstruction(TranslationProgressWidget) | ||
41 | |||
42 | iDeclareType(Sprite) | ||
43 | |||
44 | struct Impl_Sprite { | ||
45 | iInt2 pos; | ||
46 | iInt2 size; | ||
47 | int color; | ||
48 | int xoff; | ||
49 | iString text; | ||
50 | }; | ||
51 | |||
52 | struct Impl_TranslationProgressWidget { | ||
53 | iWidget widget; | ||
54 | uint32_t startTime; | ||
55 | int font; | ||
56 | iArray sprites; | ||
57 | }; | ||
58 | |||
59 | void init_TranslationProgressWidget(iTranslationProgressWidget *d) { | ||
60 | iWidget *w = &d->widget; | ||
61 | init_Widget(w); | ||
62 | setId_Widget(w, "xlt.progress"); | ||
63 | init_Array(&d->sprites, sizeof(iSprite)); | ||
64 | d->startTime = SDL_GetTicks(); | ||
65 | /* Set up some letters to animate. */ | ||
66 | const char *chars = "ARGOS"; | ||
67 | const size_t n = strlen(chars); | ||
68 | resize_Array(&d->sprites, n); | ||
69 | d->font = uiContentBold_FontId; | ||
70 | const int width = lineHeight_Text(d->font); | ||
71 | const int gap = gap_Text / 2; | ||
72 | int x = (int) (n * width + (n - 1) * gap) / -2; | ||
73 | const int y = -lineHeight_Text(d->font) / 2; | ||
74 | for (size_t i = 0; i < n; i++) { | ||
75 | iSprite *spr = at_Array(&d->sprites, i); | ||
76 | spr->pos = init_I2(x, y); | ||
77 | spr->color = 0; | ||
78 | init_String(&spr->text); | ||
79 | appendChar_String(&spr->text, chars[i]); | ||
80 | spr->xoff = (width - advanceRange_Text(d->font, range_String(&spr->text)).x) / 2; | ||
81 | spr->size = init_I2(width, lineHeight_Text(d->font)); | ||
82 | x += width + gap; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | void deinit_TranslationProgressWidget(iTranslationProgressWidget *d) { | ||
87 | iForEach(Array, i, &d->sprites) { | ||
88 | iSprite *spr = i.value; | ||
89 | deinit_String(&spr->text); | ||
90 | } | ||
91 | deinit_Array(&d->sprites); | ||
92 | } | ||
93 | |||
94 | iDefineObjectConstruction(TranslationProgressWidget) | ||
95 | |||
96 | static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d) { | ||
97 | const iWidget *w = &d->widget; | ||
98 | const float t = (float) (SDL_GetTicks() - d->startTime) / 1000.0f; | ||
99 | const iRect bounds = bounds_Widget(w); | ||
100 | iPaint p; | ||
101 | init_Paint(&p); | ||
102 | const iInt2 mid = mid_Rect(bounds); | ||
103 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND); | ||
104 | iConstForEach(Array, i, &d->sprites) { | ||
105 | const int index = index_ArrayConstIterator(&i); | ||
106 | const float angle = (float) index; | ||
107 | const iSprite *spr = i.value; | ||
108 | const float opacity = iClamp(t - index * 0.5f, 0.0, 1.0f); | ||
109 | int bg = uiBackgroundSelected_ColorId; | ||
110 | int fg = uiTextSelected_ColorId; | ||
111 | iInt2 pos = add_I2(mid, spr->pos); | ||
112 | pos.y += sin(angle + t) * spr->size.y * iClamp(t * 0.25f - 0.3f, 0.0f, 1.0f); | ||
113 | if (bg >= 0) { | ||
114 | p.alpha = opacity * 255; | ||
115 | fillRect_Paint(&p, (iRect){ pos, spr->size }, bg); | ||
116 | } | ||
117 | if (fg >= 0) { | ||
118 | setOpacity_Text(opacity * 2); | ||
119 | drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text)); | ||
120 | } | ||
121 | } | ||
122 | setOpacity_Text(1.0f); | ||
123 | SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); | ||
124 | } | ||
125 | |||
126 | static iBool processEvent_TranslationProgressWidget_(iTranslationProgressWidget *d, | ||
127 | const SDL_Event *ev) { | ||
128 | iUnused(d, ev); | ||
129 | return iFalse; | ||
130 | } | ||
131 | |||
132 | iBeginDefineSubclass(TranslationProgressWidget, Widget) | ||
133 | .draw = (iAny *) draw_TranslationProgressWidget_, | ||
134 | .processEvent = (iAny *) processEvent_TranslationProgressWidget_, | ||
135 | iEndDefineSubclass(TranslationProgressWidget) | ||
136 | |||
137 | /*----------------------------------------------------------------------------------------------*/ | ||
138 | |||
139 | iDefineTypeConstructionArgs(Translation, (iDocumentWidget *doc), doc) | ||
140 | |||
141 | static const char * translationServiceHost = "xlt.skyjake.fi"; | ||
142 | static const uint16_t translationServicePort = 443; | ||
143 | |||
144 | static const char *doubleArrowSymbol = "\u20e2"; /* prevent getting mangled */ | ||
145 | static const char *tripleBacktickSymbol = "\u20e3"; | ||
146 | static const char *h1Symbol = "\u20e4"; | ||
147 | static const char *h2Symbol = "\u20e5"; | ||
148 | static const char *h3Symbol = "\u20e6"; | ||
149 | |||
150 | static iString *quote_String_(const iString *d) { | ||
151 | iString *quot = new_String(); | ||
152 | iConstForEach(String, i, d) { | ||
153 | const iChar ch = i.value; | ||
154 | if (ch == '"') { | ||
155 | appendCStr_String(quot, "\\\""); | ||
156 | } | ||
157 | else if (ch == '\\') { | ||
158 | appendCStr_String(quot, "\\\\"); | ||
159 | } | ||
160 | else if (ch == '\n') { | ||
161 | appendCStr_String(quot, "\\n"); | ||
162 | } | ||
163 | else if (ch == '\r') { | ||
164 | appendCStr_String(quot, "\\r"); | ||
165 | } | ||
166 | else if (ch == '\t') { | ||
167 | appendCStr_String(quot, "\\t"); | ||
168 | } | ||
169 | else if (ch >= 0x100) { | ||
170 | appendFormat_String(quot, "\\u%04x", ch); | ||
171 | } | ||
172 | else { | ||
173 | appendChar_String(quot, ch); | ||
174 | } | ||
175 | } | ||
176 | return quot; | ||
177 | } | ||
178 | |||
179 | static iString *unquote_String_(const iString *d) { | ||
180 | iString *unquot = new_String(); | ||
181 | iConstForEach(String, i, d) { | ||
182 | const iChar ch = i.value; | ||
183 | if (ch == '\\') { | ||
184 | next_StringConstIterator(&i); | ||
185 | const iChar esc = i.value; | ||
186 | if (esc == '\\') { | ||
187 | appendChar_String(unquot, esc); | ||
188 | } | ||
189 | else if (esc == 'n') { | ||
190 | appendChar_String(unquot, '\n'); | ||
191 | } | ||
192 | else if (esc == 'r') { | ||
193 | appendChar_String(unquot, '\r'); | ||
194 | } | ||
195 | else if (esc == 't') { | ||
196 | appendChar_String(unquot, '\t'); | ||
197 | } | ||
198 | else if (esc == '"') { | ||
199 | appendChar_String(unquot, '"'); | ||
200 | } | ||
201 | else if (esc == 'u') { | ||
202 | char digits[5]; | ||
203 | iZap(digits); | ||
204 | iForIndices(j, digits) { | ||
205 | next_StringConstIterator(&i); | ||
206 | digits[j] = *i.pos; | ||
207 | } | ||
208 | iChar codepoint = strtoul(digits, NULL, 16); | ||
209 | if (codepoint) { | ||
210 | appendChar_String(unquot, codepoint); | ||
211 | } | ||
212 | } | ||
213 | else { | ||
214 | iAssert(0); | ||
215 | } | ||
216 | } | ||
217 | else { | ||
218 | appendChar_String(unquot, ch); | ||
219 | } | ||
220 | } | ||
221 | return unquot; | ||
222 | } | ||
223 | |||
224 | static void finished_Translation_(iTlsRequest *d, iTlsRequest *req) { | ||
225 | iUnused(req); | ||
226 | postCommandf_App("translation.finished ptr:%p", userData_Object(d)); | ||
227 | } | ||
228 | |||
229 | void init_Translation(iTranslation *d, iDocumentWidget *doc) { | ||
230 | d->dlg = makeTranslation_Widget(as_Widget(doc)); | ||
231 | d->startTime = 0; | ||
232 | d->doc = doc; /* owner */ | ||
233 | d->request = new_TlsRequest(); | ||
234 | d->timer = 0; | ||
235 | setUserData_Object(d->request, d->doc); | ||
236 | setHost_TlsRequest(d->request, | ||
237 | collectNewCStr_String(translationServiceHost), | ||
238 | translationServicePort); | ||
239 | iConnect(TlsRequest, d->request, finished, d->request, finished_Translation_); | ||
240 | } | ||
241 | |||
242 | void deinit_Translation(iTranslation *d) { | ||
243 | if (d->timer) { | ||
244 | SDL_RemoveTimer(d->timer); | ||
245 | } | ||
246 | cancel_TlsRequest(d->request); | ||
247 | iRelease(d->request); | ||
248 | destroy_Widget(d->dlg); | ||
249 | } | ||
250 | |||
251 | static uint32_t animate_Translation_(uint32_t interval, iAny *ptr) { | ||
252 | postCommandf_App("translation.update ptr:%p", ((iTranslation *) ptr)->doc); | ||
253 | return interval; | ||
254 | } | ||
255 | |||
256 | void submit_Translation(iTranslation *d) { | ||
257 | /* Check the selected languages from the dialog. */ | ||
258 | const char *idFrom = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.from"))); | ||
259 | const char *idTo = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.to"))); | ||
260 | iAssert(status_TlsRequest(d->request) != submitted_TlsRequestStatus); | ||
261 | iBlock *json = collect_Block(new_Block(0)); | ||
262 | iString *docSrc = collect_String(copy_String(source_GmDocument(document_DocumentWidget(d->doc)))); | ||
263 | replace_String(docSrc, "=>", doubleArrowSymbol); | ||
264 | replace_String(docSrc, "```", tripleBacktickSymbol); | ||
265 | replace_String(docSrc, "###", h3Symbol); | ||
266 | replace_String(docSrc, "##", h2Symbol); | ||
267 | replace_String(docSrc, "#", h1Symbol); | ||
268 | printf_Block(json, | ||
269 | "{\"q\":\"%s\",\"source\":\"%s\",\"target\":\"%s\"}", | ||
270 | cstrCollect_String(quote_String_(docSrc)), | ||
271 | idFrom, | ||
272 | idTo); | ||
273 | iBlock *msg = collect_Block(new_Block(0)); | ||
274 | printf_Block(msg, "POST /translate HTTP/1.1\r\n" | ||
275 | "Host: xlt.skyjake.fi\r\n" | ||
276 | "Connection: close\r\n" | ||
277 | "Content-Type: application/json\r\n" | ||
278 | "Content-Length: %zu\r\n\r\n", size_Block(json)); | ||
279 | append_Block(msg, json); | ||
280 | setContent_TlsRequest(d->request, msg); | ||
281 | submit_TlsRequest(d->request); | ||
282 | d->startTime = SDL_GetTicks(); | ||
283 | d->timer = SDL_AddTimer(1000 / 30, animate_Translation_, d); | ||
284 | } | ||
285 | |||
286 | static void processResult_Translation_(iTranslation *d) { | ||
287 | SDL_RemoveTimer(d->timer); | ||
288 | d->timer = 0; | ||
289 | if (status_TlsRequest(d->request) == error_TlsRequestStatus) { | ||
290 | return; | ||
291 | } | ||
292 | iBlock *resultData = collect_Block(readAll_TlsRequest(d->request)); | ||
293 | printf("result(%zu):\n%s\n", size_Block(resultData), cstr_Block(resultData)); | ||
294 | fflush(stdout); | ||
295 | iRegExp *pattern = iClob(new_RegExp(".*translatedText\":\"(.*)\"\\}", caseSensitive_RegExpOption)); | ||
296 | iRegExpMatch m; | ||
297 | init_RegExpMatch(&m); | ||
298 | if (matchRange_RegExp(pattern, range_Block(resultData), &m)) { | ||
299 | iString *translation = unquote_String_(collect_String(captured_RegExpMatch(&m, 1))); | ||
300 | replace_String(translation, tripleBacktickSymbol, "```"); | ||
301 | replace_String(translation, doubleArrowSymbol, "=>"); | ||
302 | replace_String(translation, h3Symbol, "###"); | ||
303 | replace_String(translation, h2Symbol, "##"); | ||
304 | replace_String(translation, h1Symbol, "#"); | ||
305 | setSource_DocumentWidget(d->doc, translation); | ||
306 | postCommand_App("sidebar.update"); | ||
307 | delete_String(translation); | ||
308 | } | ||
309 | else { | ||
310 | /* TODO: Report failure! */ | ||
311 | } | ||
312 | } | ||
313 | |||
314 | static iLabelWidget *acceptButton_Translation_(const iTranslation *d) { | ||
315 | return (iLabelWidget *) lastChild_Widget(findChild_Widget(d->dlg, "dialogbuttons")); | ||
316 | } | ||
317 | |||
318 | iBool handleCommand_Translation(iTranslation *d, const char *cmd) { | ||
319 | iWidget *w = as_Widget(d->doc); | ||
320 | if (equalWidget_Command(cmd, w, "translation.submit")) { | ||
321 | if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) { | ||
322 | iWidget *langs = findChild_Widget(d->dlg, "xlt.langs"); | ||
323 | setFlags_Widget(langs, hidden_WidgetFlag, iTrue); | ||
324 | iLabelWidget *acceptButton = acceptButton_Translation_(d); | ||
325 | updateTextCStr_LabelWidget(acceptButton, "00:00"); | ||
326 | setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue); | ||
327 | iTranslationProgressWidget *prog = new_TranslationProgressWidget(); | ||
328 | setPos_Widget(as_Widget(prog), langs->rect.pos); | ||
329 | setSize_Widget(as_Widget(prog), langs->rect.size); | ||
330 | addChild_Widget(d->dlg, iClob(prog)); | ||
331 | submit_Translation(d); | ||
332 | } | ||
333 | return iTrue; | ||
334 | } | ||
335 | if (equalWidget_Command(cmd, w, "translation.update")) { | ||
336 | const uint32_t elapsed = SDL_GetTicks() - d->startTime; | ||
337 | const unsigned seconds = (elapsed / 1000) % 60; | ||
338 | const unsigned minutes = (elapsed / 60000); | ||
339 | updateText_LabelWidget(acceptButton_Translation_(d), | ||
340 | collectNewFormat_String("%02u:%02u", minutes, seconds)); | ||
341 | return iTrue; | ||
342 | } | ||
343 | if (equalWidget_Command(cmd, w, "translation.finished")) { | ||
344 | if (!isFinished_Translation(d)) { | ||
345 | processResult_Translation_(d); | ||
346 | destroy_Widget(d->dlg); | ||
347 | d->dlg = NULL; | ||
348 | } | ||
349 | return iTrue; | ||
350 | } | ||
351 | if (equalWidget_Command(cmd, d->dlg, "translation.cancel")) { | ||
352 | cancel_TlsRequest(d->request); | ||
353 | return iTrue; | ||
354 | } | ||
355 | return iFalse; | ||
356 | } | ||
357 | |||
358 | iBool isFinished_Translation(const iTranslation *d) { | ||
359 | return d->dlg == NULL; | ||
360 | } | ||