diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e58201..d3145d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,84 +1,84 @@ ####################################################################### # Copyright 2008-2014 Martin Sandsmark # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License or (at your option) version 3 or any later version # accepted by the membership of KDE e.V. (or its successor approved # by the membership of KDE e.V.), which shall act as a proxy # defined in Section 14 of version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ####################################################################### # KDE Application Version, managed by release script set(RELEASE_SERVICE_VERSION_MAJOR "20") set(RELEASE_SERVICE_VERSION_MINOR "03") set(RELEASE_SERVICE_VERSION_MICRO "70") set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") set(FILELIGHT_VERSION ${RELEASE_SERVICE_VERSION}) # minimum requirements cmake_minimum_required (VERSION 3.5 FATAL_ERROR) project(Filelight VERSION ${FILELIGHT_VERSION}) set(PROJECT_VERSION ${RELEASE_SERVICE_VERSION}) set(KF5_MIN_VERSION "5.46.0") set(QT_REQUIRED_VERSION "5.11.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(ECMAddAppIcon) include(ECMGenerateHeaders) include(ECMInstallIcons) include(ECMMarkNonGuiExecutable) include(ECMOptionalAddSubdirectory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) -find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Core Widgets) +find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Core Widgets Svg) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED XmlGui # For app KIO # For part I18n ) find_package(KF5DocTools) # Optional, not needed on Windows for example. add_definitions(-DTRANSLATION_DOMAIN=\"filelight\") if (KF5Config_VERSION VERSION_GREATER "5.56.0") add_definitions(-DQT_NO_FOREACH) MESSAGE(STATUS "compile without foreach") endif() if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x060000) endif() include_directories(src) add_subdirectory(src) add_subdirectory(misc) if (KF5DocTools_FOUND) add_subdirectory(doc) endif() if (ECM_VERSION VERSION_GREATER "5.58.0") install(FILES filelight.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) else() install(FILES filelight.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2d344c3..4385a6f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,67 +1,68 @@ ####################################################################### # Copyright 2008-2014 Martin Sandsmark # Copyright 2017 Harald Sitter # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License or (at your option) version 3 or any later version # accepted by the membership of KDE e.V. (or its successor approved # by the membership of KDE e.V.), which shall act as a proxy # defined in Section 14 of version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ####################################################################### ecm_setup_version(${PROJECT_VERSION} VARIABLE_PREFIX FILELIGHT VERSION_HEADER version.h) set(filelight_SRCS radialMap/widget.cpp radialMap/map.cpp radialMap/widgetEvents.cpp radialMap/labels.cpp scan.cpp progressBox.cpp Config.cpp settingsDialog.cpp fileTree.cpp localLister.cpp remoteLister.cpp summaryWidget.cpp historyAction.cpp mainWindow.cpp main.cpp ) ecm_qt_declare_logging_category(filelight_SRCS HEADER filelight_debug.h IDENTIFIER FILELIGHT_LOG CATEGORY_NAME org.kde.filelight) set(filelight_ICONS ${CMAKE_CURRENT_SOURCE_DIR}/../misc/16-apps-filelight.png ${CMAKE_CURRENT_SOURCE_DIR}/../misc/32-apps-filelight.png ${CMAKE_CURRENT_SOURCE_DIR}/../misc/48-apps-filelight.png ${CMAKE_CURRENT_SOURCE_DIR}/../misc/64-apps-filelight.png ${CMAKE_CURRENT_SOURCE_DIR}/../misc/128-apps-filelight.png ) ecm_add_app_icon(filelight_SRCS ICONS ${filelight_ICONS}) ki18n_wrap_ui(filelight_SRCS dialog.ui) add_executable(filelight ${filelight_SRCS}) target_link_libraries(filelight KF5::I18n KF5::XmlGui KF5::KIOWidgets # Only used for KDirLister, may be able to move away from that. + Qt5::Svg ) if (WIN32) find_package(KDEWin REQUIRED) target_link_libraries(filelight kdewin) endif() install(TARGETS filelight ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/mainWindow.cpp b/src/mainWindow.cpp index 9c82250..6e3a775 100644 --- a/src/mainWindow.cpp +++ b/src/mainWindow.cpp @@ -1,552 +1,566 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * Copyright 2017 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include "mainWindow.h" #include "historyAction.h" #include "Config.h" #include "define.h" #include "fileTree.h" #include "progressBox.h" #include "radialMap/widget.h" #include "scan.h" #include "settingsDialog.h" #include "summaryWidget.h" #include //std::exit() #include #include #include #include //for editToolbar dialog #include #include // upUrl #include #include //::start() #include #include #include #include //locationbar #include //setupActions() #include #include #include #include #include namespace Filelight { MainWindow::MainWindow() : KXmlGuiWindow() , m_histories(nullptr) , m_summary(nullptr) , m_map(nullptr) , m_started(false) { Config::read(); QScrollArea *scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); setCentralWidget(scrollArea); QWidget *partWidget = new QWidget(scrollArea); scrollArea->setWidget(partWidget); partWidget->setBackgroundRole(QPalette::Base); partWidget->setAutoFillBackground(true); m_layout = new QGridLayout(); partWidget->setLayout(m_layout); m_manager = new ScanManager(partWidget); m_map = new RadialMap::Widget(partWidget); m_layout->addWidget(m_map); // FIXME: drop stupid nullptr argument m_stateWidget = new ProgressBox(statusBar(), this, m_manager); m_layout->addWidget(m_stateWidget); m_stateWidget->hide(); m_numberOfFiles = new QLabel(); statusBar()->addPermanentWidget(m_numberOfFiles); KStandardAction::zoomIn(m_map, &RadialMap::Widget::zoomIn, actionCollection()); KStandardAction::zoomOut(m_map, &RadialMap::Widget::zoomOut, actionCollection()); KStandardAction::preferences(this, &MainWindow::configFilelight, actionCollection()); connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::completed); connect(m_map, &RadialMap::Widget::folderCreated, this, &MainWindow::mapChanged); connect(m_map, &RadialMap::Widget::activated, this, &MainWindow::updateURL); // TODO make better system connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::updateURL); connect(m_map, &RadialMap::Widget::giveMeTreeFor, this, &MainWindow::openUrl); connect(m_manager, &ScanManager::completed, this, &MainWindow::folderScanCompleted); connect(m_manager, &ScanManager::aboutToEmptyCache, m_map, &RadialMap::Widget::invalidate); setStandardToolBarMenuEnabled(true); setupActions(); createGUI(QStringLiteral("filelightui.rc")); stateChanged(QStringLiteral("scan_failed")); //bah! doesn't affect the parts' actions, should I add them to the actionCollection here? connect(this, &MainWindow::started, this, &MainWindow::scanStarted); connect(this, &MainWindow::completed, this, &MainWindow::scanCompleted); connect(this, &MainWindow::canceled, this, &MainWindow::scanFailed); connect(this, &MainWindow::canceled, m_histories, &HistoryCollection::stop); const KConfigGroup config = KSharedConfig::openConfig()->group("general"); m_combo->setHistoryItems(config.readPathEntry("comboHistory", QStringList())); setAutoSaveSettings(QStringLiteral("window")); QTimer::singleShot(0, this, &MainWindow::postInit); } void MainWindow::scan(const QUrl &u) { slotScanUrl(u); } void MainWindow::setupActions() //singleton function { KActionCollection *const ac = actionCollection(); m_combo = new KHistoryComboBox(this); m_combo->setCompletionObject(new KUrlCompletion(KUrlCompletion::DirCompletion)); m_combo->setAutoDeleteCompletionObject(true); m_combo->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); m_combo->setDuplicatesEnabled(false); KStandardAction::open(this, SLOT(slotScanFolder()), ac); + KStandardAction::save(this, SLOT(slotSaveSvg()), ac)->setEnabled(false); KStandardAction::quit(this, SLOT(close()), ac); KStandardAction::up(this, SLOT(slotUp()), ac); KStandardAction::configureToolbars(this, SLOT(configToolbars()), ac); KStandardAction::keyBindings(this, SLOT(configKeys()), ac); QAction *action; action = ac->addAction(QStringLiteral("scan_home"), this, &MainWindow::slotScanHomeFolder); action->setText(i18n("Scan &Home Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_Home)); action = ac->addAction(QStringLiteral("scan_root"), this, &MainWindow::slotScanRootFolder); action->setText(i18n("Scan &Root Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("folder-red"))); action = ac->addAction(QStringLiteral("scan_rescan"), this, &MainWindow::rescan); action->setText(i18n("Rescan")); action->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); ac->setDefaultShortcut(action, QKeySequence::Refresh); action = ac->addAction(QStringLiteral("scan_stop"), this, &MainWindow::slotAbortScan); action->setText(i18n("Stop")); action->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); ac->setDefaultShortcut(action, Qt::Key_Escape); action = ac->addAction(QStringLiteral("go"), m_combo, static_cast(&KHistoryComboBox::returnPressed)); action->setText(i18n("Go")); action->setIcon(QIcon::fromTheme(QStringLiteral("go-jump-locationbar"))); action = ac->addAction(QStringLiteral("scan_folder"), this, &MainWindow::slotScanFolder); action->setText(i18n("Scan Folder")); action->setIcon(QIcon::fromTheme(QStringLiteral("folder"))); QWidgetAction *locationAction = ac->add(QStringLiteral("location_bar"), nullptr, nullptr); locationAction->setText(i18n("Location Bar")); locationAction->setDefaultWidget(m_combo); m_recentScans = new KRecentFilesAction(i18n("&Recent Scans"), ac); m_recentScans->setMaxItems(8); m_histories = new HistoryCollection(ac, this); m_recentScans->loadEntries(KSharedConfig::openConfig()->group("general")); connect(m_recentScans, &KRecentFilesAction::urlSelected, this, &MainWindow::slotScanUrl); connect(m_combo, static_cast(&KHistoryComboBox::returnPressed), this, &MainWindow::slotComboScan); connect(m_histories, &HistoryCollection::activated, this, &MainWindow::slotScanUrl); } void MainWindow::closeEvent(QCloseEvent *event) { KConfigGroup config = KSharedConfig::openConfig()->group("general"); m_recentScans->saveEntries(config); config.writePathEntry("comboHistory", m_combo->historyItems()); config.sync(); KXmlGuiWindow::closeEvent(event); } void MainWindow::configToolbars() //slot { KEditToolBar dialog(factory(), this); if (dialog.exec()) //krazy:exclude=crashy { createGUI(QStringLiteral("filelightui.rc")); applyMainWindowSettings(KSharedConfig::openConfig()->group("window")); } } void MainWindow::configKeys() //slot { KShortcutsDialog::configure(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this, true); } void MainWindow::slotScanFolder() { slotScanUrl(QFileDialog::getExistingDirectoryUrl(this, i18n("Select Folder to Scan"), url())); } void MainWindow::slotScanHomeFolder() { slotScanPath(QDir::homePath()); } +void MainWindow::slotSaveSvg() +{ + qDebug() << action("save"); + QString path = QFileDialog::getSaveFileName(this, i18n("Save to SVG")); + if (path.isEmpty()) { + return; + } + + m_map->saveSvg(path); +} + void MainWindow::slotScanRootFolder() { slotScanPath(QDir::rootPath()); } void MainWindow::slotUp() { slotScanUrl(KIO::upUrl(url())); } void MainWindow::slotComboScan() { QString path = m_combo->lineEdit()->text(); QUrl url = QUrl::fromUserInput(path); if (url.isRelative()) path = QLatin1String("~/") + path; // KUrlCompletion completes relative to ~, not CWD path = KShell::tildeExpand(path); if (slotScanPath(path)) m_combo->addToHistory(path); } bool MainWindow::slotScanPath(const QString &path) { return slotScanUrl(QUrl::fromUserInput(path)); } bool MainWindow::slotScanUrl(const QUrl &url) { const QUrl oldUrl = this->url(); if (openUrl(url)) { m_histories->push(oldUrl); return true; } else return false; } void MainWindow::slotAbortScan() { if (closeUrl()) action("scan_stop")->setEnabled(false); } void MainWindow::scanStarted() { stateChanged(QStringLiteral("scan_started")); m_combo->clearFocus(); } void MainWindow::scanFailed() { stateChanged(QStringLiteral("scan_failed")); action("go_up")->setStatusTip(QString()); action("go_up")->setToolTip(QString()); m_combo->lineEdit()->clear(); } void MainWindow::scanCompleted() { const QUrl url = this->url(); stateChanged(QStringLiteral("scan_complete")); + action("file_save")->setEnabled(true); + m_combo->lineEdit()->setText(prettyUrl()); if (url.toLocalFile() == QLatin1String("/")) { action("go_up")->setEnabled(false); action("go_up")->setStatusTip(QString()); action("go_up")->setToolTip(QString()); } else { action("go_up")->setStatusTip(KIO::upUrl(url).path()); action("go_up")->setToolTip(KIO::upUrl(url).path()); } m_recentScans->addUrl(url); //FIXME doesn't set the tick } void MainWindow::urlAboutToChange() { //called when part's URL is about to change internally //the part will then create the Map and emit completed() m_histories->push(url()); } /********************************************** SESSION MANAGEMENT **********************************************/ void MainWindow::saveProperties(KConfigGroup &configgroup) //virtual { if (!m_histories) return; m_histories->save(configgroup); configgroup.writeEntry("currentMap", url().path()); } void MainWindow::readProperties(const KConfigGroup &configgroup) //virtual { m_histories->restore(configgroup); slotScanPath(configgroup.group("General").readEntry("currentMap", QString())); } void MainWindow::postInit() { if (url().isEmpty()) //if url is not empty openUrl() has been called immediately after ctor, which happens { m_map->hide(); showSummary(); //FIXME KXMLGUI is b0rked, it should allow us to set this //BEFORE createGUI is called but it doesn't stateChanged(QStringLiteral("scan_failed")); } } bool MainWindow::openUrl(const QUrl &u) { //TODO everyone hates dialogs, instead render the text in big fonts on the Map //TODO should have an empty QUrl until scan is confirmed successful //TODO probably should set caption to QString::null while map is unusable #define KMSG(s) KMessageBox::information(widget(), s) QUrl uri = u.adjusted(QUrl::NormalizePathSegments); const QString localPath = uri.toLocalFile(); const bool isLocal = uri.isLocalFile(); if (uri.isEmpty()) { //do nothing, chances are the user accidentally pressed ENTER } else if (!uri.isValid()) { KMSG(i18n("The entered URL cannot be parsed; it is invalid.")); } else if (isLocal && !QDir::isAbsolutePath(localPath)) { KMSG(i18n("Filelight only accepts absolute paths, eg. /%1", localPath)); } else if (isLocal && !QDir(localPath).exists()) { KMSG(i18n("Folder not found: %1", localPath)); } else if (isLocal && !QDir(localPath).isReadable()) { KMSG(i18n("Unable to enter: %1\nYou do not have access rights to this location.", localPath)); } else { //we don't want to be using the summary screen anymore if (m_summary != nullptr) m_summary->hide(); m_stateWidget->show(); m_layout->addWidget(m_stateWidget); return start(uri); } return false; } bool MainWindow::closeUrl() { if (m_manager->abort()) statusBar()->showMessage(i18n("Aborting Scan...")); m_map->hide(); m_stateWidget->hide(); showSummary(); return true; } QString MainWindow::prettyUrl() const { return url().isLocalFile() ? QDir::toNativeSeparators(url().toLocalFile()) : url().toString(); } void MainWindow::updateURL(const QUrl &u) { if (m_manager->running()) m_manager->abort(); if (u == url()) m_manager->emptyCache(); //same as rescan() //do this last, or it breaks Konqi location bar setUrl(u); } QUrl MainWindow::url() const { return m_url; } void MainWindow::setUrl(const QUrl &url) { m_url = url; } void MainWindow::configFilelight() { SettingsDialog *dialog = new SettingsDialog(widget()); connect(dialog, &SettingsDialog::canvasIsDirty, m_map, &RadialMap::Widget::refresh); connect(dialog, &SettingsDialog::mapIsInvalid, m_manager, &ScanManager::emptyCache); dialog->show(); //deletes itself } bool MainWindow::start(const QUrl &url) { if (!m_started) { connect(m_map, &RadialMap::Widget::mouseHover, [&](const QString &msg) { statusBar()->showMessage(msg); }); connect(m_map, &RadialMap::Widget::folderCreated, statusBar(), &QStatusBar::clearMessage); m_started = true; } if (m_manager->running()) m_manager->abort(); m_numberOfFiles->setText(QString()); if (m_manager->start(url)) { setUrl(url); const QString s = i18n("Scanning: %1", prettyUrl()); stateChanged(QStringLiteral("scan_started")); emit started(); //as a MainWindow, we have to do this emit setWindowCaption(s); statusBar()->showMessage(s); m_map->hide(); m_map->invalidate(); //to maintain ui consistency return true; } return false; } void MainWindow::rescan() { if (m_summary && !m_summary->isHidden()) { delete m_summary; m_summary = nullptr; showSummary(); return; } //FIXME we have to empty the cache because otherwise rescan picks up the old tree.. m_manager->emptyCache(); //causes canvas to invalidate m_map->hide(); m_stateWidget->show(); start(url()); } void MainWindow::folderScanCompleted(Folder *tree) { if (tree) { statusBar()->showMessage(i18n("Scan completed, generating map...")); m_stateWidget->hide(); m_map->show(); m_map->create(tree); stateChanged(QStringLiteral("scan_complete")); } else { stateChanged(QStringLiteral("scan_failed")); emit canceled(i18n("Scan failed: %1", prettyUrl())); emit setWindowCaption(QString()); statusBar()->clearMessage(); m_map->hide(); m_stateWidget->hide(); showSummary(); setUrl(QUrl()); } } void MainWindow::mapChanged(const Folder *tree) { //IMPORTANT -> url() has already been set emit setWindowCaption(prettyUrl()); const int fileCount = tree->children(); const QString text = (fileCount == 0) ? i18n("No files.") : i18np("1 file", "%1 files",fileCount); m_numberOfFiles->setText(text); } void MainWindow::showSummary() { if (m_summary == nullptr) { m_summary = new SummaryWidget(widget()); m_summary->setObjectName(QStringLiteral("summaryWidget")); connect(m_summary, &SummaryWidget::activated, this, &MainWindow::openUrl); m_summary->show(); m_layout->addWidget(m_summary); } else m_summary->show(); } } //namespace Filelight diff --git a/src/mainWindow.h b/src/mainWindow.h index 1e8a13b..db7d36a 100644 --- a/src/mainWindow.h +++ b/src/mainWindow.h @@ -1,130 +1,131 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * Copyright 2017 Harald Sitter * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include class QLabel; namespace RadialMap { class Widget; } class Folder; class KSqueezedTextLabel; class KHistoryComboBox; class KRecentFilesAction; class ProgressBox; class HistoryCollection; namespace Filelight { class ScanManager; class SummaryWidget; class MainWindow : public KXmlGuiWindow // Maybe use qmainwindow { Q_OBJECT public: MainWindow(); void scan(const QUrl &u); Q_SIGNALS: void started(); // FIXME: Could be replaced by direct func call once merged with mainwindow void completed(); void canceled(const QString&); void setWindowCaption(const QString&); private Q_SLOTS: void slotUp(); void slotComboScan(); void slotScanFolder(); void slotScanHomeFolder(); + void slotSaveSvg(); void slotScanRootFolder(); bool slotScanUrl(const QUrl&); bool slotScanPath(const QString&); void slotAbortScan(); void configToolbars(); void configKeys(); void scanStarted(); void scanFailed(); void scanCompleted(); void urlAboutToChange(); bool openUrl(const QUrl&); void configFilelight(); void rescan(); void postInit(); void folderScanCompleted(Folder*); void mapChanged(const Folder*); void updateURL(const QUrl &); protected: void saveProperties(KConfigGroup&) override; void readProperties(const KConfigGroup&) override; void closeEvent(QCloseEvent *event) override; private: void setupStatusBar(); void setupActions(); bool closeUrl(); QString prettyUrl() const; void showSummary(); bool start(const QUrl&); KSqueezedTextLabel *m_status[2]; KHistoryComboBox *m_combo; HistoryCollection *m_histories; KRecentFilesAction *m_recentScans; QLayout *m_layout; SummaryWidget *m_summary; RadialMap::Widget *m_map; ProgressBox *m_stateWidget; ScanManager *m_manager; QLabel *m_numberOfFiles; bool m_started; // KPart Compat helper public: QUrl url() const; private: void setUrl(const QUrl &url); QUrl m_url; }; } // namespace Filelight #endif diff --git a/src/radialMap/map.cpp b/src/radialMap/map.cpp index 5324c80..bb7345f 100644 --- a/src/radialMap/map.cpp +++ b/src/radialMap/map.cpp @@ -1,469 +1,486 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include //make() #include //make() & paint() #include //ctor #include //ctor #include #include +#include #include "filelight_debug.h" #include //make() #include #include "radialMap.h" // defines #include "Config.h" #include "fileTree.h" #define SINCOS_H_IMPLEMENTATION (1) #include "sincos.h" #include "widget.h" RadialMap::Map::Map(bool summary) : m_signature(nullptr) , m_visibleDepth(DEFAULT_RING_DEPTH) , m_ringBreadth(MIN_RING_BREADTH) , m_innerRadius(0) , m_summary(summary) { //FIXME this is all broken. No longer is a maximum depth! const int fmh = QFontMetrics(QFont()).height(); const int fmhD4 = fmh / 4; MAP_2MARGIN = 2 * (fmh - (fmhD4 - LABEL_MAP_SPACER)); //margin is dependent on fitting in labels at top and bottom } RadialMap::Map::~Map() { delete [] m_signature; } void RadialMap::Map::invalidate() { delete [] m_signature; m_signature = nullptr; m_visibleDepth = Config::defaultRingDepth; } +void RadialMap::Map::saveSvg(const QString &path) +{ + QSvgGenerator svgGenerator; + svgGenerator.setFileName(path); + paint(&svgGenerator); +} + void RadialMap::Map::make(const Folder *tree, bool refresh) { //slow operation so set the wait cursor QApplication::setOverrideCursor(Qt::WaitCursor); //build a signature of visible components { //**** REMOVE NEED FOR the +1 with MAX_RING_DEPTH uses //**** add some angle bounds checking (possibly in Segment ctor? can I delete in a ctor?) //**** this is a mess delete [] m_signature; m_signature = new QList[m_visibleDepth + 1]; m_root = tree; if (!refresh) { m_minSize = (tree->size() * 3) / (PI * height() - MAP_2MARGIN); findVisibleDepth(tree); } setRingBreadth(); // Calculate ring size limits m_limits.resize(m_visibleDepth + 1); const double size = m_root->size(); const double pi2B = M_PI * 4 * m_ringBreadth; for (uint depth = 0; depth <= m_visibleDepth; ++depth) { m_limits[depth] = uint(size / double(pi2B * (depth + 1))); //min is angle that gives 3px outer diameter for that depth } build(tree); } //colour the segments colorise(); m_centerText = tree->humanReadableSize(); //paint the pixmap paint(); QApplication::restoreOverrideCursor(); } void RadialMap::Map::setRingBreadth() { //FIXME called too many times on creation m_ringBreadth = (height() - MAP_2MARGIN) / (2 * m_visibleDepth + 4); m_ringBreadth = qBound(MIN_RING_BREADTH, m_ringBreadth, MAX_RING_BREADTH); } void RadialMap::Map::findVisibleDepth(const Folder *dir, uint currentDepth) { //**** because I don't use the same minimumSize criteria as in the visual function // this can lead to incorrect visual representation //**** BUT, you can't set those limits until you know m_depth! //**** also this function doesn't check to see if anything is actually visible // it just assumes that when it reaches a new level everything in it is visible // automatically. This isn't right especially as there might be no files in the // dir provided to this function! static uint stopDepth = 0; if (dir == m_root) { stopDepth = m_visibleDepth; m_visibleDepth = 0; } if (m_visibleDepth < currentDepth) m_visibleDepth = currentDepth; if (m_visibleDepth >= stopDepth) return; for (File *file : dir->files) { if (file->isFolder() && file->size() > m_minSize) { findVisibleDepth((Folder *)file, currentDepth + 1); //if no files greater than min size the depth is still recorded } } } //**** segments currently overlap at edges (i.e. end of first is start of next) bool RadialMap::Map::build(const Folder * const dir, const uint depth, uint a_start, const uint a_end) { //first iteration: dir == m_root if (dir->children() == 0) //we do fileCount rather than size to avoid chance of divide by zero later return false; FileSize hiddenSize = 0; uint hiddenFileCount = 0; for (File *file : dir->files) { if (file->size() < m_limits[depth] * 6) { // limit is half a degree? we want at least 3 degrees hiddenSize += file->size(); if (file->isFolder()) { //**** considered virtual, but dir wouldn't count itself! hiddenFileCount += static_cast(file)->children(); //need to add one to count the dir as well } ++hiddenFileCount; continue; } unsigned int a_len = (unsigned int)(5760 * ((double)file->size() / (double)m_root->size())); Segment *s = new Segment(file, a_start, a_len); m_signature[depth].append(s); if (file->isFolder()) { if (depth != m_visibleDepth) { //recurse s->m_hasHiddenChildren = build((Folder*)file, depth + 1, a_start, a_start + a_len); } else { s->m_hasHiddenChildren = true; } } a_start += a_len; //**** should we add 1? } if (hiddenFileCount == dir->children() && !Config::showSmallFiles) { return true; } if ((depth == 0 || Config::showSmallFiles) && hiddenSize >= m_limits[depth] && hiddenFileCount > 0) { //append a segment for unrepresented space - a "fake" segment const QString s = i18np("1 file, with an average size of %2", "%1 files, with an average size of %2", hiddenFileCount, KFormat().formatByteSize(hiddenSize/hiddenFileCount)); (m_signature + depth)->append(new Segment(new File(QFile::encodeName(s).constData(), hiddenSize), a_start, a_end - a_start, true)); } return false; } bool RadialMap::Map::resize(const QRectF &newRect) { //there's a MAP_2MARGIN border if (newRect.width() < width() && newRect.height() < height() && !newRect.contains(m_rect)) { return false; } uint size = qMin(newRect.width(), newRect.height()) - MAP_2MARGIN; //this also causes uneven sizes to always resize when resizing but map is small in that dimension //size -= size % 2; //even sizes mean less staggered non-antialiased resizing const uint minSize = MIN_RING_BREADTH * 2 * (m_visibleDepth + 2); if (size < minSize) { size = minSize; } //this QRectF is used by paint() m_rect.setRect(0,0,size,size); m_pixmap = QPixmap(m_rect.width() * m_dpr, m_rect.height() * m_dpr); m_pixmap.setDevicePixelRatio(m_dpr); //resize the pixmap size += MAP_2MARGIN; if (m_signature) { setRingBreadth(); paint(); } return true; } void RadialMap::Map::colorise() { if (!m_signature || m_signature->isEmpty()) { qCDebug(FILELIGHT_LOG) << "no signature yet"; return; } QColor cp, cb; double darkness = 1; double contrast = (double)Config::contrast / (double)100; int h, s1, s2, v1, v2; QPalette palette; const QColor kdeColour[2] = { palette.windowText().color(), palette.window().color() }; double deltaRed = (double)(kdeColour[0].red() - kdeColour[1].red()) / 2880; //2880 for semicircle double deltaGreen = (double)(kdeColour[0].green() - kdeColour[1].green()) / 2880; double deltaBlue = (double)(kdeColour[0].blue() - kdeColour[1].blue()) / 2880; if (m_summary) { // Summary view has its own colors, special cased. cp = Qt::gray; cb = Qt::white; m_signature[0][0]->setPalette(cp, cb); // need to check in case there's no free space if (m_signature[0].size() > 1) { cb = QApplication::palette().highlight().color(); cb.getHsv(&h, &s1, &v1); if (s1 > 80) { s1 = 80; } v2 = v1 - int(contrast * v1); s2 = s1 + int(contrast * (255 - s1)); cb.setHsv(h, s1, v1); cp.setHsv(h, s2, v2); m_signature[0][1]->setPalette(cp, cb); } return; } for (uint i = 0; i <= m_visibleDepth; ++i, darkness += 0.04) { for (Segment *segment : m_signature[i]) { switch (Config::scheme) { case Filelight::KDE: { //gradient will work by figuring out rgb delta values for 360 degrees //then each component is angle*delta int a = segment->start(); if (a > 2880) a = 2880 - (a - 2880); h = (int)(deltaRed * a) + kdeColour[1].red(); s1 = (int)(deltaGreen * a) + kdeColour[1].green(); v1 = (int)(deltaBlue * a) + kdeColour[1].blue(); cb.setRgb(h, s1, v1); cb.getHsv(&h, &s1, &v1); break; } case Filelight::HighContrast: cp.setHsv(0, 0, 0); //values of h, s and v are irrelevant cb.setHsv(180, 0, int(255.0 * contrast)); segment->setPalette(cp, cb); continue; default: h = int(segment->start() / 16); s1 = 160; v1 = (int)(255.0 / darkness); //****doing this more often than once seems daft! } v2 = v1 - int(contrast * v1); s2 = s1 + int(contrast * (255 - s1)); if (s1 < 80) s1 = 80; //can fall too low and makes contrast between the files hard to discern if (segment->isFake()) { //multi-file cb.setHsv(h, s2, (v2 < 90) ? 90 : v2); //too dark if < 100 cp.setHsv(h, 17, v1); } else if (!segment->file()->isFolder()) { //file cb.setHsv(h, 17, v1); cp.setHsv(h, 17, v2); } else { //folder cb.setHsv(h, s1, v1); //v was 225 cp.setHsv(h, s2, v2); //v was 225 - delta } segment->setPalette(cp, cb); //TODO: //**** may be better to store KDE colours as H and S and vary V as others //**** perhaps make saturation difference for s2 dependent on contrast too //**** fake segments don't work with highContrast //**** may work better with cp = cb rather than Qt::white //**** you have to ensure the grey of files is sufficient, currently it works only with rainbow (perhaps use contrast there too) //**** change v1,v2 to vp, vb etc. //**** using percentages is not strictly correct as the eye doesn't work like that //**** 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 like rainbow one is } } } -void RadialMap::Map::paint(bool antialias) +void RadialMap::Map::paint(QPaintDevice *paintDevice) { KColorScheme scheme(QPalette::Active, KColorScheme::View); QPainter paint; QRectF rect = m_rect; rect.adjust(MAP_HIDDEN_TRIANGLE_SIZE, MAP_HIDDEN_TRIANGLE_SIZE, -MAP_HIDDEN_TRIANGLE_SIZE, -MAP_HIDDEN_TRIANGLE_SIZE); - m_pixmap.fill(Qt::transparent); + if (!paintDevice) { + if (m_pixmap.isNull()) { + return; + } + + m_pixmap.fill(Qt::transparent); + paintDevice = &m_pixmap; + } //m_rect.moveRight(1); // Uncommenting this breaks repainting when recreating map from cache //**** best option you can think of is to make the circles slightly less perfect, // ** i.e. slightly eliptic when resizing inbetween - if (m_pixmap.isNull()) - return; - - if (!paint.begin(&m_pixmap)) { + if (!paint.begin(paintDevice)) { qWarning() << "Filelight::RadialMap Failed to initialize painting, returning..."; return; } - if (antialias && Config::antialias) { + if (Config::antialias) { paint.translate(0.7, 0.7); paint.setRenderHint(QPainter::Antialiasing); } int step = m_ringBreadth; int excess = -1; //do intelligent distribution of excess to prevent nasty resizing if (m_ringBreadth != MAX_RING_BREADTH && m_ringBreadth != MIN_RING_BREADTH) { excess = int(rect.width()) % m_ringBreadth; ++step; } + if (!m_signature) { + qWarning() << "Map not created yet"; + return; + } + for (int x = m_visibleDepth; x >= 0; --x) { int width = rect.width() / 2; //clever geometric trick to find largest angle that will give biggest arrow head uint a_max = int(acos((double)width / double((width + MAP_HIDDEN_TRIANGLE_SIZE))) * (180*16 / M_PI)); for (Segment *segment : m_signature[x]) { //draw the pie segments, most of this code is concerned with drawing the little //arrows on the ends of segments when they have hidden files paint.setPen(segment->pen()); paint.setPen(segment->pen()); paint.setBrush(segment->brush()); paint.drawPie(rect, segment->start(), segment->length()); if (!segment->hasHiddenChildren()) { continue; } //draw arrow head to indicate undisplayed files/directories QPolygonF pts; QPointF pos, cpos = rect.center(); uint a[3] = { segment->start(), segment->length(), 0 }; a[2] = a[0] + (a[1] / 2); //assign to halfway between if (a[1] > a_max) { a[1] = a_max; a[0] = a[2] - a_max / 2; } a[1] += a[0]; for (int i = 0, radius = width; i < 3; ++i) { double ra = M_PI/(180*16) * a[i], sinra, cosra; if (i == 2) { radius += MAP_HIDDEN_TRIANGLE_SIZE; } sincos(ra, &sinra, &cosra); pos.rx() = cpos.x() + cosra * radius; pos.ry() = cpos.y() - sinra * radius; pts << pos; } paint.setBrush(segment->pen()); paint.drawPolygon(pts); // Draw outline on the arc for hidden children QPen pen = paint.pen(); pen.setCapStyle(Qt::FlatCap); int width = 2; pen.setWidth(width); paint.setPen(pen); QRectF rect2 = rect; width /= 2; rect2.adjust(width, width, -width, -width); paint.drawArc(rect2, segment->start(), segment->length()); } if (excess >= 0) { //excess allows us to resize more smoothly (still crud tho) if (excess < 2) //only decrease rect by more if even number of excesses left --step; excess -= 2; } rect.adjust(step, step, -step, -step); } // if(excess > 0) rect.addCoords(excess, excess, 0, 0); //ugly paint.setPen(scheme.foreground().color()); paint.setBrush(scheme.background().color()); paint.drawEllipse(rect); paint.drawText(rect, Qt::AlignCenter, m_centerText); m_innerRadius = rect.width() / 2; //rect.width should be multiple of 2 paint.end(); } diff --git a/src/radialMap/map.h b/src/radialMap/map.h index 37f540a..a4f5f90 100644 --- a/src/radialMap/map.h +++ b/src/radialMap/map.h @@ -1,88 +1,90 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #ifndef MAP_H #define MAP_H #include "fileTree.h" #include #include #include #include namespace RadialMap { class Segment; class Map { public: explicit Map(bool summary); ~Map(); void make(const Folder *, bool = false); bool resize(const QRectF&); bool isNull() const { return (m_signature == nullptr); } void invalidate(); qreal height() const { return m_rect.height(); } qreal width() const { return m_rect.width(); } QPixmap pixmap() const { return m_pixmap; } + void saveSvg(const QString &path); + friend class Widget; private: - void paint(bool antialias = true); + void paint(QPaintDevice *paintDevice = nullptr); void colorise(); void setRingBreadth(); void findVisibleDepth(const Folder *dir, uint currentDepth = 0); bool build(const Folder* const dir, const uint depth =0, uint a_start =0, const uint a_end =5760); QList *m_signature; const Folder *m_root; uint m_minSize; QVector m_limits; QRectF m_rect; uint m_visibleDepth; ///visible level depth of system QPixmap m_pixmap; int m_ringBreadth; uint m_innerRadius; ///radius of inner circle QString m_centerText; bool m_summary; qreal m_dpr; uint MAP_2MARGIN; }; } #endif diff --git a/src/radialMap/widget.cpp b/src/radialMap/widget.cpp index f535e3c..6683a8c 100644 --- a/src/radialMap/widget.cpp +++ b/src/radialMap/widget.cpp @@ -1,224 +1,224 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #include "widget.h" #include "Config.h" #include "fileTree.h" #include "radialMap.h" //constants #include "map.h" #include //ctor #include #include //sendEvent #include //ctor - finding cursor size #include //slotPostMouseEvent() #include //member #include RadialMap::Widget::Widget(QWidget *parent, bool isSummary) : QWidget(parent) , m_tree(nullptr) , m_focus(nullptr) , m_map(isSummary) , m_rootSegment(nullptr) //TODO we don't delete it, *shrug* , m_isSummary(isSummary) , m_toBeDeleted(nullptr) { setAcceptDrops(true); setMinimumSize(350, 250); connect(this, &Widget::folderCreated, this, &Widget::sendFakeMouseEvent); connect(&m_timer, &QTimer::timeout, this, &Widget::resizeTimeout); m_tooltip.setFrameShape(QFrame::StyledPanel); m_tooltip.setWindowFlags(Qt::ToolTip | Qt::WindowTransparentForInput); m_map.m_dpr = devicePixelRatioF(); } RadialMap::Widget::~Widget() { delete m_rootSegment; } QString RadialMap::Widget::path() const { return m_tree->displayPath(); } QUrl RadialMap::Widget::url(File const * const file) const { return file ? file->url() : m_tree->url(); } void RadialMap::Widget::invalidate() { if (isValid()) { //**** have to check that only way to invalidate is this function frankly //**** otherwise you may get bugs.. //disable mouse tracking setMouseTracking(false); // Get this before reseting m_tree below QUrl invalidatedUrl(url()); //ensure this class won't think we have a map still m_tree = nullptr; m_focus = nullptr; delete m_rootSegment; m_rootSegment = nullptr; //FIXME move this disablement thing no? // it is confusing in other areas, like the whole createFromCache() thing m_map.invalidate(); update(); //tell rest of Filelight emit invalidated(invalidatedUrl); } } void RadialMap::Widget::create(const Folder *tree) { //it is not the responsibility of create() to invalidate first //skip invalidation at your own risk //FIXME make it the responsibility of create to invalidate first if (tree) { m_focus = nullptr; //generate the filemap image m_map.make(tree); //this is the inner circle in the center m_rootSegment = new Segment(tree, 0, 16*360); setMouseTracking(true); } m_tree = tree; //tell rest of Filelight emit folderCreated(tree); } void RadialMap::Widget::createFromCache(const Folder *tree) { //no scan was necessary, use cached tree, however we MUST still emit invalidate invalidate(); create(tree); } void RadialMap::Widget::sendFakeMouseEvent() //slot { // If we're not the focused window (or on another desktop), don't pop up our tooltip if (!qApp->focusWindow()) { return; } QMouseEvent me(QEvent::MouseMove, mapFromGlobal(QCursor::pos()), Qt::NoButton, Qt::NoButton, Qt::NoModifier); QApplication::sendEvent(this, &me); update(); } void RadialMap::Widget::resizeTimeout() //slot { // the segments are about to erased! // this was a horrid bug, and proves the OO programming should be obeyed always! m_focus = nullptr; if (m_tree) m_map.make(m_tree, true); update(); } void RadialMap::Widget::refresh(int filth) { //TODO consider a more direct connection if (!m_map.isNull()) { switch (filth) { case 1: m_focus=nullptr; m_map.make(m_tree, true); //true means refresh only break; case 2: - m_map.paint(true); //antialiased painting + m_map.paint(); break; case 3: m_map.colorise(); //FALL THROUGH! case 4: m_map.paint(); default: break; } update(); } } void RadialMap::Widget::zoomIn() //slot { if (m_map.m_visibleDepth > MIN_RING_DEPTH) { --m_map.m_visibleDepth; m_focus = nullptr; m_map.make(m_tree); Config::defaultRingDepth = m_map.m_visibleDepth; update(); } } void RadialMap::Widget::zoomOut() //slot { m_focus = nullptr; ++m_map.m_visibleDepth; m_map.make(m_tree); if (m_map.m_visibleDepth > Config::defaultRingDepth) Config::defaultRingDepth = m_map.m_visibleDepth; update(); } RadialMap::Segment::~Segment() { if (isFake()) delete m_file; //created by us in Builder::build() } diff --git a/src/radialMap/widget.h b/src/radialMap/widget.h index 17ec23f..1c96068 100644 --- a/src/radialMap/widget.h +++ b/src/radialMap/widget.h @@ -1,124 +1,126 @@ /*********************************************************************** * Copyright 2003-2004 Max Howell * Copyright 2008-2009 Martin Sandsmark * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . ***********************************************************************/ #ifndef WIDGET_H #define WIDGET_H #include #include #include #include #include #include #include #include #include #include #include "map.h" class Folder; class File; namespace KIO { class Job; } namespace RadialMap { class Segment; class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget* = nullptr, bool = false); ~Widget() override; QString path() const; QUrl url(File const * const = nullptr) const; bool isValid() const { return m_tree != nullptr; } bool isSummary() const { return m_isSummary; } friend class Label; //FIXME badness + void saveSvg(const QString &path) { m_map.saveSvg(path); } + public Q_SLOTS: void zoomIn(); void zoomOut(); void create(const Folder*); void invalidate(); void refresh(int); private Q_SLOTS: void resizeTimeout(); void sendFakeMouseEvent(); void deleteJobFinished(KJob*); void createFromCache(const Folder*); Q_SIGNALS: void activated(const QUrl&); void invalidated(const QUrl&); void folderCreated(const Folder*); void mouseHover(const QString&); void giveMeTreeFor(const QUrl&); protected: void changeEvent(QEvent*) override; void dragEnterEvent(QDragEnterEvent*) override; void dropEvent(QDropEvent*) override; void mouseMoveEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void paintEvent(QPaintEvent*) override; void resizeEvent(QResizeEvent*) override; void enterEvent(QEvent*) override; void leaveEvent(QEvent*) override; protected: const Segment *segmentAt(QPointF position) const; //FIXME const reference for a library others can use const Segment *rootSegment() const { return m_rootSegment; ///never == 0 } const Segment *focusSegment() const { return m_focus; ///0 == nothing in focus } private: void paintExplodedLabels(QPainter&) const; const Folder *m_tree; const Segment *m_focus; QPointF m_offset; QTimer m_timer; Map m_map; Segment *m_rootSegment; const bool m_isSummary; const Segment *m_toBeDeleted; QLabel m_tooltip; }; } #endif