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 "defs.h"
27 #include "gmdocument.h"
28 #include "ui/command.h"
29 #include "ui/documentwidget.h"
30 #include "ui/labelwidget.h"
31 #include "ui/paint.h"
32 #include "ui/util.h"
33
34 #include <the_Foundation/regexp.h>
35 #include <SDL_timer.h>
36 #include <math.h>
37
38 /*----------------------------------------------------------------------------------------------*/
39
40 iDeclareWidgetClass(TranslationProgressWidget)
41 iDeclareObjectConstruction(TranslationProgressWidget)
42
43 iDeclareType(Sprite)
44
45 struct Impl_Sprite {
46 iInt2 pos;
47 iInt2 size;
48 int color;
49 int xoff;
50 iString text;
51 };
52
53 struct Impl_TranslationProgressWidget {
54 iWidget widget;
55 uint32_t startTime;
56 int font;
57 iArray sprites;
58 iString message;
59 };
60
init_TranslationProgressWidget(iTranslationProgressWidget * d)61 void init_TranslationProgressWidget(iTranslationProgressWidget *d) {
62 iWidget *w = &d->widget;
63 init_Widget(w);
64 setId_Widget(w, "xlt.progress");
65 init_Array(&d->sprites, sizeof(iSprite));
66 d->startTime = SDL_GetTicks();
67 init_String(&d->message);
68 /* Set up some letters to animate. */
69 const char *chars = "ARGOS";
70 const size_t n = strlen(chars);
71 resize_Array(&d->sprites, n);
72 d->font = uiContentBold_FontId;
73 const int width = lineHeight_Text(d->font);
74 const int gap = gap_Text / 2;
75 int x = (int) (n * width + (n - 1) * gap) / -2;
76 const int y = -lineHeight_Text(d->font) / 2;
77 for (size_t i = 0; i < n; i++) {
78 iSprite *spr = at_Array(&d->sprites, i);
79 spr->pos = init_I2(x, y);
80 spr->color = 0;
81 init_String(&spr->text);
82 appendChar_String(&spr->text, chars[i]);
83 spr->xoff = (width - measureRange_Text(d->font, range_String(&spr->text)).advance.x) / 2;
84 spr->size = init_I2(width, lineHeight_Text(d->font));
85 x += width + gap;
86 }
87 }
88
deinit_TranslationProgressWidget(iTranslationProgressWidget * d)89 void deinit_TranslationProgressWidget(iTranslationProgressWidget *d) {
90 deinit_String(&d->message);
91 iForEach(Array, i, &d->sprites) {
92 iSprite *spr = i.value;
93 deinit_String(&spr->text);
94 }
95 deinit_Array(&d->sprites);
96 }
97
iDefineObjectConstruction(TranslationProgressWidget)98 iDefineObjectConstruction(TranslationProgressWidget)
99
100 static void draw_TranslationProgressWidget_(const iTranslationProgressWidget *d) {
101 const iWidget *w = &d->widget;
102 const float t = (float) (SDL_GetTicks() - d->startTime) / 1000.0f;
103 const iRect bounds = bounds_Widget(w);
104 if (!isEmpty_String(&d->message)) {
105 drawCentered_Text(
106 uiLabel_FontId, bounds, iFalse, uiText_ColorId, "%s", cstr_String(&d->message));
107 return;
108 }
109 iPaint p;
110 init_Paint(&p);
111 const iInt2 mid = mid_Rect(bounds);
112 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
113 const int palette[] = {
114 uiBackgroundSelected_ColorId,
115 red_ColorId,
116 blue_ColorId,
117 green_ColorId,
118 };
119 iConstForEach(Array, i, &d->sprites) {
120 const int index = (int) index_ArrayConstIterator(&i);
121 const float angle = (float) index;
122 const iSprite *spr = i.value;
123 const float opacity = iClamp(t - index * 0.5f, 0.0, 1.0f);
124 const float palPos = index * 0.025f + t / 10;
125 const int palCur = (size_t)(palPos) % iElemCount(palette);
126 const int palNext = (palCur + 1) % iElemCount(palette);
127
128 int fg = palCur == 0 ? uiTextSelected_ColorId
129 : isLight_ColorTheme(colorTheme_App()) ? white_ColorId
130 : black_ColorId;
131 iInt2 pos = add_I2(mid, spr->pos);
132 float t2 = sin(0.2f * t);
133 pos.y += sin(angle + t) * spr->size.y * t2 * t2 * iClamp(t * 0.25f - 0.3f, 0.0f, 1.0f);
134 p.alpha = opacity * 255;
135 const iColor back = mix_Color(
136 get_Color(palette[palCur]), get_Color(palette[palNext]), palPos - (int) palPos);
137 SDL_SetRenderDrawColor(renderer_Window(get_Window()), back.r, back.g, back.b, p.alpha);
138 SDL_RenderFillRect(renderer_Window(get_Window()),
139 &(SDL_Rect){ pos.x + origin_Paint.x, pos.y + origin_Paint.y,
140 spr->size.x, spr->size.y });
141 if (fg >= 0) {
142 setOpacity_Text(opacity * 2);
143 drawRange_Text(d->font, addX_I2(pos, spr->xoff), fg, range_String(&spr->text));
144 }
145 }
146 setOpacity_Text(1.0f);
147 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
148 }
149
processEvent_TranslationProgressWidget_(iTranslationProgressWidget * d,const SDL_Event * ev)150 static iBool processEvent_TranslationProgressWidget_(iTranslationProgressWidget *d,
151 const SDL_Event *ev) {
152 iUnused(d, ev);
153 return iFalse;
154 }
155
156 iBeginDefineSubclass(TranslationProgressWidget, Widget)
157 .draw = (iAny *) draw_TranslationProgressWidget_,
158 .processEvent = (iAny *) processEvent_TranslationProgressWidget_,
159 iEndDefineSubclass(TranslationProgressWidget)
160
161 /*----------------------------------------------------------------------------------------------*/
162
163 iDefineTypeConstructionArgs(Translation, (iDocumentWidget *doc), doc)
164
165 static const char * translationServiceHost = "xlt.skyjake.fi";
166 static const uint16_t translationServicePort = 443;
167
168 /* TODO: Move these quote/unquote methods to the_Foundation. */
169
quote_String_(const iString * d)170 static iString *quote_String_(const iString *d) {
171 iString *quot = new_String();
172 iConstForEach(String, i, d) {
173 const iChar ch = i.value;
174 if (ch == '"') {
175 appendCStr_String(quot, "\\\"");
176 }
177 else if (ch == '\\') {
178 appendCStr_String(quot, "\\\\");
179 }
180 else if (ch == '\n') {
181 appendCStr_String(quot, "\\n");
182 }
183 else if (ch == '\r') {
184 appendCStr_String(quot, "\\r");
185 }
186 else if (ch == '\t') {
187 appendCStr_String(quot, "\\t");
188 }
189 else if (ch >= 0x80) {
190 if ((ch >= 0xD800 && ch < 0xE000) || ch >= 0x10000) {
191 /* TODO: Add a helper function? */
192 /* UTF-16 surrogate pair */
193 iString *chs = newUnicodeN_String(&ch, 1);
194 iBlock *u16 = toUtf16_String(chs);
195 delete_String(chs);
196 const uint16_t *ch16 = constData_Block(u16);
197 appendFormat_String(quot, "\\u%04x\\u%04x", ch16[0], ch16[1]);
198 }
199 else {
200 appendFormat_String(quot, "\\u%04x", ch);
201 }
202 }
203 else {
204 appendChar_String(quot, ch);
205 }
206 }
207 return quot;
208 }
209
unquote_String_(const iString * d)210 static iString *unquote_String_(const iString *d) {
211 iString *unquot = new_String();
212 iConstForEach(String, i, d) {
213 const iChar ch = i.value;
214 if (ch == '\\') {
215 next_StringConstIterator(&i);
216 const iChar esc = i.value;
217 if (esc == '\\') {
218 appendChar_String(unquot, esc);
219 }
220 else if (esc == 'n') {
221 appendChar_String(unquot, '\n');
222 }
223 else if (esc == 'r') {
224 appendChar_String(unquot, '\r');
225 }
226 else if (esc == 't') {
227 appendChar_String(unquot, '\t');
228 }
229 else if (esc == '"') {
230 appendChar_String(unquot, '"');
231 }
232 else if (esc == 'u') {
233 char digits[5];
234 iZap(digits);
235 for (size_t j = 0; j < 4; j++) {
236 next_StringConstIterator(&i);
237 digits[j] = *i.pos;
238 }
239 uint16_t ch16[2] = { strtoul(digits, NULL, 16), 0 };
240 if (ch16[0] < 0xD800 || ch16[0] >= 0xE000) {
241 appendChar_String(unquot, ch16[0]);
242 }
243 else {
244 /* UTF-16 surrogate pair */
245 next_StringConstIterator(&i);
246 next_StringConstIterator(&i);
247 iZap(digits);
248 for (size_t j = 0; j < 4; j++) {
249 next_StringConstIterator(&i);
250 digits[j] = *i.pos;
251 }
252 ch16[1] = strtoul(digits, NULL, 16);
253 iString *u16 = newUtf16N_String(ch16, 2);
254 append_String(unquot, u16);
255 delete_String(u16);
256 }
257 }
258 else {
259 iAssert(0);
260 }
261 }
262 else {
263 appendChar_String(unquot, ch);
264 }
265 }
266 return unquot;
267 }
268
finished_Translation_(iTlsRequest * d,iTlsRequest * req)269 static void finished_Translation_(iTlsRequest *d, iTlsRequest *req) {
270 iUnused(req);
271 postCommandf_App("translation.finished ptr:%p", userData_Object(d));
272 }
273
init_Translation(iTranslation * d,iDocumentWidget * doc)274 void init_Translation(iTranslation *d, iDocumentWidget *doc) {
275 d->dlg = makeTranslation_Widget(as_Widget(doc));
276 d->startTime = 0;
277 d->doc = doc; /* owner */
278 d->request = new_TlsRequest();
279 d->timer = 0;
280 init_Array(&d->lineTypes, sizeof(int));
281 setUserData_Object(d->request, d->doc);
282 setHost_TlsRequest(d->request,
283 collectNewCStr_String(translationServiceHost),
284 translationServicePort);
285 iConnect(TlsRequest, d->request, finished, d->request, finished_Translation_);
286 }
287
deinit_Translation(iTranslation * d)288 void deinit_Translation(iTranslation *d) {
289 if (d->timer) {
290 SDL_RemoveTimer(d->timer);
291 }
292 cancel_TlsRequest(d->request);
293 iRelease(d->request);
294 destroy_Widget(d->dlg);
295 deinit_Array(&d->lineTypes);
296 }
297
animate_Translation_(uint32_t interval,iAny * ptr)298 static uint32_t animate_Translation_(uint32_t interval, iAny *ptr) {
299 postCommandf_App("translation.update ptr:%p", ((iTranslation *) ptr)->doc);
300 return interval;
301 }
302
submit_Translation(iTranslation * d)303 void submit_Translation(iTranslation *d) {
304 iAssert(status_TlsRequest(d->request) != submitted_TlsRequestStatus);
305 /* Check the selected languages from the dialog. */
306 const char *idFrom = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.from")));
307 const char *idTo = languageId_String(text_LabelWidget(findChild_Widget(d->dlg, "xlt.to")));
308 /* Remember these in Preferences. */
309 postCommandf_App("translation.languages from:%d to:%d",
310 languageIndex_CStr(idFrom),
311 languageIndex_CStr(idTo));
312 iBlock * json = collect_Block(new_Block(0));
313 iString *docSrc = collectNew_String();
314 /* The translation engine doesn't preserve Gemtext markup so we'll strip all of it and
315 remember each line's type. These are reapplied when reading the response. Newlines seem
316 to be preserved pretty well. */ {
317 iRangecc line = iNullRange;
318 while (nextSplit_Rangecc(
319 range_String(source_GmDocument(document_DocumentWidget(d->doc))), "\n", &line)) {
320 iRangecc cleanLine = trimmed_Rangecc(line);
321 const int lineType = lineType_Rangecc(cleanLine);
322 pushBack_Array(&d->lineTypes, &lineType);
323 if (lineType == link_GmLineType) {
324 cleanLine.start += 2; /* skip over the => */
325 }
326 else {
327 trimLine_Rangecc(&cleanLine, lineType, iTrue); /* removes the prefix */
328 }
329 if (!isEmpty_String(docSrc)) {
330 appendCStr_String(docSrc, "\n");
331 }
332 appendRange_String(docSrc, cleanLine);
333 }
334 }
335 printf_Block(json,
336 "{\"q\":\"%s\",\"source\":\"%s\",\"target\":\"%s\"}",
337 cstrCollect_String(quote_String_(docSrc)),
338 idFrom,
339 idTo);
340 iBlock *msg = collect_Block(new_Block(0));
341 printf_Block(msg,
342 "POST /translate HTTP/1.1\r\n"
343 "Host: %s\r\n"
344 "Connection: close\r\n"
345 "Content-Type: application/json; charset=utf-8\r\n"
346 "Content-Length: %zu\r\n\r\n",
347 translationServiceHost,
348 size_Block(json));
349 append_Block(msg, json);
350 setContent_TlsRequest(d->request, msg);
351 submit_TlsRequest(d->request);
352 d->startTime = SDL_GetTicks();
353 d->timer = SDL_AddTimer(1000 / 30, animate_Translation_, d);
354 }
355
setFailed_Translation_(iTranslation * d,const char * msg)356 static void setFailed_Translation_(iTranslation *d, const char *msg) {
357 iTranslationProgressWidget *prog = findChild_Widget(d->dlg, "xlt.progress");
358 if (prog && isEmpty_String(&prog->message)) {
359 setCStr_String(&prog->message, msg);
360 }
361 }
362
processResult_Translation_(iTranslation * d)363 static iBool processResult_Translation_(iTranslation *d) {
364 SDL_RemoveTimer(d->timer);
365 d->timer = 0;
366 if (status_TlsRequest(d->request) == error_TlsRequestStatus) {
367 setFailed_Translation_(d, explosion_Icon " ${dlg.translate.fail}");
368 return iFalse;
369 }
370 iBlock *resultData = collect_Block(readAll_TlsRequest(d->request));
371 // printf("result(%zu):\n%s\n", size_Block(resultData), cstr_Block(resultData));
372 // fflush(stdout);
373 iRegExp *pattern = iClob(new_RegExp(".*translatedText\":\"(.*)\"\\}", caseSensitive_RegExpOption));
374 iRegExpMatch m;
375 init_RegExpMatch(&m);
376 if (matchRange_RegExp(pattern, range_Block(resultData), &m)) {
377 iString *translation = unquote_String_(collect_String(captured_RegExpMatch(&m, 1)));
378 iString *marked = collectNew_String();
379 iRangecc line = iNullRange;
380 size_t lineIndex = 0;
381 while (nextSplit_Rangecc(range_String(translation), "\n", &line)) {
382 iRangecc cleanLine = trimmed_Rangecc(line);
383 if (!isEmpty_String(marked)) {
384 appendCStr_String(marked, "\n");
385 }
386 if (lineIndex < size_Array(&d->lineTypes)) {
387 switch (value_Array(&d->lineTypes, lineIndex, int)) {
388 case bullet_GmLineType:
389 appendCStr_String(marked, "* ");
390 break;
391 case link_GmLineType:
392 appendCStr_String(marked, "=> ");
393 break;
394 case quote_GmLineType:
395 appendCStr_String(marked, "> ");
396 break;
397 case preformatted_GmLineType:
398 appendCStr_String(marked, "```");
399 break;
400 case heading1_GmLineType:
401 appendCStr_String(marked, "# ");
402 break;
403 case heading2_GmLineType:
404 appendCStr_String(marked, "## ");
405 break;
406 case heading3_GmLineType:
407 appendCStr_String(marked, "### ");
408 break;
409 default:
410 break;
411 }
412 }
413 appendRange_String(marked, cleanLine);
414 lineIndex++;
415 }
416 setSource_DocumentWidget(d->doc, marked);
417 postCommand_App("sidebar.update");
418 delete_String(translation);
419 }
420 else {
421 setFailed_Translation_(d, unhappy_Icon " ${dlg.translate.unavail}");
422 return iFalse;
423 }
424 return iTrue;
425 }
426
acceptButton_Translation_(const iTranslation * d)427 static iLabelWidget *acceptButton_Translation_(const iTranslation *d) {
428 return dialogAcceptButton_Widget(d->dlg);
429 }
430
handleCommand_Translation(iTranslation * d,const char * cmd)431 iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
432 iWidget *w = as_Widget(d->doc);
433 if (equalWidget_Command(cmd, w, "translation.submit")) {
434 if (status_TlsRequest(d->request) == initialized_TlsRequestStatus) {
435 iWidget *langs = findChild_Widget(d->dlg, "xlt.langs");
436 if (langs) {
437 setFlags_Widget(langs, hidden_WidgetFlag, iTrue);
438 }
439 setFlags_Widget(findChild_Widget(d->dlg, "xlt.from"), hidden_WidgetFlag, iTrue);
440 setFlags_Widget(findChild_Widget(d->dlg, "xlt.to"), hidden_WidgetFlag, iTrue);
441 if (!langs) langs = d->dlg;
442 iLabelWidget *acceptButton = acceptButton_Translation_(d);
443 updateTextCStr_LabelWidget(acceptButton, "00:00");
444 setFlags_Widget(as_Widget(acceptButton), disabled_WidgetFlag, iTrue);
445 iTranslationProgressWidget *prog = new_TranslationProgressWidget();
446 setPos_Widget(as_Widget(prog), langs->rect.pos);
447 setFixedSize_Widget(as_Widget(prog), init_I2(width_Rect(innerBounds_Widget(d->dlg)),
448 langs->rect.size.y));
449 addChildFlags_Widget(d->dlg, iClob(prog), 0);
450 submit_Translation(d);
451 }
452 return iTrue;
453 }
454 if (equalWidget_Command(cmd, w, "translation.update")) {
455 const uint32_t elapsed = SDL_GetTicks() - d->startTime;
456 const unsigned seconds = (elapsed / 1000) % 60;
457 const unsigned minutes = (elapsed / 60000);
458 updateText_LabelWidget(acceptButton_Translation_(d),
459 collectNewFormat_String("%02u:%02u", minutes, seconds));
460 return iTrue;
461 }
462 if (equalWidget_Command(cmd, w, "translation.finished")) {
463 if (!isFinished_Translation(d)) {
464 if (processResult_Translation_(d)) {
465 setupSheetTransition_Mobile(d->dlg, iFalse);
466 destroy_Widget(d->dlg);
467 d->dlg = NULL;
468 }
469 }
470 return iTrue;
471 }
472 if (equalWidget_Command(cmd, d->dlg, "translation.cancel")) {
473 if (status_TlsRequest(d->request) == submitted_TlsRequestStatus) {
474 setFailed_Translation_(d, "Cancelled");
475 updateTextCStr_LabelWidget(
476 findMenuItem_Widget(findChild_Widget(d->dlg, "dialogbuttons"),
477 "translation.cancel"),
478 "${close}");
479 cancel_TlsRequest(d->request);
480 }
481 else {
482 setupSheetTransition_Mobile(d->dlg, iFalse);
483 destroy_Widget(d->dlg);
484 d->dlg = NULL;
485 }
486 return iTrue;
487 }
488 return iFalse;
489 }
490
isFinished_Translation(const iTranslation * d)491 iBool isFinished_Translation(const iTranslation *d) {
492 return d->dlg == NULL;
493 }
494