1 | // Copyright (C) 2020 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | /* TRANSLATOR MainWindow |
5 | |
6 | This is the application's main window. |
7 | */ |
8 | |
9 | #include "mainwindow.h" |
10 | |
11 | #include "batchtranslationdialog.h" |
12 | #include "errorsview.h" |
13 | #include "finddialog.h" |
14 | #include "formpreviewview.h" |
15 | #include "globals.h" |
16 | #include "messageeditor.h" |
17 | #include "messagemodel.h" |
18 | #include "phrasebookbox.h" |
19 | #include "phrasemodel.h" |
20 | #include "phraseview.h" |
21 | #include "printout.h" |
22 | #include "sourcecodeview.h" |
23 | #include "statistics.h" |
24 | #include "translatedialog.h" |
25 | #include "translationsettingsdialog.h" |
26 | |
27 | #include <QAction> |
28 | #include <QApplication> |
29 | #include <QBitmap> |
30 | #include <QCloseEvent> |
31 | #include <QDebug> |
32 | #include <QDockWidget> |
33 | #include <QFile> |
34 | #include <QFileDialog> |
35 | #include <QFileInfo> |
36 | #include <QHeaderView> |
37 | #include <QInputDialog> |
38 | #include <QItemDelegate> |
39 | #include <QLabel> |
40 | #include <QLayout> |
41 | #include <QLibraryInfo> |
42 | #include <QMenu> |
43 | #include <QMenuBar> |
44 | #include <QMessageBox> |
45 | #include <QMimeData> |
46 | #include <QPrintDialog> |
47 | #include <QPrinter> |
48 | #include <QProcess> |
49 | #include <QRegularExpression> |
50 | #include <QScreen> |
51 | #include <QShortcut> |
52 | #include <QSettings> |
53 | #include <QSortFilterProxyModel> |
54 | #include <QStackedWidget> |
55 | #include <QStatusBar> |
56 | #include <QTextStream> |
57 | #include <QToolBar> |
58 | #include <QUrl> |
59 | #include <QWhatsThis> |
60 | |
61 | #include <ctype.h> |
62 | |
63 | QT_BEGIN_NAMESPACE |
64 | |
65 | static const int MessageMS = 2500; |
66 | |
67 | enum Ending { |
68 | End_None, |
69 | End_FullStop, |
70 | End_Interrobang, |
71 | End_Colon, |
72 | End_Ellipsis |
73 | }; |
74 | |
75 | static bool hasFormPreview(const QString &fileName) |
76 | { |
77 | return fileName.endsWith(s: QLatin1String(".ui" )) |
78 | || fileName.endsWith(s: QLatin1String(".jui" )); |
79 | } |
80 | |
81 | static QString leadingWhitespace(const QString &str) |
82 | { |
83 | int i = 0; |
84 | for (; i < str.size(); i++) { |
85 | if (!str[i].isSpace()) { |
86 | break; |
87 | } |
88 | } |
89 | return str.left(n: i); |
90 | } |
91 | |
92 | static QString trailingWhitespace(const QString &str) |
93 | { |
94 | int i = str.size(); |
95 | while (--i >= 0) { |
96 | if (!str[i].isSpace()) { |
97 | break; |
98 | } |
99 | } |
100 | return str.mid(position: i + 1); |
101 | } |
102 | |
103 | static Ending ending(QString str, QLocale::Language lang) |
104 | { |
105 | str = str.simplified(); |
106 | if (str.isEmpty()) |
107 | return End_None; |
108 | |
109 | switch (str.at(i: str.size() - 1).unicode()) { |
110 | case 0x002e: // full stop |
111 | if (str.endsWith(s: QLatin1String("..." ))) |
112 | return End_Ellipsis; |
113 | else |
114 | return End_FullStop; |
115 | case 0x0589: // armenian full stop |
116 | case 0x06d4: // arabic full stop |
117 | case 0x3002: // ideographic full stop |
118 | return End_FullStop; |
119 | case 0x0021: // exclamation mark |
120 | case 0x003f: // question mark |
121 | case 0x00a1: // inverted exclamation mark |
122 | case 0x00bf: // inverted question mark |
123 | case 0x01c3: // latin letter retroflex click |
124 | case 0x037e: // greek question mark |
125 | case 0x061f: // arabic question mark |
126 | case 0x203c: // double exclamation mark |
127 | case 0x203d: // interrobang |
128 | case 0x2048: // question exclamation mark |
129 | case 0x2049: // exclamation question mark |
130 | case 0x2762: // heavy exclamation mark ornament |
131 | case 0xff01: // full width exclamation mark |
132 | case 0xff1f: // full width question mark |
133 | return End_Interrobang; |
134 | case 0x003b: // greek 'compatibility' questionmark |
135 | return lang == QLocale::Greek ? End_Interrobang : End_None; |
136 | case 0x003a: // colon |
137 | case 0xff1a: // full width colon |
138 | return End_Colon; |
139 | case 0x2026: // horizontal ellipsis |
140 | return End_Ellipsis; |
141 | default: |
142 | return End_None; |
143 | } |
144 | } |
145 | |
146 | |
147 | class ContextItemDelegate : public QItemDelegate |
148 | { |
149 | public: |
150 | ContextItemDelegate(QObject *parent, MultiDataModel *model) : QItemDelegate(parent), m_dataModel(model) {} |
151 | |
152 | void paint(QPainter *painter, const QStyleOptionViewItem &option, |
153 | const QModelIndex &index) const override |
154 | { |
155 | const QAbstractItemModel *model = index.model(); |
156 | Q_ASSERT(model); |
157 | |
158 | if (!model->parent(child: index).isValid()) { |
159 | if (index.column() - 1 == m_dataModel->modelCount()) { |
160 | QStyleOptionViewItem opt = option; |
161 | opt.font.setBold(true); |
162 | QItemDelegate::paint(painter, option: opt, index); |
163 | return; |
164 | } |
165 | } |
166 | QItemDelegate::paint(painter, option, index); |
167 | } |
168 | |
169 | private: |
170 | MultiDataModel *m_dataModel; |
171 | }; |
172 | |
173 | static const QVariant &pxObsolete() |
174 | { |
175 | static const QVariant v = |
176 | QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_obsolete.png" ))); |
177 | return v; |
178 | } |
179 | |
180 | |
181 | class SortedMessagesModel : public QSortFilterProxyModel |
182 | { |
183 | public: |
184 | SortedMessagesModel(QObject *parent, MultiDataModel *model) : QSortFilterProxyModel(parent), m_dataModel(model) {} |
185 | |
186 | QVariant (int section, Qt::Orientation orientation, int role) const override |
187 | { |
188 | if (role == Qt::DisplayRole && orientation == Qt::Horizontal) |
189 | switch (section - m_dataModel->modelCount()) { |
190 | case 0: return QString(); |
191 | case 1: return MainWindow::tr(s: "Source text" ); |
192 | case 2: return MainWindow::tr(s: "Index" ); |
193 | } |
194 | |
195 | if (role == Qt::DecorationRole && orientation == Qt::Horizontal && section - 1 < m_dataModel->modelCount()) |
196 | return pxObsolete(); |
197 | |
198 | return QVariant(); |
199 | } |
200 | |
201 | private: |
202 | MultiDataModel *m_dataModel; |
203 | }; |
204 | |
205 | class SortedContextsModel : public QSortFilterProxyModel |
206 | { |
207 | public: |
208 | SortedContextsModel(QObject *parent, MultiDataModel *model) : QSortFilterProxyModel(parent), m_dataModel(model) {} |
209 | |
210 | QVariant (int section, Qt::Orientation orientation, int role) const override |
211 | { |
212 | if (role == Qt::DisplayRole && orientation == Qt::Horizontal) |
213 | switch (section - m_dataModel->modelCount()) { |
214 | case 0: return QString(); |
215 | case 1: return MainWindow::tr(s: "Context" ); |
216 | case 2: return MainWindow::tr(s: "Items" ); |
217 | case 3: return MainWindow::tr(s: "Index" ); |
218 | } |
219 | |
220 | if (role == Qt::DecorationRole && orientation == Qt::Horizontal && section - 1 < m_dataModel->modelCount()) |
221 | return pxObsolete(); |
222 | |
223 | return QVariant(); |
224 | } |
225 | |
226 | private: |
227 | MultiDataModel *m_dataModel; |
228 | }; |
229 | |
230 | class FocusWatcher : public QObject |
231 | { |
232 | public: |
233 | FocusWatcher(MessageEditor *msgedit, QObject *parent) : QObject(parent), m_messageEditor(msgedit) {} |
234 | |
235 | protected: |
236 | bool eventFilter(QObject *object, QEvent *event) override; |
237 | |
238 | private: |
239 | MessageEditor *m_messageEditor; |
240 | }; |
241 | |
242 | bool FocusWatcher::eventFilter(QObject *, QEvent *event) |
243 | { |
244 | if (event->type() == QEvent::FocusIn) |
245 | m_messageEditor->setEditorFocusForModel(-1); |
246 | return false; |
247 | } |
248 | |
249 | MainWindow::MainWindow() |
250 | : QMainWindow(0, Qt::Window), |
251 | m_assistantProcess(0), |
252 | m_printer(0), |
253 | m_findWhere(DataModel::NoLocation), |
254 | m_translationSettingsDialog(0), |
255 | m_settingCurrentMessage(false), |
256 | m_fileActiveModel(-1), |
257 | m_editActiveModel(-1), |
258 | m_statistics(0) |
259 | { |
260 | setUnifiedTitleAndToolBarOnMac(true); |
261 | m_ui.setupUi(this); |
262 | |
263 | #if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN) |
264 | setWindowIcon(QPixmap(QLatin1String(":/images/appicon.png" ) )); |
265 | #endif |
266 | |
267 | m_dataModel = new MultiDataModel(this); |
268 | m_messageModel = new MessageModel(this, m_dataModel); |
269 | |
270 | // Set up the context dock widget |
271 | m_contextDock = new QDockWidget(this); |
272 | m_contextDock->setObjectName(QLatin1String("ContextDockWidget" )); |
273 | m_contextDock->setAllowedAreas(Qt::AllDockWidgetAreas); |
274 | m_contextDock->setWindowTitle(tr(s: "Context" )); |
275 | m_contextDock->setAcceptDrops(true); |
276 | m_contextDock->installEventFilter(this); |
277 | |
278 | m_sortedContextsModel = new SortedContextsModel(this, m_dataModel); |
279 | m_sortedContextsModel->setSortRole(MessageModel::SortRole); |
280 | m_sortedContextsModel->setSortCaseSensitivity(Qt::CaseInsensitive); |
281 | m_sortedContextsModel->setSourceModel(m_messageModel); |
282 | |
283 | m_contextView = new QTreeView(this); |
284 | m_contextView->setRootIsDecorated(false); |
285 | m_contextView->setItemsExpandable(false); |
286 | m_contextView->setUniformRowHeights(true); |
287 | m_contextView->setAlternatingRowColors(true); |
288 | m_contextView->setAllColumnsShowFocus(true); |
289 | m_contextView->setItemDelegate(new ContextItemDelegate(this, m_dataModel)); |
290 | m_contextView->setSortingEnabled(true); |
291 | m_contextView->setWhatsThis(tr(s: "This panel lists the source contexts." )); |
292 | m_contextView->setModel(m_sortedContextsModel); |
293 | m_contextView->header()->setSectionsMovable(false); |
294 | m_contextView->setColumnHidden(column: 0, hide: true); |
295 | m_contextView->header()->setStretchLastSection(false); |
296 | |
297 | m_contextDock->setWidget(m_contextView); |
298 | |
299 | // Set up the messages dock widget |
300 | m_messagesDock = new QDockWidget(this); |
301 | m_messagesDock->setObjectName(QLatin1String("StringsDockWidget" )); |
302 | m_messagesDock->setAllowedAreas(Qt::AllDockWidgetAreas); |
303 | m_messagesDock->setWindowTitle(tr(s: "Strings" )); |
304 | m_messagesDock->setAcceptDrops(true); |
305 | m_messagesDock->installEventFilter(this); |
306 | |
307 | m_sortedMessagesModel = new SortedMessagesModel(this, m_dataModel); |
308 | m_sortedMessagesModel->setSortRole(MessageModel::SortRole); |
309 | m_sortedMessagesModel->setSortCaseSensitivity(Qt::CaseInsensitive); |
310 | m_sortedMessagesModel->setSortLocaleAware(true); |
311 | m_sortedMessagesModel->setSourceModel(m_messageModel); |
312 | |
313 | m_messageView = new QTreeView(m_messagesDock); |
314 | m_messageView->setSortingEnabled(true); |
315 | m_messageView->setRootIsDecorated(false); |
316 | m_messageView->setUniformRowHeights(true); |
317 | m_messageView->setAllColumnsShowFocus(true); |
318 | m_messageView->setItemsExpandable(false); |
319 | m_messageView->setModel(m_sortedMessagesModel); |
320 | m_messageView->header()->setSectionsMovable(false); |
321 | m_messageView->setColumnHidden(column: 0, hide: true); |
322 | |
323 | m_messagesDock->setWidget(m_messageView); |
324 | |
325 | // Set up main message view |
326 | m_messageEditor = new MessageEditor(m_dataModel, this); |
327 | m_messageEditor->setAcceptDrops(true); |
328 | m_messageEditor->installEventFilter(this); |
329 | // We can't call setCentralWidget(m_messageEditor), since it is already called in m_ui.setupUi() |
330 | QBoxLayout *lout = new QBoxLayout(QBoxLayout::TopToBottom, m_ui.centralwidget); |
331 | lout->addWidget(m_messageEditor); |
332 | lout->setContentsMargins(QMargins()); |
333 | m_ui.centralwidget->setLayout(lout); |
334 | |
335 | // Set up the phrases & guesses dock widget |
336 | m_phrasesDock = new QDockWidget(this); |
337 | m_phrasesDock->setObjectName(QLatin1String("PhrasesDockwidget" )); |
338 | m_phrasesDock->setAllowedAreas(Qt::AllDockWidgetAreas); |
339 | m_phrasesDock->setWindowTitle(tr(s: "Phrases and guesses" )); |
340 | |
341 | m_phraseView = new PhraseView(m_dataModel, &m_phraseDict, this); |
342 | m_phrasesDock->setWidget(m_phraseView); |
343 | |
344 | // Set up source code and form preview dock widget |
345 | m_sourceAndFormDock = new QDockWidget(this); |
346 | m_sourceAndFormDock->setObjectName(QLatin1String("SourceAndFormDock" )); |
347 | m_sourceAndFormDock->setAllowedAreas(Qt::AllDockWidgetAreas); |
348 | m_sourceAndFormDock->setWindowTitle(tr(s: "Sources and Forms" )); |
349 | m_sourceAndFormView = new QStackedWidget(this); |
350 | m_sourceAndFormDock->setWidget(m_sourceAndFormView); |
351 | m_formPreviewView = new FormPreviewView(0, m_dataModel); |
352 | m_sourceCodeView = new SourceCodeView(0); |
353 | m_sourceAndFormView->addWidget(w: m_sourceCodeView); |
354 | m_sourceAndFormView->addWidget(w: m_formPreviewView); |
355 | |
356 | // Set up errors dock widget |
357 | m_errorsDock = new QDockWidget(this); |
358 | m_errorsDock->setObjectName(QLatin1String("ErrorsDockWidget" )); |
359 | m_errorsDock->setAllowedAreas(Qt::AllDockWidgetAreas); |
360 | m_errorsDock->setWindowTitle(tr(s: "Warnings" )); |
361 | m_errorsView = new ErrorsView(m_dataModel, this); |
362 | m_errorsDock->setWidget(m_errorsView); |
363 | |
364 | // Arrange dock widgets |
365 | setDockNestingEnabled(true); |
366 | setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); |
367 | setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); |
368 | setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); |
369 | setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); |
370 | addDockWidget(Qt::LeftDockWidgetArea, m_contextDock); |
371 | addDockWidget(Qt::TopDockWidgetArea, m_messagesDock); |
372 | addDockWidget(Qt::BottomDockWidgetArea, m_phrasesDock); |
373 | addDockWidget(Qt::TopDockWidgetArea, m_sourceAndFormDock); |
374 | addDockWidget(Qt::BottomDockWidgetArea, m_errorsDock); |
375 | //tabifyDockWidget(m_errorsDock, m_sourceAndFormDock); |
376 | //tabifyDockWidget(m_sourceCodeDock, m_phrasesDock); |
377 | |
378 | // Allow phrases doc to intercept guesses shortcuts |
379 | m_messageEditor->installEventFilter(filterObj: m_phraseView); |
380 | |
381 | // Set up shortcuts for the dock widgets |
382 | QShortcut *contextShortcut = new QShortcut(QKeySequence(Qt::Key_F6), this); |
383 | connect(contextShortcut, &QShortcut::activated, |
384 | this, &MainWindow::showContextDock); |
385 | QShortcut *messagesShortcut = new QShortcut(QKeySequence(Qt::Key_F7), this); |
386 | connect(messagesShortcut, &QShortcut::activated, |
387 | this, &MainWindow::showMessagesDock); |
388 | QShortcut * = new QShortcut(QKeySequence(Qt::Key_F8), this); |
389 | connect(errorsShortcut, &QShortcut::activated, |
390 | this, &MainWindow::showErrorDock); |
391 | QShortcut *sourceCodeShortcut = new QShortcut(QKeySequence(Qt::Key_F9), this); |
392 | connect(sourceCodeShortcut, &QShortcut::activated, |
393 | this, &MainWindow::showSourceCodeDock); |
394 | QShortcut *phrasesShortcut = new QShortcut(QKeySequence(Qt::Key_F10), this); |
395 | connect(phrasesShortcut, &QShortcut::activated, |
396 | this, &MainWindow::showPhrasesDock); |
397 | |
398 | connect(sender: m_phraseView, signal: &PhraseView::phraseSelected, |
399 | context: m_messageEditor, slot: &MessageEditor::setTranslation); |
400 | connect(m_phraseView, &PhraseView::setCurrentMessageFromGuess, |
401 | this, &MainWindow::setCurrentMessageFromGuess); |
402 | connect(m_contextView->selectionModel(), &QItemSelectionModel::currentRowChanged, |
403 | this, &MainWindow::selectedContextChanged); |
404 | connect(m_messageView->selectionModel(), &QItemSelectionModel::currentRowChanged, |
405 | this, &MainWindow::selectedMessageChanged); |
406 | connect(m_contextView->selectionModel(), &QItemSelectionModel::currentColumnChanged, |
407 | this, &MainWindow::updateLatestModel); |
408 | connect(m_messageView->selectionModel(), &QItemSelectionModel::currentColumnChanged, |
409 | this, &MainWindow::updateLatestModel); |
410 | |
411 | connect(m_messageEditor, &MessageEditor::activeModelChanged, |
412 | this, &MainWindow::updateActiveModel); |
413 | |
414 | m_translateDialog = new TranslateDialog(this); |
415 | m_batchTranslateDialog = new BatchTranslationDialog(m_dataModel, this); |
416 | m_findDialog = new FindDialog(this); |
417 | |
418 | setupMenuBar(); |
419 | setupToolBars(); |
420 | |
421 | m_progressLabel = new QLabel(); |
422 | statusBar()->addPermanentWidget(m_progressLabel); |
423 | m_modifiedLabel = new QLabel(tr(s: " MOD " , c: "status bar: file(s) modified" )); |
424 | statusBar()->addPermanentWidget(m_modifiedLabel); |
425 | |
426 | modelCountChanged(); |
427 | initViewHeaders(); |
428 | resetSorting(); |
429 | |
430 | connect(m_dataModel, &MultiDataModel::modifiedChanged, |
431 | this, &QWidget::setWindowModified); |
432 | connect(sender: m_dataModel, signal: &MultiDataModel::modifiedChanged, |
433 | context: m_modifiedLabel, slot: &QWidget::setVisible); |
434 | connect(m_dataModel, &MultiDataModel::multiContextDataChanged, |
435 | this, &MainWindow::updateProgress); |
436 | connect(m_dataModel, &MultiDataModel::messageDataChanged, |
437 | this, &MainWindow::maybeUpdateStatistics); |
438 | connect(m_dataModel, &MultiDataModel::translationChanged, |
439 | this, &MainWindow::translationChanged); |
440 | connect(m_dataModel, &MultiDataModel::languageChanged, |
441 | this, &MainWindow::updatePhraseDict); |
442 | |
443 | setWindowModified(m_dataModel->isModified()); |
444 | m_modifiedLabel->setVisible(m_dataModel->isModified()); |
445 | |
446 | connect(m_messageView, &QAbstractItemView::clicked, |
447 | this, &MainWindow::toggleFinished); |
448 | connect(sender: m_messageView, signal: &QAbstractItemView::activated, |
449 | context: m_messageEditor, slot: &MessageEditor::setEditorFocus); |
450 | connect(sender: m_contextView, signal: &QAbstractItemView::activated, |
451 | context: m_messageView, slot: qOverload<>(&QWidget::setFocus)); |
452 | connect(m_messageEditor, &MessageEditor::translationChanged, |
453 | this, &MainWindow::updateTranslation); |
454 | connect(m_messageEditor, &MessageEditor::translatorCommentChanged, |
455 | this, &MainWindow::updateTranslatorComment); |
456 | connect(m_findDialog, &FindDialog::findNext, |
457 | this, &MainWindow::findNext); |
458 | connect(m_translateDialog, &TranslateDialog::requestMatchUpdate, |
459 | this, &MainWindow::updateTranslateHit); |
460 | connect(m_translateDialog, &TranslateDialog::activated, |
461 | this, &MainWindow::translate); |
462 | |
463 | QSize as(screen()->size()); |
464 | as -= QSize(30, 30); |
465 | resize(QSize(1000, 800).boundedTo(otherSize: as)); |
466 | show(); |
467 | readConfig(); |
468 | m_statistics = 0; |
469 | |
470 | connect(m_ui.actionLengthVariants, &QAction::toggled, |
471 | m_messageEditor, &MessageEditor::setLengthVariants); |
472 | m_messageEditor->setLengthVariants(m_ui.actionLengthVariants->isChecked()); |
473 | m_messageEditor->setVisualizeWhitespace(m_ui.actionVisualizeWhitespace->isChecked()); |
474 | |
475 | m_focusWatcher = new FocusWatcher(m_messageEditor, this); |
476 | m_contextView->installEventFilter(filterObj: m_focusWatcher); |
477 | m_messageView->installEventFilter(filterObj: m_focusWatcher); |
478 | m_messageEditor->installEventFilter(filterObj: m_focusWatcher); |
479 | m_sourceAndFormView->installEventFilter(filterObj: m_focusWatcher); |
480 | m_phraseView->installEventFilter(filterObj: m_focusWatcher); |
481 | m_errorsView->installEventFilter(filterObj: m_focusWatcher); |
482 | } |
483 | |
484 | MainWindow::~MainWindow() |
485 | { |
486 | writeConfig(); |
487 | if (m_assistantProcess && m_assistantProcess->state() == QProcess::Running) { |
488 | m_assistantProcess->terminate(); |
489 | m_assistantProcess->waitForFinished(msecs: 3000); |
490 | } |
491 | qDeleteAll(c: m_phraseBooks); |
492 | delete m_dataModel; |
493 | delete m_statistics; |
494 | delete m_printer; |
495 | } |
496 | |
497 | void MainWindow::initViewHeaders() |
498 | { |
499 | m_contextView->header()->setSectionResizeMode(logicalIndex: 1, mode: QHeaderView::Stretch); |
500 | m_contextView->header()->setSectionResizeMode(logicalIndex: 2, mode: QHeaderView::ResizeToContents); |
501 | m_messageView->setColumnHidden(column: 2, hide: true); |
502 | // last visible column auto-stretches |
503 | } |
504 | |
505 | void MainWindow::modelCountChanged() |
506 | { |
507 | int mc = m_dataModel->modelCount(); |
508 | |
509 | for (int i = 0; i < mc; ++i) { |
510 | m_contextView->header()->setSectionResizeMode(logicalIndex: i + 1, mode: QHeaderView::Fixed); |
511 | m_contextView->header()->resizeSection(logicalIndex: i + 1, size: 24); |
512 | |
513 | m_messageView->header()->setSectionResizeMode(logicalIndex: i + 1, mode: QHeaderView::Fixed); |
514 | m_messageView->header()->resizeSection(logicalIndex: i + 1, size: 24); |
515 | } |
516 | |
517 | if (!mc) { |
518 | selectedMessageChanged(sortedIndex: QModelIndex(), oldIndex: QModelIndex()); |
519 | doUpdateLatestModel(model: -1); |
520 | } else { |
521 | if (!m_contextView->currentIndex().isValid()) { |
522 | // Ensure that something is selected |
523 | m_contextView->setCurrentIndex(m_sortedContextsModel->index(row: 0, column: 0)); |
524 | } else { |
525 | // Plug holes that turn up in the selection due to inserting columns |
526 | m_contextView->selectionModel()->select(index: m_contextView->currentIndex(), |
527 | command: QItemSelectionModel::SelectCurrent|QItemSelectionModel::Rows); |
528 | m_messageView->selectionModel()->select(index: m_messageView->currentIndex(), |
529 | command: QItemSelectionModel::SelectCurrent|QItemSelectionModel::Rows); |
530 | } |
531 | // Field insertions/removals are automatic, but not the re-fill |
532 | m_messageEditor->showMessage(index: m_currentIndex); |
533 | if (mc == 1) |
534 | doUpdateLatestModel(model: 0); |
535 | else if (m_currentIndex.model() >= mc) |
536 | doUpdateLatestModel(model: mc - 1); |
537 | } |
538 | |
539 | m_contextView->setUpdatesEnabled(true); |
540 | m_messageView->setUpdatesEnabled(true); |
541 | |
542 | updateProgress(); |
543 | updateCaption(); |
544 | |
545 | m_ui.actionFind->setEnabled(m_dataModel->contextCount() > 0); |
546 | m_ui.actionFindNext->setEnabled(false); |
547 | m_ui.actionFindPrev->setEnabled(false); |
548 | |
549 | m_formPreviewView->setSourceContext(model: -1, messageItem: 0); |
550 | } |
551 | |
552 | struct OpenedFile { |
553 | OpenedFile(DataModel *_dataModel, bool _readWrite, bool _langGuessed) |
554 | { dataModel = _dataModel; readWrite = _readWrite; langGuessed = _langGuessed; } |
555 | DataModel *dataModel; |
556 | bool readWrite; |
557 | bool langGuessed; |
558 | }; |
559 | |
560 | bool MainWindow::openFiles(const QStringList &names, bool globalReadWrite) |
561 | { |
562 | if (names.isEmpty()) |
563 | return false; |
564 | |
565 | bool waitCursor = false; |
566 | statusBar()->showMessage(tr(s: "Loading..." )); |
567 | qApp->processEvents(); |
568 | |
569 | QList<OpenedFile> opened; |
570 | bool closeOld = false; |
571 | for (QString name : names) { |
572 | if (!waitCursor) { |
573 | QApplication::setOverrideCursor(Qt::WaitCursor); |
574 | waitCursor = true; |
575 | } |
576 | |
577 | bool readWrite = globalReadWrite; |
578 | if (name.startsWith(c: QLatin1Char('='))) { |
579 | name.remove(i: 0, len: 1); |
580 | readWrite = false; |
581 | } |
582 | QFileInfo fi(name); |
583 | if (fi.exists()) // Make the loader error out instead of reading stdin |
584 | name = fi.canonicalFilePath(); |
585 | if (m_dataModel->isFileLoaded(name) >= 0) |
586 | continue; |
587 | |
588 | bool langGuessed; |
589 | DataModel *dm = new DataModel(m_dataModel); |
590 | if (!dm->load(name, &langGuessed, this)) { |
591 | delete dm; |
592 | continue; |
593 | } |
594 | if (opened.isEmpty()) { |
595 | if (!m_dataModel->isWellMergeable(dm)) { |
596 | QApplication::restoreOverrideCursor(); |
597 | waitCursor = false; |
598 | switch (QMessageBox::information(this, tr(s: "Loading File - Qt Linguist" ), |
599 | tr(s: "The file '%1' does not seem to be related to the currently open file(s) '%2'.\n\n" |
600 | "Close the open file(s) first?" ) |
601 | .arg(args: DataModel::prettifyPlainFileName(fn: name), args: m_dataModel->condensedSrcFileNames(pretty: true)), |
602 | QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes)) |
603 | { |
604 | case QMessageBox::Cancel: |
605 | delete dm; |
606 | return false; |
607 | case QMessageBox::Yes: |
608 | closeOld = true; |
609 | break; |
610 | default: |
611 | break; |
612 | } |
613 | } |
614 | } else { |
615 | if (!opened.first().dataModel->isWellMergeable(other: dm)) { |
616 | QApplication::restoreOverrideCursor(); |
617 | waitCursor = false; |
618 | switch (QMessageBox::information(this, tr(s: "Loading File - Qt Linguist" ), |
619 | tr(s: "The file '%1' does not seem to be related to the file '%2'" |
620 | " which is being loaded as well.\n\n" |
621 | "Skip loading the first named file?" ) |
622 | .arg(args: DataModel::prettifyPlainFileName(fn: name), args: opened.first().dataModel->srcFileName(pretty: true)), |
623 | QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes)) |
624 | { |
625 | case QMessageBox::Cancel: |
626 | delete dm; |
627 | for (const OpenedFile &op : std::as_const(t&: opened)) |
628 | delete op.dataModel; |
629 | return false; |
630 | case QMessageBox::Yes: |
631 | delete dm; |
632 | continue; |
633 | default: |
634 | break; |
635 | } |
636 | } |
637 | } |
638 | opened.append(t: OpenedFile(dm, readWrite, langGuessed)); |
639 | } |
640 | |
641 | if (closeOld) { |
642 | if (waitCursor) { |
643 | QApplication::restoreOverrideCursor(); |
644 | waitCursor = false; |
645 | } |
646 | if (!closeAll()) { |
647 | for (const OpenedFile &op : std::as_const(t&: opened)) |
648 | delete op.dataModel; |
649 | return false; |
650 | } |
651 | } |
652 | |
653 | for (const OpenedFile &op : std::as_const(t&: opened)) { |
654 | if (op.langGuessed) { |
655 | if (waitCursor) { |
656 | QApplication::restoreOverrideCursor(); |
657 | waitCursor = false; |
658 | } |
659 | if (!m_translationSettingsDialog) |
660 | m_translationSettingsDialog = new TranslationSettingsDialog(this); |
661 | m_translationSettingsDialog->setDataModel(op.dataModel); |
662 | m_translationSettingsDialog->exec(); |
663 | } |
664 | } |
665 | |
666 | if (!waitCursor) |
667 | QApplication::setOverrideCursor(Qt::WaitCursor); |
668 | m_contextView->setUpdatesEnabled(false); |
669 | m_messageView->setUpdatesEnabled(false); |
670 | int totalCount = 0; |
671 | for (const OpenedFile &op : std::as_const(t&: opened)) { |
672 | m_phraseDict.append(t: QHash<QString, QList<Phrase *> >()); |
673 | m_dataModel->append(dm: op.dataModel, readWrite: op.readWrite); |
674 | if (op.readWrite) |
675 | updatePhraseDictInternal(model: m_phraseDict.size() - 1); |
676 | totalCount += op.dataModel->messageCount(); |
677 | } |
678 | statusBar()->showMessage(tr(s: "%n translation unit(s) loaded." , c: 0, n: totalCount), MessageMS); |
679 | modelCountChanged(); |
680 | recentFiles().addFiles(names: m_dataModel->srcFileNames()); |
681 | |
682 | revalidate(); |
683 | QApplication::restoreOverrideCursor(); |
684 | return true; |
685 | } |
686 | |
687 | RecentFiles &MainWindow::recentFiles() |
688 | { |
689 | static RecentFiles recentFiles(10); |
690 | return recentFiles; |
691 | } |
692 | |
693 | void MainWindow::open() |
694 | { |
695 | openFiles(names: pickTranslationFiles()); |
696 | } |
697 | |
698 | void MainWindow::openAux() |
699 | { |
700 | openFiles(names: pickTranslationFiles(), globalReadWrite: false); |
701 | } |
702 | |
703 | void MainWindow::closeFile() |
704 | { |
705 | int model = m_currentIndex.model(); |
706 | if (model >= 0 && maybeSave(model)) { |
707 | m_phraseDict.removeAt(i: model); |
708 | m_contextView->setUpdatesEnabled(false); |
709 | m_messageView->setUpdatesEnabled(false); |
710 | m_dataModel->close(model); |
711 | modelCountChanged(); |
712 | } |
713 | } |
714 | |
715 | bool MainWindow::closeAll() |
716 | { |
717 | if (maybeSaveAll()) { |
718 | m_phraseDict.clear(); |
719 | m_contextView->setUpdatesEnabled(false); |
720 | m_messageView->setUpdatesEnabled(false); |
721 | m_dataModel->closeAll(); |
722 | modelCountChanged(); |
723 | initViewHeaders(); |
724 | recentFiles().closeGroup(); |
725 | return true; |
726 | } |
727 | return false; |
728 | } |
729 | |
730 | static QString fileFilters(bool allFirst) |
731 | { |
732 | static const QString pattern(QLatin1String("%1 (*.%2);;" )); |
733 | QStringList allExtensions; |
734 | QString filter; |
735 | for (const Translator::FileFormat &format : std::as_const(t&: Translator::registeredFileFormats())) { |
736 | if (format.fileType == Translator::FileFormat::TranslationSource && format.priority >= 0) { |
737 | filter.append(s: pattern.arg(args: format.description(), args: format.extension)); |
738 | allExtensions.append(t: QLatin1String("*." ) + format.extension); |
739 | } |
740 | } |
741 | QString allFilter = QObject::tr(s: "Translation files (%1);;" ).arg(a: allExtensions.join(sep: QLatin1Char(' '))); |
742 | if (allFirst) |
743 | filter.prepend(s: allFilter); |
744 | else |
745 | filter.append(s: allFilter); |
746 | filter.append(s: QObject::tr(s: "All files (*)" )); |
747 | return filter; |
748 | } |
749 | |
750 | QStringList MainWindow::pickTranslationFiles() |
751 | { |
752 | QString dir; |
753 | if (!recentFiles().isEmpty()) |
754 | dir = QFileInfo(recentFiles().lastOpenedFile()).path(); |
755 | |
756 | QString varFilt; |
757 | if (m_dataModel->modelCount()) { |
758 | QFileInfo mainFile(m_dataModel->srcFileName(model: 0)); |
759 | QString mainFileBase = mainFile.baseName(); |
760 | int pos = mainFileBase.indexOf(c: QLatin1Char('_')); |
761 | if (pos > 0) |
762 | varFilt = tr(s: "Related files (%1);;" ) |
763 | .arg(a: mainFileBase.left(n: pos) + QLatin1String("_*." ) + mainFile.completeSuffix()); |
764 | } |
765 | |
766 | return QFileDialog::getOpenFileNames(this, tr(s: "Open Translation Files" ), dir, |
767 | varFilt + |
768 | fileFilters(allFirst: true)); |
769 | } |
770 | |
771 | void MainWindow::saveInternal(int model) |
772 | { |
773 | QApplication::setOverrideCursor(Qt::WaitCursor); |
774 | if (m_dataModel->save(model, this)) { |
775 | updateCaption(); |
776 | statusBar()->showMessage(tr(s: "File saved." ), MessageMS); |
777 | } |
778 | QApplication::restoreOverrideCursor(); |
779 | } |
780 | |
781 | void MainWindow::saveAll() |
782 | { |
783 | for (int i = 0; i < m_dataModel->modelCount(); ++i) |
784 | if (m_dataModel->isModelWritable(model: i)) |
785 | saveInternal(model: i); |
786 | recentFiles().closeGroup(); |
787 | } |
788 | |
789 | void MainWindow::save() |
790 | { |
791 | if (m_currentIndex.model() < 0) |
792 | return; |
793 | |
794 | saveInternal(model: m_currentIndex.model()); |
795 | } |
796 | |
797 | void MainWindow::saveAs() |
798 | { |
799 | if (m_currentIndex.model() < 0) |
800 | return; |
801 | |
802 | QString newFilename = QFileDialog::getSaveFileName(this, QString(), m_dataModel->srcFileName(model: m_currentIndex.model()), |
803 | fileFilters(allFirst: false)); |
804 | if (!newFilename.isEmpty()) { |
805 | if (m_dataModel->saveAs(m_currentIndex.model(), newFilename, this)) { |
806 | updateCaption(); |
807 | statusBar()->showMessage(tr(s: "File saved." ), MessageMS); |
808 | recentFiles().addFiles(names: m_dataModel->srcFileNames()); |
809 | } |
810 | } |
811 | } |
812 | |
813 | void MainWindow::releaseAs() |
814 | { |
815 | if (m_currentIndex.model() < 0) |
816 | return; |
817 | |
818 | QFileInfo oldFile(m_dataModel->srcFileName(model: m_currentIndex.model())); |
819 | QString newFilename = oldFile.path() + QLatin1String("/" ) |
820 | + oldFile.completeBaseName() + QLatin1String(".qm" ); |
821 | |
822 | newFilename = QFileDialog::getSaveFileName(this, tr(s: "Release" ), newFilename, |
823 | tr(s: "Qt message files for released applications (*.qm)\nAll files (*)" )); |
824 | if (!newFilename.isEmpty()) { |
825 | if (m_dataModel->release(m_currentIndex.model(), newFilename, false, false, SaveEverything, this)) |
826 | statusBar()->showMessage(tr(s: "File created." ), MessageMS); |
827 | } |
828 | } |
829 | |
830 | void MainWindow::releaseInternal(int model) |
831 | { |
832 | QFileInfo oldFile(m_dataModel->srcFileName(model)); |
833 | QString newFilename = oldFile.path() + QLatin1Char('/') |
834 | + oldFile.completeBaseName() + QLatin1String(".qm" ); |
835 | |
836 | if (!newFilename.isEmpty()) { |
837 | if (m_dataModel->release(model, newFilename, false, false, SaveEverything, this)) |
838 | statusBar()->showMessage(tr(s: "File created." ), MessageMS); |
839 | } |
840 | } |
841 | |
842 | // No-question |
843 | void MainWindow::release() |
844 | { |
845 | if (m_currentIndex.model() < 0) |
846 | return; |
847 | |
848 | releaseInternal(model: m_currentIndex.model()); |
849 | } |
850 | |
851 | void MainWindow::releaseAll() |
852 | { |
853 | for (int i = 0; i < m_dataModel->modelCount(); ++i) |
854 | if (m_dataModel->isModelWritable(model: i)) |
855 | releaseInternal(model: i); |
856 | } |
857 | |
858 | QPrinter *MainWindow::printer() |
859 | { |
860 | if (!m_printer) |
861 | m_printer = new QPrinter; |
862 | return m_printer; |
863 | } |
864 | |
865 | void MainWindow::print() |
866 | { |
867 | int pageNum = 0; |
868 | QPrintDialog dlg(printer(), this); |
869 | if (dlg.exec()) { |
870 | QApplication::setOverrideCursor(Qt::WaitCursor); |
871 | printer()->setDocName(m_dataModel->condensedSrcFileNames(pretty: true)); |
872 | statusBar()->showMessage(tr(s: "Printing..." )); |
873 | PrintOut pout(printer()); |
874 | |
875 | for (int i = 0; i < m_dataModel->contextCount(); ++i) { |
876 | MultiContextItem *mc = m_dataModel->multiContextItem(ctxIdx: i); |
877 | pout.vskip(); |
878 | pout.setRule(PrintOut::ThickRule); |
879 | pout.setGuide(mc->context()); |
880 | pout.addBox(percent: 100, text: tr(s: "Context: %1" ).arg(a: mc->context()), |
881 | style: PrintOut::Strong); |
882 | pout.flushLine(); |
883 | pout.addBox(percent: 4); |
884 | pout.addBox(percent: 92, text: mc->comment(), style: PrintOut::Emphasis); |
885 | pout.flushLine(); |
886 | pout.setRule(PrintOut::ThickRule); |
887 | |
888 | for (int j = 0; j < mc->messageCount(); ++j) { |
889 | pout.setRule(PrintOut::ThinRule); |
890 | bool printedSrc = false; |
891 | QString ; |
892 | for (int k = 0; k < m_dataModel->modelCount(); ++k) { |
893 | if (const MessageItem *m = mc->messageItem(model: k, msgIdx: j)) { |
894 | if (!printedSrc) { |
895 | pout.addBox(percent: 40, text: m->text()); |
896 | pout.addBox(percent: 4); |
897 | comment = m->comment(); |
898 | printedSrc = true; |
899 | } else { |
900 | pout.addBox(percent: 44); // Maybe put the name of the translation here |
901 | } |
902 | if (m->message().isPlural() && m_dataModel->language(model: k) != QLocale::C) { |
903 | QStringList transls = m->translations(); |
904 | pout.addBox(percent: 40, text: transls.join(sep: QLatin1Char('\n'))); |
905 | } else { |
906 | pout.addBox(percent: 40, text: m->translation()); |
907 | } |
908 | pout.addBox(percent: 4); |
909 | QString type; |
910 | switch (m->message().type()) { |
911 | case TranslatorMessage::Finished: |
912 | type = tr(s: "finished" ); |
913 | break; |
914 | case TranslatorMessage::Unfinished: |
915 | type = m->danger() ? tr(s: "unresolved" ) : QLatin1String("unfinished" ); |
916 | break; |
917 | case TranslatorMessage::Obsolete: |
918 | case TranslatorMessage::Vanished: |
919 | type = tr(s: "obsolete" ); |
920 | break; |
921 | } |
922 | pout.addBox(percent: 12, text: type, style: PrintOut::Normal, halign: Qt::AlignRight); |
923 | pout.flushLine(); |
924 | } |
925 | } |
926 | if (!comment.isEmpty()) { |
927 | pout.addBox(percent: 4); |
928 | pout.addBox(percent: 92, text: comment, style: PrintOut::Emphasis); |
929 | pout.flushLine(mayBreak: true); |
930 | } |
931 | |
932 | if (pout.pageNum() != pageNum) { |
933 | pageNum = pout.pageNum(); |
934 | statusBar()->showMessage(tr(s: "Printing... (page %1)" ) |
935 | .arg(a: pageNum)); |
936 | } |
937 | } |
938 | } |
939 | pout.flushLine(mayBreak: true); |
940 | QApplication::restoreOverrideCursor(); |
941 | statusBar()->showMessage(tr(s: "Printing completed" ), MessageMS); |
942 | } else { |
943 | statusBar()->showMessage(tr(s: "Printing aborted" ), MessageMS); |
944 | } |
945 | } |
946 | |
947 | bool MainWindow::searchItem(DataModel::FindLocation where, const QString &searchWhat) |
948 | { |
949 | if ((m_findWhere & where) == 0) |
950 | return false; |
951 | |
952 | QString text = searchWhat; |
953 | |
954 | if (m_findOptions.testFlag(flag: FindDialog::IgnoreAccelerators)) |
955 | // FIXME: This removes too much. The proper solution might be too slow, though. |
956 | text.remove(c: QLatin1Char('&')); |
957 | |
958 | if (m_findOptions.testFlag(flag: FindDialog::UseRegExp)) |
959 | return m_findDialog->getRegExp().match(subject: text).hasMatch(); |
960 | else |
961 | return text.indexOf(s: m_findText, from: 0, cs: m_findOptions.testFlag(flag: FindDialog::MatchCase) |
962 | ? Qt::CaseSensitive : Qt::CaseInsensitive) >= 0; |
963 | } |
964 | |
965 | void MainWindow::findAgain(FindDirection direction) |
966 | { |
967 | if (m_dataModel->contextCount() == 0) |
968 | return; |
969 | |
970 | const QModelIndex &startIndex = m_messageView->currentIndex(); |
971 | QModelIndex index = (direction == FindNext |
972 | ? nextMessage(currentIndex: startIndex) |
973 | : prevMessage(currentIndex: startIndex)); |
974 | |
975 | while (index.isValid()) { |
976 | QModelIndex realIndex = m_sortedMessagesModel->mapToSource(proxyIndex: index); |
977 | MultiDataIndex dataIndex = m_messageModel->dataIndex(index: realIndex, model: -1); |
978 | bool hadMessage = false; |
979 | for (int i = 0; i < m_dataModel->modelCount(); ++i) { |
980 | if (MessageItem *m = m_dataModel->messageItem(index: dataIndex, model: i)) { |
981 | if (m_findStatusFilter != -1 && m_findStatusFilter != m->type()) |
982 | continue; |
983 | |
984 | if (m_findOptions.testFlag(flag: FindDialog::SkipObsolete) |
985 | && m->isObsolete()) |
986 | continue; |
987 | |
988 | bool found = true; |
989 | do { |
990 | if (!hadMessage) { |
991 | if (searchItem(where: DataModel::SourceText, searchWhat: m->text())) |
992 | break; |
993 | if (searchItem(where: DataModel::SourceText, searchWhat: m->pluralText())) |
994 | break; |
995 | if (searchItem(where: DataModel::Comments, searchWhat: m->comment())) |
996 | break; |
997 | if (searchItem(where: DataModel::Comments, searchWhat: m->extraComment())) |
998 | break; |
999 | } |
1000 | const auto translations = m->translations(); |
1001 | for (const QString &trans : translations) |
1002 | if (searchItem(where: DataModel::Translations, searchWhat: trans)) |
1003 | goto didfind; |
1004 | if (searchItem(where: DataModel::Comments, searchWhat: m->translatorComment())) |
1005 | break; |
1006 | found = false; |
1007 | // did not find the search string in this message |
1008 | } while (0); |
1009 | if (found) { |
1010 | didfind: |
1011 | setCurrentMessage(index: realIndex, model: i); |
1012 | |
1013 | // determine whether the search wrapped |
1014 | const QModelIndex &c1 = m_sortedContextsModel->mapFromSource( |
1015 | sourceIndex: m_sortedMessagesModel->mapToSource(proxyIndex: startIndex)).parent(); |
1016 | const QModelIndex &c2 = m_sortedContextsModel->mapFromSource(sourceIndex: realIndex).parent(); |
1017 | const QModelIndex &m = m_sortedMessagesModel->mapFromSource(sourceIndex: realIndex); |
1018 | |
1019 | if (c2.row() < c1.row() || (c1.row() == c2.row() && m.row() <= startIndex.row())) |
1020 | statusBar()->showMessage(tr(s: "Search wrapped." ), MessageMS); |
1021 | |
1022 | m_findDialog->hide(); |
1023 | return; |
1024 | } |
1025 | hadMessage = true; |
1026 | } |
1027 | } |
1028 | |
1029 | // since we don't search startIndex at the beginning, only now we have searched everything |
1030 | if (index == startIndex) |
1031 | break; |
1032 | |
1033 | index = (direction == FindNext |
1034 | ? nextMessage(currentIndex: index) |
1035 | : prevMessage(currentIndex: index)); |
1036 | } |
1037 | |
1038 | qApp->beep(); |
1039 | QMessageBox::warning(parent: m_findDialog, title: tr(s: "Qt Linguist" ), |
1040 | text: tr(s: "Cannot find the string '%1'." ).arg(a: m_findText)); |
1041 | } |
1042 | |
1043 | void MainWindow::showBatchTranslateDialog() |
1044 | { |
1045 | m_messageModel->blockSignals(b: true); |
1046 | m_batchTranslateDialog->setPhraseBooks(phrasebooks: m_phraseBooks, modelIndex: m_currentIndex.model()); |
1047 | if (m_batchTranslateDialog->exec() != QDialog::Accepted) |
1048 | m_messageModel->blockSignals(b: false); |
1049 | // else signal finished() calls refreshItemViews() |
1050 | } |
1051 | |
1052 | void MainWindow::showTranslateDialog() |
1053 | { |
1054 | m_latestCaseSensitivity = -1; |
1055 | QModelIndex idx = m_messageView->currentIndex(); |
1056 | QModelIndex idx2 = m_sortedMessagesModel->index(row: idx.row(), column: m_currentIndex.model() + 1, parent: idx.parent()); |
1057 | m_messageView->setCurrentIndex(idx2); |
1058 | QString fn = QFileInfo(m_dataModel->srcFileName(model: m_currentIndex.model())).baseName(); |
1059 | m_translateDialog->setWindowTitle(tr(s: "Search And Translate in '%1' - Qt Linguist" ).arg(a: fn)); |
1060 | m_translateDialog->exec(); |
1061 | } |
1062 | |
1063 | void MainWindow::updateTranslateHit(bool &hit) |
1064 | { |
1065 | MessageItem *m; |
1066 | hit = (m = m_dataModel->messageItem(index: m_currentIndex)) |
1067 | && !m->isObsolete() |
1068 | && m->compare(findText: m_translateDialog->findText(), matchSubstring: false, cs: m_translateDialog->caseSensitivity()); |
1069 | } |
1070 | |
1071 | void MainWindow::translate(int mode) |
1072 | { |
1073 | QString findText = m_translateDialog->findText(); |
1074 | QString replaceText = m_translateDialog->replaceText(); |
1075 | bool markFinished = m_translateDialog->markFinished(); |
1076 | Qt::CaseSensitivity caseSensitivity = m_translateDialog->caseSensitivity(); |
1077 | |
1078 | int translatedCount = 0; |
1079 | |
1080 | if (mode == TranslateDialog::TranslateAll) { |
1081 | for (MultiDataModelIterator it(m_dataModel, m_currentIndex.model()); it.isValid(); ++it) { |
1082 | MessageItem *m = it.current(); |
1083 | if (m && !m->isObsolete() && m->compare(findText, matchSubstring: false, cs: caseSensitivity)) { |
1084 | if (!translatedCount) |
1085 | m_messageModel->blockSignals(b: true); |
1086 | m_dataModel->setTranslation(index: it, translation: replaceText); |
1087 | m_dataModel->setFinished(index: it, finished: markFinished); |
1088 | ++translatedCount; |
1089 | } |
1090 | } |
1091 | if (translatedCount) { |
1092 | refreshItemViews(); |
1093 | QMessageBox::warning(m_translateDialog, tr(s: "Translate - Qt Linguist" ), |
1094 | tr(s: "Translated %n entry(s)" , c: 0, n: translatedCount)); |
1095 | } |
1096 | } else { |
1097 | if (mode == TranslateDialog::Translate) { |
1098 | m_dataModel->setTranslation(index: m_currentIndex, translation: replaceText); |
1099 | m_dataModel->setFinished(index: m_currentIndex, finished: markFinished); |
1100 | } |
1101 | |
1102 | if (findText != m_latestFindText || caseSensitivity != m_latestCaseSensitivity) { |
1103 | m_latestFindText = findText; |
1104 | m_latestCaseSensitivity = caseSensitivity; |
1105 | m_remainingCount = m_dataModel->messageCount(); |
1106 | m_hitCount = 0; |
1107 | } |
1108 | |
1109 | QModelIndex index = m_messageView->currentIndex(); |
1110 | int prevRemained = m_remainingCount; |
1111 | forever { |
1112 | if (--m_remainingCount <= 0) { |
1113 | if (!m_hitCount) |
1114 | break; |
1115 | m_remainingCount = m_dataModel->messageCount() - 1; |
1116 | if (QMessageBox::question(m_translateDialog, tr(s: "Translate - Qt Linguist" ), |
1117 | tr(s: "No more occurrences of '%1'. Start over?" ).arg(a: findText), |
1118 | QMessageBox::Yes|QMessageBox::No) != QMessageBox::Yes) |
1119 | return; |
1120 | m_remainingCount -= prevRemained; |
1121 | } |
1122 | |
1123 | index = nextMessage(currentIndex: index); |
1124 | |
1125 | QModelIndex realIndex = m_sortedMessagesModel->mapToSource(proxyIndex: index); |
1126 | MultiDataIndex dataIndex = m_messageModel->dataIndex(index: realIndex, model: m_currentIndex.model()); |
1127 | if (MessageItem *m = m_dataModel->messageItem(index: dataIndex)) { |
1128 | if (!m->isObsolete() && m->compare(findText, matchSubstring: false, cs: caseSensitivity)) { |
1129 | setCurrentMessage(index: realIndex, model: m_currentIndex.model()); |
1130 | ++translatedCount; |
1131 | ++m_hitCount; |
1132 | break; |
1133 | } |
1134 | } |
1135 | } |
1136 | } |
1137 | |
1138 | if (!translatedCount) { |
1139 | qApp->beep(); |
1140 | QMessageBox::warning(m_translateDialog, tr(s: "Translate - Qt Linguist" ), |
1141 | tr(s: "Cannot find the string '%1'." ).arg(a: findText)); |
1142 | } |
1143 | } |
1144 | |
1145 | void MainWindow::newPhraseBook() |
1146 | { |
1147 | QString name = QFileDialog::getSaveFileName(this, tr(s: "Create New Phrase Book" ), |
1148 | m_phraseBookDir, tr(s: "Qt phrase books (*.qph)\nAll files (*)" )); |
1149 | if (!name.isEmpty()) { |
1150 | PhraseBook pb; |
1151 | if (!m_translationSettingsDialog) |
1152 | m_translationSettingsDialog = new TranslationSettingsDialog(this); |
1153 | m_translationSettingsDialog->setPhraseBook(&pb); |
1154 | if (!m_translationSettingsDialog->exec()) |
1155 | return; |
1156 | m_phraseBookDir = QFileInfo(name).absolutePath(); |
1157 | if (savePhraseBook(name: &name, pb)) { |
1158 | if (doOpenPhraseBook(name)) |
1159 | statusBar()->showMessage(tr(s: "Phrase book created." ), MessageMS); |
1160 | } |
1161 | } |
1162 | } |
1163 | |
1164 | bool MainWindow::isPhraseBookOpen(const QString &name) |
1165 | { |
1166 | for (const PhraseBook *pb : std::as_const(t&: m_phraseBooks)) { |
1167 | if (pb->fileName() == name) |
1168 | return true; |
1169 | } |
1170 | |
1171 | return false; |
1172 | } |
1173 | |
1174 | void MainWindow::openPhraseBook() |
1175 | { |
1176 | QString name = QFileDialog::getOpenFileName(this, tr(s: "Open Phrase Book" ), |
1177 | m_phraseBookDir, tr(s: "Qt phrase books (*.qph);;All files (*)" )); |
1178 | |
1179 | if (!name.isEmpty()) { |
1180 | m_phraseBookDir = QFileInfo(name).absolutePath(); |
1181 | if (!isPhraseBookOpen(name)) { |
1182 | if (PhraseBook *phraseBook = doOpenPhraseBook(name)) { |
1183 | int n = phraseBook->phrases().size(); |
1184 | statusBar()->showMessage(tr(s: "%n phrase(s) loaded." , c: 0, n), MessageMS); |
1185 | } |
1186 | } |
1187 | } |
1188 | } |
1189 | |
1190 | void MainWindow::closePhraseBook(QAction *action) |
1191 | { |
1192 | PhraseBook *pb = m_phraseBookMenu[PhraseCloseMenu].value(key: action); |
1193 | if (!maybeSavePhraseBook(phraseBook: pb)) |
1194 | return; |
1195 | |
1196 | m_phraseBookMenu[PhraseCloseMenu].remove(key: action); |
1197 | m_ui.menuClosePhraseBook->removeAction(action); |
1198 | |
1199 | QAction *act = m_phraseBookMenu[PhraseEditMenu].key(value: pb); |
1200 | m_phraseBookMenu[PhraseEditMenu].remove(key: act); |
1201 | m_ui.menuEditPhraseBook->removeAction(act); |
1202 | |
1203 | act = m_phraseBookMenu[PhrasePrintMenu].key(value: pb); |
1204 | m_ui.menuPrintPhraseBook->removeAction(act); |
1205 | |
1206 | m_phraseBooks.removeOne(t: pb); |
1207 | disconnect(sender: pb, signal: &PhraseBook::listChanged, |
1208 | receiver: this, slot: &MainWindow::updatePhraseDicts); |
1209 | updatePhraseDicts(); |
1210 | delete pb; |
1211 | updatePhraseBookActions(); |
1212 | } |
1213 | |
1214 | void MainWindow::editPhraseBook(QAction *action) |
1215 | { |
1216 | PhraseBook *pb = m_phraseBookMenu[PhraseEditMenu].value(key: action); |
1217 | PhraseBookBox box(pb, this); |
1218 | box.exec(); |
1219 | |
1220 | updatePhraseDicts(); |
1221 | } |
1222 | |
1223 | void MainWindow::printPhraseBook(QAction *action) |
1224 | { |
1225 | PhraseBook *phraseBook = m_phraseBookMenu[PhrasePrintMenu].value(key: action); |
1226 | |
1227 | int pageNum = 0; |
1228 | |
1229 | QPrintDialog dlg(printer(), this); |
1230 | if (dlg.exec()) { |
1231 | printer()->setDocName(phraseBook->fileName()); |
1232 | statusBar()->showMessage(tr(s: "Printing..." )); |
1233 | PrintOut pout(printer()); |
1234 | pout.setRule(PrintOut::ThinRule); |
1235 | const auto phrases = phraseBook->phrases(); |
1236 | for (const Phrase *p : phrases) { |
1237 | pout.setGuide(p->source()); |
1238 | pout.addBox(percent: 29, text: p->source()); |
1239 | pout.addBox(percent: 4); |
1240 | pout.addBox(percent: 29, text: p->target()); |
1241 | pout.addBox(percent: 4); |
1242 | pout.addBox(percent: 34, text: p->definition(), style: PrintOut::Emphasis); |
1243 | |
1244 | if (pout.pageNum() != pageNum) { |
1245 | pageNum = pout.pageNum(); |
1246 | statusBar()->showMessage(tr(s: "Printing... (page %1)" ) |
1247 | .arg(a: pageNum)); |
1248 | } |
1249 | pout.setRule(PrintOut::NoRule); |
1250 | pout.flushLine(mayBreak: true); |
1251 | } |
1252 | pout.flushLine(mayBreak: true); |
1253 | statusBar()->showMessage(tr(s: "Printing completed" ), MessageMS); |
1254 | } else { |
1255 | statusBar()->showMessage(tr(s: "Printing aborted" ), MessageMS); |
1256 | } |
1257 | } |
1258 | |
1259 | void MainWindow::addToPhraseBook() |
1260 | { |
1261 | QStringList phraseBookList; |
1262 | QHash<QString, PhraseBook *> phraseBookHash; |
1263 | for (PhraseBook *pb : std::as_const(t&: m_phraseBooks)) { |
1264 | if (pb->language() != QLocale::C && m_dataModel->language(model: m_currentIndex.model()) != QLocale::C) { |
1265 | if (pb->language() != m_dataModel->language(model: m_currentIndex.model())) |
1266 | continue; |
1267 | if (pb->territory() == m_dataModel->model(i: m_currentIndex.model())->territory()) |
1268 | phraseBookList.prepend(t: pb->friendlyPhraseBookName()); |
1269 | else |
1270 | phraseBookList.append(t: pb->friendlyPhraseBookName()); |
1271 | } else { |
1272 | phraseBookList.append(t: pb->friendlyPhraseBookName()); |
1273 | } |
1274 | phraseBookHash.insert(key: pb->friendlyPhraseBookName(), value: pb); |
1275 | } |
1276 | if (phraseBookList.isEmpty()) { |
1277 | QMessageBox::warning(this, tr(s: "Add to phrase book" ), |
1278 | tr(s: "No appropriate phrasebook found." )); |
1279 | return; |
1280 | } |
1281 | |
1282 | QString selectedPhraseBook; |
1283 | if (phraseBookList.size() == 1) { |
1284 | selectedPhraseBook = phraseBookList.at(i: 0); |
1285 | if (QMessageBox::information(this, tr(s: "Add to phrase book" ), |
1286 | tr(s: "Adding entry to phrasebook %1" ).arg(a: selectedPhraseBook), |
1287 | QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok) |
1288 | != QMessageBox::Ok) |
1289 | return; |
1290 | } else { |
1291 | bool okPressed = false; |
1292 | selectedPhraseBook = QInputDialog::getItem(this, tr(s: "Add to phrase book" ), |
1293 | tr(s: "Select phrase book to add to" ), |
1294 | phraseBookList, 0, false, &okPressed); |
1295 | if (!okPressed) |
1296 | return; |
1297 | } |
1298 | |
1299 | MessageItem *currentMessage = m_dataModel->messageItem(index: m_currentIndex); |
1300 | Phrase *phrase = new Phrase(currentMessage->text(), currentMessage->translation(), |
1301 | QString(), nullptr); |
1302 | |
1303 | phraseBookHash.value(key: selectedPhraseBook)->append(phrase); |
1304 | } |
1305 | |
1306 | void MainWindow::resetSorting() |
1307 | { |
1308 | m_contextView->sortByColumn(column: -1, order: Qt::AscendingOrder); |
1309 | m_messageView->sortByColumn(column: -1, order: Qt::AscendingOrder); |
1310 | } |
1311 | |
1312 | void MainWindow::manual() |
1313 | { |
1314 | if (!m_assistantProcess) |
1315 | m_assistantProcess = new QProcess(); |
1316 | |
1317 | if (m_assistantProcess->state() != QProcess::Running) { |
1318 | QString app = QLibraryInfo::path(p: QLibraryInfo::BinariesPath) + QDir::separator(); |
1319 | #if !defined(Q_OS_MAC) |
1320 | app += QLatin1String("assistant" ); |
1321 | #else |
1322 | app += QLatin1String("Assistant.app/Contents/MacOS/Assistant" ); |
1323 | #endif |
1324 | |
1325 | m_assistantProcess->start(program: app, arguments: QStringList() << QLatin1String("-enableRemoteControl" )); |
1326 | if (!m_assistantProcess->waitForStarted()) { |
1327 | QMessageBox::critical(this, tr(s: "Qt Linguist" ), |
1328 | tr(s: "Unable to launch Qt Assistant (%1)" ).arg(a: app)); |
1329 | return; |
1330 | } |
1331 | } |
1332 | QTextStream str(m_assistantProcess); |
1333 | str << QLatin1String("SetSource qthelp://org.qt-project.linguist." ) |
1334 | << (QT_VERSION >> 16) << ((QT_VERSION >> 8) & 0xFF) |
1335 | << (QT_VERSION & 0xFF) |
1336 | << QLatin1String("/qtlinguist/qtlinguist-index.html" ) |
1337 | << QLatin1Char('\n') << Qt::endl; |
1338 | } |
1339 | |
1340 | void MainWindow::about() |
1341 | { |
1342 | QMessageBox box(this); |
1343 | box.setTextFormat(Qt::RichText); |
1344 | QString version = tr(s: "Version %1" ); |
1345 | version = version.arg(a: QLatin1String(QT_VERSION_STR)); |
1346 | |
1347 | const QString description |
1348 | = tr(s: "Qt Linguist is a tool for adding translations to Qt applications." ); |
1349 | const QString copyright |
1350 | = tr(s: "Copyright (C) %1 The Qt Company Ltd." ).arg(QStringLiteral("2023" )); |
1351 | box.setText(QStringLiteral("<center><img src=\":/images/icons/linguist-128-32.png\"/></img><p>%1</p></center>" |
1352 | "<p>%2</p>" |
1353 | "<p>%3</p>" ).arg(args&: version, args: description, args: copyright)); |
1354 | |
1355 | box.setWindowTitle(QApplication::translate(context: "AboutDialog" , key: "Qt Linguist" )); |
1356 | box.setIcon(QMessageBox::NoIcon); |
1357 | box.exec(); |
1358 | } |
1359 | |
1360 | void MainWindow::aboutQt() |
1361 | { |
1362 | QMessageBox::aboutQt(this, tr(s: "Qt Linguist" )); |
1363 | } |
1364 | |
1365 | void MainWindow::setupPhrase() |
1366 | { |
1367 | bool enabled = !m_phraseBooks.isEmpty(); |
1368 | m_ui.menuClosePhraseBook->setEnabled(enabled); |
1369 | m_ui.menuEditPhraseBook->setEnabled(enabled); |
1370 | m_ui.menuPrintPhraseBook->setEnabled(enabled); |
1371 | } |
1372 | |
1373 | void MainWindow::closeEvent(QCloseEvent *e) |
1374 | { |
1375 | if (maybeSaveAll() && maybeSavePhraseBooks()) |
1376 | e->accept(); |
1377 | else |
1378 | e->ignore(); |
1379 | } |
1380 | |
1381 | bool MainWindow::maybeSaveAll() |
1382 | { |
1383 | if (!m_dataModel->isModified()) |
1384 | return true; |
1385 | |
1386 | switch (QMessageBox::information(this, tr(s: "Qt Linguist" ), |
1387 | tr(s: "Do you want to save the modified files?" ), |
1388 | QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes)) |
1389 | { |
1390 | case QMessageBox::Cancel: |
1391 | return false; |
1392 | case QMessageBox::Yes: |
1393 | saveAll(); |
1394 | return !m_dataModel->isModified(); |
1395 | default: |
1396 | break; |
1397 | } |
1398 | return true; |
1399 | } |
1400 | |
1401 | bool MainWindow::maybeSave(int model) |
1402 | { |
1403 | if (!m_dataModel->isModified(model)) |
1404 | return true; |
1405 | |
1406 | switch (QMessageBox::information(this, tr(s: "Qt Linguist" ), |
1407 | tr(s: "Do you want to save '%1'?" ).arg(a: m_dataModel->srcFileName(model, pretty: true)), |
1408 | QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes)) |
1409 | { |
1410 | case QMessageBox::Cancel: |
1411 | return false; |
1412 | case QMessageBox::Yes: |
1413 | saveInternal(model); |
1414 | return !m_dataModel->isModified(model); |
1415 | default: |
1416 | break; |
1417 | } |
1418 | return true; |
1419 | } |
1420 | |
1421 | void MainWindow::updateCaption() |
1422 | { |
1423 | QString cap; |
1424 | bool enable = false; |
1425 | bool enableRw = false; |
1426 | for (int i = 0; i < m_dataModel->modelCount(); ++i) { |
1427 | enable = true; |
1428 | if (m_dataModel->isModelWritable(model: i)) { |
1429 | enableRw = true; |
1430 | break; |
1431 | } |
1432 | } |
1433 | m_ui.actionSaveAll->setEnabled(enableRw); |
1434 | m_ui.actionReleaseAll->setEnabled(enableRw); |
1435 | m_ui.actionCloseAll->setEnabled(enable); |
1436 | m_ui.actionPrint->setEnabled(enable); |
1437 | m_ui.actionAccelerators->setEnabled(enable); |
1438 | m_ui.actionSurroundingWhitespace->setEnabled(enable); |
1439 | m_ui.actionEndingPunctuation->setEnabled(enable); |
1440 | m_ui.actionPhraseMatches->setEnabled(enable); |
1441 | m_ui.actionPlaceMarkerMatches->setEnabled(enable); |
1442 | m_ui.actionResetSorting->setEnabled(enable); |
1443 | |
1444 | updateActiveModel(m_messageEditor->activeModel()); |
1445 | // Ensure that the action labels get updated |
1446 | m_fileActiveModel = m_editActiveModel = -2; |
1447 | |
1448 | if (!enable) |
1449 | cap = tr(s: "Qt Linguist[*]" ); |
1450 | else |
1451 | cap = tr(s: "%1[*] - Qt Linguist" ).arg(a: m_dataModel->condensedSrcFileNames(pretty: true)); |
1452 | setWindowTitle(cap); |
1453 | } |
1454 | |
1455 | void MainWindow::selectedContextChanged(const QModelIndex &sortedIndex, const QModelIndex &oldIndex) |
1456 | { |
1457 | if (sortedIndex.isValid()) { |
1458 | if (m_settingCurrentMessage) |
1459 | return; // Avoid playing ping-pong with the current message |
1460 | |
1461 | QModelIndex sourceIndex = m_sortedContextsModel->mapToSource(proxyIndex: sortedIndex); |
1462 | if (m_messageModel->parent(index: currentMessageIndex()).row() == sourceIndex.row()) |
1463 | return; |
1464 | |
1465 | QModelIndex contextIndex = setMessageViewRoot(sourceIndex); |
1466 | const QModelIndex &firstChild = |
1467 | m_sortedMessagesModel->index(row: 0, column: sourceIndex.column(), parent: contextIndex); |
1468 | m_messageView->setCurrentIndex(firstChild); |
1469 | } else if (oldIndex.isValid()) { |
1470 | m_contextView->setCurrentIndex(oldIndex); |
1471 | } |
1472 | } |
1473 | |
1474 | /* |
1475 | * Updates the message displayed in the message editor and related actions. |
1476 | */ |
1477 | void MainWindow::selectedMessageChanged(const QModelIndex &sortedIndex, const QModelIndex &oldIndex) |
1478 | { |
1479 | // Keep a valid selection whenever possible |
1480 | if (!sortedIndex.isValid() && oldIndex.isValid()) { |
1481 | m_messageView->setCurrentIndex(oldIndex); |
1482 | return; |
1483 | } |
1484 | |
1485 | int model = -1; |
1486 | MessageItem *m = nullptr; |
1487 | QModelIndex index = m_sortedMessagesModel->mapToSource(proxyIndex: sortedIndex); |
1488 | if (index.isValid()) { |
1489 | model = (index.column() && (index.column() - 1 < m_dataModel->modelCount())) ? |
1490 | index.column() - 1 : m_currentIndex.model(); |
1491 | m_currentIndex = m_messageModel->dataIndex(index, model); |
1492 | m_messageEditor->showMessage(index: m_currentIndex); |
1493 | if (model >= 0 && (m = m_dataModel->messageItem(index: m_currentIndex))) { |
1494 | if (m_dataModel->isModelWritable(model) && !m->isObsolete()) |
1495 | m_phraseView->setSourceText(model: m_currentIndex.model(), sourceText: m->text()); |
1496 | else |
1497 | m_phraseView->setSourceText(model: -1, sourceText: QString()); |
1498 | } else { |
1499 | if (model < 0) { |
1500 | model = m_dataModel->multiContextItem(ctxIdx: m_currentIndex.context()) |
1501 | ->firstNonobsoleteMessageIndex(msgIdx: m_currentIndex.message()); |
1502 | if (model >= 0) |
1503 | m = m_dataModel->messageItem(index: m_currentIndex, model); |
1504 | } |
1505 | m_phraseView->setSourceText(model: -1, sourceText: QString()); |
1506 | } |
1507 | m_errorsView->setEnabled(m != 0); |
1508 | updateDanger(index: m_currentIndex, verbose: true); |
1509 | } else { |
1510 | m_currentIndex = MultiDataIndex(); |
1511 | m_messageEditor->showNothing(); |
1512 | m_phraseView->setSourceText(model: -1, sourceText: QString()); |
1513 | } |
1514 | updateSourceView(model, item: m); |
1515 | |
1516 | updatePhraseBookActions(); |
1517 | m_ui.actionSelectAll->setEnabled(index.isValid()); |
1518 | } |
1519 | |
1520 | void MainWindow::translationChanged(const MultiDataIndex &index) |
1521 | { |
1522 | // We get that as a result of batch translation or search & translate, |
1523 | // so the current model is known to match. |
1524 | if (index != m_currentIndex) |
1525 | return; |
1526 | |
1527 | m_messageEditor->showMessage(index); |
1528 | updateDanger(index, verbose: true); |
1529 | |
1530 | MessageItem *m = m_dataModel->messageItem(index); |
1531 | if (hasFormPreview(fileName: m->fileName())) |
1532 | m_formPreviewView->setSourceContext(model: index.model(), messageItem: m); |
1533 | } |
1534 | |
1535 | // This and the following function operate directly on the messageitem, |
1536 | // so the model does not emit modification notifications. |
1537 | void MainWindow::updateTranslation(const QStringList &translations) |
1538 | { |
1539 | MessageItem *m = m_dataModel->messageItem(index: m_currentIndex); |
1540 | if (!m) |
1541 | return; |
1542 | if (translations == m->translations()) |
1543 | return; |
1544 | |
1545 | m->setTranslations(translations); |
1546 | if (!m->fileName().isEmpty() && hasFormPreview(fileName: m->fileName())) |
1547 | m_formPreviewView->setSourceContext(model: m_currentIndex.model(), messageItem: m); |
1548 | updateDanger(index: m_currentIndex, verbose: true); |
1549 | |
1550 | if (m->isFinished()) |
1551 | m_dataModel->setFinished(index: m_currentIndex, finished: false); |
1552 | else |
1553 | m_dataModel->setModified(model: m_currentIndex.model(), dirty: true); |
1554 | } |
1555 | |
1556 | void MainWindow::updateTranslatorComment(const QString &) |
1557 | { |
1558 | MessageItem *m = m_dataModel->messageItem(index: m_currentIndex); |
1559 | if (!m) |
1560 | return; |
1561 | if (comment == m->translatorComment()) |
1562 | return; |
1563 | |
1564 | m->setTranslatorComment(comment); |
1565 | |
1566 | m_dataModel->setModified(model: m_currentIndex.model(), dirty: true); |
1567 | } |
1568 | |
1569 | void MainWindow::refreshItemViews() |
1570 | { |
1571 | m_messageModel->blockSignals(b: false); |
1572 | m_contextView->update(); |
1573 | m_messageView->update(); |
1574 | setWindowModified(m_dataModel->isModified()); |
1575 | m_modifiedLabel->setVisible(m_dataModel->isModified()); |
1576 | updateStatistics(); |
1577 | } |
1578 | |
1579 | void MainWindow::done() |
1580 | { |
1581 | int model = m_messageEditor->activeModel(); |
1582 | if (model >= 0 && m_dataModel->isModelWritable(model)) |
1583 | m_dataModel->setFinished(index: m_currentIndex, finished: true); |
1584 | } |
1585 | |
1586 | void MainWindow::doneAndNext() |
1587 | { |
1588 | done(); |
1589 | if (!m_messageEditor->focusNextUnfinished()) |
1590 | nextUnfinished(); |
1591 | } |
1592 | |
1593 | void MainWindow::toggleFinished(const QModelIndex &index) |
1594 | { |
1595 | if (!index.isValid() || index.column() - 1 >= m_dataModel->modelCount() |
1596 | || !m_dataModel->isModelWritable(model: index.column() - 1) || index.parent() == QModelIndex()) |
1597 | return; |
1598 | |
1599 | QModelIndex item = m_sortedMessagesModel->mapToSource(proxyIndex: index); |
1600 | MultiDataIndex dataIndex = m_messageModel->dataIndex(index: item); |
1601 | MessageItem *m = m_dataModel->messageItem(index: dataIndex); |
1602 | |
1603 | if (!m || m->message().type() == TranslatorMessage::Obsolete |
1604 | || m->message().type() == TranslatorMessage::Vanished) |
1605 | return; |
1606 | |
1607 | m_dataModel->setFinished(index: dataIndex, finished: !m->isFinished()); |
1608 | } |
1609 | |
1610 | /* |
1611 | * Receives a context index in the sorted messages model and returns the next |
1612 | * logical context index in the same model, based on the sort order of the |
1613 | * contexts in the sorted contexts model. |
1614 | */ |
1615 | QModelIndex MainWindow::nextContext(const QModelIndex &index) const |
1616 | { |
1617 | QModelIndex sortedContextIndex = m_sortedContextsModel->mapFromSource( |
1618 | sourceIndex: m_sortedMessagesModel->mapToSource(proxyIndex: index)); |
1619 | |
1620 | int nextRow = sortedContextIndex.row() + 1; |
1621 | if (nextRow >= m_sortedContextsModel->rowCount()) |
1622 | nextRow = 0; |
1623 | sortedContextIndex = m_sortedContextsModel->index(row: nextRow, column: index.column()); |
1624 | |
1625 | return m_sortedMessagesModel->mapFromSource( |
1626 | sourceIndex: m_sortedContextsModel->mapToSource(proxyIndex: sortedContextIndex)); |
1627 | } |
1628 | |
1629 | /* |
1630 | * See nextContext. |
1631 | */ |
1632 | QModelIndex MainWindow::prevContext(const QModelIndex &index) const |
1633 | { |
1634 | QModelIndex sortedContextIndex = m_sortedContextsModel->mapFromSource( |
1635 | sourceIndex: m_sortedMessagesModel->mapToSource(proxyIndex: index)); |
1636 | |
1637 | int prevRow = sortedContextIndex.row() - 1; |
1638 | if (prevRow < 0) prevRow = m_sortedContextsModel->rowCount() - 1; |
1639 | sortedContextIndex = m_sortedContextsModel->index(row: prevRow, column: index.column()); |
1640 | |
1641 | return m_sortedMessagesModel->mapFromSource( |
1642 | sourceIndex: m_sortedContextsModel->mapToSource(proxyIndex: sortedContextIndex)); |
1643 | } |
1644 | |
1645 | QModelIndex MainWindow::nextMessage(const QModelIndex ¤tIndex, bool checkUnfinished) const |
1646 | { |
1647 | QModelIndex idx = currentIndex.isValid() ? currentIndex : m_sortedMessagesModel->index(row: 0, column: 0); |
1648 | do { |
1649 | int row = 0; |
1650 | QModelIndex par = idx.parent(); |
1651 | if (par.isValid()) { |
1652 | row = idx.row() + 1; |
1653 | } else { // In case we are located on a top-level node |
1654 | par = idx; |
1655 | } |
1656 | |
1657 | if (row >= m_sortedMessagesModel->rowCount(parent: par)) { |
1658 | par = nextContext(index: par); |
1659 | row = 0; |
1660 | } |
1661 | idx = m_sortedMessagesModel->index(row, column: idx.column(), parent: par); |
1662 | |
1663 | if (!checkUnfinished) |
1664 | return idx; |
1665 | |
1666 | QModelIndex item = m_sortedMessagesModel->mapToSource(proxyIndex: idx); |
1667 | MultiDataIndex index = m_messageModel->dataIndex(index: item, model: -1); |
1668 | if (m_dataModel->multiMessageItem(index)->isUnfinished()) |
1669 | return idx; |
1670 | } while (idx != currentIndex); |
1671 | return QModelIndex(); |
1672 | } |
1673 | |
1674 | QModelIndex MainWindow::prevMessage(const QModelIndex ¤tIndex, bool checkUnfinished) const |
1675 | { |
1676 | QModelIndex idx = currentIndex.isValid() ? currentIndex : m_sortedMessagesModel->index(row: 0, column: 0); |
1677 | do { |
1678 | int row = idx.row() - 1; |
1679 | QModelIndex par = idx.parent(); |
1680 | if (!par.isValid()) { // In case we are located on a top-level node |
1681 | par = idx; |
1682 | row = -1; |
1683 | } |
1684 | |
1685 | if (row < 0) { |
1686 | par = prevContext(index: par); |
1687 | row = m_sortedMessagesModel->rowCount(parent: par) - 1; |
1688 | } |
1689 | idx = m_sortedMessagesModel->index(row, column: idx.column(), parent: par); |
1690 | |
1691 | if (!checkUnfinished) |
1692 | return idx; |
1693 | |
1694 | QModelIndex item = m_sortedMessagesModel->mapToSource(proxyIndex: idx); |
1695 | MultiDataIndex index = m_messageModel->dataIndex(index: item, model: -1); |
1696 | if (m_dataModel->multiMessageItem(index)->isUnfinished()) |
1697 | return idx; |
1698 | } while (idx != currentIndex); |
1699 | return QModelIndex(); |
1700 | } |
1701 | |
1702 | void MainWindow::nextUnfinished() |
1703 | { |
1704 | if (m_ui.actionNextUnfinished->isEnabled()) { |
1705 | if (!doNext(checkUnfinished: true)) { |
1706 | // If no Unfinished message is left, the user has finished the job. We |
1707 | // congratulate on a job well done with this ringing bell. |
1708 | statusBar()->showMessage(tr(s: "No untranslated translation units left." ), MessageMS); |
1709 | qApp->beep(); |
1710 | } |
1711 | } |
1712 | } |
1713 | |
1714 | void MainWindow::prevUnfinished() |
1715 | { |
1716 | if (m_ui.actionNextUnfinished->isEnabled()) { |
1717 | if (!doPrev(checkUnfinished: true)) { |
1718 | // If no Unfinished message is left, the user has finished the job. We |
1719 | // congratulate on a job well done with this ringing bell. |
1720 | statusBar()->showMessage(tr(s: "No untranslated translation units left." ), MessageMS); |
1721 | qApp->beep(); |
1722 | } |
1723 | } |
1724 | } |
1725 | |
1726 | void MainWindow::prev() |
1727 | { |
1728 | doPrev(checkUnfinished: false); |
1729 | } |
1730 | |
1731 | void MainWindow::next() |
1732 | { |
1733 | doNext(checkUnfinished: false); |
1734 | } |
1735 | |
1736 | bool MainWindow::doPrev(bool checkUnfinished) |
1737 | { |
1738 | QModelIndex index = prevMessage(currentIndex: m_messageView->currentIndex(), checkUnfinished); |
1739 | if (index.isValid()) |
1740 | setCurrentMessage(m_sortedMessagesModel->mapToSource(proxyIndex: index)); |
1741 | if (checkUnfinished) |
1742 | m_messageEditor->setUnfinishedEditorFocus(); |
1743 | else |
1744 | m_messageEditor->setEditorFocus(); |
1745 | return index.isValid(); |
1746 | } |
1747 | |
1748 | bool MainWindow::doNext(bool checkUnfinished) |
1749 | { |
1750 | QModelIndex index = nextMessage(currentIndex: m_messageView->currentIndex(), checkUnfinished); |
1751 | if (index.isValid()) |
1752 | setCurrentMessage(m_sortedMessagesModel->mapToSource(proxyIndex: index)); |
1753 | if (checkUnfinished) |
1754 | m_messageEditor->setUnfinishedEditorFocus(); |
1755 | else |
1756 | m_messageEditor->setEditorFocus(); |
1757 | return index.isValid(); |
1758 | } |
1759 | |
1760 | void MainWindow::findNext(const QString &text, DataModel::FindLocation where, |
1761 | FindDialog::FindOptions options, int statusFilter) |
1762 | { |
1763 | if (text.isEmpty()) |
1764 | return; |
1765 | m_findText = text; |
1766 | m_findWhere = where; |
1767 | m_findOptions = options; |
1768 | m_findStatusFilter = statusFilter; |
1769 | if (options.testFlag(flag: FindDialog::UseRegExp)) { |
1770 | m_findDialog->getRegExp().setPatternOptions(options.testFlag(flag: FindDialog::MatchCase) |
1771 | ? QRegularExpression::NoPatternOption |
1772 | : QRegularExpression::CaseInsensitiveOption); |
1773 | } |
1774 | m_ui.actionFindNext->setEnabled(true); |
1775 | m_ui.actionFindPrev->setEnabled(true); |
1776 | findAgain(); |
1777 | } |
1778 | |
1779 | void MainWindow::revalidate() |
1780 | { |
1781 | for (MultiDataModelIterator it(m_dataModel, -1); it.isValid(); ++it) |
1782 | updateDanger(index: it, verbose: false); |
1783 | |
1784 | if (m_currentIndex.isValid()) |
1785 | updateDanger(index: m_currentIndex, verbose: true); |
1786 | } |
1787 | |
1788 | QString MainWindow::friendlyString(const QString& str) |
1789 | { |
1790 | QString f = str.toLower(); |
1791 | f.replace(re: QRegularExpression(QString(QLatin1String("[.,:;!?()-]" ))), after: QString(QLatin1String(" " ))); |
1792 | f.remove(c: QLatin1Char('&')); |
1793 | return f.simplified(); |
1794 | } |
1795 | |
1796 | void MainWindow::setupMenuBar() |
1797 | { |
1798 | |
1799 | const bool hasThemeIcons = !QApplication::platformName().compare(QStringLiteral("xcb" ), cs: Qt::CaseInsensitive); |
1800 | if (hasThemeIcons) { // There are no fallback icons for these |
1801 | m_ui.menuRecentlyOpenedFiles->setIcon(QIcon::fromTheme(QStringLiteral("document-open-recent" ))); |
1802 | m_ui.actionCloseAll->setIcon(QIcon::fromTheme(QStringLiteral("window-close" ))); |
1803 | m_ui.actionExit->setIcon(QIcon::fromTheme(QStringLiteral("application-exit" ))); |
1804 | m_ui.actionSelectAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all" ))); |
1805 | } |
1806 | |
1807 | // Prefer theme icons when available for these actions |
1808 | const QString prefix = QApplication::platformName().compare(QStringLiteral("cocoa" ), cs: Qt::CaseInsensitive) ? |
1809 | QStringLiteral(":/images/win" ) : QStringLiteral(":/images/mac" ); |
1810 | |
1811 | m_ui.actionOpen->setIcon(QIcon::fromTheme(QStringLiteral("document-open" ), |
1812 | QIcon(prefix + QStringLiteral("/fileopen.png" )))); |
1813 | m_ui.actionOpenAux->setIcon(QIcon::fromTheme(QStringLiteral("document-open" ), |
1814 | QIcon(prefix + QStringLiteral("/fileopen.png" )))); |
1815 | m_ui.actionSave->setIcon(QIcon::fromTheme(QStringLiteral("document-save" ), |
1816 | QIcon(prefix + QStringLiteral("/filesave.png" )))); |
1817 | m_ui.actionSaveAll->setIcon(QIcon::fromTheme(QStringLiteral("document-save" ), |
1818 | QIcon(prefix + QStringLiteral("/filesave.png" )))); |
1819 | m_ui.actionPrint->setIcon(QIcon::fromTheme(QStringLiteral("document-print" ), |
1820 | QIcon(prefix + QStringLiteral("/print.png" )))); |
1821 | m_ui.actionRedo->setIcon(QIcon::fromTheme(QStringLiteral("edit-redo" ), |
1822 | QIcon(prefix + QStringLiteral("/redo.png" )))); |
1823 | m_ui.actionUndo->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo" ), |
1824 | QIcon(prefix + QStringLiteral("/undo.png" )))); |
1825 | m_ui.actionCut->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut" ), |
1826 | QIcon(prefix + QStringLiteral("/editcut.png" )))); |
1827 | m_ui.actionCopy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy" ), |
1828 | QIcon(prefix + QStringLiteral("/editcopy.png" )))); |
1829 | m_ui.actionPaste->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste" ), |
1830 | QIcon(prefix + QStringLiteral("/editpaste.png" )))); |
1831 | m_ui.actionFind->setIcon(QIcon::fromTheme(QStringLiteral("edit-find" ), |
1832 | QIcon(prefix + QStringLiteral("/searchfind.png" )))); |
1833 | |
1834 | // No well defined theme icons for these actions |
1835 | m_ui.actionAccelerators->setIcon(QIcon(prefix + QStringLiteral("/accelerator.png" ))); |
1836 | m_ui.actionOpenPhraseBook->setIcon(QIcon(prefix + QStringLiteral("/book.png" ))); |
1837 | m_ui.actionDone->setIcon(QIcon(prefix + QStringLiteral("/done.png" ))); |
1838 | m_ui.actionDoneAndNext->setIcon(QIcon(prefix + QStringLiteral("/doneandnext.png" ))); |
1839 | m_ui.actionNext->setIcon(QIcon(prefix + QStringLiteral("/next.png" ))); |
1840 | m_ui.actionNextUnfinished->setIcon(QIcon(prefix + QStringLiteral("/nextunfinished.png" ))); |
1841 | m_ui.actionPhraseMatches->setIcon(QIcon(prefix + QStringLiteral("/phrase.png" ))); |
1842 | m_ui.actionSurroundingWhitespace->setIcon(QIcon(prefix + QStringLiteral("/surroundingwhitespace.png" ))); |
1843 | m_ui.actionEndingPunctuation->setIcon(QIcon(prefix + QStringLiteral("/punctuation.png" ))); |
1844 | m_ui.actionPrev->setIcon(QIcon(prefix + QStringLiteral("/prev.png" ))); |
1845 | m_ui.actionPrevUnfinished->setIcon(QIcon(prefix + QStringLiteral("/prevunfinished.png" ))); |
1846 | m_ui.actionPlaceMarkerMatches->setIcon(QIcon(prefix + QStringLiteral("/validateplacemarkers.png" ))); |
1847 | m_ui.actionWhatsThis->setIcon(QIcon(prefix + QStringLiteral("/whatsthis.png" ))); |
1848 | |
1849 | // File menu |
1850 | connect(m_ui.menuFile, &QMenu::aboutToShow, this, &MainWindow::fileAboutToShow); |
1851 | connect(m_ui.actionOpen, &QAction::triggered, this, &MainWindow::open); |
1852 | connect(m_ui.actionOpenAux, &QAction::triggered, this, &MainWindow::openAux); |
1853 | connect(m_ui.actionSaveAll, &QAction::triggered, this, &MainWindow::saveAll); |
1854 | connect(m_ui.actionSave, &QAction::triggered, this, &MainWindow::save); |
1855 | connect(m_ui.actionSaveAs, &QAction::triggered, this, &MainWindow::saveAs); |
1856 | connect(m_ui.actionReleaseAll, &QAction::triggered, this, &MainWindow::releaseAll); |
1857 | connect(m_ui.actionRelease, &QAction::triggered, this, &MainWindow::release); |
1858 | connect(m_ui.actionReleaseAs, &QAction::triggered, this, &MainWindow::releaseAs); |
1859 | connect(m_ui.actionPrint, &QAction::triggered, this, &MainWindow::print); |
1860 | connect(m_ui.actionClose, &QAction::triggered, this, &MainWindow::closeFile); |
1861 | connect(m_ui.actionCloseAll, &QAction::triggered, this, &MainWindow::closeAll); |
1862 | connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close); |
1863 | |
1864 | // Edit menu |
1865 | connect(m_ui.menuEdit, &QMenu::aboutToShow, this, &MainWindow::editAboutToShow); |
1866 | |
1867 | connect(m_ui.actionUndo, &QAction::triggered, m_messageEditor, &MessageEditor::undo); |
1868 | connect(m_messageEditor, &MessageEditor::undoAvailable, m_ui.actionUndo, &QAction::setEnabled); |
1869 | |
1870 | connect(m_ui.actionRedo, &QAction::triggered, m_messageEditor, &MessageEditor::redo); |
1871 | connect(m_messageEditor, &MessageEditor::redoAvailable, m_ui.actionRedo, &QAction::setEnabled); |
1872 | |
1873 | #ifndef QT_NO_CLIPBOARD |
1874 | connect(m_ui.actionCut, &QAction::triggered, m_messageEditor, &MessageEditor::cut); |
1875 | connect(m_messageEditor, &MessageEditor::cutAvailable, m_ui.actionCut, &QAction::setEnabled); |
1876 | |
1877 | connect(m_ui.actionCopy, &QAction::triggered, m_messageEditor, &MessageEditor::copy); |
1878 | connect(m_messageEditor, &MessageEditor::copyAvailable, m_ui.actionCopy, &QAction::setEnabled); |
1879 | |
1880 | connect(m_ui.actionPaste, &QAction::triggered, m_messageEditor, &MessageEditor::paste); |
1881 | connect(m_messageEditor, &MessageEditor::pasteAvailable, m_ui.actionPaste, &QAction::setEnabled); |
1882 | #endif |
1883 | |
1884 | connect(m_ui.actionSelectAll, &QAction::triggered, |
1885 | m_messageEditor, &MessageEditor::selectAll); |
1886 | connect(m_ui.actionFind, &QAction::triggered, |
1887 | m_findDialog, &FindDialog::find); |
1888 | connect(m_ui.actionFindNext, &QAction::triggered, |
1889 | this, [this] {findAgain(FindNext);}); |
1890 | connect(m_ui.actionFindPrev, &QAction::triggered, |
1891 | this, [this] {findAgain(FindPrev);}); |
1892 | connect(m_ui.actionSearchAndTranslate, &QAction::triggered, |
1893 | this, &MainWindow::showTranslateDialog); |
1894 | connect(m_ui.actionBatchTranslation, &QAction::triggered, |
1895 | this, &MainWindow::showBatchTranslateDialog); |
1896 | connect(m_ui.actionTranslationFileSettings, &QAction::triggered, |
1897 | this, &MainWindow::showTranslationSettings); |
1898 | |
1899 | connect(m_batchTranslateDialog, &BatchTranslationDialog::finished, |
1900 | this, &MainWindow::refreshItemViews); |
1901 | |
1902 | // Translation menu |
1903 | // when updating the accelerators, remember the status bar |
1904 | connect(m_ui.actionPrevUnfinished, &QAction::triggered, this, &MainWindow::prevUnfinished); |
1905 | connect(m_ui.actionNextUnfinished, &QAction::triggered, this, &MainWindow::nextUnfinished); |
1906 | connect(m_ui.actionNext, &QAction::triggered, this, &MainWindow::next); |
1907 | connect(m_ui.actionPrev, &QAction::triggered, this, &MainWindow::prev); |
1908 | connect(m_ui.actionDone, &QAction::triggered, this, &MainWindow::done); |
1909 | connect(m_ui.actionDoneAndNext, &QAction::triggered, this, &MainWindow::doneAndNext); |
1910 | connect(m_ui.actionBeginFromSource, &QAction::triggered, m_messageEditor, &MessageEditor::beginFromSource); |
1911 | connect(m_messageEditor, &MessageEditor::beginFromSourceAvailable, |
1912 | m_ui.actionBeginFromSource, &QAction::setEnabled); |
1913 | |
1914 | // Phrasebook menu |
1915 | connect(m_ui.actionNewPhraseBook, &QAction::triggered, this, &MainWindow::newPhraseBook); |
1916 | connect(m_ui.actionOpenPhraseBook, &QAction::triggered, this, &MainWindow::openPhraseBook); |
1917 | connect(m_ui.menuClosePhraseBook, &QMenu::triggered, |
1918 | this, &MainWindow::closePhraseBook); |
1919 | connect(m_ui.menuEditPhraseBook, &QMenu::triggered, |
1920 | this, &MainWindow::editPhraseBook); |
1921 | connect(m_ui.menuPrintPhraseBook, &QMenu::triggered, |
1922 | this, &MainWindow::printPhraseBook); |
1923 | connect(m_ui.actionAddToPhraseBook, &QAction::triggered, |
1924 | this, &MainWindow::addToPhraseBook); |
1925 | |
1926 | // Validation menu |
1927 | connect(m_ui.actionAccelerators, &QAction::triggered, this, &MainWindow::revalidate); |
1928 | connect(m_ui.actionSurroundingWhitespace, &QAction::triggered, this, &MainWindow::revalidate); |
1929 | connect(m_ui.actionEndingPunctuation, &QAction::triggered, this, &MainWindow::revalidate); |
1930 | connect(m_ui.actionPhraseMatches, &QAction::triggered, this, &MainWindow::revalidate); |
1931 | connect(m_ui.actionPlaceMarkerMatches, &QAction::triggered, this, &MainWindow::revalidate); |
1932 | |
1933 | // View menu |
1934 | connect(m_ui.actionResetSorting, &QAction::triggered, |
1935 | this, &MainWindow::resetSorting); |
1936 | connect(m_ui.actionDisplayGuesses, &QAction::triggered, |
1937 | m_phraseView, &PhraseView::toggleGuessing); |
1938 | connect(m_ui.actionStatistics, &QAction::triggered, |
1939 | this, &MainWindow::toggleStatistics); |
1940 | connect(m_ui.actionVisualizeWhitespace, &QAction::triggered, |
1941 | this, &MainWindow::toggleVisualizeWhitespace); |
1942 | connect(m_ui.menuView, &QMenu::aboutToShow, |
1943 | this, &MainWindow::updateViewMenu); |
1944 | connect(m_ui.actionIncreaseZoom, &QAction::triggered, |
1945 | m_messageEditor, &MessageEditor::increaseFontSize); |
1946 | connect(m_ui.actionDecreaseZoom, &QAction::triggered, |
1947 | m_messageEditor, &MessageEditor::decreaseFontSize); |
1948 | connect(m_ui.actionResetZoomToDefault, &QAction::triggered, |
1949 | m_messageEditor, &MessageEditor::resetFontSize); |
1950 | connect(m_ui.actionShowMoreGuesses, &QAction::triggered, |
1951 | m_phraseView, &PhraseView::moreGuesses); |
1952 | connect(m_ui.actionShowFewerGuesses, &QAction::triggered, |
1953 | m_phraseView, &PhraseView::fewerGuesses); |
1954 | connect(m_phraseView, &PhraseView::showFewerGuessesAvailable, |
1955 | m_ui.actionShowFewerGuesses, &QAction::setEnabled); |
1956 | connect(m_ui.actionResetGuessesToDefault, &QAction::triggered, |
1957 | m_phraseView, &PhraseView::resetNumGuesses); |
1958 | m_ui.menuViewViews->addAction(m_contextDock->toggleViewAction()); |
1959 | m_ui.menuViewViews->addAction(m_messagesDock->toggleViewAction()); |
1960 | m_ui.menuViewViews->addAction(m_phrasesDock->toggleViewAction()); |
1961 | m_ui.menuViewViews->addAction(m_sourceAndFormDock->toggleViewAction()); |
1962 | m_ui.menuViewViews->addAction(m_errorsDock->toggleViewAction()); |
1963 | |
1964 | #if defined(Q_OS_MAC) |
1965 | // Window menu |
1966 | QMenu *windowMenu = new QMenu(tr("&Window" ), this); |
1967 | menuBar()->insertMenu(m_ui.menuHelp->menuAction(), windowMenu); |
1968 | windowMenu->addAction(tr("Minimize" ), QKeySequence(tr("Ctrl+M" )), |
1969 | this, &QWidget::showMinimized); |
1970 | #endif |
1971 | |
1972 | // Help |
1973 | connect(m_ui.actionManual, &QAction::triggered, this, &MainWindow::manual); |
1974 | connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::about); |
1975 | connect(m_ui.actionAboutQt, &QAction::triggered, this, &MainWindow::aboutQt); |
1976 | connect(m_ui.actionWhatsThis, &QAction::triggered, this, &MainWindow::onWhatsThis); |
1977 | |
1978 | connect(m_ui.menuRecentlyOpenedFiles, &QMenu::triggered, |
1979 | this, &MainWindow::recentFileActivated); |
1980 | |
1981 | m_ui.actionManual->setWhatsThis(tr("Display the manual for %1." ).arg(tr("Qt Linguist" ))); |
1982 | m_ui.actionAbout->setWhatsThis(tr("Display information about %1." ).arg(tr("Qt Linguist" ))); |
1983 | m_ui.actionDone->setShortcuts(QList<QKeySequence>() |
1984 | << QKeySequence(QLatin1String("Alt+Return" )) |
1985 | << QKeySequence(QLatin1String("Alt+Enter" ))); |
1986 | m_ui.actionDoneAndNext->setShortcuts(QList<QKeySequence>() |
1987 | << QKeySequence(QLatin1String("Ctrl+Return" )) |
1988 | << QKeySequence(QLatin1String("Ctrl+Enter" ))); |
1989 | |
1990 | // Disable the Close/Edit/Print phrasebook menuitems if they are not loaded |
1991 | connect(m_ui.menuPhrases, &QMenu::aboutToShow, this, &MainWindow::setupPhrase); |
1992 | |
1993 | connect(m_ui.menuRecentlyOpenedFiles, &QMenu::aboutToShow, |
1994 | this, &MainWindow::setupRecentFilesMenu); |
1995 | } |
1996 | |
1997 | void MainWindow::updateActiveModel(int model) |
1998 | { |
1999 | if (model >= 0) |
2000 | doUpdateLatestModel(model); |
2001 | } |
2002 | |
2003 | // Arriving here implies that the messageEditor does not have focus |
2004 | void MainWindow::updateLatestModel(const QModelIndex &index) |
2005 | { |
2006 | if (index.column() && (index.column() - 1 < m_dataModel->modelCount())) |
2007 | doUpdateLatestModel(model: index.column() - 1); |
2008 | } |
2009 | |
2010 | void MainWindow::doUpdateLatestModel(int model) |
2011 | { |
2012 | m_currentIndex = MultiDataIndex(model, m_currentIndex.context(), m_currentIndex.message()); |
2013 | bool enable = false; |
2014 | bool enableRw = false; |
2015 | MessageItem *item = nullptr; |
2016 | if (model >= 0) { |
2017 | enable = true; |
2018 | if (m_dataModel->isModelWritable(model)) |
2019 | enableRw = true; |
2020 | |
2021 | if (m_currentIndex.isValid()) { |
2022 | if ((item = m_dataModel->messageItem(index: m_currentIndex))) { |
2023 | if (enableRw && !item->isObsolete()) |
2024 | m_phraseView->setSourceText(model, sourceText: item->text()); |
2025 | else |
2026 | m_phraseView->setSourceText(model: -1, sourceText: QString()); |
2027 | } else { |
2028 | m_phraseView->setSourceText(model: -1, sourceText: QString()); |
2029 | } |
2030 | } |
2031 | } |
2032 | updateSourceView(model, item); |
2033 | m_ui.actionSave->setEnabled(enableRw); |
2034 | m_ui.actionSaveAs->setEnabled(enableRw); |
2035 | m_ui.actionRelease->setEnabled(enableRw); |
2036 | m_ui.actionReleaseAs->setEnabled(enableRw); |
2037 | m_ui.actionClose->setEnabled(enable); |
2038 | m_ui.actionTranslationFileSettings->setEnabled(enableRw); |
2039 | m_ui.actionSearchAndTranslate->setEnabled(enableRw); |
2040 | // cut & paste - edit only |
2041 | updatePhraseBookActions(); |
2042 | updateStatistics(); |
2043 | } |
2044 | |
2045 | void MainWindow::updateSourceView(int model, MessageItem *item) |
2046 | { |
2047 | if (item && !item->fileName().isEmpty()) { |
2048 | if (hasFormPreview(fileName: item->fileName())) { |
2049 | m_sourceAndFormView->setCurrentWidget(m_formPreviewView); |
2050 | m_formPreviewView->setSourceContext(model, messageItem: item); |
2051 | } else { |
2052 | m_sourceAndFormView->setCurrentWidget(m_sourceCodeView); |
2053 | QDir dir = QFileInfo(m_dataModel->srcFileName(model)).dir(); |
2054 | QString fileName = QDir::cleanPath(path: dir.absoluteFilePath(fileName: item->fileName())); |
2055 | m_sourceCodeView->setSourceContext(fileName, lineNum: item->lineNumber()); |
2056 | } |
2057 | } else { |
2058 | m_sourceAndFormView->setCurrentWidget(m_sourceCodeView); |
2059 | m_sourceCodeView->setSourceContext(fileName: QString(), lineNum: 0); |
2060 | } |
2061 | } |
2062 | |
2063 | // Note for *AboutToShow: Due to the delayed nature, only actions without shortcuts |
2064 | // and representations outside the menu may be setEnabled()/setVisible() here. |
2065 | |
2066 | void MainWindow::fileAboutToShow() |
2067 | { |
2068 | if (m_fileActiveModel != m_currentIndex.model()) { |
2069 | // We rename the actions so the shortcuts need not be reassigned. |
2070 | bool en; |
2071 | if (m_dataModel->modelCount() > 1) { |
2072 | if (m_currentIndex.model() >= 0) { |
2073 | QString fn = QFileInfo(m_dataModel->srcFileName(model: m_currentIndex.model())).baseName(); |
2074 | m_ui.actionSave->setText(tr("&Save '%1'" ).arg(fn)); |
2075 | m_ui.actionSaveAs->setText(tr("Save '%1' &As..." ).arg(fn)); |
2076 | m_ui.actionRelease->setText(tr("Release '%1'" ).arg(fn)); |
2077 | m_ui.actionReleaseAs->setText(tr("Release '%1' As..." ).arg(fn)); |
2078 | m_ui.actionClose->setText(tr("&Close '%1'" ).arg(fn)); |
2079 | } else { |
2080 | m_ui.actionSave->setText(tr("&Save" )); |
2081 | m_ui.actionSaveAs->setText(tr("Save &As..." )); |
2082 | m_ui.actionRelease->setText(tr("Release" )); |
2083 | m_ui.actionReleaseAs->setText(tr("Release As..." )); |
2084 | m_ui.actionClose->setText(tr("&Close" )); |
2085 | } |
2086 | |
2087 | m_ui.actionSaveAll->setText(tr("Save All" )); |
2088 | m_ui.actionReleaseAll->setText(tr("&Release All" )); |
2089 | m_ui.actionCloseAll->setText(tr("Close All" )); |
2090 | en = true; |
2091 | } else { |
2092 | m_ui.actionSaveAs->setText(tr("Save &As..." )); |
2093 | m_ui.actionReleaseAs->setText(tr("Release As..." )); |
2094 | |
2095 | m_ui.actionSaveAll->setText(tr("&Save" )); |
2096 | m_ui.actionReleaseAll->setText(tr("&Release" )); |
2097 | m_ui.actionCloseAll->setText(tr("&Close" )); |
2098 | en = false; |
2099 | } |
2100 | m_ui.actionSave->setVisible(en); |
2101 | m_ui.actionRelease->setVisible(en); |
2102 | m_ui.actionClose->setVisible(en); |
2103 | m_fileActiveModel = m_currentIndex.model(); |
2104 | } |
2105 | } |
2106 | |
2107 | void MainWindow::editAboutToShow() |
2108 | { |
2109 | if (m_editActiveModel != m_currentIndex.model()) { |
2110 | if (m_currentIndex.model() >= 0 && m_dataModel->modelCount() > 1) { |
2111 | QString fn = QFileInfo(m_dataModel->srcFileName(model: m_currentIndex.model())).baseName(); |
2112 | m_ui.actionTranslationFileSettings->setText(tr("Translation File &Settings for '%1'..." ).arg(fn)); |
2113 | m_ui.actionBatchTranslation->setText(tr("&Batch Translation of '%1'..." ).arg(fn)); |
2114 | m_ui.actionSearchAndTranslate->setText(tr("Search And &Translate in '%1'..." ).arg(fn)); |
2115 | } else { |
2116 | m_ui.actionTranslationFileSettings->setText(tr("Translation File &Settings..." )); |
2117 | m_ui.actionBatchTranslation->setText(tr("&Batch Translation..." )); |
2118 | m_ui.actionSearchAndTranslate->setText(tr("Search And &Translate..." )); |
2119 | } |
2120 | m_editActiveModel = m_currentIndex.model(); |
2121 | } |
2122 | } |
2123 | |
2124 | void MainWindow::updateViewMenu() |
2125 | { |
2126 | bool check = m_statistics ? m_statistics->isVisible() : false; |
2127 | m_ui.actionStatistics->setChecked(check); |
2128 | } |
2129 | |
2130 | void MainWindow::showContextDock() |
2131 | { |
2132 | m_contextDock->show(); |
2133 | m_contextDock->raise(); |
2134 | } |
2135 | |
2136 | void MainWindow::showMessagesDock() |
2137 | { |
2138 | m_messagesDock->show(); |
2139 | m_messagesDock->raise(); |
2140 | } |
2141 | |
2142 | void MainWindow::showPhrasesDock() |
2143 | { |
2144 | m_phrasesDock->show(); |
2145 | m_phrasesDock->raise(); |
2146 | } |
2147 | |
2148 | void MainWindow::showSourceCodeDock() |
2149 | { |
2150 | m_sourceAndFormDock->show(); |
2151 | m_sourceAndFormDock->raise(); |
2152 | } |
2153 | |
2154 | void MainWindow::showErrorDock() |
2155 | { |
2156 | m_errorsDock->show(); |
2157 | m_errorsDock->raise(); |
2158 | } |
2159 | |
2160 | void MainWindow::onWhatsThis() |
2161 | { |
2162 | QWhatsThis::enterWhatsThisMode(); |
2163 | } |
2164 | |
2165 | void MainWindow::setupToolBars() |
2166 | { |
2167 | QToolBar *filet = new QToolBar(this); |
2168 | filet->setObjectName(QLatin1String("FileToolbar" )); |
2169 | filet->setWindowTitle(tr(s: "File" )); |
2170 | this->addToolBar(filet); |
2171 | m_ui.menuToolbars->addAction(filet->toggleViewAction()); |
2172 | |
2173 | QToolBar *editt = new QToolBar(this); |
2174 | editt->setVisible(false); |
2175 | editt->setObjectName(QLatin1String("EditToolbar" )); |
2176 | editt->setWindowTitle(tr(s: "Edit" )); |
2177 | this->addToolBar(editt); |
2178 | m_ui.menuToolbars->addAction(editt->toggleViewAction()); |
2179 | |
2180 | QToolBar *translationst = new QToolBar(this); |
2181 | translationst->setObjectName(QLatin1String("TranslationToolbar" )); |
2182 | translationst->setWindowTitle(tr(s: "Translation" )); |
2183 | this->addToolBar(translationst); |
2184 | m_ui.menuToolbars->addAction(translationst->toggleViewAction()); |
2185 | |
2186 | QToolBar *validationt = new QToolBar(this); |
2187 | validationt->setObjectName(QLatin1String("ValidationToolbar" )); |
2188 | validationt->setWindowTitle(tr(s: "Validation" )); |
2189 | this->addToolBar(validationt); |
2190 | m_ui.menuToolbars->addAction(validationt->toggleViewAction()); |
2191 | |
2192 | QToolBar *helpt = new QToolBar(this); |
2193 | helpt->setVisible(false); |
2194 | helpt->setObjectName(QLatin1String("HelpToolbar" )); |
2195 | helpt->setWindowTitle(tr(s: "Help" )); |
2196 | this->addToolBar(helpt); |
2197 | m_ui.menuToolbars->addAction(helpt->toggleViewAction()); |
2198 | |
2199 | |
2200 | filet->addAction(m_ui.actionOpen); |
2201 | filet->addAction(m_ui.actionSaveAll); |
2202 | filet->addAction(m_ui.actionPrint); |
2203 | filet->addSeparator(); |
2204 | filet->addAction(m_ui.actionOpenPhraseBook); |
2205 | |
2206 | editt->addAction(m_ui.actionUndo); |
2207 | editt->addAction(m_ui.actionRedo); |
2208 | editt->addSeparator(); |
2209 | editt->addAction(m_ui.actionCut); |
2210 | editt->addAction(m_ui.actionCopy); |
2211 | editt->addAction(m_ui.actionPaste); |
2212 | editt->addSeparator(); |
2213 | editt->addAction(m_ui.actionFind); |
2214 | |
2215 | translationst->addAction(m_ui.actionPrev); |
2216 | translationst->addAction(m_ui.actionNext); |
2217 | translationst->addAction(m_ui.actionPrevUnfinished); |
2218 | translationst->addAction(m_ui.actionNextUnfinished); |
2219 | translationst->addAction(m_ui.actionDone); |
2220 | translationst->addAction(m_ui.actionDoneAndNext); |
2221 | |
2222 | validationt->addAction(m_ui.actionAccelerators); |
2223 | validationt->addAction(m_ui.actionSurroundingWhitespace); |
2224 | validationt->addAction(m_ui.actionEndingPunctuation); |
2225 | validationt->addAction(m_ui.actionPhraseMatches); |
2226 | validationt->addAction(m_ui.actionPlaceMarkerMatches); |
2227 | |
2228 | helpt->addAction(m_ui.actionWhatsThis); |
2229 | } |
2230 | |
2231 | QModelIndex MainWindow::setMessageViewRoot(const QModelIndex &index) |
2232 | { |
2233 | const QModelIndex &sortedContextIndex = m_sortedMessagesModel->mapFromSource(sourceIndex: index); |
2234 | const QModelIndex &trueContextIndex = m_sortedMessagesModel->index(row: sortedContextIndex.row(), column: 0); |
2235 | if (m_messageView->rootIndex() != trueContextIndex) |
2236 | m_messageView->setRootIndex(trueContextIndex); |
2237 | return trueContextIndex; |
2238 | } |
2239 | |
2240 | /* |
2241 | * Updates the selected entries in the context and message views. |
2242 | */ |
2243 | void MainWindow::setCurrentMessage(const QModelIndex &index) |
2244 | { |
2245 | const QModelIndex &contextIndex = m_messageModel->parent(index); |
2246 | if (!contextIndex.isValid()) |
2247 | return; |
2248 | |
2249 | const QModelIndex &trueIndex = m_messageModel->index(row: contextIndex.row(), column: index.column(), parent: QModelIndex()); |
2250 | m_settingCurrentMessage = true; |
2251 | m_contextView->setCurrentIndex(m_sortedContextsModel->mapFromSource(sourceIndex: trueIndex)); |
2252 | m_settingCurrentMessage = false; |
2253 | |
2254 | setMessageViewRoot(contextIndex); |
2255 | m_messageView->setCurrentIndex(m_sortedMessagesModel->mapFromSource(sourceIndex: index)); |
2256 | } |
2257 | |
2258 | void MainWindow::setCurrentMessage(const QModelIndex &index, int model) |
2259 | { |
2260 | const QModelIndex &theIndex = m_messageModel->index(row: index.row(), column: model + 1, parent: index.parent()); |
2261 | setCurrentMessage(theIndex); |
2262 | m_messageEditor->setEditorFocusForModel(model); |
2263 | } |
2264 | |
2265 | void MainWindow::setCurrentMessageFromGuess(int modelIndex, const Candidate &cand) |
2266 | { |
2267 | int contextIndex = m_dataModel->findContextIndex(context: cand.context); |
2268 | int messageIndex = m_dataModel->multiContextItem(ctxIdx: contextIndex)->findMessage(sourcetext: cand.source, |
2269 | comment: cand.disambiguation); |
2270 | setCurrentMessage(m_messageModel->modelIndex(index: MultiDataIndex(modelIndex, contextIndex, |
2271 | messageIndex))); |
2272 | } |
2273 | |
2274 | QModelIndex MainWindow::currentContextIndex() const |
2275 | { |
2276 | return m_sortedContextsModel->mapToSource(proxyIndex: m_contextView->currentIndex()); |
2277 | } |
2278 | |
2279 | QModelIndex MainWindow::currentMessageIndex() const |
2280 | { |
2281 | return m_sortedMessagesModel->mapToSource(proxyIndex: m_messageView->currentIndex()); |
2282 | } |
2283 | |
2284 | PhraseBook *MainWindow::doOpenPhraseBook(const QString& name) |
2285 | { |
2286 | PhraseBook *pb = new PhraseBook(); |
2287 | bool langGuessed; |
2288 | if (!pb->load(fileName: name, langGuessed: &langGuessed)) { |
2289 | QMessageBox::warning(this, tr(s: "Qt Linguist" ), |
2290 | tr(s: "Cannot read from phrase book '%1'." ).arg(a: name)); |
2291 | delete pb; |
2292 | return 0; |
2293 | } |
2294 | if (langGuessed) { |
2295 | if (!m_translationSettingsDialog) |
2296 | m_translationSettingsDialog = new TranslationSettingsDialog(this); |
2297 | m_translationSettingsDialog->setPhraseBook(pb); |
2298 | m_translationSettingsDialog->exec(); |
2299 | } |
2300 | |
2301 | m_phraseBooks.append(t: pb); |
2302 | |
2303 | QAction *a = m_ui.menuClosePhraseBook->addAction(pb->friendlyPhraseBookName()); |
2304 | m_phraseBookMenu[PhraseCloseMenu].insert(key: a, value: pb); |
2305 | a->setWhatsThis(tr(s: "Close this phrase book." )); |
2306 | |
2307 | a = m_ui.menuEditPhraseBook->addAction(pb->friendlyPhraseBookName()); |
2308 | m_phraseBookMenu[PhraseEditMenu].insert(key: a, value: pb); |
2309 | a->setWhatsThis(tr(s: "Enables you to add, modify, or delete" |
2310 | " entries in this phrase book." )); |
2311 | |
2312 | a = m_ui.menuPrintPhraseBook->addAction(pb->friendlyPhraseBookName()); |
2313 | m_phraseBookMenu[PhrasePrintMenu].insert(key: a, value: pb); |
2314 | a->setWhatsThis(tr(s: "Print the entries in this phrase book." )); |
2315 | |
2316 | connect(pb, &PhraseBook::listChanged, this, &MainWindow::updatePhraseDicts); |
2317 | updatePhraseDicts(); |
2318 | updatePhraseBookActions(); |
2319 | |
2320 | return pb; |
2321 | } |
2322 | |
2323 | bool MainWindow::savePhraseBook(QString *name, PhraseBook &pb) |
2324 | { |
2325 | if (!name->contains(c: QLatin1Char('.'))) |
2326 | *name += QLatin1String(".qph" ); |
2327 | |
2328 | if (!pb.save(fileName: *name)) { |
2329 | QMessageBox::warning(this, tr(s: "Qt Linguist" ), |
2330 | tr(s: "Cannot create phrase book '%1'." ).arg(a: *name)); |
2331 | return false; |
2332 | } |
2333 | return true; |
2334 | } |
2335 | |
2336 | bool MainWindow::maybeSavePhraseBook(PhraseBook *pb) |
2337 | { |
2338 | if (pb->isModified()) |
2339 | switch (QMessageBox::information(this, tr(s: "Qt Linguist" ), |
2340 | tr(s: "Do you want to save phrase book '%1'?" ).arg(a: pb->friendlyPhraseBookName()), |
2341 | QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes)) |
2342 | { |
2343 | case QMessageBox::Cancel: |
2344 | return false; |
2345 | case QMessageBox::Yes: |
2346 | if (!pb->save(fileName: pb->fileName())) |
2347 | return false; |
2348 | break; |
2349 | default: |
2350 | break; |
2351 | } |
2352 | return true; |
2353 | } |
2354 | |
2355 | bool MainWindow::maybeSavePhraseBooks() |
2356 | { |
2357 | for (PhraseBook *phraseBook : std::as_const(t&: m_phraseBooks)) |
2358 | if (!maybeSavePhraseBook(pb: phraseBook)) |
2359 | return false; |
2360 | return true; |
2361 | } |
2362 | |
2363 | void MainWindow::updateProgress() |
2364 | { |
2365 | int numEditable = m_dataModel->getNumEditable(); |
2366 | int numFinished = m_dataModel->getNumFinished(); |
2367 | if (!m_dataModel->modelCount()) { |
2368 | m_progressLabel->setText(QString(QLatin1String(" " ))); |
2369 | m_progressLabel->setToolTip(QString()); |
2370 | } else { |
2371 | m_progressLabel->setText(QStringLiteral(" %1/%2 " ).arg(a: numFinished).arg(a: numEditable)); |
2372 | m_progressLabel->setToolTip(tr(s: "%n unfinished message(s) left." , c: 0, |
2373 | n: numEditable - numFinished)); |
2374 | } |
2375 | bool enable = numFinished != numEditable; |
2376 | m_ui.actionPrevUnfinished->setEnabled(enable); |
2377 | m_ui.actionNextUnfinished->setEnabled(enable); |
2378 | m_ui.actionDone->setEnabled(enable); |
2379 | m_ui.actionDoneAndNext->setEnabled(enable); |
2380 | |
2381 | m_ui.actionPrev->setEnabled(m_dataModel->contextCount() > 0); |
2382 | m_ui.actionNext->setEnabled(m_dataModel->contextCount() > 0); |
2383 | } |
2384 | |
2385 | void MainWindow::updatePhraseBookActions() |
2386 | { |
2387 | bool phraseBookLoaded = (m_currentIndex.model() >= 0) && !m_phraseBooks.isEmpty(); |
2388 | m_ui.actionBatchTranslation->setEnabled(m_dataModel->contextCount() > 0 && phraseBookLoaded |
2389 | && m_dataModel->isModelWritable(m_currentIndex.model())); |
2390 | m_ui.actionAddToPhraseBook->setEnabled(currentMessageIndex().isValid() && phraseBookLoaded); |
2391 | } |
2392 | |
2393 | void MainWindow::updatePhraseDictInternal(int model) |
2394 | { |
2395 | QHash<QString, QList<Phrase *> > &pd = m_phraseDict[model]; |
2396 | |
2397 | pd.clear(); |
2398 | for (PhraseBook *pb : std::as_const(t&: m_phraseBooks)) { |
2399 | bool before; |
2400 | if (pb->language() != QLocale::C && m_dataModel->language(model) != QLocale::C) { |
2401 | if (pb->language() != m_dataModel->language(model)) |
2402 | continue; |
2403 | before = (pb->territory() == m_dataModel->model(i: model)->territory()); |
2404 | } else { |
2405 | before = false; |
2406 | } |
2407 | const auto phrases = pb->phrases(); |
2408 | for (Phrase *p : phrases) { |
2409 | QString f = friendlyString(str: p->source()); |
2410 | if (f.size() > 0) { |
2411 | f = f.split(sep: QLatin1Char(' ')).first(); |
2412 | if (!pd.contains(key: f)) { |
2413 | pd.insert(key: f, value: QList<Phrase *>()); |
2414 | } |
2415 | if (before) |
2416 | pd[f].prepend(t: p); |
2417 | else |
2418 | pd[f].append(t: p); |
2419 | } |
2420 | } |
2421 | } |
2422 | } |
2423 | |
2424 | void MainWindow::updatePhraseDict(int model) |
2425 | { |
2426 | updatePhraseDictInternal(model); |
2427 | m_phraseView->update(); |
2428 | } |
2429 | |
2430 | void MainWindow::updatePhraseDicts() |
2431 | { |
2432 | for (int i = 0; i < m_phraseDict.size(); ++i) |
2433 | if (!m_dataModel->isModelWritable(model: i)) |
2434 | m_phraseDict[i].clear(); |
2435 | else |
2436 | updatePhraseDictInternal(model: i); |
2437 | revalidate(); |
2438 | m_phraseView->update(); |
2439 | } |
2440 | |
2441 | static bool haveMnemonic(const QString &str) |
2442 | { |
2443 | for (const ushort *p = (ushort *)str.constData();; ) { // Assume null-termination |
2444 | ushort c = *p++; |
2445 | if (!c) |
2446 | break; |
2447 | if (c == '&') { |
2448 | c = *p++; |
2449 | if (!c) |
2450 | return false; |
2451 | // "Nobody" ever really uses these alt-space, and they are highly annoying |
2452 | // because we get a lot of false positives. |
2453 | if (c != '&' && c != ' ' && QChar(c).isPrint()) { |
2454 | const ushort *pp = p; |
2455 | for (; *p < 256 && isalpha(*p); p++) ; |
2456 | if (pp == p || *p != ';') |
2457 | return true; |
2458 | // This looks like a HTML &entity;, so ignore it. As a HTML string |
2459 | // won't contain accels anyway, we can stop scanning here. |
2460 | break; |
2461 | } |
2462 | } |
2463 | } |
2464 | return false; |
2465 | } |
2466 | |
2467 | void MainWindow::updateDanger(const MultiDataIndex &index, bool verbose) |
2468 | { |
2469 | MultiDataIndex curIdx = index; |
2470 | m_errorsView->clear(); |
2471 | |
2472 | QString source; |
2473 | for (int mi = 0; mi < m_dataModel->modelCount(); ++mi) { |
2474 | if (!m_dataModel->isModelWritable(model: mi)) |
2475 | continue; |
2476 | curIdx.setModel(mi); |
2477 | MessageItem *m = m_dataModel->messageItem(index: curIdx); |
2478 | if (!m || m->isObsolete()) |
2479 | continue; |
2480 | |
2481 | bool danger = false; |
2482 | if (m->message().isTranslated()) { |
2483 | if (source.isEmpty()) { |
2484 | source = m->pluralText(); |
2485 | if (source.isEmpty()) |
2486 | source = m->text(); |
2487 | } |
2488 | QStringList translations = m->translations(); |
2489 | |
2490 | // Truncated variants are permitted to be "denormalized" |
2491 | for (int i = 0; i < translations.size(); ++i) { |
2492 | int sep = translations.at(i).indexOf(c: QChar(Translator::BinaryVariantSeparator)); |
2493 | if (sep >= 0) |
2494 | translations[i].truncate(pos: sep); |
2495 | } |
2496 | |
2497 | if (m_ui.actionAccelerators->isChecked()) { |
2498 | bool sk = haveMnemonic(str: source); |
2499 | bool tk = true; |
2500 | for (int i = 0; i < translations.size() && tk; ++i) { |
2501 | tk &= haveMnemonic(str: translations[i]); |
2502 | } |
2503 | |
2504 | if (!sk && tk) { |
2505 | if (verbose) |
2506 | m_errorsView->addError(model: mi, type: ErrorsView::SuperfluousAccelerator); |
2507 | danger = true; |
2508 | } else if (sk && !tk) { |
2509 | if (verbose) |
2510 | m_errorsView->addError(model: mi, type: ErrorsView::MissingAccelerator); |
2511 | danger = true; |
2512 | } |
2513 | } |
2514 | if (m_ui.actionSurroundingWhitespace->isChecked()) { |
2515 | bool whitespaceok = true; |
2516 | for (int i = 0; i < translations.size() && whitespaceok; ++i) { |
2517 | whitespaceok &= (leadingWhitespace(str: source) == leadingWhitespace(str: translations[i])); |
2518 | whitespaceok &= (trailingWhitespace(str: source) == trailingWhitespace(str: translations[i])); |
2519 | } |
2520 | |
2521 | if (!whitespaceok) { |
2522 | if (verbose) |
2523 | m_errorsView->addError(model: mi, type: ErrorsView::SurroundingWhitespaceDiffers); |
2524 | danger = true; |
2525 | } |
2526 | } |
2527 | if (m_ui.actionEndingPunctuation->isChecked()) { |
2528 | bool endingok = true; |
2529 | for (int i = 0; i < translations.size() && endingok; ++i) { |
2530 | endingok &= (ending(str: source, lang: m_dataModel->sourceLanguage(model: mi)) == |
2531 | ending(str: translations[i], lang: m_dataModel->language(model: mi))); |
2532 | } |
2533 | |
2534 | if (!endingok) { |
2535 | if (verbose) |
2536 | m_errorsView->addError(model: mi, type: ErrorsView::PunctuationDiffers); |
2537 | danger = true; |
2538 | } |
2539 | } |
2540 | if (m_ui.actionPhraseMatches->isChecked()) { |
2541 | QString fsource = friendlyString(str: source); |
2542 | QString ftranslation = friendlyString(str: translations.first()); |
2543 | QStringList lookupWords = fsource.split(sep: QLatin1Char(' ')); |
2544 | |
2545 | bool phraseFound; |
2546 | for (const QString &s : std::as_const(t&: lookupWords)) { |
2547 | if (m_phraseDict[mi].contains(key: s)) { |
2548 | phraseFound = true; |
2549 | const auto phrases = m_phraseDict[mi].value(key: s); |
2550 | for (const Phrase *p : phrases) { |
2551 | if (fsource == friendlyString(str: p->source())) { |
2552 | if (ftranslation.indexOf(s: friendlyString(str: p->target())) >= 0) { |
2553 | phraseFound = true; |
2554 | break; |
2555 | } else { |
2556 | phraseFound = false; |
2557 | } |
2558 | } |
2559 | } |
2560 | if (!phraseFound) { |
2561 | if (verbose) |
2562 | m_errorsView->addError(model: mi, type: ErrorsView::IgnoredPhrasebook, arg: s); |
2563 | danger = true; |
2564 | } |
2565 | } |
2566 | } |
2567 | } |
2568 | |
2569 | if (m_ui.actionPlaceMarkerMatches->isChecked()) { |
2570 | // Stores the occurrence count of the place markers in the map placeMarkerIndexes. |
2571 | // i.e. the occurrence count of %1 is stored at placeMarkerIndexes[1], |
2572 | // count of %2 is stored at placeMarkerIndexes[2] etc. |
2573 | // In the first pass, it counts all place markers in the sourcetext. |
2574 | // In the second pass it (de)counts all place markers in the translation. |
2575 | // When finished, all elements should have returned to a count of 0, |
2576 | // if not there is a mismatch |
2577 | // between place markers in the source text and the translation text. |
2578 | QHash<int, int> placeMarkerIndexes; |
2579 | QString translation; |
2580 | int numTranslations = translations.size(); |
2581 | for (int pass = 0; pass < numTranslations + 1; ++pass) { |
2582 | const QChar *uc_begin = source.unicode(); |
2583 | const QChar *uc_end = uc_begin + source.size(); |
2584 | if (pass >= 1) { |
2585 | translation = translations[pass - 1]; |
2586 | uc_begin = translation.unicode(); |
2587 | uc_end = uc_begin + translation.size(); |
2588 | } |
2589 | const QChar *c = uc_begin; |
2590 | while (c < uc_end) { |
2591 | if (c->unicode() == '%') { |
2592 | const QChar *escape_start = ++c; |
2593 | while (c->isDigit()) |
2594 | ++c; |
2595 | const QChar *escape_end = c; |
2596 | bool ok = true; |
2597 | int markerIndex = QString::fromRawData( |
2598 | escape_start, size: escape_end - escape_start).toInt(ok: &ok); |
2599 | if (ok) |
2600 | placeMarkerIndexes[markerIndex] += (pass == 0 ? numTranslations : -1); |
2601 | } else { |
2602 | ++c; |
2603 | } |
2604 | } |
2605 | } |
2606 | |
2607 | for (int i : std::as_const(t&: placeMarkerIndexes)) { |
2608 | if (i != 0) { |
2609 | if (verbose) |
2610 | m_errorsView->addError(model: mi, type: ErrorsView::PlaceMarkersDiffer); |
2611 | danger = true; |
2612 | break; |
2613 | } |
2614 | } |
2615 | |
2616 | // Piggy-backed on the general place markers, we check the plural count marker. |
2617 | if (m->message().isPlural()) { |
2618 | for (int i = 0; i < numTranslations; ++i) |
2619 | if (m_dataModel->model(i: mi)->countRefNeeds().at(i) |
2620 | && !(translations[i].contains(s: QLatin1String("%n" )) |
2621 | || translations[i].contains(s: QLatin1String("%Ln" )))) { |
2622 | if (verbose) |
2623 | m_errorsView->addError(model: mi, type: ErrorsView::NumerusMarkerMissing); |
2624 | danger = true; |
2625 | break; |
2626 | } |
2627 | } |
2628 | } |
2629 | } |
2630 | |
2631 | if (danger != m->danger()) |
2632 | m_dataModel->setDanger(index: curIdx, danger); |
2633 | } |
2634 | |
2635 | if (verbose) |
2636 | statusBar()->showMessage(m_errorsView->firstError()); |
2637 | } |
2638 | |
2639 | void MainWindow::readConfig() |
2640 | { |
2641 | QSettings config; |
2642 | |
2643 | restoreGeometry(config.value(key: settingPath("Geometry/WindowGeometry" )).toByteArray()); |
2644 | restoreState(config.value(key: settingPath("MainWindowState" )).toByteArray()); |
2645 | |
2646 | m_ui.actionAccelerators->setChecked( |
2647 | config.value(settingPath("Validators/Accelerator" ), true).toBool()); |
2648 | m_ui.actionSurroundingWhitespace->setChecked( |
2649 | config.value(settingPath("Validators/SurroundingWhitespace" ), true).toBool()); |
2650 | m_ui.actionEndingPunctuation->setChecked( |
2651 | config.value(settingPath("Validators/EndingPunctuation" ), true).toBool()); |
2652 | m_ui.actionPhraseMatches->setChecked( |
2653 | config.value(settingPath("Validators/PhraseMatch" ), true).toBool()); |
2654 | m_ui.actionPlaceMarkerMatches->setChecked( |
2655 | config.value(settingPath("Validators/PlaceMarkers" ), true).toBool()); |
2656 | m_ui.actionLengthVariants->setChecked( |
2657 | config.value(settingPath("Options/LengthVariants" ), false).toBool()); |
2658 | m_ui.actionVisualizeWhitespace->setChecked( |
2659 | config.value(settingPath("Options/VisualizeWhitespace" ), true).toBool()); |
2660 | |
2661 | m_messageEditor->setFontSize( |
2662 | config.value(settingPath("Options/EditorFontsize" ), font().pointSize()).toReal()); |
2663 | m_phraseView->setMaxCandidates(config.value(key: settingPath("Options/NumberOfGuesses" ), |
2664 | defaultValue: PhraseView::getDefaultMaxCandidates()).toInt()); |
2665 | |
2666 | recentFiles().readConfig(); |
2667 | |
2668 | int size = config.beginReadArray(prefix: settingPath("OpenedPhraseBooks" )); |
2669 | for (int i = 0; i < size; ++i) { |
2670 | config.setArrayIndex(i); |
2671 | doOpenPhraseBook(name: config.value(key: QLatin1String("FileName" )).toString()); |
2672 | } |
2673 | config.endArray(); |
2674 | } |
2675 | |
2676 | void MainWindow::writeConfig() |
2677 | { |
2678 | QSettings config; |
2679 | config.setValue(key: settingPath("Geometry/WindowGeometry" ), |
2680 | value: saveGeometry()); |
2681 | config.setValue(settingPath("Validators/Accelerator" ), |
2682 | m_ui.actionAccelerators->isChecked()); |
2683 | config.setValue(settingPath("Validators/SurroundingWhitespace" ), |
2684 | m_ui.actionSurroundingWhitespace->isChecked()); |
2685 | config.setValue(settingPath("Validators/EndingPunctuation" ), |
2686 | m_ui.actionEndingPunctuation->isChecked()); |
2687 | config.setValue(settingPath("Validators/PhraseMatch" ), |
2688 | m_ui.actionPhraseMatches->isChecked()); |
2689 | config.setValue(settingPath("Validators/PlaceMarkers" ), |
2690 | m_ui.actionPlaceMarkerMatches->isChecked()); |
2691 | config.setValue(settingPath("Options/LengthVariants" ), |
2692 | m_ui.actionLengthVariants->isChecked()); |
2693 | config.setValue(settingPath("Options/VisualizeWhitespace" ), |
2694 | m_ui.actionVisualizeWhitespace->isChecked()); |
2695 | config.setValue(key: settingPath("MainWindowState" ), |
2696 | value: saveState()); |
2697 | recentFiles().writeConfig(); |
2698 | |
2699 | config.setValue(key: settingPath("Options/EditorFontsize" ), value: m_messageEditor->fontSize()); |
2700 | config.setValue(key: settingPath("Options/NumberOfGuesses" ), value: m_phraseView->getMaxCandidates()); |
2701 | |
2702 | config.beginWriteArray(prefix: settingPath("OpenedPhraseBooks" ), |
2703 | size: m_phraseBooks.size()); |
2704 | for (int i = 0; i < m_phraseBooks.size(); ++i) { |
2705 | config.setArrayIndex(i); |
2706 | config.setValue(key: QLatin1String("FileName" ), value: m_phraseBooks.at(i)->fileName()); |
2707 | } |
2708 | config.endArray(); |
2709 | } |
2710 | |
2711 | void MainWindow::setupRecentFilesMenu() |
2712 | { |
2713 | m_ui.menuRecentlyOpenedFiles->clear(); |
2714 | for (const QStringList &strList : recentFiles().filesLists()) |
2715 | if (strList.size() == 1) { |
2716 | const QString &str = strList.first(); |
2717 | m_ui.menuRecentlyOpenedFiles->addAction( |
2718 | DataModel::prettifyFileName(str))->setData(str); |
2719 | } else { |
2720 | QMenu * = m_ui.menuRecentlyOpenedFiles->addMenu( |
2721 | MultiDataModel::condenseFileNames( |
2722 | MultiDataModel::prettifyFileNames(strList))); |
2723 | menu->addAction(text: tr(s: "All" ))->setData(strList); |
2724 | for (const QString &str : strList) |
2725 | menu->addAction(text: DataModel::prettifyFileName(fn: str))->setData(str); |
2726 | } |
2727 | } |
2728 | |
2729 | void MainWindow::recentFileActivated(QAction *action) |
2730 | { |
2731 | openFiles(names: action->data().toStringList()); |
2732 | } |
2733 | |
2734 | void MainWindow::toggleStatistics() |
2735 | { |
2736 | if (m_ui.actionStatistics->isChecked()) { |
2737 | if (!m_statistics) { |
2738 | m_statistics = new Statistics(this); |
2739 | connect(sender: m_dataModel, signal: &MultiDataModel::statsChanged, |
2740 | context: m_statistics, slot: &Statistics::updateStats); |
2741 | } |
2742 | m_statistics->show(); |
2743 | updateStatistics(); |
2744 | } |
2745 | else if (m_statistics) { |
2746 | m_statistics->close(); |
2747 | } |
2748 | } |
2749 | |
2750 | void MainWindow::toggleVisualizeWhitespace() |
2751 | { |
2752 | m_messageEditor->setVisualizeWhitespace(m_ui.actionVisualizeWhitespace->isChecked()); |
2753 | } |
2754 | |
2755 | void MainWindow::maybeUpdateStatistics(const MultiDataIndex &index) |
2756 | { |
2757 | if (index.model() == m_currentIndex.model()) |
2758 | updateStatistics(); |
2759 | } |
2760 | |
2761 | void MainWindow::updateStatistics() |
2762 | { |
2763 | // don't call this if stats dialog is not open |
2764 | // because this can be slow... |
2765 | if (!m_statistics || !m_statistics->isVisible() || m_currentIndex.model() < 0) |
2766 | return; |
2767 | |
2768 | m_dataModel->model(i: m_currentIndex.model())->updateStatistics(); |
2769 | } |
2770 | |
2771 | void MainWindow::doShowTranslationSettings(int model) |
2772 | { |
2773 | if (!m_translationSettingsDialog) |
2774 | m_translationSettingsDialog = new TranslationSettingsDialog(this); |
2775 | m_translationSettingsDialog->setDataModel(m_dataModel->model(i: model)); |
2776 | m_translationSettingsDialog->exec(); |
2777 | } |
2778 | |
2779 | void MainWindow::showTranslationSettings() |
2780 | { |
2781 | doShowTranslationSettings(model: m_currentIndex.model()); |
2782 | } |
2783 | |
2784 | bool MainWindow::eventFilter(QObject *object, QEvent *event) |
2785 | { |
2786 | if (event->type() == QEvent::DragEnter) { |
2787 | QDragEnterEvent *e = static_cast<QDragEnterEvent*>(event); |
2788 | if (e->mimeData()->hasFormat(mimetype: QLatin1String("text/uri-list" ))) { |
2789 | e->acceptProposedAction(); |
2790 | return true; |
2791 | } |
2792 | } else if (event->type() == QEvent::Drop) { |
2793 | QDropEvent *e = static_cast<QDropEvent*>(event); |
2794 | if (!e->mimeData()->hasFormat(mimetype: QLatin1String("text/uri-list" ))) |
2795 | return false; |
2796 | QStringList urls; |
2797 | for (const QUrl &url : e->mimeData()->urls()) |
2798 | if (!url.toLocalFile().isEmpty()) |
2799 | urls << url.toLocalFile(); |
2800 | if (!urls.isEmpty()) |
2801 | openFiles(names: urls); |
2802 | e->acceptProposedAction(); |
2803 | return true; |
2804 | } else if (event->type() == QEvent::KeyPress) { |
2805 | QKeyEvent *ke = static_cast<QKeyEvent *>(event); |
2806 | if (ke->key() == Qt::Key_Escape) { |
2807 | if (object == m_messageEditor) |
2808 | m_messageView->setFocus(); |
2809 | else if (object == m_messagesDock) |
2810 | m_contextView->setFocus(); |
2811 | } else if ((ke->key() == Qt::Key_Plus || ke->key() == Qt::Key_Equal) |
2812 | && (ke->modifiers() & Qt::ControlModifier)) { |
2813 | m_messageEditor->increaseFontSize(); |
2814 | } else if (ke->key() == Qt::Key_Minus |
2815 | && (ke->modifiers() & Qt::ControlModifier)) { |
2816 | m_messageEditor->decreaseFontSize(); |
2817 | } |
2818 | } else if (event->type() == QEvent::Wheel) { |
2819 | QWheelEvent *we = static_cast<QWheelEvent *>(event); |
2820 | if (we->modifiers() & Qt::ControlModifier) { |
2821 | if (we->angleDelta().y() > 0) |
2822 | m_messageEditor->increaseFontSize(); |
2823 | else |
2824 | m_messageEditor->decreaseFontSize(); |
2825 | } |
2826 | } |
2827 | return false; |
2828 | } |
2829 | |
2830 | QT_END_NAMESPACE |
2831 | |