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 }