1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "globals.h" |
5 | #include "mainwindow.h" |
6 | #include "messagemodel.h" |
7 | #include "phrase.h" |
8 | #include "phraseview.h" |
9 | #include "phrasemodel.h" |
10 | #include "simtexth.h" |
11 | |
12 | #include <QHeaderView> |
13 | #include <QKeyEvent> |
14 | #include <QSettings> |
15 | #include <QShortcut> |
16 | #include <QTreeView> |
17 | #include <QWidget> |
18 | #include <QDebug> |
19 | |
20 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | static QString () |
24 | { |
25 | return settingPath("PhraseViewHeader" ); |
26 | } |
27 | |
28 | PhraseView::PhraseView(MultiDataModel *model, QList<QHash<QString, QList<Phrase *> > > *phraseDict, QWidget *parent) |
29 | : QTreeView(parent), |
30 | m_dataModel(model), |
31 | m_phraseDict(phraseDict), |
32 | m_modelIndex(-1), |
33 | m_doGuesses(true) |
34 | { |
35 | setObjectName(QLatin1String("phrase list view" )); |
36 | |
37 | m_phraseModel = new PhraseModel(this); |
38 | |
39 | setModel(m_phraseModel); |
40 | setAlternatingRowColors(true); |
41 | setSelectionBehavior(QAbstractItemView::SelectRows); |
42 | setSelectionMode(QAbstractItemView::SingleSelection); |
43 | setRootIsDecorated(false); |
44 | setItemsExpandable(false); |
45 | |
46 | for (int i = 0; i < 9; ++i) { |
47 | const auto key = static_cast<Qt::Key>(int(Qt::Key_1) + i); |
48 | auto shortCut = new QShortcut(Qt::CTRL | key, this); |
49 | connect(sender: shortCut, signal: &QShortcut::activated, context: this, |
50 | slot: [i, this]() { this->guessShortcut(nkey: i); }); |
51 | } |
52 | |
53 | header()->setSectionResizeMode(QHeaderView::Interactive); |
54 | header()->setSectionsClickable(true); |
55 | header()->restoreState(state: QSettings().value(key: phraseViewHeaderKey()).toByteArray()); |
56 | |
57 | connect(sender: this, signal: &QAbstractItemView::activated, |
58 | context: this, slot: &PhraseView::selectPhrase); |
59 | } |
60 | |
61 | PhraseView::~PhraseView() |
62 | { |
63 | QSettings().setValue(key: phraseViewHeaderKey(), value: header()->saveState()); |
64 | deleteGuesses(); |
65 | } |
66 | |
67 | void PhraseView::toggleGuessing() |
68 | { |
69 | m_doGuesses = !m_doGuesses; |
70 | update(); |
71 | } |
72 | |
73 | void PhraseView::update() |
74 | { |
75 | setSourceText(model: m_modelIndex, sourceText: m_sourceText); |
76 | } |
77 | |
78 | |
79 | void PhraseView::(QContextMenuEvent *event) |
80 | { |
81 | QModelIndex index = indexAt(p: event->pos()); |
82 | if (!index.isValid()) |
83 | return; |
84 | |
85 | QMenu * = new QMenu(this); |
86 | |
87 | QAction *insertAction = new QAction(tr("Insert" ), contextMenu); |
88 | connect(sender: insertAction, signal: &QAction::triggered, |
89 | context: this, slot: &PhraseView::selectCurrentPhrase); |
90 | |
91 | QAction *editAction = new QAction(tr("Edit" ), contextMenu); |
92 | connect(sender: editAction, signal: &QAction::triggered, |
93 | context: this, slot: &PhraseView::editPhrase); |
94 | Qt::ItemFlags isFromPhraseBook = model()->flags(index) & Qt::ItemIsEditable; |
95 | editAction->setEnabled(isFromPhraseBook); |
96 | |
97 | QAction *gotoAction = new QAction(tr("Go to" ), contextMenu); |
98 | connect(sender: gotoAction, signal: &QAction::triggered, |
99 | context: this, slot: &PhraseView::gotoMessageFromGuess); |
100 | gotoAction->setEnabled(!isFromPhraseBook); |
101 | |
102 | contextMenu->addAction(insertAction); |
103 | contextMenu->addAction(editAction); |
104 | contextMenu->addAction(gotoAction); |
105 | |
106 | contextMenu->exec(event->globalPos()); |
107 | event->accept(); |
108 | } |
109 | |
110 | void PhraseView::mouseDoubleClickEvent(QMouseEvent *event) |
111 | { |
112 | QModelIndex index = indexAt(p: event->pos()); |
113 | if (!index.isValid()) |
114 | return; |
115 | |
116 | emit phraseSelected(latestModel: m_modelIndex, phrase: m_phraseModel->phrase(index)->target()); |
117 | event->accept(); |
118 | } |
119 | |
120 | void PhraseView::guessShortcut(int key) |
121 | { |
122 | const auto phrases = m_phraseModel->phraseList(); |
123 | for (const Phrase *phrase : phrases) |
124 | if (phrase->shortcut() == key) { |
125 | emit phraseSelected(latestModel: m_modelIndex, phrase: phrase->target()); |
126 | return; |
127 | } |
128 | } |
129 | |
130 | void PhraseView::selectPhrase(const QModelIndex &index) |
131 | { |
132 | emit phraseSelected(latestModel: m_modelIndex, phrase: m_phraseModel->phrase(index)->target()); |
133 | } |
134 | |
135 | void PhraseView::selectCurrentPhrase() |
136 | { |
137 | emit phraseSelected(latestModel: m_modelIndex, phrase: m_phraseModel->phrase(index: currentIndex())->target()); |
138 | } |
139 | |
140 | void PhraseView::editPhrase() |
141 | { |
142 | edit(index: currentIndex()); |
143 | } |
144 | |
145 | void PhraseView::gotoMessageFromGuess() |
146 | { |
147 | emit setCurrentMessageFromGuess(modelIndex: m_modelIndex, |
148 | cand: m_phraseModel->phrase(index: currentIndex())->candidate()); |
149 | } |
150 | |
151 | void PhraseView::setMaxCandidates(const int max) |
152 | { |
153 | m_maxCandidates = max; |
154 | emit showFewerGuessesAvailable(canShow: m_maxCandidates > DefaultMaxCandidates); |
155 | } |
156 | |
157 | void PhraseView::moreGuesses() |
158 | { |
159 | setMaxCandidates(m_maxCandidates + DefaultMaxCandidates); |
160 | setSourceText(model: m_modelIndex, sourceText: m_sourceText); |
161 | } |
162 | |
163 | void PhraseView::fewerGuesses() |
164 | { |
165 | setMaxCandidates(m_maxCandidates - DefaultMaxCandidates); |
166 | setSourceText(model: m_modelIndex, sourceText: m_sourceText); |
167 | } |
168 | |
169 | void PhraseView::resetNumGuesses() |
170 | { |
171 | setMaxCandidates(DefaultMaxCandidates); |
172 | setSourceText(model: m_modelIndex, sourceText: m_sourceText); |
173 | } |
174 | |
175 | static CandidateList similarTextHeuristicCandidates(MultiDataModel *model, int mi, |
176 | const char *text, int maxCandidates) |
177 | { |
178 | QList<int> scores; |
179 | CandidateList candidates; |
180 | |
181 | StringSimilarityMatcher stringmatcher(QString::fromLatin1(ba: text)); |
182 | |
183 | for (MultiDataModelIterator it(model, mi); it.isValid(); ++it) { |
184 | MessageItem *m = it.current(); |
185 | if (!m) |
186 | continue; |
187 | |
188 | TranslatorMessage mtm = m->message(); |
189 | if (mtm.type() == TranslatorMessage::Unfinished |
190 | || mtm.translation().isEmpty()) |
191 | continue; |
192 | |
193 | QString s = m->text(); |
194 | |
195 | int score = stringmatcher.getSimilarityScore(strCandidate: s); |
196 | |
197 | if (candidates.size() == maxCandidates && score > scores[maxCandidates - 1]) |
198 | candidates.removeLast(); |
199 | if (candidates.size() < maxCandidates && score >= textSimilarityThreshold ) { |
200 | Candidate cand(mtm.context(), s, mtm.comment(), mtm.translation()); |
201 | |
202 | int i; |
203 | for (i = 0; i < candidates.size(); ++i) { |
204 | if (score >= scores.at(i)) { |
205 | if (score == scores.at(i)) { |
206 | if (candidates.at(i) == cand) |
207 | goto continue_outer_loop; |
208 | } else { |
209 | break; |
210 | } |
211 | } |
212 | } |
213 | scores.insert(i, t: score); |
214 | candidates.insert(i, t: cand); |
215 | } |
216 | continue_outer_loop: |
217 | ; |
218 | } |
219 | return candidates; |
220 | } |
221 | |
222 | |
223 | void PhraseView::setSourceText(int model, const QString &sourceText) |
224 | { |
225 | m_modelIndex = model; |
226 | m_sourceText = sourceText; |
227 | m_phraseModel->removePhrases(); |
228 | deleteGuesses(); |
229 | |
230 | if (model < 0) |
231 | return; |
232 | |
233 | const auto phrases = getPhrases(model, sourceText); |
234 | for (Phrase *p : phrases) |
235 | m_phraseModel->addPhrase(p); |
236 | |
237 | if (!sourceText.isEmpty() && m_doGuesses) { |
238 | const CandidateList cl = similarTextHeuristicCandidates(model: m_dataModel, mi: model, |
239 | text: sourceText.toLatin1(), maxCandidates: m_maxCandidates); |
240 | int n = 0; |
241 | for (const Candidate &candidate : cl) { |
242 | QString def; |
243 | if (n < 9) |
244 | def = tr(s: "Guess from '%1' (%2)" ) |
245 | .arg(args: candidate.context, args: QKeySequence(Qt::CTRL | (Qt::Key_0 + (n + 1))) |
246 | .toString(format: QKeySequence::NativeText)); |
247 | else |
248 | def = tr(s: "Guess from '%1'" ).arg(a: candidate.context); |
249 | Phrase *guess = new Phrase(candidate.source, candidate.translation, def, candidate, n); |
250 | m_guesses.append(t: guess); |
251 | m_phraseModel->addPhrase(p: guess); |
252 | ++n; |
253 | } |
254 | } |
255 | } |
256 | |
257 | QList<Phrase *> PhraseView::getPhrases(int model, const QString &source) |
258 | { |
259 | QList<Phrase *> phrases; |
260 | const QString f = MainWindow::friendlyString(str: source); |
261 | const QStringList lookupWords = f.split(sep: QLatin1Char(' ')); |
262 | |
263 | for (const QString &s : lookupWords) { |
264 | if (m_phraseDict->at(i: model).contains(key: s)) { |
265 | const auto phraseList = m_phraseDict->at(i: model).value(key: s); |
266 | for (Phrase *p : phraseList) { |
267 | if (f.contains(s: MainWindow::friendlyString(str: p->source()))) |
268 | phrases.append(t: p); |
269 | } |
270 | } |
271 | } |
272 | return phrases; |
273 | } |
274 | |
275 | void PhraseView::deleteGuesses() |
276 | { |
277 | qDeleteAll(c: m_guesses); |
278 | m_guesses.clear(); |
279 | } |
280 | |
281 | QT_END_NAMESPACE |
282 | |