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