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