File indexing completed on 2024-05-05 05:51:12

0001 /***********************************************************************
0002  * SPDX-FileCopyrightText: 2003-2004 Max Howell <max.howell@methylblue.com>
0003  * SPDX-FileCopyrightText: 2008-2009 Martin Sandsmark <martin.sandsmark@kde.org>
0004  * SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007  ***********************************************************************/
0008 
0009 #include "filelight_debug.h"
0010 
0011 #include <array>
0012 #include <utility>
0013 
0014 #ifdef Q_OS_WINDOWS
0015 #include <winrt/Windows.UI.ViewManagement.h>
0016 #pragma comment(lib, "windowsapp")
0017 #endif
0018 
0019 #include <QApplication> //make()
0020 #include <QFont> //ctor
0021 #include <QFontMetrics> //ctor
0022 #include <QImage> //make() & paint()
0023 #include <QPainter>
0024 
0025 #include <KColorScheme>
0026 #include <KLocalizedString>
0027 
0028 #include "map.h"
0029 #include "radialMap.h" // defines
0030 #include "sincos.h"
0031 
0032 RadialMap::Map::Map()
0033     : m_visibleDepth(DEFAULT_RING_DEPTH)
0034     , m_ringBreadth(MIN_RING_BREADTH)
0035     , m_innerRadius(0)
0036 {
0037     // FIXME this is all broken. No longer is a maximum depth!
0038     const int fmh = QFontMetrics(QFont()).height();
0039     const int fmhD4 = fmh / 4;
0040     MAP_2MARGIN = 2 * (fmh - (fmhD4 - LABEL_MAP_SPACER)); // margin is dependent on fitting in labels at top and bottom
0041     // Initialize breadth
0042     resize(QRectF());
0043 
0044     connect(qGuiApp, &QGuiApplication::paletteChanged, this, [this] {
0045         colorise();
0046     });
0047 }
0048 
0049 // Helps to represent a group of files like a single segment on the map
0050 class FilesGroup : public File
0051 {
0052 public:
0053     FilesGroup(int fileCount, FileSize totalSize, Folder *parent)
0054         : File("", totalSize, parent)
0055     {
0056         const QString fakeName = i18np("\n%1 file, with an average size of %2",
0057                                        "\n%1 files, with an average size of %2",
0058                                        fileCount,
0059                                        KFormat().formatByteSize(totalSize / fileCount));
0060         m_name = fakeName.toUtf8().constData();
0061     };
0062 };
0063 
0064 namespace
0065 {
0066 enum class Delete { Now = true, Later = false };
0067 void deleteAllSegments(QList<QList<RadialMap::Segment *>> &signature, Delete del = Delete::Later)
0068 {
0069     for (auto &segments : signature) {
0070         for (const auto &segment : segments) {
0071             if (segment->file()) {
0072                 Q_ASSERT(segment->file()->segment() == segment->uuid());
0073                 segment->file()->setSegment({});
0074             }
0075             del == Delete::Now ? delete segment : segment->deleteLater();
0076         }
0077     }
0078     signature.clear();
0079 }
0080 } // namespace
0081 
0082 RadialMap::Map::~Map()
0083 {
0084     deleteAllSegments(m_signature, Delete::Now);
0085 }
0086 
0087 void RadialMap::Map::invalidate()
0088 {
0089     deleteAllSegments(m_signature);
0090     Q_EMIT signatureChanged();
0091 
0092     m_visibleDepth = Config::instance()->defaultRingDepth;
0093 }
0094 
0095 void RadialMap::Map::make(const std::shared_ptr<Folder> &tree, bool refresh)
0096 {
0097     // slow operation so set the wait cursor
0098     QApplication::setOverrideCursor(Qt::WaitCursor);
0099 
0100     // build a signature of visible components
0101     {
0102         //**** REMOVE NEED FOR the +1 with MAX_RING_DEPTH uses
0103         //**** add some angle bounds checking (possibly in Segment ctor? can I delete in a ctor?)
0104         //**** this is a mess
0105 
0106         deleteAllSegments(m_signature);
0107         m_signature.resize(m_visibleDepth + 1);
0108         Q_EMIT signatureChanged();
0109 
0110         m_root = tree;
0111         if (m_rootSegment && m_rootSegment->file()) {
0112             m_rootSegment->file()->setSegment({});
0113         }
0114         m_rootSegment = std::make_unique<Segment>(tree, 0, MAX_DEGREE);
0115 
0116         if (!refresh) {
0117             m_minSize = (tree->size() * 3) / (PI * height() - MAP_2MARGIN);
0118             findVisibleDepth(tree);
0119         }
0120 
0121         setRingBreadth();
0122 
0123         // Calculate ring size limits
0124         m_limits.resize(m_visibleDepth + 1);
0125         const double size = m_root->size();
0126         const double pi2B = M_PI * 4 * m_ringBreadth;
0127         for (uint depth = 0; depth <= m_visibleDepth; ++depth) {
0128             m_limits[depth] = uint(size / double(pi2B * (depth + 1))); // min is angle that gives 3px outer diameter for that depth
0129         }
0130 
0131         build(tree);
0132     }
0133 
0134     // colour the segments
0135     colorise();
0136 
0137     QApplication::restoreOverrideCursor();
0138 }
0139 
0140 void RadialMap::Map::setRingBreadth()
0141 {
0142     // FIXME called too many times on creation
0143 
0144     m_ringBreadth = (height() - MAP_2MARGIN) / (2 * m_visibleDepth + 4);
0145     m_ringBreadth = qBound(MIN_RING_BREADTH, m_ringBreadth, MAX_RING_BREADTH);
0146 }
0147 
0148 void RadialMap::Map::findVisibleDepth(const std::shared_ptr<Folder> &dir, uint currentDepth)
0149 {
0150     //**** because I don't use the same minimumSize criteria as in the visual function
0151     //     this can lead to incorrect visual representation
0152     //**** BUT, you can't set those limits until you know m_depth!
0153 
0154     //**** also this function doesn't check to see if anything is actually visible
0155     //     it just assumes that when it reaches a new level everything in it is visible
0156     //     automatically. This isn't right especially as there might be no files in the
0157     //     dir provided to this function!
0158 
0159     static uint stopDepth = 0;
0160 
0161     if (dir == m_root) {
0162         stopDepth = m_visibleDepth;
0163         m_visibleDepth = 0;
0164     }
0165 
0166     if (m_visibleDepth < currentDepth) {
0167         qCDebug(FILELIGHT_LOG) << "changing visual depth" << m_visibleDepth << currentDepth;
0168         m_visibleDepth = currentDepth;
0169     }
0170     if (m_visibleDepth >= stopDepth) {
0171         return;
0172     }
0173 
0174     for (const auto &file : dir->files) {
0175         if (file->isFolder() && file->size() > m_minSize) {
0176             findVisibleDepth(std::dynamic_pointer_cast<Folder>(file), currentDepth + 1); // if no files greater than min size the depth is still recorded
0177         }
0178     }
0179 }
0180 
0181 //**** segments currently overlap at edges (i.e. end of first is start of next)
0182 bool RadialMap::Map::build(const std::shared_ptr<Folder> &dir, const uint depth, uint a_start, const uint a_end)
0183 {
0184     // first iteration: dir == m_root
0185 
0186     if (dir->children() == 0) { // we do fileCount rather than size to avoid chance of divide by zero later
0187         return false;
0188     }
0189 
0190     FileSize hiddenSize = 0;
0191     uint hiddenFileCount = 0;
0192 
0193     for (const auto &file : dir->files) {
0194         if (file->size() < m_limits[depth] * 6) { // limit is half a degree? we want at least 3 degrees
0195             hiddenSize += file->size();
0196             if (file->isFolder()) { //**** considered virtual, but dir wouldn't count itself!
0197                 hiddenFileCount += std::dynamic_pointer_cast<Folder>(file)->children(); // need to add one to count the dir as well
0198             }
0199             ++hiddenFileCount;
0200             continue;
0201         }
0202 
0203         auto a_len = (unsigned int)(MAX_DEGREE * ((double)file->size() / (double)m_root->size()));
0204 
0205         auto *s = new Segment(file, a_start, a_len);
0206         m_signature[depth].append(s);
0207 
0208         if (file->isFolder()) {
0209             if (depth != m_visibleDepth) {
0210                 // recurse
0211                 s->m_hasHiddenChildren = build(std::dynamic_pointer_cast<Folder>(file), depth + 1, a_start, a_start + a_len);
0212             } else {
0213                 s->m_hasHiddenChildren = true;
0214             }
0215         }
0216 
0217         a_start += a_len; //**** should we add 1?
0218     }
0219     if (depth == 0) {
0220         Q_EMIT signatureChanged();
0221     }
0222 
0223     if (hiddenFileCount == dir->children() && !Config::instance()->showSmallFiles) {
0224         return true;
0225     }
0226 
0227     if ((depth == 0 || Config::instance()->showSmallFiles) && hiddenSize >= m_limits[depth] && hiddenFileCount > 0) {
0228         m_signature[depth].append(new Segment(std::make_shared<FilesGroup>(hiddenFileCount, hiddenSize, dir.get()), a_start, a_end - a_start, true));
0229         Q_EMIT signatureChanged();
0230     }
0231 
0232     return false;
0233 }
0234 
0235 bool RadialMap::Map::resize(const QRectF &newRect)
0236 {
0237     // there's a MAP_2MARGIN border
0238 
0239     if (newRect.width() < width() && newRect.height() < height() && !newRect.contains(m_rect)) {
0240         return false;
0241     }
0242 
0243     qreal size = qMin(newRect.width(), newRect.height()) - MAP_2MARGIN;
0244 
0245     // this also causes uneven sizes to always resize when resizing but map is small in that dimension
0246     // size -= size % 2; //even sizes mean less staggered non-antialiased resizing
0247 
0248     const uint minSize = MIN_RING_BREADTH * 2 * (m_visibleDepth + 2);
0249 
0250     if (size < minSize) {
0251         size = minSize;
0252     }
0253 
0254     // this QRectF is used by paint()
0255     m_rect.setRect(0, 0, size, size);
0256     Q_EMIT rectChanged();
0257 
0258     // resize the pixmap
0259     size += MAP_2MARGIN;
0260 
0261     if (!m_signature.isEmpty()) {
0262         setRingBreadth();
0263     }
0264 
0265     return true;
0266 }
0267 
0268 void RadialMap::Map::colorise()
0269 {
0270     if (m_signature.isEmpty()) {
0271         qCDebug(FILELIGHT_LOG) << "no signature yet";
0272         return;
0273     }
0274 
0275     QColor cp;
0276     QColor cb;
0277     double darkness = 1;
0278     double contrast = (double)Config::instance()->contrast / (double)100;
0279     int h = 0;
0280     int s1 = 0;
0281     int s2 = 0;
0282     int v1 = 0;
0283     int v2 = 0;
0284 
0285     QPalette palette = qGuiApp->palette();
0286 #ifdef Q_OS_WINDOWS
0287     winrt::Windows::UI::ViewManagement::UISettings settings;
0288     winrt::Windows::UI::Color color = settings.GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Accent);
0289     const QColor kdeColour[2] = {palette.window().color(), QColor(color.R, color.G, color.B, color.A)};
0290 #else
0291     const QColor kdeColour[2] = {palette.windowText().color(), palette.window().color()};
0292 #endif
0293 
0294     double deltaRed = (double)(kdeColour[0].red() - kdeColour[1].red()) / 2880; // 2880 for semicircle
0295     double deltaGreen = (double)(kdeColour[0].green() - kdeColour[1].green()) / 2880;
0296     double deltaBlue = (double)(kdeColour[0].blue() - kdeColour[1].blue()) / 2880;
0297 
0298     for (uint i = 0; i <= m_visibleDepth; ++i, darkness += 0.04) {
0299         for (const auto &segment : std::as_const(m_signature[i])) {
0300             switch (Config::instance()->scheme) {
0301             case Filelight::KDE: {
0302                 // gradient will work by figuring out rgb delta values for 360 degrees
0303                 // then each component is angle*delta
0304 
0305                 int a = segment->start();
0306 
0307                 if (a > 2880) {
0308                     a = 2880 - (a - 2880);
0309                 }
0310 
0311                 h = (int)(deltaRed * a) + kdeColour[1].red();
0312                 s1 = (int)(deltaGreen * a) + kdeColour[1].green();
0313                 v1 = (int)(deltaBlue * a) + kdeColour[1].blue();
0314 
0315                 cb.setRgb(h, s1, v1);
0316                 cb.getHsv(&h, &s1, &v1);
0317 
0318                 break;
0319             }
0320 
0321             case Filelight::HighContrast:
0322                 cp.setHsv(0, 0, 0); // values of h, s and v are irrelevant
0323                 cb.setHsv(180, 0, int(255.0 * contrast));
0324                 segment->setPalette(cp, cb);
0325                 continue;
0326 
0327             default:
0328                 h = int(segment->start() / DEGREE_FACTOR);
0329                 s1 = 160;
0330                 v1 = (int)(255.0 / darkness); //****doing this more often than once seems daft!
0331             }
0332 
0333             v2 = v1 - int(contrast * v1);
0334             s2 = s1 + int(contrast * (255 - s1));
0335 
0336             if (s1 < 80) {
0337                 s1 = 80; // can fall too low and makes contrast between the files hard to discern
0338             }
0339 
0340             if (segment->isFake()) { // multi-file
0341                 cb.setHsv(h, s2, (v2 < 90) ? 90 : v2); // too dark if < 100
0342                 cp.setHsv(h, 17, v1);
0343             } else if (!segment->file()->isFolder()) { // file
0344                 cb.setHsv(h, 17, v1);
0345                 cp.setHsv(h, 17, v2);
0346             } else { // folder
0347                 cb.setHsv(h, s1, v1); // v was 225
0348                 cp.setHsv(h, s2, v2); // v was 225 - delta
0349             }
0350 
0351             segment->setPalette(cp, cb);
0352 
0353             // TODO:
0354             //**** may be better to store KDE colours as H and S and vary V as others
0355             //**** perhaps make saturation difference for s2 dependent on contrast too
0356             //**** fake segments don't work with highContrast
0357             //**** may work better with cp = cb rather than Qt::white
0358             //**** you have to ensure the grey of files is sufficient, currently it works only with rainbow (perhaps use contrast there too)
0359             //**** change v1,v2 to vp, vb etc.
0360             //**** using percentages is not strictly correct as the eye doesn't work like that
0361             //**** darkness factor is not done for kde_colour scheme, and also value for files is incorrect really for files in this scheme as it is not set
0362             // like rainbow one is
0363         }
0364     }
0365 }
0366 
0367 QList<QVariant> RadialMap::Map::signature()
0368 {
0369     QList<QVariant> ret;
0370 
0371     for (auto &list : m_signature) {
0372         QList<QObject *> r;
0373 
0374         for (auto &element : list) {
0375             r << element;
0376         }
0377         ret << QVariant::fromValue(r);
0378         // break;
0379     }
0380     return ret;
0381 }
0382 
0383 void RadialMap::Map::zoomIn() // slot
0384 {
0385     if (m_visibleDepth > MIN_RING_DEPTH) {
0386         --m_visibleDepth;
0387         make(m_root);
0388         Config::instance()->defaultRingDepth = m_visibleDepth;
0389     }
0390 }
0391 
0392 void RadialMap::Map::zoomOut() // slot
0393 {
0394     ++m_visibleDepth;
0395     make(m_root);
0396     if (m_visibleDepth > Config::instance()->defaultRingDepth) {
0397         Config::instance()->defaultRingDepth = m_visibleDepth;
0398     }
0399 }
0400 
0401 void RadialMap::Map::refresh(const Filelight::Dirty filth)
0402 {
0403     // TODO consider a more direct connection
0404 
0405     if (!isNull()) {
0406         switch (filth) {
0407         case Filelight::Dirty::Layout:
0408             make(m_root, true); // true means refresh only
0409             break;
0410         case Filelight::Dirty::Colors:
0411             colorise();
0412             break;
0413         }
0414     }
0415 }
0416 
0417 void RadialMap::Map::createFromCache(const std::shared_ptr<Folder> &tree)
0418 {
0419     qCDebug(FILELIGHT_LOG) << "Creating cached tree";
0420     // no scan was necessary, use cached tree, however we MUST still emit invalidate
0421     invalidate();
0422     make(tree);
0423 }
0424 
0425 void RadialMap::Map::createFromCacheObject(RadialMap::Segment *segment)
0426 {
0427     createFromCache(std::dynamic_pointer_cast<Folder>(segment->file()));
0428 }
0429 
0430 QUrl RadialMap::Map::rootUrl() const
0431 {
0432     return m_root ? m_root->url() : QUrl();
0433 }
0434 
0435 QObject *RadialMap::Map::rootSegment() const
0436 {
0437     return m_rootSegment.get();
0438 }