1 /*
2  *  This file is part of Poedit (http://poedit.net)
3  *
4  *  Copyright (C) 2014-2015 Vaclav Slavik
5  *
6  *  Permission is hereby granted, free of charge, to any person obtaining a
7  *  copy of this software and associated documentation files (the "Software"),
8  *  to deal in the Software without restriction, including without limitation
9  *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
10  *  and/or sell copies of the Software, and to permit persons to whom the
11  *  Software is furnished to do so, subject to the following conditions:
12  *
13  *  The above copyright notice and this permission notice shall be included in
14  *  all copies or substantial portions of the Software.
15  *
16  *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21  *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22  *  DEALINGS IN THE SOFTWARE.
23  *
24  */
25 
26 #include "sidebar.h"
27 
28 #include "catalog.h"
29 #include "customcontrols.h"
30 #include "commentdlg.h"
31 #include "concurrency.h"
32 #include "errors.h"
33 #include "hidpi.h"
34 
35 #include "tm/suggestions.h"
36 #include "tm/transmem.h"
37 
38 #include <wx/app.h>
39 #include <wx/button.h>
40 #include <wx/config.h>
41 #include <wx/dcclient.h>
42 #include <wx/menu.h>
43 #include <wx/sizer.h>
44 #include <wx/statbmp.h>
45 #include <wx/stattext.h>
46 #include <wx/wupdlock.h>
47 
48 #include <algorithm>
49 
50 #define SIDEBAR_BACKGROUND      wxColour("#EDF0F4")
51 #define GRAY_LINES_COLOR        wxColour(220,220,220)
52 #define GRAY_LINES_COLOR_DARK   wxColour(180,180,180)
53 
54 
55 class SidebarSeparator : public wxWindow
56 {
57 public:
SidebarSeparator(wxWindow * parent)58     SidebarSeparator(wxWindow *parent)
59         : wxWindow(parent, wxID_ANY),
60           m_sides(SIDEBAR_BACKGROUND),
61           m_center(GRAY_LINES_COLOR_DARK)
62     {
63         Bind(wxEVT_PAINT, &SidebarSeparator::OnPaint, this);
64     }
65 
DoGetBestSize() const66     wxSize DoGetBestSize() const override
67     {
68         return wxSize(-1, 1);
69     }
70 
71 private:
OnPaint(wxPaintEvent &)72     void OnPaint(wxPaintEvent&)
73     {
74         wxPaintDC dc(this);
75         auto w = dc.GetSize().x;
76         dc.GradientFillLinear(wxRect(0,0,PX(15),PX(1)), m_sides, m_center);
77         dc.GradientFillLinear(wxRect(PX(15),0,w,PX(1)), m_center, m_sides);
78     }
79 
80     wxColour m_sides, m_center;
81 };
82 
83 
SidebarBlock(Sidebar * parent,const wxString & label,int flags)84 SidebarBlock::SidebarBlock(Sidebar *parent, const wxString& label, int flags)
85 {
86     m_parent = parent;
87     m_sizer = new wxBoxSizer(wxVERTICAL);
88     if (!(flags & NoUpperMargin))
89         m_sizer->AddSpacer(PX(15));
90     if (!label.empty())
91     {
92         if (!(flags & NoUpperMargin))
93         {
94             m_sizer->Add(new SidebarSeparator(parent),
95                          wxSizerFlags().Expand().Border(wxBOTTOM|wxLEFT, PX(2)));
96         }
97         m_headerSizer = new wxBoxSizer(wxHORIZONTAL);
98         m_headerSizer->Add(new HeadingLabel(parent, label), wxSizerFlags().Expand().Center());
99         m_sizer->Add(m_headerSizer, wxSizerFlags().Expand().PXDoubleBorder(wxLEFT|wxRIGHT));
100     }
101     m_innerSizer = new wxBoxSizer(wxVERTICAL);
102     m_sizer->Add(m_innerSizer, wxSizerFlags(1).Expand().PXDoubleBorder(wxLEFT|wxRIGHT));
103 }
104 
Show(bool show)105 void SidebarBlock::Show(bool show)
106 {
107     m_sizer->ShowItems(show);
108 }
109 
SetItem(const CatalogItemPtr & item)110 void SidebarBlock::SetItem(const CatalogItemPtr& item)
111 {
112     if (!item)
113     {
114         Show(false);
115         return;
116     }
117 
118     bool use = ShouldShowForItem(item);
119     if (use)
120         Update(item);
121     Show(use);
122 }
123 
124 
125 class OldMsgidSidebarBlock : public SidebarBlock
126 {
127 public:
OldMsgidSidebarBlock(Sidebar * parent)128     OldMsgidSidebarBlock(Sidebar *parent)
129           /// TRANSLATORS: "Previous" as in used in the past, now replaced with newer.
130         : SidebarBlock(parent, _("Previous source text:"))
131     {
132         m_innerSizer->AddSpacer(PX(2));
133         m_innerSizer->Add(new ExplanationLabel(parent, _("The old source text (before it changed during an update) that the fuzzy translation corresponds to.")),
134                      wxSizerFlags().Expand());
135         m_innerSizer->AddSpacer(PX(5));
136         m_text = new SelectableAutoWrappingText(parent, "");
137         m_innerSizer->Add(m_text, wxSizerFlags().Expand());
138     }
139 
ShouldShowForItem(const CatalogItemPtr & item) const140     bool ShouldShowForItem(const CatalogItemPtr& item) const override
141     {
142         return item->HasOldMsgid();
143     }
144 
Update(const CatalogItemPtr & item)145     void Update(const CatalogItemPtr& item) override
146     {
147         auto txt = wxJoin(item->GetOldMsgid(), ' ', '\0');
148         m_text->SetAndWrapLabel(txt);
149     }
150 
151 private:
152     SelectableAutoWrappingText *m_text;
153 };
154 
155 
156 class ExtractedCommentSidebarBlock : public SidebarBlock
157 {
158 public:
ExtractedCommentSidebarBlock(Sidebar * parent)159     ExtractedCommentSidebarBlock(Sidebar *parent)
160         : SidebarBlock(parent, _("Notes for translators:"))
161     {
162         m_innerSizer->AddSpacer(PX(5));
163         m_comment = new SelectableAutoWrappingText(parent, "");
164         m_innerSizer->Add(m_comment, wxSizerFlags().Expand());
165     }
166 
ShouldShowForItem(const CatalogItemPtr & item) const167     bool ShouldShowForItem(const CatalogItemPtr& item) const override
168     {
169         return item->HasExtractedComments();
170     }
171 
Update(const CatalogItemPtr & item)172     void Update(const CatalogItemPtr& item) override
173     {
174         auto comment = wxJoin(item->GetExtractedComments(), '\n', '\0');
175         if (comment.StartsWith("TRANSLATORS:") || comment.StartsWith("translators:"))
176         {
177             comment.Remove(0, 12);
178             if (!comment.empty() && comment[0] == ' ')
179                 comment.Remove(0, 1);
180         }
181         m_comment->SetAndWrapLabel(comment);
182     }
183 
184 private:
185     SelectableAutoWrappingText *m_comment;
186 };
187 
188 
189 class CommentSidebarBlock : public SidebarBlock
190 {
191 public:
CommentSidebarBlock(Sidebar * parent)192     CommentSidebarBlock(Sidebar *parent)
193         : SidebarBlock(parent, _("Comment:"))
194     {
195         m_innerSizer->AddSpacer(PX(5));
196         m_comment = new SelectableAutoWrappingText(parent, "");
197         m_innerSizer->Add(m_comment, wxSizerFlags().Expand());
198     }
199 
ShouldShowForItem(const CatalogItemPtr & item) const200     bool ShouldShowForItem(const CatalogItemPtr& item) const override
201     {
202         return item->HasComment();
203     }
204 
Update(const CatalogItemPtr & item)205     void Update(const CatalogItemPtr& item) override
206     {
207         auto text = CommentDialog::RemoveStartHash(item->GetComment());
208         text.Trim();
209         m_comment->SetAndWrapLabel(text);
210     }
211 
212 private:
213     SelectableAutoWrappingText *m_comment;
214 };
215 
216 
217 class AddCommentSidebarBlock : public SidebarBlock
218 {
219 public:
AddCommentSidebarBlock(Sidebar * parent)220     AddCommentSidebarBlock(Sidebar *parent) : SidebarBlock(parent, "")
221     {
222     #ifdef __WXMSW__
223         auto label = _("Add comment");
224     #else
225         auto label = _("Add Comment");
226     #endif
227         m_btn = new wxButton(parent, XRCID("menu_comment"), _("Add Comment"));
228         m_innerSizer->AddStretchSpacer();
229         m_innerSizer->Add(m_btn, wxSizerFlags().Right());
230     }
231 
IsGrowable() const232     bool IsGrowable() const override { return true; }
233 
ShouldShowForItem(const CatalogItemPtr &) const234     bool ShouldShowForItem(const CatalogItemPtr&) const override
235     {
236         return m_parent->FileHasCapability(Catalog::Cap::UserComments);
237     }
238 
Update(const CatalogItemPtr & item)239     void Update(const CatalogItemPtr& item) override
240     {
241     #ifdef __WXMSW__
242         auto add = _("Add comment");
243         auto edit = _("Edit comment");
244     #else
245         auto add = _("Add Comment");
246         auto edit = _("Edit Comment");
247     #endif
248         m_btn->SetLabel(item->HasComment() ? edit : add);
249     }
250 
251 private:
252     wxButton *m_btn;
253 };
254 
255 
256 wxDEFINE_EVENT(EVT_SUGGESTION_SELECTED, wxCommandEvent);
257 
258 class SuggestionWidget : public wxPanel
259 {
260 public:
SuggestionWidget(wxWindow * parent)261     SuggestionWidget(wxWindow *parent) : wxPanel(parent, wxID_ANY)
262     {
263         m_appIsRTL = (wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft);
264 
265         m_icon = new wxStaticBitmap(this, wxID_ANY, wxNullBitmap);
266         m_text = new AutoWrappingText(this, "TEXT");
267         m_info = new InfoStaticText(this);
268 
269         auto top = new wxBoxSizer(wxHORIZONTAL);
270         auto right = new wxBoxSizer(wxVERTICAL);
271         top->AddSpacer(PX(2));
272         top->Add(m_icon, wxSizerFlags().Top().PXBorder(wxTOP|wxBOTTOM));
273         top->Add(right, wxSizerFlags(1).Expand().PXBorder(wxLEFT));
274         right->Add(m_text, wxSizerFlags().Expand().Border(wxTOP, PX(4)));
275         right->Add(m_info, wxSizerFlags().Expand().Border(wxTOP|wxBOTTOM, PX(2)));
276         SetSizerAndFit(top);
277 
278         // setup mouse hover highlighting:
279         m_bg = parent->GetBackgroundColour();
280         m_bgHighlight = m_bg.ChangeLightness(160);
281 
282         wxWindow* parts [] = { this, m_icon, m_text, m_info };
283         for (auto w : parts)
284         {
285             w->Bind(wxEVT_MOTION,       &SuggestionWidget::OnMouseMove, this);
286             w->Bind(wxEVT_LEAVE_WINDOW, &SuggestionWidget::OnMouseMove, this);
287             w->Bind(wxEVT_LEFT_UP,      &SuggestionWidget::OnMouseClick, this);
288         }
289     }
290 
SetValue(int index,const Suggestion & s,bool isRTL,const wxBitmap & icon,const wxString & tooltip)291     void SetValue(int index, const Suggestion& s, bool isRTL, const wxBitmap& icon, const wxString& tooltip)
292     {
293         m_value = s;
294 
295         auto percent = wxString::Format("%d%%", int(100 * s.score));
296 
297         index++;
298         if (index < 10)
299         {
300         #ifdef __WXOSX__
301             auto shortcut = wxString::Format(L"⌘%d", index);
302         #else
303             // TRANSLATORS: This is the key shortcut used in menus on Windows, some languages call them differently
304             auto shortcut = wxString::Format("%s%d", _("Ctrl+"), index);
305         #endif
306             m_info->SetLabel(wxString::Format(L"%s • %s", shortcut, percent));
307         }
308         else
309         {
310             m_info->SetLabel(percent);
311         }
312 
313         m_icon->SetBitmap(icon);
314 
315         auto text = wxControl::EscapeMnemonics(s.text);
316         if (isRTL)
317             text = L"\u202b" + text;
318         else
319             text = L"\u202a" + text;
320         m_text->SetAndWrapLabel(text);
321 
322         // a quirk of wx API: if the current locale is RTL, the meaning of L and R is reversed
323         // for alignments
324         if (m_appIsRTL)
325             isRTL = !isRTL;
326 
327         m_text->SetAlignment(isRTL ? wxALIGN_RIGHT : wxALIGN_LEFT);
328         m_text->SetAndWrapLabel(text);
329 
330 #ifndef __WXOSX__
331         // FIXME: Causes weird issues on OS X: tooltips appearing on the main list control,
332         //        over toolbar, where the mouse just was etc.
333         m_icon->SetToolTip(tooltip);
334         m_text->SetToolTip(tooltip);
335 #endif
336         (void)tooltip;
337 
338         SetBackgroundColour(m_bg);
339         InvalidateBestSize();
340         SetMinSize(wxDefaultSize);
341         SetMinSize(GetBestSize());
342     }
343 
344 private:
345     class InfoStaticText : public wxStaticText
346     {
347     public:
InfoStaticText(wxWindow * parent)348         InfoStaticText(wxWindow *parent) : wxStaticText(parent, wxID_ANY, "100%")
349         {
350             SetForegroundColour(ExplanationLabel::GetTextColor());
351         #ifdef __WXMSW__
352             SetFont(GetFont().Smaller());
353         #else
354             SetWindowVariant(wxWINDOW_VARIANT_SMALL);
355         #endif
356         }
357 
DoEnable(bool)358         void DoEnable(bool) override {} // wxOSX's disabling would break color
359     };
360 
OnMouseMove(wxMouseEvent & e)361     void OnMouseMove(wxMouseEvent& e)
362     {
363         auto rectWin = GetClientRect();
364         rectWin.Deflate(1); // work around off-by-one issue on OS X
365         auto evtWin = static_cast<wxWindow*>(e.GetEventObject());
366         auto mpos = e.GetPosition();
367         if (evtWin != this)
368             mpos += evtWin->GetPosition();
369         bool highlighted = rectWin.Contains(mpos);
370         SetBackgroundColour(highlighted ? m_bgHighlight : m_bg);
371         Refresh();
372     }
373 
OnMouseClick(wxMouseEvent &)374     void OnMouseClick(wxMouseEvent&)
375     {
376         wxCommandEvent event(EVT_SUGGESTION_SELECTED);
377         event.SetEventObject(this);
378         event.SetString(m_value.text);
379         ProcessWindowEvent(event);
380     }
381 
382     Suggestion m_value;
383     wxStaticBitmap *m_icon;
384     AutoWrappingText *m_text;
385     wxStaticText *m_info;
386     wxColour m_bg, m_bgHighlight;
387     bool m_appIsRTL;
388 };
389 
390 
SuggestionsSidebarBlock(Sidebar * parent,wxMenu * menu)391 SuggestionsSidebarBlock::SuggestionsSidebarBlock(Sidebar *parent, wxMenu *menu)
392     : SidebarBlock(parent, _("Translation suggestions:"), NoUpperMargin),
393       m_suggestionsMenu(menu),
394       m_msgPresent(false),
395       m_pendingQueries(0),
396       m_latestQueryId(0)
397 {
398     m_provider.reset(new SuggestionsProvider);
399 
400     m_msgSizer = new wxBoxSizer(wxHORIZONTAL);
401     m_msgIcon = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap);
402     m_msgText = new ExplanationLabel(parent, "");
403     m_msgSizer->Add(m_msgIcon, wxSizerFlags().Center().PXBorderAll());
404     m_msgSizer->Add(m_msgText, wxSizerFlags(1).Center().PXBorder(wxTOP|wxBOTTOM));
405     m_innerSizer->Add(m_msgSizer, wxSizerFlags().Expand());
406 
407     m_innerSizer->AddSpacer(PX(10));
408 
409     m_suggestionsSizer = new wxBoxSizer(wxVERTICAL);
410     m_extrasSizer = new wxBoxSizer(wxVERTICAL);
411     m_innerSizer->Add(m_suggestionsSizer, wxSizerFlags().Expand());
412     m_innerSizer->Add(m_extrasSizer, wxSizerFlags().Expand());
413 
414     m_iGotNothing = new wxStaticText(parent, wxID_ANY,
415                                 #ifdef __WXMSW__
416                                      // TRANSLATORS: This is shown when no translation suggestions can be found in the TM (Windows).
417                                      _("No matches found")
418                                 #else
419                                      // TRANSLATORS: This is shown when no translation suggestions can be found in the TM (OS X, Linux).
420                                      _("No Matches Found")
421                                 #endif
422                                      );
423     m_iGotNothing->SetForegroundColour(ExplanationLabel::GetTextColor().ChangeLightness(150));
424     m_iGotNothing->SetWindowVariant(wxWINDOW_VARIANT_NORMAL);
425 #ifdef __WXMSW__
426     m_iGotNothing->SetFont(m_iGotNothing->GetFont().Larger());
427 #endif
428     m_innerSizer->Add(m_iGotNothing, wxSizerFlags().Center().Border(wxTOP|wxBOTTOM, PX(100)));
429 
430     BuildSuggestionsMenu();
431 }
432 
~SuggestionsSidebarBlock()433 SuggestionsSidebarBlock::~SuggestionsSidebarBlock()
434 {
435 }
436 
GetIconForSuggestion(const Suggestion &) const437 wxBitmap SuggestionsSidebarBlock::GetIconForSuggestion(const Suggestion&) const
438 {
439     return wxArtProvider::GetBitmap("SuggestionTM");
440 }
441 
GetTooltipForSuggestion(const Suggestion &) const442 wxString SuggestionsSidebarBlock::GetTooltipForSuggestion(const Suggestion&) const
443 {
444     return _(L"This string was found in Poedit’s translation memory.");
445 }
446 
ClearMessage()447 void SuggestionsSidebarBlock::ClearMessage()
448 {
449     m_msgPresent = false;
450     m_msgText->SetAndWrapLabel("");
451     UpdateVisibility();
452     m_parent->Layout();
453 }
454 
SetMessage(const wxString & icon,const wxString & text)455 void SuggestionsSidebarBlock::SetMessage(const wxString& icon, const wxString& text)
456 {
457     m_msgPresent = true;
458     m_msgIcon->SetBitmap(wxArtProvider::GetBitmap(icon));
459     m_msgText->SetAndWrapLabel(text);
460     UpdateVisibility();
461     m_parent->Layout();
462 }
463 
ReportError(SuggestionsBackend *,std::exception_ptr e)464 void SuggestionsSidebarBlock::ReportError(SuggestionsBackend*, std::exception_ptr e)
465 {
466     SetMessage("SuggestionError", DescribeException(e));
467 }
468 
ClearSuggestions()469 void SuggestionsSidebarBlock::ClearSuggestions()
470 {
471     m_pendingQueries = 0;
472     m_suggestions.clear();
473     UpdateSuggestionsMenu();
474     UpdateVisibility();
475 }
476 
UpdateSuggestions(const SuggestionsList & hits)477 void SuggestionsSidebarBlock::UpdateSuggestions(const SuggestionsList& hits)
478 {
479     wxWindowUpdateLocker lock(m_parent);
480 
481     for (auto& h: hits)
482     {
483         // empty entries screw up menus (treated as stock items), don't use them:
484         if (!h.text.empty())
485             m_suggestions.push_back(h);
486     }
487 
488     std::stable_sort(m_suggestions.begin(), m_suggestions.end());
489 
490     // create any necessary controls:
491     while (m_suggestions.size() > m_suggestionsWidgets.size())
492     {
493         auto w = new SuggestionWidget(m_parent);
494         m_suggestionsSizer->Add(w, wxSizerFlags().Expand());
495         m_suggestionsWidgets.push_back(w);
496     }
497     m_innerSizer->Layout();
498 
499     // update shown suggestions:
500     bool isRTL = m_parent->GetCurrentLanguage().IsRTL();
501     for (size_t i = 0; i < m_suggestions.size(); ++i)
502     {
503         auto s = m_suggestions[i];
504         m_suggestionsWidgets[i]->SetValue((int)i, s, isRTL, GetIconForSuggestion(s), GetTooltipForSuggestion(s));
505     }
506 
507     m_innerSizer->Layout();
508     UpdateVisibility();
509     m_parent->Layout();
510 
511     UpdateSuggestionsMenu();
512 }
513 
BuildSuggestionsMenu(int count)514 void SuggestionsSidebarBlock::BuildSuggestionsMenu(int count)
515 {
516     m_suggestionMenuItems.reserve(SUGGESTIONS_MENU_ENTRIES);
517     auto menu = m_suggestionsMenu;
518     for (int i = 0; i < count; i++)
519     {
520         auto text = wxString::Format("(empty)\t%s%d", _("Ctrl+"), i+1);
521         auto item = new wxMenuItem(menu, wxID_ANY, text);
522         item->SetBitmap(wxArtProvider::GetBitmap("SuggestionTM"));
523 
524         m_suggestionMenuItems.push_back(item);
525         menu->Append(item);
526 
527         m_suggestionsMenu->Bind(wxEVT_MENU, [this,i,menu](wxCommandEvent&){
528             if (i >= (int)m_suggestions.size())
529                 return;
530             wxCommandEvent event(EVT_SUGGESTION_SELECTED);
531             event.SetEventObject(menu);
532             event.SetString(m_suggestions[i].text);
533             menu->GetWindow()->ProcessWindowEvent(event);
534         }, item->GetId());
535     }
536 }
537 
UpdateSuggestionsMenu()538 void SuggestionsSidebarBlock::UpdateSuggestionsMenu()
539 {
540     ClearSuggestionsMenu();
541 
542     bool isRTL = m_parent->GetCurrentLanguage().IsRTL();
543     wxString formatMask;
544     if (isRTL)
545         formatMask = L"\u202b%s\u202c\t" + _("Ctrl+") + "%d";
546     else
547         formatMask = L"\u202a%s\u202c\t" + _("Ctrl+") + "%d";
548 
549     int index = 0;
550     for (auto s: m_suggestions)
551     {
552         if (index >= SUGGESTIONS_MENU_ENTRIES)
553             break;
554 
555         auto text = wxString::Format(formatMask, s.text, index+1);
556 
557         auto item = m_suggestionMenuItems[index];
558         m_suggestionsMenu->Append(item);
559 
560         item->SetItemLabel(text);
561         item->SetBitmap(GetIconForSuggestion(s));
562 
563         index++;
564     }
565 }
566 
ClearSuggestionsMenu()567 void SuggestionsSidebarBlock::ClearSuggestionsMenu()
568 {
569     auto m = m_suggestionsMenu;
570 
571     auto menuItems = m->GetMenuItems();
572     for (auto i: menuItems)
573     {
574         if (std::find(m_suggestionMenuItems.begin(), m_suggestionMenuItems.end(), i) != m_suggestionMenuItems.end())
575             m->Remove(i);
576     }
577 }
578 
579 
OnQueriesFinished()580 void SuggestionsSidebarBlock::OnQueriesFinished()
581 {
582     if (m_suggestions.empty())
583     {
584         m_innerSizer->Show(m_iGotNothing);
585         m_parent->Layout();
586     }
587 }
588 
UpdateVisibility()589 void SuggestionsSidebarBlock::UpdateVisibility()
590 {
591     m_msgSizer->ShowItems(m_msgPresent);
592     m_innerSizer->Show(m_iGotNothing, m_suggestions.empty() && !m_pendingQueries);
593 
594     int heightRemaining = m_innerSizer->GetSize().y;
595     size_t w = 0;
596     for (w = 0; w < m_suggestions.size(); w++)
597     {
598         heightRemaining -= m_suggestionsWidgets[w]->GetSize().y;
599         if (heightRemaining < 20)
600             break;
601         m_suggestionsSizer->Show(m_suggestionsWidgets[w]);
602     }
603 
604     for (; w < m_suggestionsWidgets.size(); w++)
605         m_suggestionsSizer->Hide(m_suggestionsWidgets[w]);
606 
607 }
608 
Show(bool show)609 void SuggestionsSidebarBlock::Show(bool show)
610 {
611     SidebarBlock::Show(show);
612     if (show)
613     {
614         UpdateVisibility();
615     }
616     else
617     {
618         ClearSuggestionsMenu();
619     }
620 }
621 
ShouldShowForItem(const CatalogItemPtr &) const622 bool SuggestionsSidebarBlock::ShouldShowForItem(const CatalogItemPtr&) const
623 {
624     return m_parent->FileHasCapability(Catalog::Cap::Translations) &&
625            wxConfig::Get()->ReadBool("use_tm", true);
626 }
627 
Update(const CatalogItemPtr & item)628 void SuggestionsSidebarBlock::Update(const CatalogItemPtr& item)
629 {
630     // FIXME: Cancel previous pending async operation if any
631 
632     ClearMessage();
633     ClearSuggestions();
634 
635     auto srclang = m_parent->GetCurrentSourceLanguage();
636     auto lang = m_parent->GetCurrentLanguage();
637     if (!srclang.IsValid() || !lang.IsValid() || srclang == lang)
638     {
639         OnQueriesFinished();
640         return;
641     }
642 
643     QueryAllProviders(item);
644 }
645 
QueryAllProviders(const CatalogItemPtr & item)646 void SuggestionsSidebarBlock::QueryAllProviders(const CatalogItemPtr& item)
647 {
648     auto thisQueryId = ++m_latestQueryId;
649     QueryProvider(TranslationMemory::Get(), item, thisQueryId);
650 }
651 
QueryProvider(SuggestionsBackend & backend,const CatalogItemPtr & item,uint64_t queryId)652 void SuggestionsSidebarBlock::QueryProvider(SuggestionsBackend& backend, const CatalogItemPtr& item, uint64_t queryId)
653 {
654     m_pendingQueries++;
655 
656     // we need something to talk to GUI thread through that is guaranteed
657     // to exist, and the app object is a good choice:
658     auto backendPtr = &backend;
659     std::weak_ptr<SuggestionsSidebarBlock> weakSelf = std::dynamic_pointer_cast<SuggestionsSidebarBlock>(shared_from_this());
660 
661     m_provider->SuggestTranslation
662     (
663         backend,
664         m_parent->GetCurrentSourceLanguage(),
665         m_parent->GetCurrentLanguage(),
666         item->GetString().ToStdWstring(),
667 
668         // when receiving data
669         [=](const SuggestionsList& hits){
670             call_on_main_thread([weakSelf,queryId,hits]{
671                 auto self = weakSelf.lock();
672                 // maybe this call is already out of date:
673                 if (!self || self->m_latestQueryId != queryId)
674                     return;
675                 self->UpdateSuggestions(hits);
676                 if (--self->m_pendingQueries == 0)
677                     self->OnQueriesFinished();
678             });
679         },
680 
681         // on error:
682         [=](std::exception_ptr e){
683             call_on_main_thread([weakSelf,queryId,backendPtr,e]{
684                 auto self = weakSelf.lock();
685                 // maybe this call is already out of date:
686                 if (!self || self->m_latestQueryId != queryId)
687                     return;
688                 self->ReportError(backendPtr, e);
689                 if (--self->m_pendingQueries == 0)
690                     self->OnQueriesFinished();
691             });
692         }
693     );
694 }
695 
696 
697 
Sidebar(wxWindow * parent,wxMenu * suggestionsMenu)698 Sidebar::Sidebar(wxWindow *parent, wxMenu *suggestionsMenu)
699     : wxPanel(parent, wxID_ANY),
700       m_catalog(nullptr),
701       m_selectedItem(nullptr)
702 {
703     SetBackgroundColour(SIDEBAR_BACKGROUND);
704     Bind(wxEVT_PAINT, &Sidebar::OnPaint, this);
705 #ifdef __WXOSX__
706     SetWindowVariant(wxWINDOW_VARIANT_SMALL);
707 #endif
708 
709     auto *topSizer = new wxBoxSizer(wxVERTICAL);
710     topSizer->SetMinSize(wxSize(PX(300), -1));
711 
712     m_blocksSizer = new wxBoxSizer(wxVERTICAL);
713     topSizer->Add(m_blocksSizer, wxSizerFlags(1).Expand().PXDoubleBorder(wxTOP|wxBOTTOM));
714 
715     m_topBlocksSizer = new wxBoxSizer(wxVERTICAL);
716     m_bottomBlocksSizer = new wxBoxSizer(wxVERTICAL);
717 
718     m_blocksSizer->Add(m_topBlocksSizer, wxSizerFlags(1).Expand().ReserveSpaceEvenIfHidden());
719     m_blocksSizer->Add(m_bottomBlocksSizer, wxSizerFlags().Expand());
720 
721     AddBlock(new SuggestionsSidebarBlock(this, suggestionsMenu), Top);
722     AddBlock(new OldMsgidSidebarBlock(this), Bottom);
723     AddBlock(new ExtractedCommentSidebarBlock(this), Bottom);
724     AddBlock(new CommentSidebarBlock(this), Bottom);
725     AddBlock(new AddCommentSidebarBlock(this), Bottom);
726 
727     SetSizerAndFit(topSizer);
728 
729     SetSelectedItem(nullptr, nullptr);
730 }
731 
732 
AddBlock(SidebarBlock * block,BlockPos pos)733 void Sidebar::AddBlock(SidebarBlock *block, BlockPos pos)
734 {
735     m_blocks.emplace_back(block);
736 
737     auto sizer = (pos == Top) ? m_topBlocksSizer : m_bottomBlocksSizer;
738     auto grow = (block->IsGrowable()) ? 1 : 0;
739     sizer->Add(block->GetSizer(), wxSizerFlags(grow).Expand());
740 }
741 
742 
~Sidebar()743 Sidebar::~Sidebar()
744 {
745 }
746 
747 
SetSelectedItem(const CatalogPtr & catalog,const CatalogItemPtr & item)748 void Sidebar::SetSelectedItem(const CatalogPtr& catalog, const CatalogItemPtr& item)
749 {
750     m_catalog = catalog;
751     m_selectedItem = item;
752     RefreshContent();
753 }
754 
SetMultipleSelection()755 void Sidebar::SetMultipleSelection()
756 {
757     SetSelectedItem(nullptr, nullptr);
758 }
759 
GetCurrentLanguage() const760 Language Sidebar::GetCurrentLanguage() const
761 {
762     if (!m_catalog)
763         return Language();
764     return m_catalog->GetLanguage();
765 }
766 
GetCurrentSourceLanguage() const767 Language Sidebar::GetCurrentSourceLanguage() const
768 {
769     if (!m_catalog)
770         return Language::English();
771     return m_catalog->GetSourceLanguage();
772 }
773 
FileHasCapability(Catalog::Cap cap) const774 bool Sidebar::FileHasCapability(Catalog::Cap cap) const
775 {
776     return m_catalog && m_catalog->HasCapability(cap);
777 }
778 
RefreshContent()779 void Sidebar::RefreshContent()
780 {
781     if (!IsShown())
782         return;
783 
784     auto item = m_selectedItem;
785     if (!IsThisEnabled())
786         item = nullptr;
787 
788     wxWindowUpdateLocker lock(this);
789     for (auto& b: m_blocks)
790         b->SetItem(item);
791     Layout();
792 }
793 
SetUpperHeight(int size)794 void Sidebar::SetUpperHeight(int size)
795 {
796     wxWindowUpdateLocker lock(this);
797 
798     int pos = GetSize().y - size;
799 #ifdef __WXOSX__
800     pos += 4;
801 #else
802     pos += 6;
803 #endif
804     m_bottomBlocksSizer->SetMinSize(wxSize(-1, pos));
805     Layout();
806 }
807 
DoEnable(bool)808 void Sidebar::DoEnable(bool)
809 {
810     RefreshContent();
811 }
812 
OnPaint(wxPaintEvent &)813 void Sidebar::OnPaint(wxPaintEvent&)
814 {
815     wxPaintDC dc(this);
816 
817     dc.SetPen(wxPen(GRAY_LINES_COLOR));
818 #ifndef __WXMSW__
819     dc.DrawLine(0, 0, 0, dc.GetSize().y-1);
820 #endif
821 #ifndef __WXOSX__
822     dc.DrawLine(0, 0, dc.GetSize().x - 1, 0);
823 #endif
824 }
825