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