]> git.lyx.org Git - lyx.git/blobdiff - src/frontends/qt/GuiView.cpp
Attempt to fix bug #13017.
[lyx.git] / src / frontends / qt / GuiView.cpp
index 24969c21129e36316880fe600efe077909f26944..5a5c1dac8d8026f33d73120eed6d3405125e1a96 100644 (file)
@@ -69,6 +69,8 @@
 #include "Toolbars.h"
 #include "version.h"
 
+#include "graphics/PreviewLoader.h"
+
 #include "support/convert.h"
 #include "support/debug.h"
 #include "support/ExceptionMessage.h"
@@ -155,11 +157,10 @@ public:
                        return;
                /// The text to be written on top of the pixmap
                QString const htext = qt_("The Document\nProcessor[[welcome banner]]");
-               QString const htextsize = qt_("1.0[[possibly scale the welcome banner text size]]");
+               QString const htextsize = qt_("1.0[[translating this to different value scales the welcome banner text size for your language]]");
                /// The text to be written on top of the pixmap
                QString const text = lyx_version ?
                        qt_("version ") + lyx_version : qt_("unknown version");
-#if QT_VERSION >= 0x050000
                QString imagedir = "images/";
                FileName fname = imageLibFileSearch(imagedir, "banner", "svgz");
                QSvgRenderer svgRenderer(toqstr(fname.absFileName()));
@@ -171,9 +172,6 @@ public:
                } else {
                        splash_ = getPixmap("images/", "banner", "png");
                }
-#else
-               splash_ = getPixmap("images/", "banner", "svgz,png");
-#endif
 
                QPainter pain(&splash_);
                pain.setPen(QColor(0, 0, 0));
@@ -262,11 +260,7 @@ private:
 
        /// Current ratio between physical pixels and device-independent pixels
        double pixelRatio() const {
-#if QT_VERSION >= 0x050000
                return qt_scale_factor * devicePixelRatio();
-#else
-               return 1.0;
-#endif
        }
 
        qreal fontSize() const {
@@ -286,11 +280,7 @@ private:
 
        /// Ratio between physical pixels and device-independent pixels of splash image
        double splashPixelRatio() const {
-#if QT_VERSION >= 0x050000
                return splash_.devicePixelRatio();
-#else
-               return 1.0;
-#endif
        }
 };
 
@@ -523,6 +513,7 @@ public:
 
        ///
        QTimer statusbar_timer_;
+       QTimer statusbar_stats_timer_;
        /// auto-saving of buffers
        Timeout autosave_timeout_;
 
@@ -536,6 +527,7 @@ public:
        string last_export_format;
        string processing_format;
 
+       // Buffers that are being exported
        static QSet<Buffer const *> busyBuffers;
 
        unsigned int smallIconSize;
@@ -546,6 +538,20 @@ public:
 
        /// flag against a race condition due to multiclicks, see bug #1119
        bool in_show_;
+
+       // Timers for statistic updates in buffer
+       /// Current time left to the nearest info update
+       int time_to_update = 1000;
+       ///Basic step for timer in ms. Basically reaction time for short selections
+       int const timer_rate = 500;
+       /// Real stats updates infrequently. First they take long time for big buffers, second
+       /// they are visible for fast-repeat keyboards even for mid documents.
+       int const default_stats_rate = 5000;
+       /// Detection of new selection, so we can react fast
+       bool already_in_selection_ = false;
+       /// Maximum size of "short" selection for which we can update with faster timer_rate
+       int const max_sel_chars = 5000;
+
 };
 
 QSet<Buffer const *> GuiView::GuiViewPrivate::busyBuffers;
@@ -553,8 +559,9 @@ QSet<Buffer const *> GuiView::GuiViewPrivate::busyBuffers;
 
 GuiView::GuiView(int id)
        : d(*new GuiViewPrivate(this)), id_(id), closing_(false), busy_(0),
-         command_execute_(false), minibuffer_focus_(false), toolbarsMovable_(true),
-         devel_mode_(false)
+         command_execute_(false), minibuffer_focus_(false), word_count_enabled_(true),
+         char_count_enabled_(true),  char_nb_count_enabled_(false),
+         toolbarsMovable_(true), devel_mode_(false)
 {
        connect(this, SIGNAL(bufferViewChanged()),
                this, SLOT(onBufferViewChanged()));
@@ -582,20 +589,18 @@ GuiView::GuiView(int id)
        }
        connect(&d.statusbar_timer_, SIGNAL(timeout()),
                this, SLOT(clearMessage()));
+       connect(&d.statusbar_stats_timer_, SIGNAL(timeout()),
+               this, SLOT(showStats()));
+       d.statusbar_stats_timer_.start(d.timer_rate);
 
        // We don't want to keep the window in memory if it is closed.
        setAttribute(Qt::WA_DeleteOnClose, true);
 
 #if !(defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)) && !defined(Q_OS_MAC)
-       // QIcon::fromTheme was introduced in Qt 4.6
-#if (QT_VERSION >= 0x040600)
        // assign an icon to main form. We do not do it under Qt/Win or Qt/Mac,
        // since the icon is provided in the application bundle. We use a themed
        // version when available and use the bundled one as fallback.
        setWindowIcon(QIcon::fromTheme("lyx", getPixmap("images/", "lyx", "svg,png")));
-#else
-       setWindowIcon(getPixmap("images/", "lyx", "svg,png"));
-#endif
 
 #endif
        resetWindowTitle();
@@ -612,6 +617,10 @@ GuiView::GuiView(int id)
        // For Drag&Drop.
        setAcceptDrops(true);
 
+       QFontMetrics const fm(statusBar()->fontMetrics());
+       int const iconheight = max(int(d.normalIconSize), fm.height());
+       QSize const iconsize(iconheight, iconheight);
+
        // add busy indicator to statusbar
        search_mode mode = theGuiApp()->imageSearchMode();
        QString fn = toqstr(lyx::libFileSearch("images", "busy", "svgz", mode).absFileName());
@@ -620,18 +629,36 @@ GuiView::GuiView(int id)
        // make busy indicator square with 5px margins
        busySVG->setMaximumSize(busySVG->height() - 5, busySVG->height() - 5);
        busySVG->hide();
+       // Add cancel button
+       QPixmap ps = QIcon(getPixmap("images/", "process-stop", "svgz")).pixmap(iconsize);
+       GuiClickableLabel * processStop = new GuiClickableLabel(statusBar());
+       processStop->setPixmap(ps);
+       processStop->setToolTip(qt_("Click here to stop export/output process"));
+       processStop->hide();
+       statusBar()->addPermanentWidget(processStop);
 
        connect(&d.processing_thread_watcher_, SIGNAL(started()),
                busySVG, SLOT(show()));
        connect(&d.processing_thread_watcher_, SIGNAL(finished()),
                busySVG, SLOT(hide()));
-       connect(busySVG, SIGNAL(pressed()), this, SLOT(checkCancelBackground()));
+       connect(&d.processing_thread_watcher_, SIGNAL(started()),
+               processStop, SLOT(show()));
+       connect(&d.processing_thread_watcher_, SIGNAL(finished()),
+               processStop, SLOT(hide()));
+       connect(processStop, SIGNAL(clicked()), this, SLOT(checkCancelBackground()));
 
-       QFontMetrics const fm(statusBar()->fontMetrics());
+       connect(this, SIGNAL(scriptKilled()), busySVG, SLOT(hide()));
+       connect(this, SIGNAL(scriptKilled()), processStop, SLOT(hide()));
+
+       stat_counts_ = new GuiClickableLabel(statusBar());
+       stat_counts_->setAlignment(Qt::AlignCenter);
+       stat_counts_->setStyleSheet("padding-left: 5px; padding-right: 5px;");
+       stat_counts_->hide();
+       statusBar()->addPermanentWidget(stat_counts_);
+
+       connect(stat_counts_, SIGNAL(clicked()), this, SLOT(statsPressed()));
 
        zoom_slider_ = new QSlider(Qt::Horizontal, statusBar());
-       // Small size slider for macOS to prevent the status bar from enlarging
-       zoom_slider_->setAttribute(Qt::WA_MacSmallSize);
 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
        zoom_slider_->setFixedWidth(fm.horizontalAdvance('x') * 15);
 #else
@@ -663,12 +690,22 @@ GuiView::GuiView(int id)
        zoom_out_->setFixedSize(s);
        zoom_out_->setAlignment(Qt::AlignCenter);
 
-       statusBar()->addPermanentWidget(zoom_out_);
-       zoom_out_->setEnabled(currentBufferView());
-       statusBar()->addPermanentWidget(zoom_slider_);
+
+       zoom_widget_ = new QWidget(statusBar());
+       zoom_widget_->setAttribute(Qt::WA_MacSmallSize);
+       zoom_widget_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
+       zoom_widget_->setLayout(new QHBoxLayout());
+       zoom_widget_->layout()->setSpacing(5);
+       zoom_widget_->layout()->setContentsMargins(0,0,0,0);
+       zoom_widget_->layout()->addWidget(zoom_out_);
+       zoom_widget_->layout()->addWidget(zoom_slider_);
+       zoom_widget_->layout()->addWidget(zoom_in_);
+       statusBar()->addPermanentWidget(zoom_widget_);
+       zoom_out_->setEnabled(currentBufferView()
+                             && zoom_slider_->value() > zoom_slider_->minimum());
        zoom_slider_->setEnabled(currentBufferView());
-       zoom_in_->setEnabled(currentBufferView());
-       statusBar()->addPermanentWidget(zoom_in_);
+       zoom_in_->setEnabled(currentBufferView()
+                            && zoom_slider_->value() < zoom_slider_->maximum());
 
        connect(zoom_slider_, SIGNAL(sliderMoved(int)), this, SLOT(zoomSliderMoved(int)));
        connect(zoom_slider_, SIGNAL(valueChanged(int)), this, SLOT(zoomValueChanged(int)));
@@ -678,7 +715,8 @@ GuiView::GuiView(int id)
 
        // QPalette palette = statusBar()->palette();
 
-       zoom_value_ = new QLabel(statusBar());
+       zoom_value_ = new GuiClickableLabel(statusBar());
+       connect(zoom_value_, SIGNAL(pressed()), this, SLOT(showZoomContextMenu()));
        // zoom_value_->setPalette(palette);
        zoom_value_->setForegroundRole(statusBar()->foregroundRole());
        zoom_value_->setFixedHeight(fm.height());
@@ -694,14 +732,11 @@ GuiView::GuiView(int id)
 
        statusBar()->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(statusBar(), SIGNAL(customContextMenuRequested(QPoint)),
-               this, SLOT(showZoomContextMenu()));
+               this, SLOT(showStatusBarContextMenu()));
 
        // enable pinch to zoom
        grabGesture(Qt::PinchGesture);
 
-       int const iconheight = max(int(d.normalIconSize), fm.height());
-       QSize const iconsize(iconheight, iconheight);
-
        QPixmap shellescape = QIcon(getPixmap("images/", "emblem-shellescape", "svgz,png")).pixmap(iconsize);
        shell_escape_ = new QLabel(statusBar());
        shell_escape_->setPixmap(shellescape);
@@ -743,11 +778,6 @@ GuiView::GuiView(int id)
        connect(this, SIGNAL(triggerShowDialog(QString const &, QString const &, Inset *)),
                SLOT(doShowDialog(QString const &, QString const &, Inset *)));
 
-       // set custom application bars context menu, e.g. tool bar and menu bar
-       setContextMenuPolicy(Qt::CustomContextMenu);
-       connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
-               SLOT(toolBarPopup(const QPoint &)));
-
        // Forbid too small unresizable window because it can happen
        // with some window manager under X11.
        setMinimumSize(300, 200);
@@ -791,10 +821,16 @@ void GuiView::checkCancelBackground()
        int const ret =
                Alert::prompt(ttl, msg, 1, 1,
                        _("&Cancel export"), _("Co&ntinue"));
-       if (ret == 0)
-               Systemcall::killscript();
+       if (ret == 0) {
+               cancelExport();
+       }
 }
 
+void GuiView::statsPressed()
+{
+       DispatchResult dr;
+       dispatch(FuncRequest(LFUN_STATISTICS), dr);
+}
 
 void GuiView::zoomSliderMoved(int value)
 {
@@ -802,6 +838,10 @@ void GuiView::zoomSliderMoved(int value)
        dispatch(FuncRequest(LFUN_BUFFER_ZOOM, convert<string>(value)), dr);
        scheduleRedrawWorkAreas();
        zoom_value_->setText(toqstr(bformat(_("[[ZOOM]]%1$d%"), value)));
+       zoom_in_->setEnabled(currentBufferView()
+                            && value < zoom_slider_->maximum());
+       zoom_out_->setEnabled(currentBufferView()
+                             && value > zoom_slider_->minimum());
 }
 
 
@@ -829,6 +869,15 @@ void GuiView::zoomOutPressed()
 
 
 void GuiView::showZoomContextMenu()
+{
+       QMenu * menu = guiApp->menus().menu(toqstr("context-zoom"), * this);
+       if (!menu)
+               return;
+       menu->exec(QCursor::pos());
+}
+
+
+void GuiView::showStatusBarContextMenu()
 {
        QMenu * menu = guiApp->menus().menu(toqstr("context-statusbar"), * this);
        if (!menu)
@@ -943,7 +992,7 @@ void GuiView::saveLayout() const
        settings.setValue("devel_mode", devel_mode_);
        settings.beginGroup("views");
        settings.beginGroup(QString::number(id_));
-       if (guiApp->platformName() == "qt4x11" || guiApp->platformName() == "xcb") {
+       if (guiApp->platformName() == "xcb") {
                settings.setValue("pos", pos());
                settings.setValue("size", size());
        } else
@@ -951,7 +1000,10 @@ void GuiView::saveLayout() const
        settings.setValue("layout", saveState(0));
        settings.setValue("icon_size", toqstr(d.iconSize(iconSize())));
        settings.setValue("zoom_value_visible", zoom_value_->isVisible());
-       settings.setValue("zoom_slider_visible", zoom_slider_->isVisible());
+       settings.setValue("zoom_slider_visible", zoom_widget_->isVisible());
+       settings.setValue("word_count_enabled", word_count_enabled_);
+       settings.setValue("char_count_enabled", char_count_enabled_);
+       settings.setValue("char_nb_count_enabled", char_nb_count_enabled_);
 }
 
 
@@ -970,9 +1022,11 @@ void GuiView::saveUISettings() const
 
 void GuiView::setCurrentZoom(const int v)
 {
+       Q_EMIT currentZoomChanged(v);
        lyxrc.currentZoom = v;
        zoom_value_->setText(toqstr(bformat(_("[[ZOOM]]%1$d%"), v)));
-       Q_EMIT currentZoomChanged(v);
+       zoom_in_->setEnabled(currentBufferView() && v < zoom_slider_->maximum());
+       zoom_out_->setEnabled(currentBufferView() && v > zoom_slider_->minimum());
 }
 
 
@@ -997,11 +1051,14 @@ bool GuiView::restoreLayout()
        zoom_value_->setVisible(settings.value("zoom_value_visible", true).toBool());
 
        bool const show_zoom_slider = settings.value("zoom_slider_visible", true).toBool();
-       zoom_slider_->setVisible(show_zoom_slider);
-       zoom_in_->setVisible(show_zoom_slider);
-       zoom_out_->setVisible(show_zoom_slider);
+       zoom_widget_->setVisible(show_zoom_slider);
+
+       word_count_enabled_ = settings.value("word_count_enabled", true).toBool();
+       char_count_enabled_ = settings.value("char_count_enabled", true).toBool();
+       char_nb_count_enabled_ = settings.value("char_nb_count_enabled", true).toBool();
+       stat_counts_->setVisible(word_count_enabled_ || char_count_enabled_ || char_nb_count_enabled_);
 
-       if (guiApp->platformName() == "qt4x11" || guiApp->platformName() == "xcb") {
+       if (guiApp->platformName() == "xcb") {
                QPoint pos = settings.value("pos", QPoint(50, 50)).toPoint();
                QSize size = settings.value("size", QSize(690, 510)).toSize();
                resize(size);
@@ -1066,11 +1123,9 @@ void GuiView::updateLockToolbars()
                if (tb && tb->isMovable())
                        toolbarsMovable_ = true;
        }
-#if QT_VERSION >= 0x050200
        // set unified mac toolbars only when not movable as recommended:
        // https://doc.qt.io/qt-5/qmainwindow.html#unifiedTitleAndToolBarOnMac-prop
        setUnifiedTitleAndToolBarOnMac(!toolbarsMovable_);
-#endif
 }
 
 
@@ -1236,6 +1291,9 @@ void GuiView::closeEvent(QCloseEvent * close_event)
 {
        LYXERR(Debug::DEBUG, "GuiView::closeEvent()");
 
+       // FIXME Bug #12828 bites here. If there is some other View open, then
+       // we really should only refuse to close if one of the Buffers open here
+       // is being processed.
        if (!GuiViewPrivate::busyBuffers.isEmpty()) {
                Alert::warning(_("Exit LyX"),
                        _("LyX could not be closed because documents are being processed by LyX."));
@@ -1254,6 +1312,7 @@ void GuiView::closeEvent(QCloseEvent * close_event)
        // it can happen that this event arrives without selecting the view,
        // e.g. when clicking the close button on a background window.
        setFocus();
+       Q_EMIT closing(id_);
        if (!closeWorkAreaAll()) {
                closing_ = false;
                close_event->ignore();
@@ -1271,6 +1330,7 @@ void GuiView::closeEvent(QCloseEvent * close_event)
 
        // Make sure the timer time out will not trigger a statusbar update.
        d.statusbar_timer_.stop();
+       d.statusbar_stats_timer_.stop();
 
        // Saving fullscreen requires additional tweaks in the toolbar code.
        // It wouldn't also work under linux natively.
@@ -1376,6 +1436,73 @@ void GuiView::clearMessage()
        d.statusbar_timer_.stop();
 }
 
+void GuiView::showStats()
+{
+       if (!statsEnabled())
+               return;
+
+       d.time_to_update -= d.timer_rate;
+
+       BufferView * bv = currentBufferView();
+       Buffer * buf = bv ? &bv->buffer() : nullptr;
+       if (!buf) {
+               stat_counts_->hide();
+               return;
+       }
+
+       Cursor const & cur = bv->cursor();
+
+       // we start new selection and need faster update
+       if (!d.already_in_selection_ && cur.selection())
+               d.time_to_update = 0;
+
+       if (d.time_to_update > 0)
+               return;
+
+       DocIterator from, to;
+       if (cur.selection()) {
+               from = cur.selectionBegin();
+               to = cur.selectionEnd();
+               d.already_in_selection_ = true;
+       } else {
+               from = doc_iterator_begin(buf);
+               to = doc_iterator_end(buf);
+               d.already_in_selection_ = false;
+       }
+
+       buf->updateStatistics(from, to);
+
+       QStringList stats;
+       if (word_count_enabled_) {
+               int const words = buf->wordCount();
+               if (words == 1)
+                       stats << toqstr(bformat(_("%1$d Word"), words));
+               else
+                       stats << toqstr(bformat(_("%1$d Words"), words));
+       }
+       int const chars_with_blanks = buf->charCount(true);
+       if (char_count_enabled_) {
+               if (chars_with_blanks == 1)
+                       stats << toqstr(bformat(_("%1$d Character"), chars_with_blanks));
+               else
+                       stats << toqstr(bformat(_("%1$d Characters"), chars_with_blanks));
+       }
+       if (char_nb_count_enabled_) {
+               int const chars = buf->charCount(false);
+               if (chars == 1)
+                       stats << toqstr(bformat(_("%1$d Character (no Blanks)"), chars));
+               else
+                       stats << toqstr(bformat(_("%1$d Characters (no Blanks)"), chars));
+       }
+       stat_counts_->setText(stats.join(qt_(", [[stats separator]]")));
+       stat_counts_->show();
+
+       d.time_to_update = d.default_stats_rate;
+       // fast updates for small selections
+       if (chars_with_blanks < d.max_sel_chars && cur.selection())
+               d.time_to_update = d.timer_rate;
+}
+
 
 void GuiView::updateWindowTitle(GuiWorkArea * wa)
 {
@@ -1391,9 +1518,7 @@ void GuiView::updateWindowTitle(GuiWorkArea * wa)
                // buffer-save has changed too.
                updateToolbars();
        }
-#ifndef Q_WS_MAC
        title += from_ascii(" - LyX");
-#endif
        setWindowTitle(toqstr(title));
        // Sets the path for the window: this is used by OSX to
        // allow a context click on the title bar showing a menu
@@ -1450,8 +1575,10 @@ void GuiView::onBufferViewChanged()
        updateDialogs();
        zoom_slider_->setEnabled(currentBufferView());
        zoom_value_->setEnabled(currentBufferView());
-       zoom_in_->setEnabled(currentBufferView());
-       zoom_out_->setEnabled(currentBufferView());
+       zoom_in_->setEnabled(currentBufferView()
+                            && zoom_slider_->value() < zoom_slider_->maximum());
+       zoom_out_->setEnabled(currentBufferView()
+                             && zoom_slider_->value() > zoom_slider_->minimum());
 }
 
 
@@ -1515,6 +1642,12 @@ void GuiView::showMessage()
 }
 
 
+bool GuiView::statsEnabled() const
+{
+       return word_count_enabled_ || char_count_enabled_ || char_nb_count_enabled_;
+}
+
+
 bool GuiView::event(QEvent * e)
 {
        switch (e->type())
@@ -1619,12 +1752,6 @@ bool GuiView::event(QEvent * e)
                return QMainWindow::event(e);
        }
 
-       case QEvent::ApplicationPaletteChange: {
-               // runtime switch from/to dark mode
-               refillToolbars();
-               return QMainWindow::event(e);
-       }
-
        case QEvent::Gesture: {
                QGestureEvent *ge = static_cast<QGestureEvent*>(e);
                QGesture *gp = ge->gesture(Qt::PinchGesture);
@@ -1646,6 +1773,25 @@ bool GuiView::event(QEvent * e)
                return QMainWindow::event(e);
        }
 
+       // dark/light mode runtime switch support, OS-dependent.
+       // 1. Mac OS X
+       // Limit to Q_OS_MAC as this unnecessarily would also
+       // trigger on Linux with grave performance issues
+#ifdef Q_OS_MAC
+       case QEvent::ApplicationPaletteChange: {
+               // We need to update metrics here to avoid a crash (#12786)
+               theBufferList().changed(true);
+               refillToolbars();
+               return QMainWindow::event(e);
+       }
+#endif
+       // 2. Linux
+       case QEvent::StyleChange: {
+               // We need to update metrics here to avoid a crash (#12786)
+               theBufferList().changed(true);
+               return QMainWindow::event(e);
+       }
+
        default:
                return QMainWindow::event(e);
        }
@@ -1695,11 +1841,7 @@ void GuiView::resetCommandExecute()
 
 double GuiView::pixelRatio() const
 {
-#if QT_VERSION >= 0x050000
        return qt_scale_factor * devicePixelRatio();
-#else
-       return 1.0;
-#endif
 }
 
 
@@ -1740,7 +1882,8 @@ TabWorkArea * GuiView::addTabWorkArea()
        QObject::connect(twa, SIGNAL(lastWorkAreaRemoved()),
                         this, SLOT(on_lastWorkAreaRemoved()));
 
-       d.splitter_->addWidget(twa);
+       d.splitter_->insertWidget(d.splitter_->indexOf(d.currentTabWorkArea()) + 1,
+                                 twa);
        d.stack_widget_->setCurrentWidget(d.splitter_);
        return twa;
 }
@@ -2324,6 +2467,14 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                break;
        }
 
+       case LFUN_CHANGES_TRACK: {
+               if (!doc_buffer) {
+                       enable = false;
+                       break;
+               }
+               return doc_buffer->getStatus(cmd, flag);
+       }
+
        case LFUN_VIEW_SPLIT:
                if (cmd.getArg(0) == "vertical")
                        enable = doc_buffer && (d.splitter_->count() == 1 ||
@@ -2333,6 +2484,11 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                                         d.splitter_->orientation() == Qt::Horizontal);
                break;
 
+       case LFUN_TAB_GROUP_NEXT:
+       case LFUN_TAB_GROUP_PREVIOUS:
+               enable = (d.splitter_->count() > 1);
+               break;
+
        case LFUN_TAB_GROUP_CLOSE:
                enable = d.tabWorkAreaCount() > 1;
                break;
@@ -2418,7 +2574,13 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                if (cmd.argument() == "zoomlevel") {
                        flag.setOnOff(zoom_value_ ? zoom_value_->isVisible() : false);
                } else if (cmd.argument() == "zoomslider") {
-                       flag.setOnOff(zoom_slider_ ? zoom_slider_->isVisible() : false);
+                       flag.setOnOff(zoom_widget_ ? zoom_widget_->isVisible() : false);
+               } else if (cmd.argument() == "statistics-w") {
+                       flag.setOnOff(word_count_enabled_);
+               } else if (cmd.argument() == "statistics-cb") {
+                       flag.setOnOff(char_count_enabled_);
+               } else if (cmd.argument() == "statistics-c") {
+                       flag.setOnOff(char_nb_count_enabled_);
                } else
                        flag.setOnOff(isFullScreen());
                break;
@@ -2594,7 +2756,8 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
        case LFUN_WINDOW_RAISE:
                break;
        case LFUN_FORWARD_SEARCH:
-               enable = !(lyxrc.forward_search_dvi.empty() && lyxrc.forward_search_pdf.empty());
+               enable = !(lyxrc.forward_search_dvi.empty() && lyxrc.forward_search_pdf.empty()) &&
+                       doc_buffer && doc_buffer->isSyncTeXenabled();
                break;
 
        case LFUN_FILE_INSERT_PLAINTEXT:
@@ -2668,7 +2831,7 @@ Buffer * GuiView::loadDocument(FileName const & filename, bool tolastfiles)
 }
 
 
-void GuiView::openDocument(string const & fname)
+void GuiView::openDocuments(string const & fname, int origin)
 {
        string initpath = lyxrc.document_path;
 
@@ -2679,73 +2842,91 @@ void GuiView::openDocument(string const & fname)
                        initpath = trypath;
        }
 
-       string filename;
+       QStringList files;
 
        if (fname.empty()) {
-               FileDialog dlg(qt_("Select document to open"));
+               FileDialog dlg(qt_("Select documents to open"));
                dlg.setButton1(qt_("D&ocuments"), toqstr(lyxrc.document_path));
                dlg.setButton2(qt_("&Examples"), toqstr(lyxrc.example_path));
 
                QStringList const filter({
                                qt_("LyX Documents (*.lyx)"),
                                qt_("LyX Document Backups (*.lyx~)"),
-                               qt_("All Files (*.*)")
+                               qt_("All Files") + " " + wildcardAllFiles()
                });
-               FileDialog::Result result =
-                       dlg.open(toqstr(initpath), filter);
+               FileDialog::Results results =
+                       dlg.openMulti(toqstr(initpath), filter);
 
-               if (result.first == FileDialog::Later)
+               if (results.first == FileDialog::Later)
                        return;
 
-               filename = fromqstr(result.second);
+               files = results.second;
 
                // check selected filename
-               if (filename.empty()) {
+               if (files.isEmpty()) {
                        message(_("Canceled."));
                        return;
                }
        } else
-               filename = fname;
-
-       // get absolute path of file and add ".lyx" to the filename if
-       // necessary.
-       FileName const fullname =
-                       fileSearch(string(), filename, "lyx", support::may_not_exist);
-       if (!fullname.empty())
-               filename = fullname.absFileName();
-
-       if (!fullname.onlyPath().isDirectory()) {
-               Alert::warning(_("Invalid filename"),
-                               bformat(_("The directory in the given path\n%1$s\ndoes not exist."),
-                               from_utf8(fullname.absFileName())));
-               return;
-       }
+               files << toqstr(fname);
+
+       // iterate over all selected files
+       for (auto const & file : files) {
+               string filename = fromqstr(file);
+
+               // get absolute path of file and add ".lyx" to the filename if
+               // necessary.
+               FileName const fullname =
+                               fileSearch(string(), filename, "lyx", support::may_not_exist);
+               if (!fullname.empty())
+                       filename = fullname.absFileName();
+
+               if (!fullname.onlyPath().isDirectory()) {
+                       Alert::warning(_("Invalid filename"),
+                                       bformat(_("The directory in the given path\n%1$s\ndoes not exist."),
+                                       from_utf8(fullname.absFileName())));
+                       continue;
+               }
 
-       // if the file doesn't exist and isn't already open (bug 6645),
-       // let the user create one
-       if (!fullname.exists() && !theBufferList().exists(fullname) &&
-           !LyXVC::file_not_found_hook(fullname)) {
-               // the user specifically chose this name. Believe him.
-               Buffer * const b = newFile(filename, string(), true);
-               if (b)
-                       setBuffer(b);
-               return;
-       }
+               // if the file doesn't exist and isn't already open (bug 6645),
+               // let the user create one
+               if (!fullname.exists() && !theBufferList().exists(fullname) &&
+                       !LyXVC::file_not_found_hook(fullname)) {
+                       // see bug #12609
+                       if (origin == FuncRequest::MENU) {
+                               docstring const & msg =
+                                       bformat(_("File\n"
+                                               "%1$s\n"
+                                               "does not exist. Create empty file?"),
+                                                       from_utf8(filename));
+                               int ret = Alert::prompt(_("File does not exist"),
+                                                       msg, 0, 1,
+                                                       _("Create &File"),
+                                                       _("&Cancel"));
+                               if (ret == 1)
+                                       continue;
+                       }
+                       Buffer * const b = newFile(filename, string(), true);
+                       if (b)
+                               setBuffer(b);
+                       continue;
+               }
 
-       docstring const disp_fn = makeDisplayPath(filename);
-       message(bformat(_("Opening document %1$s..."), disp_fn));
+               docstring const disp_fn = makeDisplayPath(filename);
+               message(bformat(_("Opening document %1$s..."), disp_fn));
 
-       docstring str2;
-       Buffer * buf = loadDocument(fullname);
-       if (buf) {
-               str2 = bformat(_("Document %1$s opened."), disp_fn);
-               if (buf->lyxvc().inUse())
-                       str2 += " " + from_utf8(buf->lyxvc().versionString()) +
-                               " " + _("Version control detected.");
-       } else {
-               str2 = bformat(_("Could not open document %1$s"), disp_fn);
+               docstring str2;
+               Buffer * buf = loadDocument(fullname);
+               if (buf) {
+                       str2 = bformat(_("Document %1$s opened."), disp_fn);
+                       if (buf->lyxvc().inUse())
+                               str2 += " " + from_utf8(buf->lyxvc().versionString()) +
+                                       " " + _("Version control detected.");
+               } else {
+                       str2 = bformat(_("Could not open document %1$s"), disp_fn);
+               }
+               message(str2);
        }
-       message(str2);
 }
 
 // FIXME: clean that
@@ -3080,8 +3261,7 @@ bool GuiView::renameBuffer(Buffer & b, docstring const & newname, RenameKind kin
                dlg.setButton1(qt_("D&ocuments"), toqstr(lyxrc.document_path));
                dlg.setButton2(qt_("&Templates"), toqstr(lyxrc.template_path));
 
-               if (!isLyXFileName(fname.absFileName()))
-                       fname.changeExtension(".lyx");
+               fname.ensureExtension(".lyx");
 
                string const path = as_template ?
                                        getTemplatesPath(b)
@@ -3099,8 +3279,7 @@ bool GuiView::renameBuffer(Buffer & b, docstring const & newname, RenameKind kin
                if (fname.empty())
                        return false;
 
-               if (!isLyXFileName(fname.absFileName()))
-                       fname.changeExtension(".lyx");
+               fname.ensureExtension(".lyx");
        }
 
        // fname is now the new Buffer location.
@@ -3248,6 +3427,8 @@ bool GuiView::exportBufferAs(Buffer & b, docstring const & iformat)
        if (fmt_name.empty() || fname.empty())
                return false;
 
+       fname.ensureExtension(theFormats().extension(fmt_name));
+
        // fname is now the new Buffer location.
        if (fname.exists()) {
                docstring const file = makeDisplayPath(fname.absFileName(), 30);
@@ -3271,6 +3452,12 @@ bool GuiView::exportBufferAs(Buffer & b, docstring const & iformat)
 }
 
 
+bool GuiView::isBufferBusy(Buffer const * b)
+{
+       return GuiViewPrivate::busyBuffers.contains(b);
+}
+
+
 bool GuiView::saveBuffer(Buffer & b)
 {
        return saveBuffer(b, FileName());
@@ -3669,7 +3856,7 @@ void GuiView::gotoNextOrPreviousBuffer(NextOrPrevious np, bool const move)
                for (int i = 0; i < nwa; ++i) {
                        if (&workArea(i)->bufferView().buffer() == curbuf) {
                                int next_index;
-                               if (np == NEXTBUFFER)
+                               if (np == NEXT)
                                        next_index = (i == nwa - 1 ? 0 : i + 1);
                                else
                                        next_index = (i == 0 ? nwa - 1 : i - 1);
@@ -3684,6 +3871,23 @@ void GuiView::gotoNextOrPreviousBuffer(NextOrPrevious np, bool const move)
 }
 
 
+void GuiView::gotoNextTabWorkArea(NextOrPrevious np)
+{
+       int count = d.splitter_->count();
+       for (int i = 0; i < count; ++i) {
+               if (d.tabWorkArea(i) == d.currentTabWorkArea()) {
+                       int new_index;
+                       if (np == NEXT)
+                               new_index = (i == count - 1 ? 0 : i + 1);
+                       else
+                               new_index = (i == 0 ? count - 1 : i - 1);
+                       setCurrentWorkArea(d.tabWorkArea(new_index)->currentWorkArea());
+                       break;
+               }
+       }
+}
+
+
 /// make sure the document is saved
 static bool ensureBufferClean(Buffer * buffer)
 {
@@ -4020,13 +4224,6 @@ bool GuiView::goToFileRow(string const & argument)
 }
 
 
-void GuiView::toolBarPopup(const QPoint & /*pos*/)
-{
-       QMenu * menu = guiApp->menus().menu(toqstr("context-toolbars"), * this);
-       menu->exec(QCursor::pos());
-}
-
-
 template<class T>
 Buffer::ExportStatus GuiView::GuiViewPrivate::runAndDestroy(const T& func,
                Buffer const * orig, Buffer * clone, string const & format)
@@ -4037,6 +4234,8 @@ Buffer::ExportStatus GuiView::GuiViewPrivate::runAndDestroy(const T& func,
        // documents, starting from the master. so we must delete those.
        Buffer * mbuf = const_cast<Buffer *>(clone->masterBuffer());
        delete mbuf;
+       if (orig->needToRemoveBiblioTemps())
+               orig->removeBiblioTempFiles();
        busyBuffers.remove(orig);
        return status;
 }
@@ -4298,7 +4497,7 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                        break;
                }
                case LFUN_EXPORT_CANCEL: {
-                       Systemcall::killscript();
+                       cancelExport();
                        break;
                }
                case LFUN_BUFFER_SWITCH: {
@@ -4346,19 +4545,19 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                }
 
                case LFUN_BUFFER_NEXT:
-                       gotoNextOrPreviousBuffer(NEXTBUFFER, false);
+                       gotoNextOrPreviousBuffer(NEXT, false);
                        break;
 
                case LFUN_BUFFER_MOVE_NEXT:
-                       gotoNextOrPreviousBuffer(NEXTBUFFER, true);
+                       gotoNextOrPreviousBuffer(NEXT, true);
                        break;
 
                case LFUN_BUFFER_PREVIOUS:
-                       gotoNextOrPreviousBuffer(PREVBUFFER, false);
+                       gotoNextOrPreviousBuffer(PREV, false);
                        break;
 
                case LFUN_BUFFER_MOVE_PREVIOUS:
-                       gotoNextOrPreviousBuffer(PREVBUFFER, true);
+                       gotoNextOrPreviousBuffer(PREV, true);
                        break;
 
                case LFUN_BUFFER_CHKTEX:
@@ -4366,6 +4565,15 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                        doc_buffer->runChktex();
                        break;
 
+               case LFUN_CHANGES_TRACK: {
+                       // the actual dispatch is done in Buffer
+                       dispatchToBufferView(cmd, dr);
+                       // but we inform the GUI (document settings) if this is toggled
+                       LASSERT(doc_buffer, break);
+                       Q_EMIT changeTrackingToggled(doc_buffer->params().track_changes);
+                       break;
+               }
+
                case LFUN_COMMAND_EXECUTE: {
                        command_execute_ = true;
                        minibuffer_focus_ = true;
@@ -4402,7 +4610,7 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                                FileDialog dlg(qt_("Select file to insert"));
 
                                FileDialog::Result result = dlg.open(toqstr(bv->buffer().filePath()),
-                                       QStringList(qt_("All Files (*)")));
+                                       QStringList(qt_("All Files")+ " " + wildcardAllFiles()));
 
                                if (result.first == FileDialog::Later || result.second.isEmpty()) {
                                        dr.setMessage(_("Canceled."));
@@ -4681,9 +4889,21 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                                ? Qt::Vertical : Qt::Horizontal);
                        TabWorkArea * twa = addTabWorkArea();
                        GuiWorkArea * wa = twa->addWorkArea(*doc_buffer, *this);
+
+                       wa->bufferView().copySettingsFrom(*bv);
+                       dr.screenUpdate(Update::ForceAll);
                        setCurrentWorkArea(wa);
                        break;
                }
+
+               case LFUN_TAB_GROUP_NEXT:
+                       gotoNextTabWorkArea(NEXT);
+                       break;
+
+               case LFUN_TAB_GROUP_PREVIOUS:
+                       gotoNextTabWorkArea(PREV);
+                       break;
+
                case LFUN_TAB_GROUP_CLOSE:
                        if (TabWorkArea * twa = d.currentTabWorkArea()) {
                                closeTabWorkArea(twa);
@@ -4759,6 +4979,10 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                                              lyxrc.currentZoom, lyxrc.defaultZoom));
 
                        guiApp->fontLoader().update();
+                       // Regenerate instant previews
+                       if (lyxrc.preview != LyXRC::PREVIEW_OFF
+                           && doc_buffer && doc_buffer->loader())
+                               doc_buffer->loader()->refreshPreviews();
                        dr.screenUpdate(Update::ForceAll | Update::FitCursor);
                        break;
                }
@@ -4817,10 +5041,12 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                                dr.setMessage(_("Please, preview the document first."));
                                break;
                        }
+                       bool const goto_dvi = have_dvi && !lyxrc.forward_search_dvi.empty();
+                       bool const goto_pdf = have_pdf && !lyxrc.forward_search_pdf.empty();
                        string outname = dviname.onlyFileName();
                        string command = lyxrc.forward_search_dvi;
-                       if (!have_dvi || (have_pdf &&
-                           pdfname.lastModified() > dviname.lastModified())) {
+                       if ((!goto_dvi || goto_pdf) &&
+                           pdfname.lastModified() > dviname.lastModified()) {
                                outname = pdfname.onlyFileName();
                                command = lyxrc.forward_search_pdf;
                        }
@@ -4852,12 +5078,8 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                        break;
 
                case LFUN_CITATION_OPEN: {
-                       string pdfv, psv;
-                       if (theFormats().getFormat("pdf"))
-                               pdfv = theFormats().getFormat("pdf")->viewer();
-                       if (theFormats().getFormat("ps"))
-                               psv = theFormats().getFormat("ps")->viewer();
-                       frontend::showTarget(argument, pdfv, psv);
+                       LASSERT(doc_buffer, break);
+                       frontend::showTarget(argument, *doc_buffer);
                        break;
                }
 
@@ -4901,9 +5123,19 @@ bool GuiView::lfunUiToggle(string const & ui_component)
        } else if (ui_component == "zoomlevel") {
                zoom_value_->setVisible(!zoom_value_->isVisible());
        } else if (ui_component == "zoomslider") {
-               zoom_slider_->setVisible(!zoom_slider_->isVisible());
-               zoom_in_->setVisible(zoom_slider_->isVisible());
-               zoom_out_->setVisible(zoom_slider_->isVisible());
+               zoom_widget_->setVisible(!zoom_widget_->isVisible());
+       } else if (ui_component == "statistics-w") {
+               word_count_enabled_ = !word_count_enabled_;
+               if (statsEnabled())
+                       showStats();
+       } else if (ui_component == "statistics-cb") {
+               char_count_enabled_ = !char_count_enabled_;
+               if (statsEnabled())
+                       showStats();
+       } else if (ui_component == "statistics-c") {
+               char_nb_count_enabled_ = !char_nb_count_enabled_;
+               if (statsEnabled())
+                       showStats();
        } else if (ui_component == "frame") {
                int const l = contentsMargins().left();
 
@@ -4925,10 +5157,20 @@ bool GuiView::lfunUiToggle(string const & ui_component)
                toggleFullScreen();
        } else
                return false;
+       stat_counts_->setVisible(statsEnabled());
        return true;
 }
 
 
+void GuiView::cancelExport()
+{
+       Systemcall::killscript();
+       // stop busy signal immediately so that in the subsequent
+       // "Export canceled" prompt the status bar icons are accurate.
+       Q_EMIT scriptKilled();
+}
+
+
 void GuiView::toggleFullScreen()
 {
        setWindowState(windowState() ^ Qt::WindowFullScreen);
@@ -5069,16 +5311,17 @@ Dialog * GuiView::findOrBuild(string const & name, bool hide_it)
                return dialog;
 
        dialog = build(name);
-       d.dialogs_[name].reset(dialog);
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
-       // Force a uniform style for group boxes
-       // On Mac non-flat works better, on Linux flat is standard
-       flatGroupBoxes(dialog->asQWidget(), guiApp->platformName() != "cocoa");
-#endif
-       if (lyxrc.allow_geometry_session)
-               dialog->restoreSession();
-       if (hide_it)
-               dialog->hideView();
+       if (dialog) {
+
+               d.dialogs_[name].reset(dialog);
+               // Force a uniform style for group boxes
+               // On Mac non-flat works better, on Linux flat is standard
+               flatGroupBoxes(dialog->asQWidget(), guiApp->platformName() != "cocoa");
+               if (lyxrc.allow_geometry_session)
+                       dialog->restoreSession();
+               if (hide_it)
+                       dialog->hideView();
+               }
        return dialog;
 }