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