summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-03-15 12:47:20 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-03-15 19:03:41 +0200
commiteeb9f8b6306524782e7dcc8ef90797dd5bdae1bf (patch)
treeacfde64b0304d563769f73a7288cacb0568bb179
parenteb8da869cf87692a5cbb38803644643cd2e192f6 (diff)
Added a page translation service
This is quite experimental. The page contents are sent to an instance of LibreTranslate (powered by Argos Translate), which may or may not successfully translate the contents without mangling the gemtext markup.
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/ui/documentwidget.c27
-rw-r--r--src/ui/documentwidget.h1
-rw-r--r--src/ui/translation.c360
-rw-r--r--src/ui/translation.h45
-rw-r--r--src/ui/util.c107
-rw-r--r--src/ui/util.h3
7 files changed, 527 insertions, 18 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 35c7cb98..b4d7e659 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -158,6 +158,8 @@ set (SOURCES
158 src/ui/text.h 158 src/ui/text.h
159 src/ui/touch.c 159 src/ui/touch.c
160 src/ui/touch.h 160 src/ui/touch.h
161 src/ui/translation.c
162 src/ui/translation.h
161 src/ui/util.c 163 src/ui/util.c
162 src/ui/util.h 164 src/ui/util.h
163 src/ui/visbuf.c 165 src/ui/visbuf.c
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 79e8b727..a468e2df 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -43,6 +43,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
43#include "paint.h" 43#include "paint.h"
44#include "mediaui.h" 44#include "mediaui.h"
45#include "scrollwidget.h" 45#include "scrollwidget.h"
46#include "translation.h"
46#include "util.h" 47#include "util.h"
47#include "visbuf.h" 48#include "visbuf.h"
48#include "visited.h" 49#include "visited.h"
@@ -218,6 +219,7 @@ struct Impl_DocumentWidget {
218 iPtrSet * invalidRuns; 219 iPtrSet * invalidRuns;
219 SDL_Texture * sideIconBuf; 220 SDL_Texture * sideIconBuf;
220 iTextBuf * timestampBuf; 221 iTextBuf * timestampBuf;
222 iTranslation * translation;
221}; 223};
222 224
223iDefineObjectConstruction(DocumentWidget) 225iDefineObjectConstruction(DocumentWidget)
@@ -273,6 +275,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
273 d->playerMenu = NULL; 275 d->playerMenu = NULL;
274 d->sideIconBuf = NULL; 276 d->sideIconBuf = NULL;
275 d->timestampBuf = NULL; 277 d->timestampBuf = NULL;
278 d->translation = NULL;
276 addChildFlags_Widget(w, 279 addChildFlags_Widget(w,
277 iClob(new_IndicatorWidget()), 280 iClob(new_IndicatorWidget()),
278 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); 281 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
@@ -289,6 +292,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
289} 292}
290 293
291void deinit_DocumentWidget(iDocumentWidget *d) { 294void deinit_DocumentWidget(iDocumentWidget *d) {
295 delete_Translation(d->translation);
292 if (d->sideIconBuf) { 296 if (d->sideIconBuf) {
293 SDL_DestroyTexture(d->sideIconBuf); 297 SDL_DestroyTexture(d->sideIconBuf);
294 } 298 }
@@ -751,7 +755,7 @@ static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
751 d->lastVisibleRun = NULL; 755 d->lastVisibleRun = NULL;
752} 756}
753 757
754static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { 758void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
755 setUrl_GmDocument(d->doc, d->mod.url); 759 setUrl_GmDocument(d->doc, d->mod.url);
756 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 760 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
757 documentRunsInvalidated_DocumentWidget_(d); 761 documentRunsInvalidated_DocumentWidget_(d);
@@ -825,7 +829,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
825 } 829 }
826 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 830 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
827 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat); 831 setFormat_GmDocument(d->doc, gemini_GmDocumentFormat);
828 setSource_DocumentWidget_(d, src); 832 setSource_DocumentWidget(d, src);
829 updateTheme_DocumentWidget_(d); 833 updateTheme_DocumentWidget_(d);
830 init_Anim(&d->scrollY, 0); 834 init_Anim(&d->scrollY, 0);
831 init_Anim(&d->sideOpacity, 0); 835 init_Anim(&d->sideOpacity, 0);
@@ -947,7 +951,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
947 } 951 }
948 } 952 }
949 if (setSource) { 953 if (setSource) {
950 setSource_DocumentWidget_(d, &str); 954 setSource_DocumentWidget(d, &str);
951 } 955 }
952 deinit_String(&str); 956 deinit_String(&str);
953 } 957 }
@@ -1777,6 +1781,20 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1777 cacheDocumentGlyphs_DocumentWidget_(d); 1781 cacheDocumentGlyphs_DocumentWidget_(d);
1778 return iFalse; 1782 return iFalse;
1779 } 1783 }
1784 else if (equalWidget_Command(cmd, w, "document.translate")) {
1785 if (!d->translation) {
1786 d->translation = new_Translation(d);
1787 }
1788 return iTrue;
1789 }
1790 else if (startsWith_CStr(cmd, "translation.") && d->translation) {
1791 const iBool wasHandled = handleCommand_Translation(d->translation, cmd);
1792 if (isFinished_Translation(d->translation)) {
1793 delete_Translation(d->translation);
1794 d->translation = NULL;
1795 }
1796 return wasHandled;
1797 }
1780 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { 1798 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) {
1781 return handleMediaCommand_DocumentWidget_(d, cmd); 1799 return handleMediaCommand_DocumentWidget_(d, cmd);
1782 } 1800 }
@@ -2524,9 +2542,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2524 { star_Icon " Subscribe to Page...", subscribeToPage_KeyModifier, "feeds.subscribe" }, 2542 { star_Icon " Subscribe to Page...", subscribeToPage_KeyModifier, "feeds.subscribe" },
2525 { "---", 0, 0, NULL }, 2543 { "---", 0, 0, NULL },
2526 { book_Icon " Import Links as Bookmarks...", 0, 0, "bookmark.links confirm:1" }, 2544 { book_Icon " Import Links as Bookmarks...", 0, 0, "bookmark.links confirm:1" },
2545 { "Translate...", 0, 0, "document.translate" },
2527 { "---", 0, 0, NULL }, 2546 { "---", 0, 0, NULL },
2528 { "Copy Page URL", 0, 0, "document.copylink" } }, 2547 { "Copy Page URL", 0, 0, "document.copylink" } },
2529 11); 2548 12);
2530 if (isEmpty_Range(&d->selectMark)) { 2549 if (isEmpty_Range(&d->selectMark)) {
2531 pushBackN_Array( 2550 pushBackN_Array(
2532 &items, 2551 &items,
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index f922e7ea..12603437 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -49,5 +49,6 @@ void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);
50void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 50void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
51void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); 51void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
52void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText);
52 53
53void updateSize_DocumentWidget (iDocumentWidget *); 54void updateSize_DocumentWidget (iDocumentWidget *);
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
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. 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
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY 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
21SOFTWARE, 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
39iDeclareWidgetClass(TranslationProgressWidget)
40iDeclareObjectConstruction(TranslationProgressWidget)
41
42iDeclareType(Sprite)
43
44struct Impl_Sprite {
45 iInt2 pos;
46 iInt2 size;
47 int color;
48 int xoff;
49 iString text;
50};
51
52struct Impl_TranslationProgressWidget {
53 iWidget widget;
54 uint32_t startTime;
55 int font;
56 iArray sprites;
57};
58
59void 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
86void 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
94iDefineObjectConstruction(TranslationProgressWidget)
95
96static 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
126static iBool processEvent_TranslationProgressWidget_(iTranslationProgressWidget *d,
127 const SDL_Event *ev) {
128 iUnused(d, ev);
129 return iFalse;
130}
131
132iBeginDefineSubclass(TranslationProgressWidget, Widget)
133 .draw = (iAny *) draw_TranslationProgressWidget_,
134 .processEvent = (iAny *) processEvent_TranslationProgressWidget_,
135 iEndDefineSubclass(TranslationProgressWidget)
136
137/*----------------------------------------------------------------------------------------------*/
138
139iDefineTypeConstructionArgs(Translation, (iDocumentWidget *doc), doc)
140
141static const char * translationServiceHost = "xlt.skyjake.fi";
142static const uint16_t translationServicePort = 443;
143
144static const char *doubleArrowSymbol = "\u20e2"; /* prevent getting mangled */
145static const char *tripleBacktickSymbol = "\u20e3";
146static const char *h1Symbol = "\u20e4";
147static const char *h2Symbol = "\u20e5";
148static const char *h3Symbol = "\u20e6";
149
150static 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
179static 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
224static void finished_Translation_(iTlsRequest *d, iTlsRequest *req) {
225 iUnused(req);
226 postCommandf_App("translation.finished ptr:%p", userData_Object(d));
227}
228
229void 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
242void 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
251static uint32_t animate_Translation_(uint32_t interval, iAny *ptr) {
252 postCommandf_App("translation.update ptr:%p", ((iTranslation *) ptr)->doc);
253 return interval;
254}
255
256void 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
286static 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
314static iLabelWidget *acceptButton_Translation_(const iTranslation *d) {
315 return (iLabelWidget *) lastChild_Widget(findChild_Widget(d->dlg, "dialogbuttons"));
316}
317
318iBool 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
358iBool isFinished_Translation(const iTranslation *d) {
359 return d->dlg == NULL;
360}
diff --git a/src/ui/translation.h b/src/ui/translation.h
new file mode 100644
index 00000000..6966e468
--- /dev/null
+++ b/src/ui/translation.h
@@ -0,0 +1,45 @@
1/* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. 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
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY 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
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include <the_Foundation/string.h>
26#include <the_Foundation/tlsrequest.h>
27
28iDeclareType(Translation)
29iDeclareType(DocumentWidget)
30iDeclareType(Widget)
31
32struct Impl_Translation {
33 iTlsRequest * request;
34 iWidget * dlg;
35 uint32_t startTime;
36 iDocumentWidget *doc;
37 int timer;
38};
39
40iDeclareTypeConstructionArgs(Translation, iDocumentWidget *)
41
42void submit_Translation (iTranslation *);
43iBool handleCommand_Translation (iTranslation *, const char *cmd);
44
45iBool isFinished_Translation (const iTranslation *);
diff --git a/src/ui/util.c b/src/ui/util.c
index a2aaa893..61ecf948 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1688,8 +1688,19 @@ static void appendFramelessTabPage_(iWidget *tabs, iWidget *page, const char *ti
1688 iTrue); 1688 iTrue);
1689} 1689}
1690 1690
1691static iWidget *makeTwoColumnWidget_(iWidget **headings, iWidget **values) {
1692 iWidget *page = new_Widget();
1693 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
1694 *headings = addChildFlags_Widget(
1695 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
1696 *values = addChildFlags_Widget(
1697 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
1698 return page;
1699}
1700
1691static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int shortcut, iWidget **headings, 1701static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int shortcut, iWidget **headings,
1692 iWidget **values) { 1702 iWidget **values) {
1703 /* TODO: Use `makeTwoColumnWidget_()`, see above. */
1693 iWidget *page = new_Widget(); 1704 iWidget *page = new_Widget();
1694 setFlags_Widget(page, arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag | 1705 setFlags_Widget(page, arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |
1695 resizeHeightOfChildren_WidgetFlag | borderTop_WidgetFlag, iTrue); 1706 resizeHeightOfChildren_WidgetFlag | borderTop_WidgetFlag, iTrue);
@@ -1935,13 +1946,8 @@ iWidget *makeBookmarkEditor_Widget(void) {
1935 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", NULL)), 1946 iClob(new_LabelWidget(uiHeading_ColorEscape "EDIT BOOKMARK", NULL)),
1936 frameless_WidgetFlag), 1947 frameless_WidgetFlag),
1937 "bmed.heading"); 1948 "bmed.heading");
1938 iWidget *page = new_Widget(); 1949 iWidget *headings, *values;
1939 addChild_Widget(dlg, iClob(page)); 1950 addChild_Widget(dlg, iClob(makeTwoColumnWidget_(&headings, &values)));
1940 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
1941 iWidget *headings = addChildFlags_Widget(
1942 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
1943 iWidget *values = addChildFlags_Widget(
1944 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
1945 iInputWidget *inputs[4]; 1951 iInputWidget *inputs[4];
1946 addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); 1952 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));
1947 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title"); 1953 setId_Widget(addChild_Widget(values, iClob(inputs[0] = new_InputWidget(0))), "bmed.title");
@@ -2074,13 +2080,8 @@ iWidget *makeFeedSettings_Widget(uint32_t bookmarkId) {
2074 NULL)), 2080 NULL)),
2075 frameless_WidgetFlag), 2081 frameless_WidgetFlag),
2076 "feedcfg.heading"); 2082 "feedcfg.heading");
2077 iWidget *page = new_Widget(); 2083 iWidget *headings, *values;
2078 addChild_Widget(dlg, iClob(page)); 2084 addChild_Widget(dlg, iClob(makeTwoColumnWidget_(&headings, &values)));
2079 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
2080 iWidget *headings = addChildFlags_Widget(
2081 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2082 iWidget *values = addChildFlags_Widget(
2083 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2084 addChild_Widget(headings, iClob(makeHeading_Widget("Title:"))); 2085 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));
2085 iInputWidget *input = new_InputWidget(0); 2086 iInputWidget *input = new_InputWidget(0);
2086 setId_Widget(addChild_Widget(values, iClob(input)), "feedcfg.title"); 2087 setId_Widget(addChild_Widget(values, iClob(input)), "feedcfg.title");
@@ -2184,3 +2185,81 @@ iWidget *makeIdentityCreation_Widget(void) {
2184 finalizeSheet_Widget(dlg); 2185 finalizeSheet_Widget(dlg);
2185 return dlg; 2186 return dlg;
2186} 2187}
2188
2189static const iMenuItem languages[] = {
2190 { "Arabic", 0, 0, "xlt.lang id:ar" },
2191 { "Chinese", 0, 0, "xlt.lang id:zh" },
2192 { "English", 0, 0, "xlt.lang id:en" },
2193 { "French", 0, 0, "xlt.lang id:fr" },
2194 { "German", 0, 0, "xlt.lang id:de" },
2195 { "Hindi", 0, 0, "xlt.lang id:hi" },
2196 { "Italian", 0, 0, "xlt.lang id:it" },
2197 { "Japanese", 0, 0, "xlt.lang id:ja" },
2198 { "Portuguese", 0, 0, "xlt.lang id:pt" },
2199 { "Russian", 0, 0, "xlt.lang id:ru" },
2200 { "Spanish", 0, 0, "xlt.lang id:es" },
2201};
2202
2203static iBool translationHandler_(iWidget *dlg, const char *cmd) {
2204 if (equal_Command(cmd, "xlt.lang")) {
2205 iLabelWidget *menuItem = pointer_Command(cmd);
2206 iWidget *button = parent_Widget(parent_Widget(menuItem));
2207 iAssert(isInstance_Object(button, &Class_LabelWidget));
2208 updateText_LabelWidget((iLabelWidget *) button, text_LabelWidget(menuItem));
2209 return iTrue;
2210 }
2211 return iFalse;
2212}
2213
2214const char *languageId_String(const iString *menuItemLabel) {
2215 iForIndices(i, languages) {
2216 if (!cmp_String(menuItemLabel, languages[i].label)) {
2217 return cstr_Rangecc(range_Command(languages[i].command, "id"));
2218 }
2219 }
2220 return "";
2221}
2222
2223iWidget *makeTranslation_Widget(iWidget *parent) {
2224 iWidget *dlg = makeSheet_Widget("xlt");
2225 setCommandHandler_Widget(dlg, translationHandler_);
2226 addChildFlags_Widget(dlg,
2227 iClob(new_LabelWidget(uiHeading_ColorEscape "TRANSLATE PAGE", NULL)),
2228 frameless_WidgetFlag);
2229 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2230 iWidget *headings, *values;
2231 iWidget *page;
2232 addChild_Widget(dlg, iClob(page = makeTwoColumnWidget_(&headings, &values)));
2233 setId_Widget(page, "xlt.langs");
2234 addChild_Widget(headings, iClob(makeHeading_Widget("From:")));
2235 iLabelWidget *fromLang, *toLang;
2236 setId_Widget(addChildFlags_Widget(values,
2237 iClob(fromLang = makeMenuButton_LabelWidget(
2238 "Portuguese", languages, iElemCount(languages))),
2239 alignLeft_WidgetFlag),
2240 "xlt.from");
2241 updateTextCStr_LabelWidget(fromLang, "French"); /* TODO: Check source media type; remember last use. */
2242 setBackgroundColor_Widget(findChild_Widget(as_Widget(fromLang), "menu"),
2243 uiBackgroundMenu_ColorId);
2244 addChild_Widget(headings, iClob(makeHeading_Widget("To:")));
2245 setId_Widget(addChildFlags_Widget(values,
2246 iClob(toLang = makeMenuButton_LabelWidget(
2247 "Portuguese", languages, iElemCount(languages))),
2248 alignLeft_WidgetFlag),
2249 "xlt.to");
2250 setBackgroundColor_Widget(findChild_Widget(as_Widget(toLang), "menu"),
2251 uiBackgroundMenu_ColorId);
2252 updateTextCStr_LabelWidget(toLang, "English"); /* TODO: User preference. */
2253 addChild_Widget(dlg, iClob(makePadding_Widget(lineHeight_Text(uiLabel_FontId))));
2254 addChild_Widget(
2255 dlg,
2256 iClob(makeDialogButtons_Widget(
2257 (iMenuItem[]){
2258 { "Cancel", SDLK_ESCAPE, 0, "translation.cancel" },
2259 { uiTextAction_ColorEscape "Translate", SDLK_RETURN, 0, "translation.submit" } },
2260 2)));
2261 addChild_Widget(parent, iClob(dlg));
2262 arrange_Widget(dlg);
2263 finalizeSheet_Widget(dlg);
2264 return dlg;
2265}
diff --git a/src/ui/util.h b/src/ui/util.h
index e2461b9d..251683f5 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -223,3 +223,6 @@ iWidget * makeBookmarkEditor_Widget (void);
223iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon); 223iWidget * makeBookmarkCreation_Widget (const iString *url, const iString *title, iChar icon);
224iWidget * makeIdentityCreation_Widget (void); 224iWidget * makeIdentityCreation_Widget (void);
225iWidget * makeFeedSettings_Widget (uint32_t bookmarkId); 225iWidget * makeFeedSettings_Widget (uint32_t bookmarkId);
226iWidget * makeTranslation_Widget (iWidget *parent);
227
228const char * languageId_String (const iString *menuItemLabel);