# HG changeset patch # User Chris Cannam # Date 1541494743 0 # Node ID 631897ba9fcab50b5138c3d7dad4b07797081cf9 # Parent e848ea0850fe4c39d2fd82676706558377e12090# Parent cca66ce390e03d0ccdc536fb0a365ef3e1b80061 Merge from default branch diff -r e848ea0850fe -r 631897ba9fca layer/Colour3DPlotLayer.cpp --- a/layer/Colour3DPlotLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/Colour3DPlotLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -47,6 +47,7 @@ m_colourScale(ColourScaleType::Linear), m_colourScaleSet(false), m_colourMap(0), + m_colourInverted(false), m_gain(1.0), m_binScale(BinScale::Linear), m_normalization(ColumnNormalization::None), @@ -302,12 +303,18 @@ { if (name == "Normalization" || name == "Colour Scale" || - name == "Gain") return tr("Scale"); + name == "Gain") { + return tr("Scale"); + } if (name == "Bin Scale" || - name == "Invert Vertical Scale") return tr("Bins"); + name == "Invert Vertical Scale") { + return tr("Bins"); + } if (name == "Opaque" || name == "Smooth" || - name == "Colour") return tr("Colour"); + name == "Colour") { + return tr("Colour"); + } return QString(); } @@ -361,7 +368,9 @@ val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea); } else if (name == "Invert Vertical Scale") { - + + *min = 0; + *max = 1; *deflt = 0; val = (m_invertVertical ? 1 : 0); @@ -374,11 +383,15 @@ } else if (name == "Opaque") { + *min = 0; + *max = 1; *deflt = 0; val = (m_opaque ? 1 : 0); } else if (name == "Smooth") { + *min = 0; + *max = 1; *deflt = 0; val = (m_smooth ? 1 : 0); @@ -394,7 +407,7 @@ int value) const { if (name == "Colour") { - return ColourMapper::getColourMapName(value); + return ColourMapper::getColourMapLabel(value); } if (name == "Colour Scale") { switch (value) { @@ -824,7 +837,9 @@ return ""; } - if (m_invertVertical) sy = m_model->getHeight() - sy - 1; + if (m_invertVertical) { + sy = m_model->getHeight() - sy - 1; + } float value = m_model->getValueAt(sx0, sy); @@ -979,7 +994,9 @@ if (i > symin) { int idx = i - 1; - if (m_invertVertical) idx = m_model->getHeight() - idx - 1; + if (m_invertVertical) { + idx = m_model->getHeight() - idx - 1; + } QString text = m_model->getBinName(idx); if (text == "") text = QString("[%1]").arg(idx + 1); @@ -1009,6 +1026,7 @@ ColourScale::Parameters cparams; cparams.colourMap = m_colourMap; + cparams.inverted = m_colourInverted; cparams.scaleType = m_colourScale; cparams.gain = m_gain; @@ -1027,7 +1045,10 @@ } SVDEBUG << "Colour3DPlotLayer: rebuilding renderer, value range is " - << minValue << " -> " << maxValue << endl; + << minValue << " -> " << maxValue + << " (model min = " << m_model->getMinimumLevel() + << ", max = " << m_model->getMaximumLevel() << ")" + << endl; if (maxValue <= minValue) { maxValue = minValue + 0.1f; @@ -1172,13 +1193,11 @@ QString indent, QString extraAttributes) const { QString s = QString("scale=\"%1\" " - "colourScheme=\"%2\" " - "minY=\"%3\" " - "maxY=\"%4\" " - "invertVertical=\"%5\" " - "opaque=\"%6\" %7") + "minY=\"%2\" " + "maxY=\"%3\" " + "invertVertical=\"%4\" " + "opaque=\"%5\" %6") .arg(convertFromColourScale(m_colourScale)) - .arg(m_colourMap) .arg(m_miny) .arg(m_maxy) .arg(m_invertVertical ? "true" : "false") @@ -1187,6 +1206,17 @@ .arg(int(m_binScale)) .arg(m_smooth ? "true" : "false") .arg(m_gain)); + + // New-style colour map attribute, by string id rather than by + // number + + s += QString("colourMap=\"%1\" ") + .arg(ColourMapper::getColourMapId(m_colourMap)); + + // Old-style colour map attribute + + s += QString("colourScheme=\"%1\" ") + .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); // New-style normalization attributes, allowing for more types of // normalization in future: write out the column normalization @@ -1219,8 +1249,16 @@ (attributes.value("scale").toInt(&ok)); if (ok) setColourScale(colourScale); - int colourMap = attributes.value("colourScheme").toInt(&ok); - if (ok) setColourMap(colourMap); + QString colourMapId = attributes.value("colourMap"); + int colourMap = ColourMapper::getColourMapById(colourMapId); + if (colourMap >= 0) { + setColourMap(colourMap); + } else { + colourMap = attributes.value("colourScheme").toInt(&ok); + if (ok && colourMap < ColourMapper::getColourMapCount()) { + setColourMap(colourMap); + } + } BinScale binScale = (BinScale) attributes.value("binScale").toInt(&ok); diff -r e848ea0850fe -r 631897ba9fca layer/Colour3DPlotLayer.h --- a/layer/Colour3DPlotLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/Colour3DPlotLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -160,6 +160,7 @@ ColourScaleType m_colourScale; bool m_colourScaleSet; int m_colourMap; + bool m_colourInverted; float m_gain; BinScale m_binScale; ColumnNormalization m_normalization; // of individual columns diff -r e848ea0850fe -r 631897ba9fca layer/Colour3DPlotRenderer.cpp --- a/layer/Colour3DPlotRenderer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/Colour3DPlotRenderer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -35,7 +35,7 @@ #include using namespace std::rel_ops; -#define DEBUG_COLOUR_PLOT_REPAINT 1 +//#define DEBUG_COLOUR_PLOT_REPAINT 1 using namespace std; @@ -353,8 +353,6 @@ Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins, int peakCacheIndex) const { - Profiler profiler("Colour3DPlotRenderer::getColumn"); - // order: // get column -> scale -> normalise -> record extents -> // peak pick -> distribute/interpolate -> apply display gain @@ -363,7 +361,38 @@ // get column -> scale -> normalise ColumnOp::Column column; - + + if (m_params.showDerivative && sx > 0) { + + auto prev = getColumnRaw(sx - 1, minbin, nbins, peakCacheIndex); + column = getColumnRaw(sx, minbin, nbins, peakCacheIndex); + + for (int i = 0; i < nbins; ++i) { + column[i] -= prev[i]; + } + + } else { + column = getColumnRaw(sx, minbin, nbins, peakCacheIndex); + } + + if (m_params.colourScale.getScale() == ColourScaleType::Phase && + m_sources.fft) { + return column; + } else { + column = ColumnOp::applyGain(column, m_params.scaleFactor); + column = ColumnOp::normalize(column, m_params.normalization); + return column; + } +} + +ColumnOp::Column +Colour3DPlotRenderer::getColumnRaw(int sx, int minbin, int nbins, + int peakCacheIndex) const +{ + Profiler profiler("Colour3DPlotRenderer::getColumn"); + + ColumnOp::Column column; + if (m_params.colourScale.getScale() == ColourScaleType::Phase && m_sources.fft) { @@ -382,10 +411,6 @@ column = vector(fullColumn.data() + minbin, fullColumn.data() + minbin + nbins); - - column = ColumnOp::applyGain(column, m_params.scaleFactor); - - column = ColumnOp::normalize(column, m_params.normalization); } return column; diff -r e848ea0850fe -r 631897ba9fca layer/Colour3DPlotRenderer.h --- a/layer/Colour3DPlotRenderer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/Colour3DPlotRenderer.h Tue Nov 06 08:59:03 2018 +0000 @@ -67,6 +67,7 @@ alwaysOpaque(false), interpolate(false), invertVertical(false), + showDerivative(false), scaleFactor(1.0), colourRotation(0) { } @@ -100,6 +101,10 @@ /** Whether to render the whole caboodle upside-down. */ bool invertVertical; + /** Whether to show the frame-to-frame difference instead of + * the actual value */ + bool showDerivative; + /** Initial scale factor (e.g. for FFT scaling). This factor * is applied to all values read from the underlying model * *before* magnitude ranges are calculated, in contrast to @@ -319,6 +324,8 @@ ColumnOp::Column getColumn(int sx, int minbin, int nbins, int peakCacheIndex) const; // -1 => don't use cache + ColumnOp::Column getColumnRaw(int sx, int minbin, int nbins, + int peakCacheIndex) const; // -1 => don't use cache void getPreferredPeakCache(const LayerGeometryProvider *, int &peakCacheIndex, int &binsPerPeak) const; diff -r e848ea0850fe -r 631897ba9fca layer/ColourDatabase.cpp --- a/layer/ColourDatabase.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/ColourDatabase.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -85,6 +85,26 @@ return -1; } +QColor +ColourDatabase::getContrastingColour(int c) const +{ + QColor col = getColour(c); + if (col.red() > col.blue()) { + if (col.green() > col.blue()) { + return Qt::blue; + } else { + return Qt::yellow; + } + } else { + if (col.green() > col.blue()) { + return Qt::yellow; + } else { + return Qt::red; + } + } + return Qt::red; +} + bool ColourDatabase::useDarkBackground(int c) const { diff -r e848ea0850fe -r 631897ba9fca layer/ColourMapper.cpp --- a/layer/ColourMapper.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/ColourMapper.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -27,11 +27,14 @@ using namespace std; -static vector convertStrings(const vector &strs) +static vector convertStrings(const vector &strs, + bool reversed) { vector converted; for (const auto &s: strs) converted.push_back(QColor(s)); - reverse(converted.begin(), converted.end()); + if (reversed) { + reverse(converted.begin(), converted.end()); + } return converted; } @@ -39,13 +42,70 @@ // Based on ColorBrewer ylGnBu "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5", "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040" - }); + }, + true); static vector cherry = convertStrings({ "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497", "#ae017e", "#7a0177", "#49006a" - }); - + }, + true); + +static vector magma = convertStrings({ + "#FCFFB2", "#FCDF96", "#FBC17D", "#FBA368", "#FA8657", "#F66B4D", + "#ED504A", "#E03B50", "#C92D59", "#B02363", "#981D69", "#81176D", + "#6B116F", "#57096E", "#43006A", "#300060", "#1E0848", "#110B2D", + "#080616", "#000005" + }, + true); + +static vector cividis = convertStrings({ + "#00204c", "#00204e", "#002150", "#002251", "#002353", "#002355", + "#002456", "#002558", "#00265a", "#00265b", "#00275d", "#00285f", + "#002861", "#002963", "#002a64", "#002a66", "#002b68", "#002c6a", + "#002d6c", "#002d6d", "#002e6e", "#002e6f", "#002f6f", "#002f6f", + "#00306f", "#00316f", "#00316f", "#00326e", "#00336e", "#00346e", + "#00346e", "#01356e", "#06366e", "#0a376d", "#0e376d", "#12386d", + "#15396d", "#17396d", "#1a3a6c", "#1c3b6c", "#1e3c6c", "#203c6c", + "#223d6c", "#243e6c", "#263e6c", "#273f6c", "#29406b", "#2b416b", + "#2c416b", "#2e426b", "#2f436b", "#31446b", "#32446b", "#33456b", + "#35466b", "#36466b", "#37476b", "#38486b", "#3a496b", "#3b496b", + "#3c4a6b", "#3d4b6b", "#3e4b6b", "#404c6b", "#414d6b", "#424e6b", + "#434e6b", "#444f6b", "#45506b", "#46506b", "#47516b", "#48526b", + "#49536b", "#4a536b", "#4b546b", "#4c556b", "#4d556b", "#4e566b", + "#4f576c", "#50586c", "#51586c", "#52596c", "#535a6c", "#545a6c", + "#555b6c", "#565c6c", "#575d6d", "#585d6d", "#595e6d", "#5a5f6d", + "#5b5f6d", "#5c606d", "#5d616e", "#5e626e", "#5f626e", "#5f636e", + "#60646e", "#61656f", "#62656f", "#63666f", "#64676f", "#65676f", + "#666870", "#676970", "#686a70", "#686a70", "#696b71", "#6a6c71", + "#6b6d71", "#6c6d72", "#6d6e72", "#6e6f72", "#6f6f72", "#6f7073", + "#707173", "#717273", "#727274", "#737374", "#747475", "#757575", + "#757575", "#767676", "#777776", "#787876", "#797877", "#7a7977", + "#7b7a77", "#7b7b78", "#7c7b78", "#7d7c78", "#7e7d78", "#7f7e78", + "#807e78", "#817f78", "#828078", "#838178", "#848178", "#858278", + "#868378", "#878478", "#888578", "#898578", "#8a8678", "#8b8778", + "#8c8878", "#8d8878", "#8e8978", "#8f8a78", "#908b78", "#918c78", + "#928c78", "#938d78", "#948e78", "#958f78", "#968f77", "#979077", + "#989177", "#999277", "#9a9377", "#9b9377", "#9c9477", "#9d9577", + "#9e9676", "#9f9776", "#a09876", "#a19876", "#a29976", "#a39a75", + "#a49b75", "#a59c75", "#a69c75", "#a79d75", "#a89e74", "#a99f74", + "#aaa074", "#aba174", "#aca173", "#ada273", "#aea373", "#afa473", + "#b0a572", "#b1a672", "#b2a672", "#b4a771", "#b5a871", "#b6a971", + "#b7aa70", "#b8ab70", "#b9ab70", "#baac6f", "#bbad6f", "#bcae6e", + "#bdaf6e", "#beb06e", "#bfb16d", "#c0b16d", "#c1b26c", "#c2b36c", + "#c3b46c", "#c5b56b", "#c6b66b", "#c7b76a", "#c8b86a", "#c9b869", + "#cab969", "#cbba68", "#ccbb68", "#cdbc67", "#cebd67", "#d0be66", + "#d1bf66", "#d2c065", "#d3c065", "#d4c164", "#d5c263", "#d6c363", + "#d7c462", "#d8c561", "#d9c661", "#dbc760", "#dcc860", "#ddc95f", + "#deca5e", "#dfcb5d", "#e0cb5d", "#e1cc5c", "#e3cd5b", "#e4ce5b", + "#e5cf5a", "#e6d059", "#e7d158", "#e8d257", "#e9d356", "#ebd456", + "#ecd555", "#edd654", "#eed753", "#efd852", "#f0d951", "#f1da50", + "#f3db4f", "#f4dc4e", "#f5dd4d", "#f6de4c", "#f7df4b", "#f9e049", + "#fae048", "#fbe147", "#fce246", "#fde345", "#ffe443", "#ffe542", + "#ffe642", "#ffe743", "#ffe844", "#ffe945" + }, + false); + static void mapDiscrete(double norm, vector &colours, double &r, double &g, double &b) { @@ -61,8 +121,9 @@ b = c0.blueF() * prop0 + c1.blueF() * prop1; } -ColourMapper::ColourMapper(int map, double min, double max) : +ColourMapper::ColourMapper(int map, bool inverted, double min, double max) : m_map(map), + m_inverted(inverted), m_min(min), m_max(max) { @@ -80,14 +141,16 @@ int ColourMapper::getColourMapCount() { - return 12; + return 15; } QString -ColourMapper::getColourMapName(int n) +ColourMapper::getColourMapLabel(int n) { + // When adding a map, be sure to also update getColourMapCount() + if (n >= getColourMapCount()) return QObject::tr(""); - StandardMap map = (StandardMap)n; + ColourMap map = (ColourMap)n; switch (map) { case Green: return QObject::tr("Green"); @@ -102,17 +165,112 @@ case Highlight: return QObject::tr("Highlight"); case Printer: return QObject::tr("Printer"); case HighGain: return QObject::tr("High Gain"); + case BlueOnBlack: return QObject::tr("Blue on Black"); + case Cividis: return QObject::tr("Cividis"); + case Magma: return QObject::tr("Magma"); } return QObject::tr(""); } +QString +ColourMapper::getColourMapId(int n) +{ + if (n >= getColourMapCount()) return ""; + ColourMap map = (ColourMap)n; + + switch (map) { + case Green: return "Green"; + case WhiteOnBlack: return "White on Black"; + case BlackOnWhite: return "Black on White"; + case Cherry: return "Cherry"; + case Wasp: return "Wasp"; + case Ice: return "Ice"; + case Sunset: return "Sunset"; + case FruitSalad: return "Fruit Salad"; + case Banded: return "Banded"; + case Highlight: return "Highlight"; + case Printer: return "Printer"; + case HighGain: return "High Gain"; + case BlueOnBlack: return "Blue on Black"; + case Cividis: return "Cividis"; + case Magma: return "Magma"; + } + + return ""; +} + +int +ColourMapper::getColourMapById(QString id) +{ + ColourMap map = (ColourMap)getColourMapCount(); + + if (id == "Green") { map = Green; } + else if (id == "White on Black") { map = WhiteOnBlack; } + else if (id == "Black on White") { map = BlackOnWhite; } + else if (id == "Cherry") { map = Cherry; } + else if (id == "Wasp") { map = Wasp; } + else if (id == "Ice") { map = Ice; } + else if (id == "Sunset") { map = Sunset; } + else if (id == "Fruit Salad") { map = FruitSalad; } + else if (id == "Banded") { map = Banded; } + else if (id == "Highlight") { map = Highlight; } + else if (id == "Printer") { map = Printer; } + else if (id == "High Gain") { map = HighGain; } + else if (id == "Blue on Black") { map = BlueOnBlack; } + else if (id == "Cividis") { map = Cividis; } + else if (id == "Magma") { map = Magma; } + + if (map == (ColourMap)getColourMapCount()) { + return -1; + } else { + return int(map); + } +} + +int +ColourMapper::getBackwardCompatibilityColourMap(int n) +{ + /* Returned value should be an index into the series + * (Default/Green, Sunset, WhiteOnBlack, BlackOnWhite, RedOnBlue, + * YellowOnBlack, BlueOnBlack, FruitSalad, Banded, Highlight, + * Printer, HighGain). Minimum 0, maximum 11. + */ + + if (n >= getColourMapCount()) return 0; + ColourMap map = (ColourMap)n; + + switch (map) { + case Green: return 0; + case WhiteOnBlack: return 2; + case BlackOnWhite: return 3; + case Cherry: return 4; + case Wasp: return 5; + case Ice: return 6; + case Sunset: return 1; + case FruitSalad: return 7; + case Banded: return 8; + case Highlight: return 9; + case Printer: return 10; + case HighGain: return 11; + case BlueOnBlack: return 6; + case Cividis: return 6; + case Magma: return 1; + } + + return 0; +} + QColor ColourMapper::map(double value) const { double norm = (value - m_min) / (m_max - m_min); if (norm < 0.0) norm = 0.0; if (norm > 1.0) norm = 1.0; + + if (m_inverted) { + norm = 1.0 - norm; + } double h = 0.0, s = 0.0, v = 0.0, r = 0.0, g = 0.0, b = 0.0; bool hsv = true; @@ -120,7 +278,7 @@ double blue = 0.6666, pieslice = 0.3333; if (m_map >= getColourMapCount()) return Qt::black; - StandardMap map = (StandardMap)m_map; + ColourMap map = (ColourMap)m_map; switch (map) { @@ -150,6 +308,18 @@ s = 1.0; v = norm; break; + + case BlueOnBlack: + h = blue; + s = 1.0; + v = norm * 2.0; + if (v > 1.0) { + v = 1.0; + s = 1.0 - (sqrt(norm) - 0.707) * 3.413; + if (s < 0.0) s = 0.0; + if (s > 1.0) s = 1.0; + } + break; case Sunset: r = (norm - 0.24) * 2.38; @@ -237,6 +407,17 @@ case Ice: hsv = false; mapDiscrete(norm, ice, r, g, b); + break; + + case Cividis: + hsv = false; + mapDiscrete(norm, cividis, r, g, b); + break; + + case Magma: + hsv = false; + mapDiscrete(norm, magma, r, g, b); + break; } if (hsv) { @@ -250,7 +431,7 @@ ColourMapper::getContrastingColour() const { if (m_map >= getColourMapCount()) return Qt::white; - StandardMap map = (StandardMap)m_map; + ColourMap map = (ColourMap)m_map; switch (map) { @@ -289,6 +470,15 @@ case HighGain: return Qt::red; + + case BlueOnBlack: + return Qt::red; + + case Cividis: + return Qt::white; + + case Magma: + return Qt::white; } return Qt::white; @@ -298,7 +488,7 @@ ColourMapper::hasLightBackground() const { if (m_map >= getColourMapCount()) return false; - StandardMap map = (StandardMap)m_map; + ColourMap map = (ColourMap)m_map; switch (map) { @@ -316,6 +506,9 @@ case FruitSalad: case Banded: case Highlight: + case BlueOnBlack: + case Cividis: + case Magma: default: return false; diff -r e848ea0850fe -r 631897ba9fca layer/ColourMapper.h --- a/layer/ColourMapper.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/ColourMapper.h Tue Nov 06 08:59:03 2018 +0000 @@ -27,13 +27,16 @@ class ColourMapper { public: - ColourMapper(int map, double minValue, double maxValue); + ColourMapper(int map, + bool inverted, + double minValue, + double maxValue); ~ColourMapper(); - ColourMapper(const ColourMapper &) = default; - ColourMapper &operator=(const ColourMapper &) = default; + ColourMapper(const ColourMapper &) =default; + ColourMapper &operator=(const ColourMapper &) =default; - enum StandardMap { + enum ColourMap { Green, Sunset, WhiteOnBlack, @@ -45,25 +48,86 @@ Banded, Highlight, Printer, - HighGain + HighGain, + BlueOnBlack, + Cividis, + Magma }; int getMap() const { return m_map; } + bool isInverted() const { return m_inverted; } double getMinValue() const { return m_min; } double getMaxValue() const { return m_max; } + /** + * Return the number of known colour maps. + */ static int getColourMapCount(); - static QString getColourMapName(int n); + /** + * Return a human-readable label for the colour map with the given + * index. This may have been subject to translation. + */ + static QString getColourMapLabel(int n); + + /** + * Return a machine-readable id string for the colour map with the + * given index. This is not translated and is intended for use in + * file I/O. + */ + static QString getColourMapId(int n); + + /** + * Return the index for the colour map with the given + * machine-readable id string, or -1 if the id is not recognised. + */ + static int getColourMapById(QString id); + + /** + * Older versions of colour-handling code save and reload colour + * maps by numerical index and can't properly handle situations in + * which the index order changes between releases, or new indices + * are added. So when we save a colour map by id, we should also + * save a compatibility value that can be re-read by such + * code. This value is an index into the series of colours used by + * pre-3.2 SV code, namely (Default/Green, Sunset, WhiteOnBlack, + * BlackOnWhite, RedOnBlue, YellowOnBlack, BlueOnBlack, + * FruitSalad, Banded, Highlight, Printer, HighGain). It should + * represent the closest equivalent to the current colour scheme + * available in that set. This function returns that index. + */ + static int getBackwardCompatibilityColourMap(int n); + + /** + * Map the given value to a colour. The value will be clamped to + * the range minValue to maxValue (where both are drawn from the + * constructor arguments). + */ QColor map(double value) const; - QColor getContrastingColour() const; // for cursors etc + /** + * Return a colour that contrasts somewhat with the colours in the + * map, so as to be used for cursors etc. + */ + QColor getContrastingColour() const; + + /** + * Return true if the colour map is intended to be placed over a + * light background, false otherwise. This is typically true if + * the colours corresponding to higher values are darker than + * those corresponding to lower values. + */ bool hasLightBackground() const; + /** + * Return a pixmap of the given size containing a preview swatch + * for the colour map. + */ QPixmap getExamplePixmap(QSize size) const; protected: int m_map; + bool m_inverted; double m_min; double m_max; }; diff -r e848ea0850fe -r 631897ba9fca layer/ColourScale.cpp --- a/layer/ColourScale.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/ColourScale.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -27,7 +27,7 @@ ColourScale::ColourScale(Parameters parameters) : m_params(parameters), - m_mapper(m_params.colourMap, 1.f, double(m_maxPixel)) + m_mapper(m_params.colourMap, m_params.inverted, 1.f, double(m_maxPixel)) { if (m_params.minValue >= m_params.maxValue) { SVCERR << "ERROR: ColourScale::ColourScale: minValue = " diff -r e848ea0850fe -r 631897ba9fca layer/ColourScale.h --- a/layer/ColourScale.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/ColourScale.h Tue Nov 06 08:59:03 2018 +0000 @@ -36,7 +36,7 @@ public: struct Parameters { Parameters() : colourMap(0), scaleType(ColourScaleType::Linear), - minValue(0.0), maxValue(1.0), + minValue(0.0), maxValue(1.0), inverted(false), threshold(0.0), gain(1.0), multiple(1.0) { } /** A colour map index as used by ColourMapper */ @@ -51,6 +51,9 @@ /** Maximum value in source range. Must be > minValue */ double maxValue; + /** Whether the colour scale should be mapped inverted */ + bool inverted; + /** Threshold below which every value is mapped to background pixel 0 */ double threshold; diff -r e848ea0850fe -r 631897ba9fca layer/NoteLayer.h --- a/layer/NoteLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/NoteLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -13,8 +13,8 @@ COPYING included with this distribution for more information. */ -#ifndef _NOTE_LAYER_H_ -#define _NOTE_LAYER_H_ +#ifndef SV_NOTE_LAYER_H +#define SV_NOTE_LAYER_H #include "SingleColourLayer.h" #include "VerticalScaleLayer.h" diff -r e848ea0850fe -r 631897ba9fca layer/PaintAssistant.cpp --- a/layer/PaintAssistant.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/PaintAssistant.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -242,7 +242,7 @@ } double -PaintAssistant::scalePenWidth(double width) +PaintAssistant::scaleSize(double size) { static double ratio = 0.0; if (ratio == 0.0) { @@ -255,21 +255,28 @@ double em = QFontMetrics(QFont()).height(); ratio = em / baseEm; - SVDEBUG << "PaintAssistant::scalePenWidth: ratio is " << ratio + SVDEBUG << "PaintAssistant::scaleSize: ratio is " << ratio << " (em = " << em << ")" << endl; + + if (ratio < 1.0) { + SVDEBUG << "PaintAssistant::scaleSize: rounding ratio up to 1.0" + << endl; + ratio = 1.0; + } } - if (ratio <= 1.0) { - // we only ever scale up in this method - return width; + return size * ratio; +} + +double +PaintAssistant::scalePenWidth(double width) +{ + if (width <= 0) { + // zero-width pen, produce a scaled one-pixel pen + width = 1; } - if (width <= 0) { - // zero-width pen, produce a scaled one-pixel pen - return ratio; - } - - return width * ratio; + return scaleSize(width); } QPen diff -r e848ea0850fe -r 631897ba9fca layer/PaintAssistant.h --- a/layer/PaintAssistant.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/PaintAssistant.h Tue Nov 06 08:59:03 2018 +0000 @@ -49,9 +49,18 @@ QString text, TextStyle style); /** + * Scale up a size in pixels for a hi-dpi display without pixel + * doubling. This is like ViewManager::scalePixelSize, but taking + * and returning floating-point values rather than integer + * pixels. It is also a little more conservative - it never + * shrinks the size, it can only increase or leave it unchanged. + */ + static double scaleSize(double size); + + /** * Scale up pen width for a hi-dpi display without pixel doubling. - * Very similar to ViewManager::scalePixelSize, but a bit more - * conservative. + * This is like scaleSize except that it also scales the + * zero-width case. */ static double scalePenWidth(double width); diff -r e848ea0850fe -r 631897ba9fca layer/RegionLayer.cpp --- a/layer/RegionLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/RegionLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -57,6 +57,7 @@ m_editingCommand(0), m_verticalScale(EqualSpaced), m_colourMap(0), + m_colourInverted(false), m_plotStyle(PlotLines) { @@ -174,7 +175,7 @@ int value) const { if (name == "Colour" && m_plotStyle == PlotSegmentation) { - return ColourMapper::getColourMapName(value); + return ColourMapper::getColourMapLabel(value); } else if (name == "Plot Type") { switch (value) { @@ -854,7 +855,7 @@ // SVDEBUG << "RegionLayer::getColourForValue: min " << min << ", max " // << max << ", log " << log << ", value " << val << endl; - QColor solid = ColourMapper(m_colourMap, min, max).map(val); + QColor solid = ColourMapper(m_colourMap, m_colourInverted, min, max).map(val); return QColor(solid.red(), solid.green(), solid.blue(), 120); } @@ -1550,10 +1551,25 @@ RegionLayer::toXml(QTextStream &stream, QString indent, QString extraAttributes) const { - SingleColourLayer::toXml(stream, indent, extraAttributes + - QString(" verticalScale=\"%1\" plotStyle=\"%2\"") - .arg(m_verticalScale) - .arg(m_plotStyle)); + QString s; + + s += QString("verticalScale=\"%1\" " + "plotStyle=\"%2\" ") + .arg(m_verticalScale) + .arg(m_plotStyle); + + // New-style colour map attribute, by string id rather than by + // number + + s += QString("fillColourMap=\"%1\" ") + .arg(ColourMapper::getColourMapId(m_colourMap)); + + // Old-style colour map attribute + + s += QString("colourMap=\"%1\" ") + .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); + + SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s); } void @@ -1568,6 +1584,17 @@ PlotStyle style = (PlotStyle) attributes.value("plotStyle").toInt(&ok); if (ok) setPlotStyle(style); + + QString colourMapId = attributes.value("fillColourMap"); + int colourMap = ColourMapper::getColourMapById(colourMapId); + if (colourMap >= 0) { + setFillColourMap(colourMap); + } else { + colourMap = attributes.value("colourMap").toInt(&ok); + if (ok && colourMap < ColourMapper::getColourMapCount()) { + setFillColourMap(colourMap); + } + } } diff -r e848ea0850fe -r 631897ba9fca layer/RegionLayer.h --- a/layer/RegionLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/RegionLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -13,8 +13,8 @@ COPYING included with this distribution for more information. */ -#ifndef _REGION_LAYER_H_ -#define _REGION_LAYER_H_ +#ifndef SV_REGION_LAYER_H +#define SV_REGION_LAYER_H #include "SingleColourLayer.h" #include "VerticalScaleLayer.h" @@ -156,6 +156,7 @@ RegionModel::EditCommand *m_editingCommand; VerticalScale m_verticalScale; int m_colourMap; + bool m_colourInverted; PlotStyle m_plotStyle; typedef std::map SpacingMap; diff -r e848ea0850fe -r 631897ba9fca layer/SliceLayer.cpp --- a/layer/SliceLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SliceLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -32,6 +32,7 @@ SliceLayer::SliceLayer() : m_sliceableModel(0), m_colourMap(int(ColourMapper::Ice)), + m_colourInverted(false), m_energyScale(dBScale), m_samplingMode(SampleMean), m_plotStyle(PlotLines), @@ -469,7 +470,7 @@ double nx = getXForBin(v, bin0); - ColourMapper mapper(m_colourMap, 0, 1); + ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1); for (int bin = 0; bin < mh; ++bin) { @@ -580,7 +581,7 @@ SliceLayer::hasLightBackground() const { if (usesSolidColour()) { - ColourMapper mapper(m_colourMap, 0, 1); + ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1); return mapper.hasLightBackground(); } else { return SingleColourLayer::hasLightBackground(); @@ -742,7 +743,7 @@ int value) const { if (name == "Colour" && usesSolidColour()) { - return ColourMapper::getColourMapName(value); + return ColourMapper::getColourMapLabel(value); } if (name == "Scale") { switch (value) { @@ -925,15 +926,13 @@ { QString s; - s += QString("colourScheme=\"%1\" " - "energyScale=\"%2\" " - "samplingMode=\"%3\" " - "plotStyle=\"%4\" " - "binScale=\"%5\" " - "gain=\"%6\" " - "threshold=\"%7\" " - "normalize=\"%8\" %9") - .arg(m_colourMap) + s += QString("energyScale=\"%1\" " + "samplingMode=\"%2\" " + "plotStyle=\"%3\" " + "binScale=\"%4\" " + "gain=\"%5\" " + "threshold=\"%6\" " + "normalize=\"%7\" %8 ") .arg(m_energyScale) .arg(m_samplingMode) .arg(m_plotStyle) @@ -946,6 +945,17 @@ .arg(m_minbin) .arg(m_maxbin)); + // New-style colour map attribute, by string id rather than by + // number + + s += QString("fillColourMap=\"%1\" ") + .arg(ColourMapper::getColourMapId(m_colourMap)); + + // Old-style colour map attribute + + s += QString("colourScheme=\"%1\" ") + .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); + SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s); } @@ -964,8 +974,16 @@ attributes.value("samplingMode").toInt(&ok); if (ok) setSamplingMode(mode); - int colourMap = attributes.value("colourScheme").toInt(&ok); - if (ok) setFillColourMap(colourMap); + QString colourMapId = attributes.value("fillColourMap"); + int colourMap = ColourMapper::getColourMapById(colourMapId); + if (colourMap >= 0) { + setFillColourMap(colourMap); + } else { + colourMap = attributes.value("colourScheme").toInt(&ok); + if (ok && colourMap < ColourMapper::getColourMapCount()) { + setFillColourMap(colourMap); + } + } PlotStyle s = (PlotStyle) attributes.value("plotStyle").toInt(&ok); diff -r e848ea0850fe -r 631897ba9fca layer/SliceLayer.h --- a/layer/SliceLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SliceLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -142,6 +142,7 @@ const DenseThreeDimensionalModel *m_sliceableModel; int m_colourMap; + bool m_colourInverted; EnergyScale m_energyScale; SamplingMode m_samplingMode; PlotStyle m_plotStyle; diff -r e848ea0850fe -r 631897ba9fca layer/SpectrogramLayer.cpp --- a/layer/SpectrogramLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SpectrogramLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -73,6 +73,7 @@ m_colourScale(ColourScaleType::Log), m_colourScaleMultiple(1.0), m_colourMap(0), + m_colourInverted(false), m_binScale(BinScale::Linear), m_binDisplay(BinDisplay::AllBins), m_normalization(ColumnNormalization::None), @@ -448,7 +449,7 @@ int value) const { if (name == "Colour") { - return ColourMapper::getColourMapName(value); + return ColourMapper::getColourMapLabel(value); } if (name == "Colour Scale") { switch (value) { @@ -1081,7 +1082,8 @@ bool SpectrogramLayer::hasLightBackground() const { - return ColourMapper(m_colourMap, 1.f, 255.f).hasLightBackground(); + return ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f) + .hasLightBackground(); } double @@ -1530,7 +1532,8 @@ m_renderers[viewId] = new Colour3DPlotRenderer(sources, params); m_crosshairColour = - ColourMapper(m_colourMap, 1.f, 255.f).getContrastingColour(); + ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f) + .getContrastingColour(); } return m_renderers[viewId]; @@ -2508,18 +2511,27 @@ s += QString("minFrequency=\"%1\" " "maxFrequency=\"%2\" " "colourScale=\"%3\" " - "colourScheme=\"%4\" " - "colourRotation=\"%5\" " - "frequencyScale=\"%6\" " - "binDisplay=\"%7\" ") + "colourRotation=\"%4\" " + "frequencyScale=\"%5\" " + "binDisplay=\"%6\" ") .arg(m_minFrequency) .arg(m_maxFrequency) .arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple)) - .arg(m_colourMap) .arg(m_colourRotation) .arg(int(m_binScale)) .arg(int(m_binDisplay)); + // New-style colour map attribute, by string id rather than by + // number + + s += QString("colourMap=\"%1\" ") + .arg(ColourMapper::getColourMapId(m_colourMap)); + + // Old-style colour map attribute + + s += QString("colourScheme=\"%1\" ") + .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); + // New-style normalization attributes, allowing for more types of // normalization in future: write out the column normalization // type separately, and then whether we are normalizing visible @@ -2596,8 +2608,16 @@ setColourScaleMultiple(colourScale.second); } - int colourMap = attributes.value("colourScheme").toInt(&ok); - if (ok) setColourMap(colourMap); + QString colourMapId = attributes.value("colourMap"); + int colourMap = ColourMapper::getColourMapById(colourMapId); + if (colourMap >= 0) { + setColourMap(colourMap); + } else { + colourMap = attributes.value("colourScheme").toInt(&ok); + if (ok && colourMap < ColourMapper::getColourMapCount()) { + setColourMap(colourMap); + } + } int colourRotation = attributes.value("colourRotation").toInt(&ok); if (ok) setColourRotation(colourRotation); diff -r e848ea0850fe -r 631897ba9fca layer/SpectrogramLayer.h --- a/layer/SpectrogramLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SpectrogramLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -258,6 +258,7 @@ ColourScaleType m_colourScale; double m_colourScaleMultiple; int m_colourMap; + bool m_colourInverted; mutable QColor m_crosshairColour; BinScale m_binScale; BinDisplay m_binDisplay; diff -r e848ea0850fe -r 631897ba9fca layer/SpectrumLayer.cpp --- a/layer/SpectrumLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SpectrumLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -418,7 +418,7 @@ paint.setFont(fn); } - ColourMapper mapper(m_colourMap, 0, 1); + ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1); paint.setPen(mapper.getContrastingColour()); int xorigin = m_xorigins[v->getId()]; @@ -618,8 +618,8 @@ ColourMapper mapper = hasLightBackground() ? - ColourMapper(ColourMapper::BlackOnWhite, 0, 1) : - ColourMapper(ColourMapper::WhiteOnBlack, 0, 1); + ColourMapper(ColourMapper::BlackOnWhite, m_colourInverted, 0, 1) : + ColourMapper(ColourMapper::WhiteOnBlack, m_colourInverted, 0, 1); int peakminbin = 0; int peakmaxbin = fft->getHeight() - 1; diff -r e848ea0850fe -r 631897ba9fca layer/SpectrumLayer.h --- a/layer/SpectrumLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/SpectrumLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -111,9 +111,9 @@ DenseTimeValueModel *m_originModel; int m_channel; bool m_channelSet; - int m_windowSize; + int m_windowSize; WindowType m_windowType; - int m_windowHopLevel; + int m_windowHopLevel; bool m_showPeaks; mutable bool m_newFFTNeeded; diff -r e848ea0850fe -r 631897ba9fca layer/TimeRulerLayer.cpp --- a/layer/TimeRulerLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/TimeRulerLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -154,8 +154,12 @@ sv_frame_t startFrame = v->getStartFrame(); sv_frame_t endFrame = v->getEndFrame(); + if (endFrame == startFrame) { + endFrame = startFrame + 1; + } - int minPixelSpacing = ViewManager::scalePixelSize(50); + int exampleWidth = QFontMetrics(QFont()).width("10:42.987654"); + int minPixelSpacing = v->getXForViewX(exampleWidth); RealTime rtStart = RealTime::frame2RealTime(startFrame, sampleRate); RealTime rtEnd = RealTime::frame2RealTime(endFrame, sampleRate); @@ -164,6 +168,15 @@ if (count < 1) count = 1; RealTime rtGap = (rtEnd - rtStart) / count; +#ifdef DEBUG_TIME_RULER_LAYER + SVCERR << "zoomLevel = " << v->getZoomLevel() + << ", startFrame = " << startFrame << ", endFrame = " << endFrame + << ", rtStart = " << rtStart << ", rtEnd = " << rtEnd + << ", paint width = " << v->getPaintWidth() + << ", minPixelSpacing = " << minPixelSpacing + << ", count = " << count << ", rtGap = " << rtGap << endl; +#endif + int64_t incus; quarterTicks = false; @@ -197,6 +210,10 @@ if (us > 0) { incus *= 2; us /= 2; } } +#ifdef DEBUG_TIME_RULER_LAYER + SVCERR << "getMajorTickUSec: returning incus = " << incus << endl; +#endif + return incus; } @@ -277,7 +294,7 @@ // We always use the exact incus in our calculations for where to // draw the actual ticks or lines. - int minPixelSpacing = 50; + int minPixelSpacing = v->getXForViewX(50); sv_frame_t incFrame = lrint((double(incus) * sampleRate) / 1000000); int incX = int(round(v->getZoomLevel().framesToPixels(double(incFrame)))); int ticks = 10; diff -r e848ea0850fe -r 631897ba9fca layer/TimeValueLayer.cpp --- a/layer/TimeValueLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/TimeValueLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -60,6 +60,7 @@ m_editingPoint(0, 0.0, tr("New Point")), m_editingCommand(0), m_colourMap(0), + m_colourInverted(false), m_plotStyle(PlotConnectedPoints), m_verticalScale(AutoAlignScale), m_drawSegmentDivisions(true), @@ -199,14 +200,14 @@ } else if (name == "Draw Segment Division Lines") { if (min) *min = 0; - if (max) *max = 0; + if (max) *max = 1; if (deflt) *deflt = 1; val = (m_drawSegmentDivisions ? 1.0 : 0.0); } else if (name == "Show Derivative") { if (min) *min = 0; - if (max) *max = 0; + if (max) *max = 1; if (deflt) *deflt = 0; val = (m_derivative ? 1.0 : 0.0); @@ -223,7 +224,7 @@ int value) const { if (name == "Colour" && m_plotStyle == PlotSegmentation) { - return ColourMapper::getColourMapName(value); + return ColourMapper::getColourMapLabel(value); } else if (name == "Plot Type") { switch (value) { default: @@ -896,7 +897,7 @@ << max << ", log " << log << ", value " << val << endl; #endif - QColor solid = ColourMapper(m_colourMap, min, max).map(val); + QColor solid = ColourMapper(m_colourMap, m_colourInverted, min, max).map(val); return QColor(solid.red(), solid.green(), solid.blue(), 120); } @@ -1907,16 +1908,33 @@ TimeValueLayer::toXml(QTextStream &stream, QString indent, QString extraAttributes) const { - SingleColourLayer::toXml(stream, indent, - extraAttributes + - QString(" colourMap=\"%1\" plotStyle=\"%2\" verticalScale=\"%3\" scaleMinimum=\"%4\" scaleMaximum=\"%5\" drawDivisions=\"%6\" derivative=\"%7\" ") - .arg(m_colourMap) - .arg(m_plotStyle) - .arg(m_verticalScale) - .arg(m_scaleMinimum) - .arg(m_scaleMaximum) - .arg(m_drawSegmentDivisions ? "true" : "false") - .arg(m_derivative ? "true" : "false")); + QString s; + + s += QString("plotStyle=\"%1\" " + "verticalScale=\"%2\" " + "scaleMinimum=\"%3\" " + "scaleMaximum=\"%4\" " + "drawDivisions=\"%5\" " + "derivative=\"%6\" ") + .arg(m_plotStyle) + .arg(m_verticalScale) + .arg(m_scaleMinimum) + .arg(m_scaleMaximum) + .arg(m_drawSegmentDivisions ? "true" : "false") + .arg(m_derivative ? "true" : "false"); + + // New-style colour map attribute, by string id rather than by + // number + + s += QString("fillColourMap=\"%1\" ") + .arg(ColourMapper::getColourMapId(m_colourMap)); + + // Old-style colour map attribute + + s += QString("colourMap=\"%1\" ") + .arg(ColourMapper::getBackwardCompatibilityColourMap(m_colourMap)); + + SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s); } void @@ -1926,8 +1944,16 @@ bool ok, alsoOk; - int cmap = attributes.value("colourMap").toInt(&ok); - if (ok) setFillColourMap(cmap); + QString colourMapId = attributes.value("fillColourMap"); + int colourMap = ColourMapper::getColourMapById(colourMapId); + if (colourMap >= 0) { + setFillColourMap(colourMap); + } else { + colourMap = attributes.value("colourMap").toInt(&ok); + if (ok && colourMap < ColourMapper::getColourMapCount()) { + setFillColourMap(colourMap); + } + } PlotStyle style = (PlotStyle) attributes.value("plotStyle").toInt(&ok); diff -r e848ea0850fe -r 631897ba9fca layer/TimeValueLayer.h --- a/layer/TimeValueLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/TimeValueLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -184,6 +184,7 @@ SparseTimeValueModel::Point m_editingPoint; SparseTimeValueModel::EditCommand *m_editingCommand; int m_colourMap; + bool m_colourInverted; PlotStyle m_plotStyle; VerticalScale m_verticalScale; bool m_drawSegmentDivisions; diff -r e848ea0850fe -r 631897ba9fca layer/WaveformLayer.cpp --- a/layer/WaveformLayer.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/WaveformLayer.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -45,7 +45,6 @@ m_gain(1.0f), m_autoNormalize(false), m_showMeans(true), - m_greyscale(true), m_channelMode(SeparateChannels), m_channel(-1), m_scale(LinearScale), @@ -274,15 +273,6 @@ } void -WaveformLayer::setUseGreyscale(bool useGreyscale) -{ - if (m_greyscale == useGreyscale) return; - m_greyscale = useGreyscale; - m_cacheValid = false; - emit layerParametersChanged(); -} - -void WaveformLayer::setChannelMode(ChannelMode channelMode) { if (m_channelMode == channelMode) return; @@ -356,14 +346,14 @@ return true; } -int +double WaveformLayer::dBscale(double sample, int m) const { if (sample < 0.0) return dBscale(-sample, m); double dB = AudioLevel::multiplier_to_dB(sample); if (dB < -50.0) return 0; if (dB > 0.0) return m; - return int(((dB + 50.0) * m) / 50.0 + 0.1); + return ((dB + 50.0) * m) / 50.0; } int @@ -547,7 +537,7 @@ paint = &viewPainter; } - paint->setRenderHint(QPainter::Antialiasing, false); + paint->setRenderHint(QPainter::Antialiasing, true); if (m_middleLineHeight != 0.5) { paint->save(); @@ -663,7 +653,7 @@ if (mixingOrMerging) { if (minChannel != 0 || maxChannel != 0) { - SVCERR << "Internal error: min & max channels should be 0 when merging or mixing all channels" << endl; + throw std::logic_error("Internal error: min & max channels should be 0 when merging or mixing all channels"); } else if (m_model->getChannelCount() > 1) { ranges.push_back({}); m_model->getSummaries @@ -674,11 +664,24 @@ void WaveformLayer::getOversampledRanges(int minChannel, int maxChannel, - bool /* mixingOrMerging */, + bool mixingOrMerging, sv_frame_t frame0, sv_frame_t frame1, int oversampleBy, RangeVec &ranges) const { + if (mixingOrMerging) { + if (minChannel != 0 || maxChannel != 0) { + throw std::logic_error("Internal error: min & max channels should be 0 when merging or mixing all channels"); + } + if (m_model->getChannelCount() > 1) { + // call back on self for the individual channels with + // mixingOrMerging false + getOversampledRanges + (0, 1, false, frame0, frame1, oversampleBy, ranges); + return; + } + } + // These frame values, tail length, etc variables are at the model // sample rate, not the oversampled rate @@ -721,8 +724,6 @@ << ", from which returning " << rr.size() << " ranges" << endl; #endif } - - //!!! + channel modes return; } @@ -753,9 +754,8 @@ if (channels == 0) return; QColor baseColour = getBaseQColor(); - vector greys = getPartialShades(v); - QColor midColour = baseColour; + if (midColour == Qt::black) { midColour = Qt::gray; } else if (v->hasLightBackground()) { @@ -764,9 +764,6 @@ midColour = midColour.light(50); } - int prevRangeBottom = -1, prevRangeTop = -1; - QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour; - double gain = m_effectiveGains[ch]; int m = (h / channels) / 2; @@ -784,7 +781,8 @@ my = m + (((ch - minChannel) * h) / channels); } - paint->setPen(greys[1]); + // Horizontal axis along middle + paint->setPen(QPen(midColour, 0)); paint->drawLine(x0, my, x1, my); paintChannelScaleGuides(v, paint, rect, ch); @@ -797,6 +795,14 @@ (void)frame1; // not actually used #endif + QPainterPath waveformPath; + QPainterPath meanPath; + QPainterPath clipPath; + vector individualSamplePoints; + + bool firstPoint = true; + double prevRangeBottom = 0, prevRangeTop = 0; + for (int x = x0; x <= x1; ++x) { sv_frame_t f0, f1; @@ -852,7 +858,7 @@ continue; } - int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0; + double rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0; if (mergingChannels && ranges.size() > 1) { @@ -884,65 +890,61 @@ } } - int greyLevels = 1; - if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4; - switch (m_scale) { case LinearScale: - rangeBottom = int(double(m * greyLevels) * range.min() * gain); - rangeTop = int(double(m * greyLevels) * range.max() * gain); - meanBottom = int(double(-m) * range.absmean() * gain); - meanTop = int(double(m) * range.absmean() * gain); + rangeBottom = range.min() * gain * m; + rangeTop = range.max() * gain * m; + meanBottom = range.absmean() * gain * (-m); + meanTop = range.absmean() * gain * m; break; case dBScale: if (!mergingChannels) { - int db0 = dBscale(range.min() * gain, m); - int db1 = dBscale(range.max() * gain, m); - rangeTop = std::max(db0, db1); - meanTop = std::min(db0, db1); + double db0 = dBscale(range.min() * gain, m); + double db1 = dBscale(range.max() * gain, m); + rangeTop = std::max(db0, db1); + meanTop = std::min(db0, db1); if (mixingChannels) rangeBottom = meanTop; else rangeBottom = dBscale(range.absmean() * gain, m); - meanBottom = rangeBottom; + meanBottom = rangeBottom; } else { - rangeBottom = -dBscale(range.min() * gain, m * greyLevels); - rangeTop = dBscale(range.max() * gain, m * greyLevels); - meanBottom = -dBscale(range.absmean() * gain, m); - meanTop = dBscale(range.absmean() * gain, m); + rangeBottom = -dBscale(range.min() * gain, m); + rangeTop = dBscale(range.max() * gain, m); + meanBottom = -dBscale(range.absmean() * gain, m); + meanTop = dBscale(range.absmean() * gain, m); } break; case MeterScale: if (!mergingChannels) { - int r0 = abs(AudioLevel::multiplier_to_preview(range.min() * gain, m)); - int r1 = abs(AudioLevel::multiplier_to_preview(range.max() * gain, m)); - rangeTop = std::max(r0, r1); - meanTop = std::min(r0, r1); + double r0 = fabs(AudioLevel::multiplier_to_preview + (range.min() * gain, m)); + double r1 = fabs(AudioLevel::multiplier_to_preview + (range.max() * gain, m)); + rangeTop = std::max(r0, r1); + meanTop = std::min(r0, r1); if (mixingChannels) rangeBottom = meanTop; - else rangeBottom = AudioLevel::multiplier_to_preview(range.absmean() * gain, m); + else rangeBottom = AudioLevel::multiplier_to_preview + (range.absmean() * gain, m); meanBottom = rangeBottom; } else { - rangeBottom = -AudioLevel::multiplier_to_preview(range.min() * gain, m * greyLevels); - rangeTop = AudioLevel::multiplier_to_preview(range.max() * gain, m * greyLevels); - meanBottom = -AudioLevel::multiplier_to_preview(range.absmean() * gain, m); - meanTop = AudioLevel::multiplier_to_preview(range.absmean() * gain, m); + rangeBottom = -AudioLevel::multiplier_to_preview + (range.min() * gain, m); + rangeTop = AudioLevel::multiplier_to_preview + (range.max() * gain, m); + meanBottom = -AudioLevel::multiplier_to_preview + (range.absmean() * gain, m); + meanTop = AudioLevel::multiplier_to_preview + (range.absmean() * gain, m); } break; } - rangeBottom = my * greyLevels - rangeBottom; - rangeTop = my * greyLevels - rangeTop; - meanBottom = my - meanBottom; - meanTop = my - meanTop; - - int topFill = (rangeTop % greyLevels); - if (topFill > 0) topFill = greyLevels - topFill; - - int bottomFill = (rangeBottom % greyLevels); - - rangeTop = rangeTop / greyLevels; - rangeBottom = rangeBottom / greyLevels; + rangeBottom = my - rangeBottom; + rangeTop = my - rangeTop; + meanBottom = my - meanBottom; + meanTop = my - meanTop; bool clipped = false; @@ -951,96 +953,117 @@ if (rangeBottom < my - m) { rangeBottom = my - m; } if (rangeBottom > my + m) { rangeBottom = my + m; } - if (range.max() <= -1.0 || - range.max() >= 1.0) clipped = true; + if (range.max() <= -1.0 || range.max() >= 1.0) { + clipped = true; + } - if (meanBottom > rangeBottom) meanBottom = rangeBottom; - if (meanTop < rangeTop) meanTop = rangeTop; + bool drawMean = m_showMeans; - bool drawMean = m_showMeans; - if (meanTop == rangeTop) { - if (meanTop < meanBottom) ++meanTop; - else drawMean = false; + meanTop = meanTop - 0.5; + meanBottom = meanBottom + 0.5; + + if (meanTop <= rangeTop + 1.0) { + meanTop = rangeTop + 1.0; } - if (meanBottom == rangeBottom && m_scale == LinearScale) { - if (meanBottom > meanTop) --meanBottom; - else drawMean = false; + if (meanBottom >= rangeBottom - 1.0 && m_scale == LinearScale) { + meanBottom = rangeBottom - 1.0; } - - if (showIndividualSample) { - paint->setPen(baseColour); - paint->drawRect(x-1, rangeTop-1, 2, 2); - } - - if (x != x0 && prevRangeBottom != -1) { - if (prevRangeBottom > rangeBottom + 1 && - prevRangeTop > rangeBottom + 1) { -// paint->setPen(midColour); - paint->setPen(baseColour); - paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1); - paint->setPen(prevRangeTopColour); - paint->drawPoint(x-1, prevRangeTop); - } else if (prevRangeBottom < rangeTop - 1 && - prevRangeTop < rangeTop - 1) { -// paint->setPen(midColour); - paint->setPen(baseColour); - paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1); - paint->setPen(prevRangeBottomColour); - paint->drawPoint(x-1, prevRangeBottom); - } - } - - if (m_model->isReady()) { - if (clipped /*!!! || - range.min() * gain <= -1.0 || - range.max() * gain >= 1.0 */) { - paint->setPen(Qt::red); //!!! getContrastingColour - } else { - paint->setPen(baseColour); - } - } else { - paint->setPen(midColour); + if (meanTop > meanBottom - 1.0) { + drawMean = false; } #ifdef DEBUG_WAVEFORM_PAINT_BY_PIXEL SVCERR << "range " << rangeBottom << " -> " << rangeTop << ", means " << meanBottom << " -> " << meanTop << ", raw range " << range.min() << " -> " << range.max() << endl; #endif - if (rangeTop == rangeBottom) { - paint->drawPoint(x, rangeTop); - } else { - paint->drawLine(x, rangeBottom, x, rangeTop); + double rangeMiddle = (rangeTop + rangeBottom) / 2.0; + bool trivialRange = (fabs(rangeTop - rangeBottom) < 1.0); + double px = x + 0.5; + + if (showIndividualSample) { + individualSamplePoints.push_back(QPointF(px, rangeTop)); + if (!trivialRange) { + // common e.g. in "butterfly" merging mode + individualSamplePoints.push_back(QPointF(px, rangeBottom)); + } } - prevRangeTopColour = baseColour; - prevRangeBottomColour = baseColour; - - if (m_greyscale && (m_scale == LinearScale) && m_model->isReady()) { - if (!clipped) { - if (rangeTop < rangeBottom) { - if (topFill > 0 && - (!drawMean || (rangeTop < meanTop - 1))) { - paint->setPen(greys[topFill - 1]); - paint->drawPoint(x, rangeTop); - prevRangeTopColour = greys[topFill - 1]; - } - if (bottomFill > 0 && - (!drawMean || (rangeBottom > meanBottom + 1))) { - paint->setPen(greys[bottomFill - 1]); - paint->drawPoint(x, rangeBottom); - prevRangeBottomColour = greys[bottomFill - 1]; - } - } + bool contiguous = true; + if (rangeTop > prevRangeBottom + 0.5 || + rangeBottom < prevRangeTop - 0.5) { + contiguous = false; + } + + if (firstPoint || (contiguous && !trivialRange)) { + waveformPath.moveTo(QPointF(px, rangeTop)); + waveformPath.lineTo(QPointF(px, rangeBottom)); + waveformPath.moveTo(QPointF(px, rangeMiddle)); + } else { + waveformPath.lineTo(QPointF(px, rangeMiddle)); + if (!trivialRange) { + waveformPath.lineTo(QPointF(px, rangeTop)); + waveformPath.lineTo(QPointF(px, rangeBottom)); + waveformPath.lineTo(QPointF(px, rangeMiddle)); } } + + firstPoint = false; + prevRangeTop = rangeTop; + prevRangeBottom = rangeBottom; if (drawMean) { - paint->setPen(midColour); - paint->drawLine(x, meanBottom, x, meanTop); + meanPath.moveTo(QPointF(px, meanBottom)); + meanPath.lineTo(QPointF(px, meanTop)); } - - prevRangeBottom = rangeBottom; - prevRangeTop = rangeTop; + + if (clipped) { + if (trivialRange) { + clipPath.moveTo(QPointF(px, rangeMiddle)); + clipPath.lineTo(QPointF(px+1, rangeMiddle)); + } else { + clipPath.moveTo(QPointF(px, rangeBottom)); + clipPath.lineTo(QPointF(px, rangeTop)); + } + } + } + + double penWidth = 1.0; + + if (m_model->isReady()) { + paint->setPen(QPen(baseColour, penWidth)); + } else { + paint->setPen(QPen(midColour, penWidth)); + } + paint->drawPath(waveformPath); + + if (!clipPath.isEmpty()) { + paint->save(); + paint->setPen(QPen(ColourDatabase::getInstance()-> + getContrastingColour(m_colour), penWidth)); + paint->drawPath(clipPath); + paint->restore(); + } + + if (!meanPath.isEmpty()) { + paint->save(); + paint->setPen(QPen(midColour, penWidth)); + paint->drawPath(meanPath); + paint->restore(); + } + + if (!individualSamplePoints.empty()) { + double sz = PaintAssistant::scaleSize(2.0); + if (v->getZoomLevel().zone == ZoomLevel::PixelsPerFrame) { + if (v->getZoomLevel().level < 10) { + sz = PaintAssistant::scaleSize(1.2); + } + } + paint->save(); + paint->setPen(QPen(baseColour, penWidth)); + for (QPointF p: individualSamplePoints) { + paint->drawRect(QRectF(p.x() - sz/2, p.y() - sz/2, sz, sz)); + } + paint->restore(); } } @@ -1225,7 +1248,7 @@ break; case dBScale: - vy = dBscale(value, m); + vy = int(dBscale(value, m)); break; } @@ -1497,7 +1520,8 @@ "autoNormalize=\"%9\"") .arg(m_gain) .arg(m_showMeans) - .arg(m_greyscale) + .arg(true) // Option removed, but effectively always on, so + // retained in the session file for compatibility .arg(m_channelMode) .arg(m_channel) .arg(m_scale) @@ -1522,10 +1546,6 @@ attributes.value("showMeans") == "true"); setShowMeans(showMeans); - bool greyscale = (attributes.value("greyscale") == "1" || - attributes.value("greyscale") == "true"); - setUseGreyscale(greyscale); - ChannelMode channelMode = (ChannelMode) attributes.value("channelMode").toInt(&ok); if (ok) setChannelMode(channelMode); @@ -1541,7 +1561,7 @@ bool aggressive = (attributes.value("aggressive") == "1" || attributes.value("aggressive") == "true"); - setUseGreyscale(aggressive); + setAggressiveCacheing(aggressive); bool autoNormalize = (attributes.value("autoNormalize") == "1" || attributes.value("autoNormalize") == "true"); diff -r e848ea0850fe -r 631897ba9fca layer/WaveformLayer.h --- a/layer/WaveformLayer.h Fri Oct 05 10:25:52 2018 +0100 +++ b/layer/WaveformLayer.h Tue Nov 06 08:59:03 2018 +0000 @@ -87,19 +87,6 @@ void setShowMeans(bool); bool getShowMeans() const { return m_showMeans; } - /** - * Set whether to use shades of grey (or of the base colour) to - * provide additional perceived vertical resolution (i.e. using - * half-filled pixels to represent levels that only just meet the - * pixel unit boundary). This provides a small improvement in - * waveform quality at a small cost in rendering speed. - * - * The default is to use greyscale. - */ - void setUseGreyscale(bool); - bool getUseGreyscale() const { return m_greyscale; } - - enum ChannelMode { SeparateChannels, MixChannels, MergeChannels }; /** @@ -205,7 +192,7 @@ virtual bool canExistWithoutModel() const { return true; } protected: - int dBscale(double sample, int m) const; + double dBscale(double sample, int m) const; const RangeSummarisableTimeValueModel *m_model; // I do not own this @@ -247,7 +234,6 @@ float m_gain; bool m_autoNormalize; bool m_showMeans; - bool m_greyscale; ChannelMode m_channelMode; int m_channel; Scale m_scale; diff -r e848ea0850fe -r 631897ba9fca view/Pane.cpp --- a/view/Pane.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/view/Pane.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -62,9 +62,6 @@ //#define DEBUG_PANE - - - QCursor *Pane::m_measureCursor1 = 0; QCursor *Pane::m_measureCursor2 = 0; @@ -110,26 +107,11 @@ if (!isVisible()) return; -/* - int count = 0; - int currentLevel = 1; - int level = 1; - while (true) { - if (getZoomLevel() == level) currentLevel = count; - int newLevel = getZoomConstraintBlockSize(level + 1, - ZoomConstraint::RoundUp); - if (newLevel == level) break; - if (newLevel == 131072) break; //!!! just because - level = newLevel; - ++count; + Layer *layer = 0; + if (getLayerCount() > 0) { + layer = getLayer(getLayerCount() - 1); } - cerr << "Have " << count+1 << " zoom levels" << endl; -*/ - - Layer *layer = 0; - if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1); - if (!m_headsUpDisplay) { m_headsUpDisplay = new QFrame(this); @@ -199,70 +181,15 @@ connect(m_reset, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget())); } - int count = 0; - int current = 0; - ZoomLevel level; - - //!!! pull out into function (presumably in View) - bool haveConstraint = false; - for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); - ++i) { - if ((*i)->getZoomConstraint() && !(*i)->supportsOtherZoomLevels()) { - haveConstraint = true; - break; - } - } - - SVCERR << "haveConstraint = " << haveConstraint << endl; - - if (haveConstraint) { - while (true) { - //!!! this won't terminate if level is in the PixelsPerFrame zone - if (getZoomLevel() == level) current = count; - ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(), - ZoomConstraint::RoundUp); - SVCERR << "newLevel = " << newLevel << endl; - if (newLevel == level) break; - level = newLevel; - if (++count == 50) break; - } - } else { - // if we have no particular constraints, we can really spread out - //!!! this is nonsense in PixelsPerFrame zone - while (true) { - using namespace std::rel_ops; - if (getZoomLevel() >= level) current = count; - int step = level.level / 10; - int pwr = 0; - while (step > 0) { - ++pwr; - step /= 2; - } - step = 1; - while (pwr > 0) { - step *= 2; - --pwr; - } - cerr << level.level << ", step " << step << endl; - level.level += step; - if (++count == 100 || level.level > 262144) break; - } - } - - //!!! - SVCERR << "Have " << count << " zoom levels" << endl; - - m_hthumb->setMinimumValue(0); + int count = countZoomLevels(); + int current = getZoomLevelIndex(getZoomLevel()); + + m_hthumb->setMinimumValue(1); m_hthumb->setMaximumValue(count); m_hthumb->setValue(count - current); -// cerr << "set value to " << count-current << endl; - -// cerr << "default value is " << m_hthumb->getDefaultValue() << endl; - - if (count != 50 && m_hthumb->getDefaultValue() == 0) { + if (m_hthumb->getDefaultValue() == 0) { m_hthumb->setDefaultValue(count - current); -// cerr << "set default value to " << m_hthumb->getDefaultValue() << endl; } bool haveVThumb = false; @@ -2441,53 +2368,7 @@ void Pane::horizontalThumbwheelMoved(int value) { - //!!! dupe with updateHeadsUpDisplay - - int count = 0; - ZoomLevel level; - - //!!! pull out into function (presumably in View) - bool haveConstraint = false; - for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); - ++i) { - if ((*i)->getZoomConstraint() && !(*i)->supportsOtherZoomLevels()) { - haveConstraint = true; - break; - } - } - - if (haveConstraint) { - while (true) { - //!!! this won't terminate if level is in the PixelsPerFrame zone - if (m_hthumb->getMaximumValue() - value == count) break; - ZoomLevel newLevel = getZoomConstraintLevel(level.incremented(), - ZoomConstraint::RoundUp); - if (newLevel == level) break; - level = newLevel; - if (++count == 50) break; - } - } else { - //!!! this is nonsense in PixelsPerFrame zone - while (true) { - if (m_hthumb->getMaximumValue() - value == count) break; - int step = level.level / 10; - int pwr = 0; - while (step > 0) { - ++pwr; - step /= 2; - } - step = 1; - while (pwr > 0) { - step *= 2; - --pwr; - } -// cerr << level << endl; - level.level += step; - if (++count == 100 || level.level > 262144) break; - } - } - -// cerr << "new level is " << level << endl; + ZoomLevel level = getZoomLevelByIndex(m_hthumb->getMaximumValue() - value); setZoomLevel(level); } diff -r e848ea0850fe -r 631897ba9fca view/Pane.h --- a/view/Pane.h Fri Oct 05 10:25:52 2018 +0100 +++ b/view/Pane.h Tue Nov 06 08:59:03 2018 +0000 @@ -13,8 +13,8 @@ COPYING included with this distribution for more information. */ -#ifndef _PANE_H_ -#define _PANE_H_ +#ifndef SV_PANE_H +#define SV_PANE_H #include #include @@ -212,7 +212,7 @@ bool m_playbackFrameMoveScheduled; sv_frame_t m_playbackFrameMoveTo; - + static QCursor *m_measureCursor1; static QCursor *m_measureCursor2; }; diff -r e848ea0850fe -r 631897ba9fca view/PaneStack.cpp --- a/view/PaneStack.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/view/PaneStack.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -286,7 +286,9 @@ void PaneStack::deletePane(Pane *pane) { - cerr << "PaneStack::deletePane(" << pane << ")" << endl; +#ifdef DEBUG_PANE_STACK + SVCERR << "PaneStack::deletePane(" << pane << ")" << endl; +#endif std::vector::iterator i; bool found = false; @@ -322,7 +324,9 @@ emit paneAboutToBeDeleted(pane); unlinkAlignmentViews(); - cerr << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl; +#ifdef DEBUG_PANE_STACK + SVCERR << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl; +#endif // The property stack associated with the parent was initially // created with the same parent as it, so it would be deleted when @@ -353,7 +357,9 @@ void PaneStack::showOrHidePaneAccessories() { - cerr << "PaneStack::showOrHidePaneAccessories: count == " << getPaneCount() << endl; +#ifdef DEBUG_PANE_STACK + SVCERR << "PaneStack::showOrHidePaneAccessories: count == " << getPaneCount() << endl; +#endif bool multi = (getPaneCount() > 1); for (std::vector::iterator i = m_panes.begin(); @@ -407,7 +413,7 @@ relinkAlignmentViews(); - cerr << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl; + SVCERR << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl; } void @@ -433,7 +439,7 @@ relinkAlignmentViews(); - cerr << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl; + SVCERR << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl; } void @@ -472,7 +478,7 @@ m_currentPane = pane; emit currentPaneChanged(m_currentPane); } else { - cerr << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl; + SVCERR << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl; } } diff -r e848ea0850fe -r 631897ba9fca view/View.cpp --- a/view/View.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/view/View.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -20,13 +20,14 @@ #include "base/Profiler.h" #include "base/Pitch.h" #include "base/Preferences.h" +#include "base/HitCount.h" #include "ViewProxy.h" #include "layer/TimeRulerLayer.h" #include "layer/SingleColourLayer.h" #include "layer/PaintAssistant.h" -#include "data/model/PowerOfSqrtTwoZoomConstraint.h" +#include "data/model/RelativelyFineZoomConstraint.h" #include "data/model/RangeSummarisableTimeValueModel.h" #include "widgets/IconLoader.h" @@ -63,6 +64,7 @@ m_showProgress(showProgress), m_cache(0), m_buffer(0), + m_cacheValid(false), m_cacheCentreFrame(0), m_cacheZoomLevel(ZoomLevel::FramesPerPixel, 1024), m_selectionCached(false), @@ -260,8 +262,7 @@ return; } - delete m_cache; - m_cache = 0; + m_cacheValid = false; Layer *selectedLayer = 0; @@ -293,8 +294,7 @@ void View::overlayModeChanged() { - delete m_cache; - m_cache = 0; + m_cacheValid = false; update(); } @@ -340,7 +340,7 @@ if (m_zoomLevel.zone == ZoomLevel::PixelsPerFrame) { -#ifdef DEBUG_VIEW_WIDGET_PAINT +#ifdef DEBUG_VIEW SVCERR << "View(" << this << ")::setCentreFrame: in PixelsPerFrame zone, so change must be visible" << endl; #endif update(); @@ -353,11 +353,18 @@ if (newPixel != formerPixel) { -#ifdef DEBUG_VIEW_WIDGET_PAINT +#ifdef DEBUG_VIEW SVCERR << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl; #endif // ensure the centre frame is a multiple of the zoom level m_centreFrame = sv_frame_t(newPixel) * m_zoomLevel.level; + +#ifdef DEBUG_VIEW + SVCERR << "View(" << this + << ")::setCentreFrame: centre frame rounded to " + << m_centreFrame << " (zoom level is " + << m_zoomLevel.level << ")" << endl; +#endif update(); changeVisible = true; @@ -365,11 +372,12 @@ } if (e) { - sv_frame_t rf = alignToReference(f); + sv_frame_t rf = alignToReference(m_centreFrame); #ifdef DEBUG_VIEW cerr << "View[" << this << "]::setCentreFrame(" << f - << "): emitting centreFrameChanged(" - << rf << ")" << endl; + << "): m_centreFrame = " << m_centreFrame + << ", emitting centreFrameChanged with aligned frame " + << rf << endl; #endif emit centreFrameChanged(rf, m_followPan, m_followPlay); } @@ -427,7 +435,7 @@ result = fdiff + m_centreFrame; } -#ifdef DEBUG_VIEW +#ifdef DEBUG_VIEW_WIDGET_PAINT if (x == 0) { SVCERR << "getFrameForX(" << x << "): diff = " << diff << ", fdiff = " << fdiff << ", m_centreFrame = " << m_centreFrame @@ -617,8 +625,7 @@ void View::addLayer(Layer *layer) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; SingleColourLayer *scl = dynamic_cast(layer); if (scl) scl->setDefaultColourFor(this); @@ -689,8 +696,7 @@ return; } - delete m_cache; - m_cache = 0; + m_cacheValid = false; for (LayerList::iterator i = m_fixedOrderLayers.begin(); i != m_fixedOrderLayers.end(); @@ -907,8 +913,7 @@ } if (recreate) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; } emit layerModelChanged(); @@ -955,8 +960,7 @@ } if (recreate) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; } if (startFrame < myStartFrame) startFrame = myStartFrame; @@ -991,9 +995,7 @@ #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::modelReplaced()" << endl; #endif - delete m_cache; - m_cache = 0; - + m_cacheValid = false; update(); } @@ -1006,8 +1008,7 @@ SVDEBUG << "View::layerParametersChanged()" << endl; #endif - delete m_cache; - m_cache = 0; + m_cacheValid = false; update(); if (layer) { @@ -1203,8 +1204,7 @@ View::selectionChanged() { if (m_selectionCached) { - delete m_cache; - m_cache = 0; + m_cacheValid = false; m_selectionCached = false; } update(); @@ -1469,32 +1469,98 @@ { using namespace std::rel_ops; - ZoomLevel candidate = zoomLevel; - bool haveCandidate = false; - - PowerOfSqrtTwoZoomConstraint defaultZoomConstraint; - - for (auto i = m_layerStack.begin(); i != m_layerStack.end(); ++i) { - - const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint(); - if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint; - + ZoomLevel candidate = + RelativelyFineZoomConstraint().getNearestZoomLevel(zoomLevel, dir); + + for (auto i : m_layerStack) { + + if (i->supportsOtherZoomLevels() || !(i->getZoomConstraint())) { + continue; + } + ZoomLevel thisLevel = - zoomConstraint->getNearestZoomLevel(zoomLevel, dir); + i->getZoomConstraint()->getNearestZoomLevel(zoomLevel, dir); // Go for the block size that's furthest from the one // passed in. Most of the time, that's what we want. - if (!haveCandidate || - (thisLevel > zoomLevel && thisLevel > candidate) || + if ((thisLevel > zoomLevel && thisLevel > candidate) || (thisLevel < zoomLevel && thisLevel < candidate)) { candidate = thisLevel; - haveCandidate = true; } } return candidate; } +int +View::countZoomLevels() const +{ + int n = 0; + ZoomLevel min = ZoomConstraint().getMinZoomLevel(); + ZoomLevel max = ZoomConstraint().getMaxZoomLevel(); + ZoomLevel level = min; + while (true) { + ++n; + if (level == max) { + break; + } + level = getZoomConstraintLevel + (level.incremented(), ZoomConstraint::RoundUp); + } +// cerr << "View::countZoomLevels: " << n << endl; + return n; +} + +ZoomLevel +View::getZoomLevelByIndex(int ix) const +{ + int n = 0; + ZoomLevel min = ZoomConstraint().getMinZoomLevel(); + ZoomLevel max = ZoomConstraint().getMaxZoomLevel(); + ZoomLevel level = min; + while (true) { + if (n == ix) { +// cerr << "View::getZoomLevelByIndex: " << ix << " -> " << level +// << endl; + return level; + } + ++n; + if (level == max) { + break; + } + level = getZoomConstraintLevel + (level.incremented(), ZoomConstraint::RoundUp); + } +// cerr << "View::getZoomLevelByIndex: " << ix << " -> " << max << " (max)" +// << endl; + return max; +} + +int +View::getZoomLevelIndex(ZoomLevel z) const +{ + int n = 0; + ZoomLevel min = ZoomConstraint().getMinZoomLevel(); + ZoomLevel max = ZoomConstraint().getMaxZoomLevel(); + ZoomLevel level = min; + while (true) { + if (z == level) { +// cerr << "View::getZoomLevelIndex: " << z << " -> " << n +// << endl; + return n; + } + ++n; + if (level == max) { + break; + } + level = getZoomConstraintLevel + (level.incremented(), ZoomConstraint::RoundUp); + } +// cerr << "View::getZoomLevelIndex: " << z << " -> " << n << " (max)" +// << endl; + return n; +} + bool View::areLayerColoursSignificant() const { @@ -1546,6 +1612,12 @@ } if (right) delta = -delta; +#ifdef DEBUG_VIEW + SVCERR << "View::scroll(" << right << ", " << lots << ", " << e << "): " + << "delta = " << delta << ", m_centreFrame = " << m_centreFrame + << endl; +#endif + if (m_centreFrame < delta) { setCentreFrame(0, e); } else if (m_centreFrame - delta >= getModelsEndFrame()) { @@ -1734,98 +1806,93 @@ } // ensure our constraints are met - -/*!!! Should we do this only if we have layers that can't support other - zoom levels? - - m_zoomLevel = getZoomConstraintBlockSize(m_zoomLevel, - ZoomConstraint::RoundUp); -*/ - - QPainter paint; - bool repaintCache = false; - bool paintedCacheRect = false; - - QRect cacheRect(rect()); - + m_zoomLevel = getZoomConstraintLevel + (m_zoomLevel, ZoomConstraint::RoundNearest); + + // We have a cache, which retains the state of scrollable (back) + // layers from one paint to the next, and a buffer, which we paint + // onto before copying directly to the widget. Both are at scaled + // resolution (e.g. 2x on a pixel-doubled display), whereas the + // paint event always comes in at formal (1x) resolution. + + // If we touch the cache, we always leave it in a valid state + // across its whole extent. When another method invalidates the + // cache, it does so by setting m_cacheValid false, so if that + // flag is true on entry, then the cache is valid across its whole + // extent - although it may be valid for a different centre frame, + // zoom level, or view size from those now in effect. + + // Our process goes: + // + // 1. Check whether we have any scrollable (cacheable) layers. If + // we don't, then invalidate and ignore the cache and go to + // step 5. Otherwise: + // + // 2. Check the cache, scroll as necessary, identify any area that + // needs to be refreshed (this might be the whole cache). + // + // 3. Paint to cache the area that needs to be refreshed, from the + // stack of scrollable layers. + // + // 4. Paint to buffer from cache: if there are no non-cached areas + // or selections and the cache has not scrolled, then paint the + // union of the area of cache that has changed and the area + // that the paint event reported as exposed; otherwise paint + // the whole. + // + // 5. Paint the exposed area to the buffer from the cache plus all + // the layers that haven't been cached, plus selections etc. + // + // 6. Paint the exposed rect from the buffer. + // + // Note that all rects except the target for the final step are at + // cache (scaled, 2x as applicable) resolution. + + int dpratio = effectiveDevicePixelRatio(); + + QRect requestedPaintArea(scaledRect(rect(), dpratio)); if (e) { - cacheRect &= e->rect(); -#ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height() - << ", my rect " << width() << "x" << height() << endl; -#endif + // cut down to only the area actually exposed + requestedPaintArea &= scaledRect(e->rect(), dpratio); } - QRect nonCacheRect(cacheRect); - - int dpratio = effectiveDevicePixelRatio(); - // If not all layers are scrollable, but some of the back layers // are, we should store only those in the cache. bool layersChanged = false; LayerList scrollables = getScrollableBackLayers(true, layersChanged); LayerList nonScrollables = getNonScrollableFrontLayers(true, layersChanged); - bool selectionCacheable = nonScrollables.empty(); - bool haveSelections = m_manager && !m_manager->getSelections().empty(); - - // If all the non-scrollable layers are non-opaque, then we draw - // the selection rectangle behind them and cache it. If any are - // opaque, however, or if our device-pixel ratio is not 1 (so we - // need to paint direct to the widget), then we can't cache. - // - if (dpratio == 1) { - - if (!selectionCacheable) { - selectionCacheable = true; - for (LayerList::const_iterator i = nonScrollables.begin(); - i != nonScrollables.end(); ++i) { - if ((*i)->isLayerOpaque()) { - selectionCacheable = false; - break; - } - } - } - - if (selectionCacheable) { - QPoint localPos; - bool closeToLeft, closeToRight; - if (shouldIlluminateLocalSelection - (localPos, closeToLeft, closeToRight)) { - selectionCacheable = false; - } - } - - } else { - - selectionCacheable = false; - } #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: have " << scrollables.size() << " scrollable back layers and " << nonScrollables.size() << " non-scrollable front layers" << endl; - cerr << "haveSelections " << haveSelections << ", selectionCacheable " - << selectionCacheable << ", m_selectionCached " << m_selectionCached << endl; #endif - if (layersChanged || scrollables.empty() || - (haveSelections && (selectionCacheable != m_selectionCached))) { - delete m_cache; - m_cache = 0; - m_selectionCached = false; + if (layersChanged || scrollables.empty()) { + m_cacheValid = false; } - QSize scaledCacheSize(scaledSize(size(), dpratio)); - QRect scaledCacheRect(scaledRect(cacheRect, dpratio)); - - if (!m_buffer || scaledCacheSize != m_buffer->size()) { + QRect wholeArea(scaledRect(rect(), dpratio)); + QSize wholeSize(scaledSize(size(), dpratio)); + + if (!m_buffer || wholeSize != m_buffer->size()) { delete m_buffer; - m_buffer = new QPixmap(scaledCacheSize); + m_buffer = new QPixmap(wholeSize); } + + bool shouldUseCache = false; + bool shouldRepaintCache = false; + QRect cacheAreaToRepaint; + static HitCount count("View cache"); + if (!scrollables.empty()) { + shouldUseCache = true; + shouldRepaintCache = true; + cacheAreaToRepaint = wholeArea; + #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << "): cache " << m_cache << ", cache zoom " << m_cacheZoomLevel << ", zoom " << m_zoomLevel << endl; @@ -1833,209 +1900,185 @@ using namespace std::rel_ops; - if (!m_cache || + if (!m_cacheValid || + !m_cache || m_cacheZoomLevel != m_zoomLevel || - scaledCacheSize != m_cache->size()) { - - // cache is not valid - - if (cacheRect.width() < width()/10) { - delete m_cache; - m_cache = 0; + m_cache->size() != wholeSize) { + + // cache is not valid at all + + if (requestedPaintArea.width() < wholeSize.width() / 10) { + + m_cacheValid = false; + shouldUseCache = false; + shouldRepaintCache = false; + #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << endl; + cerr << "View(" << this << ")::paintEvent: cache is invalid but only small area requested, will repaint directly instead" << endl; #endif } else { - delete m_cache; - m_cache = new QPixmap(scaledCacheSize); + + if (!m_cache || + m_cache->size() != wholeSize) { + delete m_cache; + m_cache = new QPixmap(wholeSize); + } + #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "View(" << this << ")::paintEvent: recreated cache" << endl; + cerr << "View(" << this << ")::paintEvent: cache is invalid, will repaint whole" << endl; #endif - cacheRect = rect(); - repaintCache = true; } + count.miss(); + } else if (m_cacheCentreFrame != m_centreFrame) { - int dx = - getXForFrame(m_cacheCentreFrame) - - getXForFrame(m_centreFrame); - - if (dx > -width() && dx < width()) { - static QPixmap *tmpPixmap = 0; - if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) { - delete tmpPixmap; - tmpPixmap = new QPixmap(scaledCacheSize); + int dx = dpratio * (getXForFrame(m_cacheCentreFrame) - + getXForFrame(m_centreFrame)); + + if (dx > -m_cache->width() && dx < m_cache->width()) { + + m_cache->scroll(dx, 0, m_cache->rect(), 0); + + if (dx < 0) { + cacheAreaToRepaint = + QRect(m_cache->width() + dx, 0, -dx, m_cache->height()); + } else { + cacheAreaToRepaint = + QRect(0, 0, dx, m_cache->height()); } - paint.begin(tmpPixmap); - paint.drawPixmap(0, 0, *m_cache); - paint.end(); - paint.begin(m_cache); - paint.drawPixmap(dx, 0, *tmpPixmap); - paint.end(); - if (dx < 0) { - cacheRect = QRect(width() + dx, 0, -dx, height()); - } else { - cacheRect = QRect(0, 0, dx, height()); - } + + count.partial(); + #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << endl; #endif } else { - cacheRect = rect(); + count.miss(); #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: scrolling too far" << endl; #endif } - repaintCache = true; } else { #ifdef DEBUG_VIEW_WIDGET_PAINT cerr << "View(" << this << ")::paintEvent: cache is good" << endl; #endif - paint.begin(m_buffer); - paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect); - paint.end(); - QFrame::paintEvent(e); - paintedCacheRect = true; + count.hit(); + shouldRepaintCache = false; } - + } + +#ifdef DEBUG_VIEW_WIDGET_PAINT + cerr << "View(" << this << ")::paintEvent: m_cacheValid = " << m_cacheValid << ", shouldUseCache = " << shouldUseCache << ", shouldRepaintCache = " << shouldRepaintCache << ", cacheAreaToRepaint = " << cacheAreaToRepaint.x() << "," << cacheAreaToRepaint.y() << " " << cacheAreaToRepaint.width() << "x" << cacheAreaToRepaint.height() << endl; +#endif + + if (shouldRepaintCache && !shouldUseCache) { + // If we are repainting the cache, then we paint the + // scrollables only to the cache, not to the buffer. So if + // shouldUseCache is also false, then the scrollables can't + // appear because they will only be on the cache + throw std::logic_error("ERROR: shouldRepaintCache is true, but shouldUseCache is false: this can't lead to the correct result"); + } + + // Scrollable (cacheable) items first. If we are repainting the + // cache, then we paint these to the cache; otherwise straight to + // the buffer. + + ViewProxy proxy(this, dpratio); + QRect areaToPaint; + QPainter paint; + + if (shouldRepaintCache) { + paint.begin(m_cache); + areaToPaint = cacheAreaToRepaint; + } else { + paint.begin(m_buffer); + areaToPaint = requestedPaintArea; + } + + setPaintFont(paint); + paint.setClipRect(areaToPaint); + + paint.setPen(getBackground()); + paint.setBrush(getBackground()); + paint.drawRect(areaToPaint); + + paint.setPen(getForeground()); + paint.setBrush(Qt::NoBrush); + + for (LayerList::iterator i = scrollables.begin(); + i != scrollables.end(); ++i) { + + paint.setRenderHint(QPainter::Antialiasing, false); + paint.save(); + +#ifdef DEBUG_VIEW_WIDGET_PAINT + cerr << "Painting scrollable layer " << *i << " using proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", areaToPaint = " << areaToPaint.x() << "," << areaToPaint.y() << " " << areaToPaint.width() << "x" << areaToPaint.height() << endl; +#endif + + (*i)->paint(&proxy, paint, areaToPaint); + + paint.restore(); + } + + paint.end(); + + if (shouldRepaintCache) { + // and now we have + m_cacheValid = true; m_cacheCentreFrame = m_centreFrame; m_cacheZoomLevel = m_zoomLevel; } -#ifdef DEBUG_VIEW_WIDGET_PAINT -// cerr << "View(" << this << ")::paintEvent: cacheRect " << cacheRect << ", nonCacheRect " << (nonCacheRect | cacheRect) << ", repaintCache " << repaintCache << ", paintedCacheRect " << paintedCacheRect << endl; -#endif - - // Scrollable (cacheable) items first - - ViewProxy proxy(this, dpratio); - - if (!paintedCacheRect) { - - QRect rectToPaint; - - if (repaintCache) { - paint.begin(m_cache); - rectToPaint = scaledCacheRect; - } else { - paint.begin(m_buffer); - rectToPaint = scaledCacheRect; - } - - setPaintFont(paint); - paint.setClipRect(rectToPaint); - - paint.setPen(getBackground()); - paint.setBrush(getBackground()); - paint.drawRect(rectToPaint); - - paint.setPen(getForeground()); - paint.setBrush(Qt::NoBrush); - - for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) { - paint.setRenderHint(QPainter::Antialiasing, false); - paint.save(); -#ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "Painting scrollable layer " << *i << " using proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << rectToPaint.x() << "," << rectToPaint.y() << " " << rectToPaint.width() << "x" << rectToPaint.height() << endl; -#endif - (*i)->paint(&proxy, paint, rectToPaint); - paint.restore(); - } - - if (haveSelections && selectionCacheable) { - drawSelections(paint); - m_selectionCached = repaintCache; - } - + if (shouldUseCache) { + paint.begin(m_buffer); + paint.drawPixmap(requestedPaintArea, *m_cache, requestedPaintArea); paint.end(); - - if (repaintCache) { - cacheRect |= (e ? e->rect() : rect()); - scaledCacheRect = scaledRect(cacheRect, dpratio); - paint.begin(m_buffer); - paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect); - paint.end(); - } } - // Now non-cacheable items. We always need to redraw the - // non-cacheable items across at least the area we drew of the - // cacheable items. - - nonCacheRect |= cacheRect; - - QRect scaledNonCacheRect = scaledRect(nonCacheRect, dpratio); - + // Now non-cacheable items. + paint.begin(m_buffer); - paint.setClipRect(scaledNonCacheRect); + paint.setClipRect(requestedPaintArea); setPaintFont(paint); if (scrollables.empty()) { paint.setPen(getBackground()); paint.setBrush(getBackground()); - paint.drawRect(scaledNonCacheRect); + paint.drawRect(requestedPaintArea); } paint.setPen(getForeground()); paint.setBrush(Qt::NoBrush); - for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) { + for (LayerList::iterator i = nonScrollables.begin(); + i != nonScrollables.end(); ++i) { + // Profiler profiler2("View::paintEvent non-cacheable"); #ifdef DEBUG_VIEW_WIDGET_PAINT - cerr << "Painting non-scrollable layer " << *i << " without proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << nonCacheRect.x() << "," << nonCacheRect.y() << " " << nonCacheRect.width() << "x" << nonCacheRect.height() << endl; + cerr << "Painting non-scrollable layer " << *i << " without proxy with shouldRepaintCache = " << shouldRepaintCache << ", dpratio = " << dpratio << ", requestedPaintArea = " << requestedPaintArea.x() << "," << requestedPaintArea.y() << " " << requestedPaintArea.width() << "x" << requestedPaintArea.height() << endl; #endif - (*i)->paint(&proxy, paint, scaledNonCacheRect); + (*i)->paint(&proxy, paint, requestedPaintArea); } paint.end(); - - paint.begin(this); - QRect finalPaintRect = e ? e->rect() : rect(); - paint.drawPixmap(finalPaintRect, *m_buffer, scaledRect(finalPaintRect, dpratio)); - paint.end(); + + // Now paint to widget from buffer: target rects from here on, + // unlike all the preceding, are at formal (1x) resolution paint.begin(this); setPaintFont(paint); if (e) paint.setClipRect(e->rect()); - if (!m_selectionCached) { - drawSelections(paint); - } + + QRect finalPaintRect = e ? e->rect() : rect(); + paint.drawPixmap(finalPaintRect, *m_buffer, + scaledRect(finalPaintRect, dpratio)); + + drawSelections(paint); + drawPlayPointer(paint); + paint.end(); - bool showPlayPointer = true; - if (m_followPlay == PlaybackScrollContinuous) { - showPlayPointer = false; - } else if (m_playPointerFrame <= getStartFrame() || - m_playPointerFrame >= getEndFrame()) { - showPlayPointer = false; - } else if (m_manager && !m_manager->isPlaying()) { - if (m_playPointerFrame == getCentreFrame() && - m_manager->shouldShowCentreLine() && - m_followPlay != PlaybackIgnore) { - // Don't show the play pointer when it is redundant with - // the centre line - showPlayPointer = false; - } - } - - if (showPlayPointer) { - - paint.begin(this); - - int playx = getXForFrame(m_playPointerFrame); - - paint.setPen(getForeground()); - paint.drawLine(playx - 1, 0, playx - 1, height() - 1); - paint.drawLine(playx + 1, 0, playx + 1, height() - 1); - paint.drawPoint(playx, 0); - paint.drawPoint(playx, height() - 1); - paint.setPen(getBackground()); - paint.drawLine(playx, 1, playx, height() - 2); - - paint.end(); - } - QFrame::paintEvent(e); } @@ -2197,6 +2240,40 @@ } void +View::drawPlayPointer(QPainter &paint) +{ + bool showPlayPointer = true; + + if (m_followPlay == PlaybackScrollContinuous) { + showPlayPointer = false; + } else if (m_playPointerFrame <= getStartFrame() || + m_playPointerFrame >= getEndFrame()) { + showPlayPointer = false; + } else if (m_manager && !m_manager->isPlaying()) { + if (m_playPointerFrame == getCentreFrame() && + m_manager->shouldShowCentreLine() && + m_followPlay != PlaybackIgnore) { + // Don't show the play pointer when it is redundant with + // the centre line + showPlayPointer = false; + } + } + + if (showPlayPointer) { + + int playx = getXForFrame(m_playPointerFrame); + + paint.setPen(getForeground()); + paint.drawLine(playx - 1, 0, playx - 1, height() - 1); + paint.drawLine(playx + 1, 0, playx + 1, height() - 1); + paint.drawPoint(playx, 0); + paint.drawPoint(playx, height() - 1); + paint.setPen(getBackground()); + paint.drawLine(playx, 1, playx, height() - 2); + } +} + +void View::drawMeasurementRect(QPainter &paint, const Layer *topLayer, QRect r, bool focus) const { diff -r e848ea0850fe -r 631897ba9fca view/View.h --- a/view/View.h Fri Oct 05 10:25:52 2018 +0100 +++ b/view/View.h Tue Nov 06 08:59:03 2018 +0000 @@ -440,6 +440,7 @@ virtual void paintEvent(QPaintEvent *e); virtual void drawSelections(QPainter &); virtual bool shouldLabelSelections() const { return true; } + virtual void drawPlayPointer(QPainter &); virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1); virtual void setPaintFont(QPainter &paint); @@ -457,10 +458,16 @@ bool areLayersScrollable() const; LayerList getScrollableBackLayers(bool testChanged, bool &changed) const; LayerList getNonScrollableFrontLayers(bool testChanged, bool &changed) const; + ZoomLevel getZoomConstraintLevel(ZoomLevel level, ZoomConstraint::RoundingDirection dir = ZoomConstraint::RoundNearest) const; + // These three are slow, intended for indexing GUI thumbwheel stuff + int countZoomLevels() const; + int getZoomLevelIndex(ZoomLevel level) const; + ZoomLevel getZoomLevelByIndex(int ix) const; + // True if the top layer(s) use colours for meaningful things. If // this is the case, selections will be shown using unfilled boxes // rather than with a translucent fill. @@ -493,6 +500,7 @@ QPixmap *m_cache; // I own this QPixmap *m_buffer; // I own this + bool m_cacheValid; sv_frame_t m_cacheCentreFrame; ZoomLevel m_cacheZoomLevel; bool m_selectionCached; diff -r e848ea0850fe -r 631897ba9fca widgets/ColourMapComboBox.cpp --- a/widgets/ColourMapComboBox.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/widgets/ColourMapComboBox.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -58,9 +58,9 @@ if (size < 12) size = 12; for (int i = 0; i < ColourMapper::getColourMapCount(); ++i) { - QString name = ColourMapper::getColourMapName(i); + QString name = ColourMapper::getColourMapLabel(i); if (m_includeSwatches) { - ColourMapper mapper(i, 0.0, 1.0); + ColourMapper mapper(i, false, 0.0, 1.0); addItem(mapper.getExamplePixmap(QSize(size * 2, size)), name); } else { addItem(name); diff -r e848ea0850fe -r 631897ba9fca widgets/SubdividingMenu.cpp --- a/widgets/SubdividingMenu.cpp Fri Oct 05 10:25:52 2018 +0100 +++ b/widgets/SubdividingMenu.cpp Tue Nov 06 08:59:03 2018 +0000 @@ -85,7 +85,7 @@ return QString::localeAwareCompare(s1, s2) < 0; }; - set sortedEntries(comparator); + set sortedEntries(comparator); sortedEntries.insert(entries.begin(), entries.end()); for (auto j = sortedEntries.begin(); j != sortedEntries.end(); ++j) { @@ -180,7 +180,7 @@ auto comparator = [](QString s1, QString s2) -> bool { return QString::localeAwareCompare(s1, s2) < 0; }; - set sortedEntries(comparator); + set sortedEntries(comparator); for (auto i: m_pendingEntries) { sortedEntries.insert(i.first); }