]> git.lyx.org Git - features.git/blobdiff - src/frontends/qt/GuiView.cpp
After export-cancel, hide busy and cancel buttons
[features.git] / src / frontends / qt / GuiView.cpp
index 9f7531c1b3680c337c680f10464c00d25518e592..8269ff09cceec5164a8882c2045b17d6992678c4 100644 (file)
@@ -18,6 +18,7 @@
 #include "DialogFactory.h"
 #include "DispatchResult.h"
 #include "FileDialog.h"
+#include "FindAndReplace.h"
 #include "FontLoader.h"
 #include "GuiApplication.h"
 #include "GuiClickableLabel.h"
@@ -68,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"
 #include <QTimer>
 #include <QUrl>
 #include <QWindowStateChangeEvent>
+#include <QGestureEvent>
+#include <QPinchGesture>
 
 
 // sync with GuiAlert.cpp
@@ -207,7 +212,6 @@ public:
                QStringList titlesegs = htext.split('\n');
                int wline = 0;
                int hline = fm.maxHeight();
-               QStringList::const_iterator sit;
                for (QString const & seg : titlesegs) {
                        if (fm.width(seg) > wline)
                                wline = fm.width(seg);
@@ -521,6 +525,7 @@ public:
 
        ///
        QTimer statusbar_timer_;
+       QTimer statusbar_stats_timer_;
        /// auto-saving of buffers
        Timeout autosave_timeout_;
 
@@ -544,6 +549,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;
@@ -551,8 +570,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()));
@@ -580,6 +600,9 @@ 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);
@@ -610,6 +633,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());
@@ -618,14 +645,34 @@ 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(pressed()), 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_->setFrameStyle(QFrame::StyledPanel);
+       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
@@ -642,20 +689,24 @@ GuiView::GuiView(int id)
        zoom_ratio_ = settings.value("zoom_ratio", 1.0).toDouble();
        // Actual zoom value: default zoom + fractional offset
        int zoom = (int)(lyxrc.defaultZoom * zoom_ratio_);
-       if (zoom < static_cast<int>(zoom_min_))
-               zoom = zoom_min_;
+       zoom = min(max(zoom, zoom_min_), zoom_max_);
        zoom_slider_->setValue(zoom);
        zoom_slider_->setToolTip(qt_("Workarea zoom level. Drag, use Ctrl-+/- or Shift-Mousewheel to adjust."));
 
        // Buttons to change zoom stepwise
-       zoom_in_ = new QPushButton(statusBar());
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
+       QSize s(fm.horizontalAdvance('+'), fm.height());
+#else
+       QSize s(fm.width('+'), fm.height());
+#endif
+       zoom_in_ = new GuiClickableLabel(statusBar());
        zoom_in_->setText("+");
-       zoom_in_->setFlat(true);
-       zoom_in_->setFixedSize(QSize(fm.height(), fm.height()));
-       zoom_out_ = new QPushButton(statusBar());
+       zoom_in_->setFixedSize(s);
+       zoom_in_->setAlignment(Qt::AlignCenter);
+       zoom_out_ = new GuiClickableLabel(statusBar());
        zoom_out_->setText(QString(QChar(0x2212)));
-       zoom_out_->setFixedSize(QSize(fm.height(), fm.height()));
-       zoom_out_->setFlat(true);
+       zoom_out_->setFixedSize(s);
+       zoom_out_->setAlignment(Qt::AlignCenter);
 
        statusBar()->addPermanentWidget(zoom_out_);
        zoom_out_->setEnabled(currentBufferView());
@@ -670,23 +721,29 @@ GuiView::GuiView(int id)
        connect(zoom_in_, SIGNAL(clicked()), this, SLOT(zoomInPressed()));
        connect(zoom_out_, SIGNAL(clicked()), this, SLOT(zoomOutPressed()));
 
-       zoom_value_ = new QLabel(statusBar());
+       // QPalette palette = statusBar()->palette();
+
+       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());
 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
-       zoom_value_->setMinimumWidth(fm.horizontalAdvance("000%"));
+       zoom_value_->setMinimumWidth(fm.horizontalAdvance("444\%"));
 #else
-       zoom_value_->setMinimumWidth(fm.width("000%"));
+       zoom_value_->setMinimumWidth(fm.width("444\%"));
 #endif
+       zoom_value_->setAlignment(Qt::AlignCenter);
        zoom_value_->setText(toqstr(bformat(_("[[ZOOM]]%1$d%"), zoom)));
        statusBar()->addPermanentWidget(zoom_value_);
        zoom_value_->setEnabled(currentBufferView());
-       zoom_value_->setContextMenuPolicy(Qt::CustomContextMenu);
 
-       connect(zoom_value_, SIGNAL(customContextMenuRequested(QPoint)),
-               this, SLOT(showZoomContextMenu()));
+       statusBar()->setContextMenuPolicy(Qt::CustomContextMenu);
+       connect(statusBar(), SIGNAL(customContextMenuRequested(QPoint)),
+               this, SLOT(showStatusBarContextMenu()));
 
-       int const iconheight = max(int(d.normalIconSize), fm.height());
-       QSize const iconsize(iconheight, iconheight);
+       // enable pinch to zoom
+       grabGesture(Qt::PinchGesture);
 
        QPixmap shellescape = QIcon(getPixmap("images/", "emblem-shellescape", "svgz,png")).pixmap(iconsize);
        shell_escape_ = new QLabel(statusBar());
@@ -777,16 +834,25 @@ void GuiView::checkCancelBackground()
        int const ret =
                Alert::prompt(ttl, msg, 1, 1,
                        _("&Cancel export"), _("Co&ntinue"));
-       if (ret == 0)
+       if (ret == 0) {
                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::statsPressed()
+{
+       DispatchResult dr;
+       dispatch(FuncRequest(LFUN_STATISTICS), dr);
+}
 
 void GuiView::zoomSliderMoved(int value)
 {
        DispatchResult dr;
        dispatch(FuncRequest(LFUN_BUFFER_ZOOM, convert<string>(value)), dr);
-       currentWorkArea()->scheduleRedraw(true);
+       scheduleRedrawWorkAreas();
        zoom_value_->setText(toqstr(bformat(_("[[ZOOM]]%1$d%"), value)));
 }
 
@@ -802,7 +868,7 @@ void GuiView::zoomInPressed()
 {
        DispatchResult dr;
        dispatch(FuncRequest(LFUN_BUFFER_ZOOM_IN), dr);
-       currentWorkArea()->scheduleRedraw(true);
+       scheduleRedrawWorkAreas();
 }
 
 
@@ -810,7 +876,7 @@ void GuiView::zoomOutPressed()
 {
        DispatchResult dr;
        dispatch(FuncRequest(LFUN_BUFFER_ZOOM_OUT), dr);
-       currentWorkArea()->scheduleRedraw(true);
+       scheduleRedrawWorkAreas();
 }
 
 
@@ -823,6 +889,26 @@ void GuiView::showZoomContextMenu()
 }
 
 
+void GuiView::showStatusBarContextMenu()
+{
+       QMenu * menu = guiApp->menus().menu(toqstr("context-statusbar"), * this);
+       if (!menu)
+               return;
+       menu->exec(QCursor::pos());
+}
+
+
+void GuiView::scheduleRedrawWorkAreas()
+{
+       for (int i = 0; i < d.tabWorkAreaCount(); i++) {
+               TabWorkArea* ta = d.tabWorkArea(i);
+               for (int u = 0; u < ta->count(); u++) {
+                       ta->workArea(u)->scheduleRedraw(true);
+               }
+       }
+}
+
+
 QVector<GuiWorkArea*> GuiView::GuiViewPrivate::guiWorkAreas()
 {
        QVector<GuiWorkArea*> areas;
@@ -925,7 +1011,11 @@ void GuiView::saveLayout() const
                settings.setValue("geometry", saveGeometry());
        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("word_count_enabled", word_count_enabled_);
+       settings.setValue("char_count_enabled", char_count_enabled_);
+       settings.setValue("char_nb_count_enabled", char_nb_count_enabled_);
 }
 
 
@@ -956,8 +1046,7 @@ bool GuiView::restoreLayout()
        zoom_ratio_ = settings.value("zoom_ratio", 1.0).toDouble();
        // Actual zoom value: default zoom + fractional offset
        int zoom = (int)(lyxrc.defaultZoom * zoom_ratio_);
-       if (zoom < static_cast<int>(zoom_min_))
-               zoom = zoom_min_;
+       zoom = min(max(zoom, zoom_min_), zoom_max_);
        setCurrentZoom(zoom);
        devel_mode_ = settings.value("devel_mode", devel_mode_).toBool();
        settings.beginGroup("views");
@@ -969,11 +1058,18 @@ bool GuiView::restoreLayout()
        //code below is skipped when when ~/.config/LyX is (re)created
        setIconSize(d.iconSize(settings.value(icon_key).toString()));
 
+       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);
 
+       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") {
                QPoint pos = settings.value("pos", QPoint(50, 50)).toPoint();
                QSize size = settings.value("size", QSize(690, 510)).toSize();
@@ -1039,6 +1135,11 @@ 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
 }
 
 
@@ -1182,7 +1283,7 @@ bool GuiView::prepareAllBuffersForLogout()
        // We cannot use a for loop as the buffer list cycles.
        Buffer * b = first;
        do {
-               if (!saveBufferIfNeeded(const_cast<Buffer &>(*b), false))
+               if (!saveBufferIfNeeded(*b, false))
                        return false;
                b = theBufferList().next(b);
        } while (b != first);
@@ -1239,6 +1340,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.
@@ -1344,6 +1446,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)
 {
@@ -1483,6 +1652,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())
@@ -1516,7 +1691,7 @@ bool GuiView::event(QEvent * e)
                                menuBar()->hide();
                        if (lyxrc.full_screen_toolbars) {
                                for (auto const & tb_p  : d.toolbars_)
-                                       if (tb_p.second->isVisibiltyOn() && tb_p.second->isVisible())
+                                       if (tb_p.second->isVisibilityOn() && tb_p.second->isVisible())
                                                tb_p.second->hide();
                        }
                        for (int i = 0; i != d.splitter_->count(); ++i)
@@ -1537,7 +1712,7 @@ bool GuiView::event(QEvent * e)
                                menuBar()->show();
                        if (lyxrc.full_screen_toolbars) {
                                for (auto const & tb_p  : d.toolbars_)
-                                       if (tb_p.second->isVisibiltyOn() && !tb_p.second->isVisible())
+                                       if (tb_p.second->isVisibilityOn() && !tb_p.second->isVisible())
                                                tb_p.second->show();
                                //updateToolbars();
                        }
@@ -1549,7 +1724,8 @@ bool GuiView::event(QEvent * e)
                        setContentsMargins(0, 0, 0, 0);
                }
                return result;
-               }
+       }
+
        case QEvent::WindowActivate: {
                GuiView * old_view = guiApp->currentView();
                if (this == old_view) {
@@ -1592,6 +1768,27 @@ bool GuiView::event(QEvent * e)
                return QMainWindow::event(e);
        }
 
+       case QEvent::Gesture: {
+               QGestureEvent *ge = static_cast<QGestureEvent*>(e);
+               QGesture *gp = ge->gesture(Qt::PinchGesture);
+               if (gp) {
+                       QPinchGesture *pinch = static_cast<QPinchGesture *>(gp);
+                       QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags();
+                       qreal totalScaleFactor = pinch->totalScaleFactor();
+                       LYXERR(Debug::GUI, "totalScaleFactor: " << totalScaleFactor);
+                       if (pinch->state() == Qt::GestureStarted) {
+                               initialZoom_ = lyxrc.currentZoom;
+                               LYXERR(Debug::GUI, "initialZoom_: " << initialZoom_);
+                       }
+                       if (changeFlags & QPinchGesture::ScaleFactorChanged) {
+                               qreal factor = initialZoom_ * totalScaleFactor;
+                               LYXERR(Debug::GUI, "scaleFactor: " << factor);
+                               zoomValueChanged(factor);
+                       }
+               }
+               return QMainWindow::event(e);
+       }
+
        default:
                return QMainWindow::event(e);
        }
@@ -1814,6 +2011,17 @@ void GuiView::removeWorkArea(GuiWorkArea * wa)
 }
 
 
+bool GuiView::hasVisibleWorkArea(GuiWorkArea * wa) const
+{
+       for (int i = 0; i < d.splitter_->count(); ++i)
+               if (d.tabWorkArea(i)->currentWorkArea() == wa)
+                       return true;
+
+       FindAndReplace * fr = static_cast<FindAndReplace*>(find("findreplaceadv", false));
+       return fr->isVisible() && fr->hasWorkArea(wa);
+}
+
+
 LayoutBox * GuiView::getLayoutDialog() const
 {
        return d.layout_;
@@ -2045,6 +2253,30 @@ void GuiView::resetAutosaveTimers()
 }
 
 
+namespace {
+
+double zoomRatio(FuncRequest const & cmd, double const zr)
+{
+       if (cmd.argument().empty()) {
+               if (cmd.action() == LFUN_BUFFER_ZOOM)
+                       return 1.0;
+               else if (cmd.action() == LFUN_BUFFER_ZOOM_IN)
+                       return zr + 0.1;
+               else // cmd.action() == LFUN_BUFFER_ZOOM_OUT
+                       return zr - 0.1;
+       } else {
+               if (cmd.action() == LFUN_BUFFER_ZOOM)
+                       return convert<int>(cmd.argument()) / double(lyxrc.defaultZoom);
+               else if (cmd.action() == LFUN_BUFFER_ZOOM_IN)
+                       return zr + convert<int>(cmd.argument()) / 100.0;
+               else // cmd.action() == LFUN_BUFFER_ZOOM_OUT
+                       return zr - convert<int>(cmd.argument()) / 100.0;
+       }
+}
+
+}
+
+
 bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
 {
        bool enable = true;
@@ -2326,9 +2558,16 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                break;
 
        case LFUN_UI_TOGGLE:
-               if (cmd.argument() == "zoomslider") {
-                       enable = doc_buffer;
+               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);
+               } 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;
@@ -2423,37 +2662,25 @@ bool GuiView::getStatus(FuncRequest const & cmd, FuncStatus & flag)
                break;
 
        case LFUN_BUFFER_ZOOM_OUT:
-       case LFUN_BUFFER_ZOOM_IN: {
-               // only diff between these two is that the default for ZOOM_OUT
-               // is a neg. number
-               bool const neg_zoom =
-                       convert<int>(cmd.argument()) < 0 ||
-                       (cmd.action() == LFUN_BUFFER_ZOOM_OUT && cmd.argument().empty());
-               if (lyxrc.currentZoom <= zoom_min_ && neg_zoom) {
+       case LFUN_BUFFER_ZOOM_IN:
+       case LFUN_BUFFER_ZOOM: {
+               int const zoom = (int)(lyxrc.defaultZoom * zoomRatio(cmd, zoom_ratio_));
+               if (zoom < zoom_min_) {
                        docstring const msg =
                                bformat(_("Zoom level cannot be less than %1$d%."), zoom_min_);
                        flag.message(msg);
                        enable = false;
-               } else
-                       enable = doc_buffer;
-               break;
-       }
-
-       case LFUN_BUFFER_ZOOM: {
-               bool const less_than_min_zoom =
-                       !cmd.argument().empty() && convert<int>(cmd.argument()) < zoom_min_;
-               if (lyxrc.currentZoom <= zoom_min_ && less_than_min_zoom) {
+               } else if (zoom > zoom_max_) {
                        docstring const msg =
-                               bformat(_("Zoom level cannot be less than %1$d%."), zoom_min_);
+                               bformat(_("Zoom level cannot be more than %1$d%."), zoom_max_);
                        flag.message(msg);
                        enable = false;
-               } else if (cmd.argument().empty() && lyxrc.currentZoom == lyxrc.defaultZoom)
-                       enable = false;
-               else
+               } else
                        enable = doc_buffer;
                break;
        }
 
+
        case LFUN_BUFFER_MOVE_NEXT:
        case LFUN_BUFFER_MOVE_PREVIOUS:
                // we do not cycle when moving
@@ -2516,7 +2743,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:
@@ -3002,8 +3230,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)
@@ -3021,8 +3248,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.
@@ -3170,8 +3396,10 @@ 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 (FileName(fname).exists()) {
+       if (fname.exists()) {
                docstring const file = makeDisplayPath(fname.absFileName(), 30);
                docstring text = bformat(_("The document %1$s already "
                                           "exists.\n\nDo you want to "
@@ -3932,7 +4160,7 @@ bool GuiView::goToFileRow(string const & argument)
        setBuffer(buf);
        bool success = documentBufferView()->setCursorFromRow(row);
        if (!success) {
-               LYXERR(Debug::LATEX,
+               LYXERR(Debug::OUTFILE,
                       "setCursorFromRow: invalid position for row " << row);
                frontend::Alert::error(_("Inverse Search Failed"),
                                       _("Invalid position requested by inverse search.\n"
@@ -4221,6 +4449,7 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                }
                case LFUN_EXPORT_CANCEL: {
                        Systemcall::killscript();
+                       Q_EMIT scriptKilled();
                        break;
                }
                case LFUN_BUFFER_SWITCH: {
@@ -4488,12 +4717,11 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                                        dr.setMessage(_("Toolbars unlocked."));
                                else
                                        dr.setMessage(_("Toolbars locked."));
-                       } else if (GuiToolbar * t = toolbar(name)) {
+                       } else if (GuiToolbar * tb = toolbar(name))
                                // toggle current toolbar movablity
-                               t->movable();
-                               // update lock (all) toolbars positions
-                               updateLockToolbars();
-                       }
+                               tb->movable();
+                       // update lock (all) toolbars positions
+                       updateLockToolbars();
                        break;
                }
 
@@ -4670,26 +4898,11 @@ void GuiView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
                case LFUN_BUFFER_ZOOM_IN:
                case LFUN_BUFFER_ZOOM_OUT:
                case LFUN_BUFFER_ZOOM: {
-                       if (cmd.argument().empty()) {
-                               if (cmd.action() == LFUN_BUFFER_ZOOM)
-                                       zoom_ratio_ = 1.0;
-                               else if (cmd.action() == LFUN_BUFFER_ZOOM_IN)
-                                       zoom_ratio_ += 0.1;
-                               else
-                                       zoom_ratio_ -= 0.1;
-                       } else {
-                               if (cmd.action() == LFUN_BUFFER_ZOOM)
-                                       zoom_ratio_ = convert<int>(cmd.argument()) / double(lyxrc.defaultZoom);
-                               else if (cmd.action() == LFUN_BUFFER_ZOOM_IN)
-                                       zoom_ratio_ += convert<int>(cmd.argument()) / 100.0;
-                               else
-                                       zoom_ratio_ -= convert<int>(cmd.argument()) / 100.0;
-                       }
+                       zoom_ratio_ = zoomRatio(cmd, zoom_ratio_);
 
                        // Actual zoom value: default zoom + fractional extra value
                        int zoom = (int)(lyxrc.defaultZoom * zoom_ratio_);
-                       if (zoom < static_cast<int>(zoom_min_))
-                               zoom = zoom_min_;
+                       zoom = min(max(zoom, zoom_min_), zoom_max_);
 
                        setCurrentZoom(zoom);
 
@@ -4697,6 +4910,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;
                }
@@ -4755,10 +4972,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;
                        }
@@ -4836,10 +5055,24 @@ bool GuiView::lfunUiToggle(string const & ui_component)
                statusBar()->setVisible(!statusBar()->isVisible());
        } else if (ui_component == "menubar") {
                menuBar()->setVisible(!menuBar()->isVisible());
+       } 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());
+       } 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();
 
@@ -4861,6 +5094,7 @@ bool GuiView::lfunUiToggle(string const & ui_component)
                toggleFullScreen();
        } else
                return false;
+       stat_counts_->setVisible(statsEnabled());
        return true;
 }
 
@@ -4982,7 +5216,7 @@ void GuiView::flatGroupBoxes(const QObject * widget, bool flag)
 }
 
 
-Dialog * GuiView::findOrBuild(string const & name, bool hide_it)
+Dialog * GuiView::find(string const & name, bool hide_it) const
 {
        if (!isValidName(name))
                return nullptr;
@@ -4994,8 +5228,17 @@ Dialog * GuiView::findOrBuild(string const & name, bool hide_it)
                        it->second->hideView();
                return it->second.get();
        }
+       return nullptr;
+}
+
+
+Dialog * GuiView::findOrBuild(string const & name, bool hide_it)
+{
+       Dialog * dialog = find(name, hide_it);
+       if (dialog != nullptr)
+               return dialog;
 
-       Dialog * dialog = build(name);
+       dialog = build(name);
        d.dialogs_[name].reset(dialog);
 #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
        // Force a uniform style for group boxes