/*
    This file is part of KCachegrind.

    SPDX-FileCopyrightText: 2002-2016 Josef Weidendorfer <Josef.Weidendorfer@gmx.de>

    SPDX-License-Identifier: GPL-2.0-only
*/

/*
 * KCachegrind top level window
 */

#define TRACE_UPDATES 0
#define ENABLE_DUMPDOCK 0

#include "toplevel.h"

#include <stdlib.h> // for system()

#include <QDebug>
#include <QDockWidget>
#include <QEventLoop>
#include <QFile>
#include <QFileDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMimeDatabase>
#include <QProcess>
#include <QProgressBar>
#include <QStatusBar>
#include <QTemporaryFile>
#include <QTimer>
#include <QUrl>
#include <QDBusConnection>

#include <KToggleAction>
#include <KToolBarPopupAction>
#include <KActionCollection>
#include <KSelectAction>
#include <KRecentFilesAction>
#include <KToolBar>
#include <KStandardGuiItem>
#include <KStandardShortcut>
#include <KStandardAction>
#include <KIO/Job>
#include <KIO/FileCopyJob>
#include <KJobWidgets>
#include <KEditToolBar>
#include <KShortcutsDialog>
#include <KMessageBox>
#include <KSharedConfig>
#include <KConfigGroup>
#include <KCompressionDevice>

#if ENABLE_DUMPDOCK
#include "dumpselection.h"
#endif

#include "partselection.h"
#include "functionselection.h"
#include "stackselection.h"
#include "stackbrowser.h"
#include "tracedata.h"
#include "globalguiconfig.h"
#include "config.h"
#include "configdlg.h"
#include "multiview.h"
#include "callgraphview.h"

TopLevel::TopLevel()
    : KXmlGuiWindow(nullptr)
{
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/KCachegrind"), this, QDBusConnection::ExportScriptableSlots);

    _progressBar = nullptr;
    _statusbar = statusBar();
    _statusLabel = new QLabel(_statusbar);
    _statusbar->addWidget(_statusLabel, 1);
    _ccProcess = nullptr;

    _layoutCount = 1;
    _layoutCurrent = 0;

    resetState();

    KConfig *kconfig = KSharedConfig::openConfig().data();
    GlobalGUIConfig::config()->readOptions();

    createDocks();

    _multiView = new MultiView(this, this );
    _multiView->setObjectName(QStringLiteral("MultiView"));
    setCentralWidget(_multiView);

    createActions();

    _partDockShown->setChecked(!_partDock->isHidden());
    _stackDockShown->setChecked(!_stackDock->isHidden());
    _functionDockShown->setChecked(!_functionDock->isHidden());

    connect(_partDock, &QDockWidget::visibilityChanged,
            this, &TopLevel::partVisibilityChanged);
    connect(_stackDock, &QDockWidget::visibilityChanged,
            this, &TopLevel::stackVisibilityChanged);
    connect(_functionDock, &QDockWidget::visibilityChanged,
            this, &TopLevel::functionVisibilityChanged);

#if ENABLE_DUMPDOCK
    _dumpDockShown->setChecked(!_dumpDock->isHidden());
    connect(_dumpDock, &QDockWidget::visibilityChanged,
            this, &TopLevel::dumpVisibilityChanged);
#endif


    // set toggle after reading configuration
    _showPercentage = GlobalConfig::showPercentage();
    _showExpanded   = GlobalConfig::showExpanded();
    _showCycles     = GlobalConfig::showCycles();
    _hideTemplates  = GlobalConfig::hideTemplates();
    _taPercentage->setChecked(_showPercentage);
    _taExpanded->setChecked(_showExpanded);
    _taCycles->setChecked(_showCycles);
    _taHideTemplates->setChecked(_hideTemplates);

    setupPartSelection(_partSelection);

    // KCachegrind for KDE 3.0.x does not allow to hide toolbars...
    setStandardToolBarMenuEnabled(true);
    _openRecent->loadEntries( KConfigGroup( kconfig, "" ) );

    // QT dock windows are created before (using QT position restoring)
    createGUI();

    setAutoSaveSettings();

    // restore current state settings (not configuration options)
    restoreCurrentState(QString());
}

void TopLevel::resetState()
{
    _activeParts.clear();
    _hiddenParts.clear();

    _data = nullptr;
    _function = nullptr;
    _eventType = nullptr;
    _eventType2 = nullptr;
    _groupType = ProfileContext::InvalidType;
    _group = nullptr;

    // for delayed slots
    _traceItemDelayed = nullptr;
    _eventTypeDelayed = nullptr;
    _eventType2Delayed = nullptr;
    _groupTypeDelayed = ProfileContext::InvalidType;
    _groupDelayed = nullptr;
    _directionDelayed = TraceItemView::None;
    _lastSender = nullptr;
}


/**
 * Setup the part selection widget.
 * Statusbar has to be created before.
 */
void TopLevel::setupPartSelection(PartSelection* ps)
{
    // setup connections from the part selection widget

    connect(ps, &PartSelection::partsHideSelected,
            this, &TopLevel::partsHideSelectedSlotDelayed);
    connect(ps, &PartSelection::partsUnhideAll,
            this, &TopLevel::partsUnhideAllSlotDelayed);
}

/**
 * This saves the current state of the main window and
 * sub widgets.
 *
 * No positions are saved. These is done automatically for
 * KToolbar, and manually in queryClose() for Qt docks.
 */
void TopLevel::saveCurrentState(const QString& postfix)
{
    QString eventType = _eventType ? _eventType->name() : QStringLiteral("?");
    QString eventType2 = _eventType2 ? _eventType2->name() : QStringLiteral("?");

    ConfigGroup* stateConfig = ConfigStorage::group(QLatin1String("CurrentState") + postfix);
    stateConfig->setValue(QStringLiteral("EventType"), eventType);
    stateConfig->setValue(QStringLiteral("EventType2"), eventType2);
    stateConfig->setValue(QStringLiteral("GroupType"), ProfileContext::typeName(_groupType));
    delete stateConfig;

    _partSelection->saveOptions(QStringLiteral("PartOverview"), postfix);
    _multiView->saveLayout(QStringLiteral("MainView"), postfix);
    _multiView->saveOptions(QStringLiteral("MainView"), postfix);
}

/**
 * This function is called when a trace is closed.
 * Save browsing position for later restoring
 */
void TopLevel::saveTraceSettings()
{
    QString key = traceKey();

    ConfigGroup* lConfig = ConfigStorage::group(QStringLiteral("Layouts"));
    lConfig->setValue(QStringLiteral("Count%1").arg(key), _layoutCount);
    lConfig->setValue(QStringLiteral("Current%1").arg(key), _layoutCurrent);
    delete lConfig;

    ConfigGroup* pConfig = ConfigStorage::group(QStringLiteral("TracePositions"));
    if (_eventType)
        pConfig->setValue(QStringLiteral("EventType%1").arg(key), _eventType->name());
    if (_eventType2)
        pConfig->setValue(QStringLiteral("EventType2%1").arg(key), _eventType2->name());
    if (_groupType != ProfileContext::InvalidType)
        pConfig->setValue(QStringLiteral("GroupType%1").arg(key),
                          ProfileContext::typeName(_groupType));

    if (_data) {
        if (_group)
            pConfig->setValue(QStringLiteral("Group%1").arg(key), _group->name());
        saveCurrentState(key);
    }
    delete pConfig;
}

/**
 * This restores the current state of the main window and
 * sub widgets.
 *
 * This does NOT restore any positions. This is done automatically for
 * KToolbar, and manually in the createDocks() for QT docks..
 */
void TopLevel::restoreCurrentState(const QString& postfix)
{
    _partSelection->restoreOptions(QStringLiteral("PartOverview"), postfix);
    _multiView->restoreLayout(QStringLiteral("MainView"), postfix);
    _multiView->restoreOptions(QStringLiteral("MainView"), postfix);

    _taSplit->setChecked(_multiView->childCount()>1);
    _taSplitDir->setEnabled(_multiView->childCount()>1);
    _taSplitDir->setChecked(_multiView->orientation() == Qt::Horizontal);
}


void TopLevel::createDocks()
{
    _partDock = new QDockWidget(this);
    _partDock->setObjectName(QStringLiteral("part dock"));
    _partDock->setWindowTitle(i18n("Parts Overview"));
    _partSelection = new PartSelection(this, _partDock);
    _partDock->setWidget(_partSelection);

    _stackDock = new QDockWidget(this);
    _stackDock->setObjectName(QStringLiteral("stack dock"));
    // Why is the caption only correct with a close button?
    _stackSelection = new StackSelection(_stackDock);
    _stackDock->setWidget(_stackSelection);
    _stackDock->setWindowTitle(i18n("Top Cost Call Stack"));
    _stackSelection->setWhatsThis( i18n(
                                       "<b>The Top Cost Call Stack</b>"
                                       "<p>This is a purely fictional 'most probable' call stack. "
                                       "It is built up by starting with the current selected "
                                       "function and adds the callers/callees with highest cost "
                                       "at the top and to bottom.</p>"
                                       "<p>The <b>Cost</b> and <b>Calls</b> columns show the "
                                       "cost used for all calls from the function in the line "
                                       "above.</p>"));

    connect(_stackSelection, &StackSelection::functionSelected,
            this, qOverload<CostItem*>(&TopLevel::setTraceItemDelayed));

    _functionDock = new QDockWidget(this);
    _functionDock->setObjectName(QStringLiteral("function dock"));
    _functionDock->setWindowTitle(i18n("Flat Profile"));
    _functionSelection = new FunctionSelection(this, _functionDock);
    _functionDock->setWidget(_functionSelection);

#if ENABLE_DUMPDOCK
    _dumpDock = new QDockWidget(this);
    _dumpDock->setWindowTitle(i18n("Profile Dumps"));
    _dumpSelection = new DumpSelection(this, _dumpDock,
                                       "dumpSelection");
    _dumpSelection->setTopLevel(this);

    _dumpDock->setWidget(_dumpSelection);
    _dumpSelection->setWhatsThis( i18n(
                                      "<b>Profile Dumps</b>"
                                      "<p>This dockable shows in the top part the list of "
                                      "loadable profile dumps in all subdirectories of: "
                                      "<ul><li>current working directory of KCachegrind, "
                                      "i.e. where it was started from, and </li>"
                                      "<li>the default profile dump directory given in the "
                                      "configuration.</li></ul> "
                                      "The list is sorted according to the target command "
                                      "profiled in the corresponding dump.</p>"
                                      "<p>On selecting a profile dump, information for it "
                                      "is shown in the bottom area of the dockable: "
                                      "<ul><li><b>Options</b> allows you to view the profiled "
                                      "command and profile options of this dump. By changing "
                                      "any item, a new (yet unexisting) profile template "
                                      "is created. Press <b>Run Profile</b> to start a "
                                      "profile run with these options in the background. </li>"
                                      "<li><b>Info</b> gives detailed info on the selected "
                                      "dump like event cost summary and properties of the "
                                      "simulated cache. </li>"
                                      "<li><b>State</b> is only available for current happening "
                                      "profiles runs. Press <b>Update</b> to see different "
                                      "counters of the run, and a stack trace of the current "
                                      "position in the program profiled. Check the <b>Every</b> "
                                      "option to let KCachegrind regularly poll these data. "
                                      "Check the <b>Sync</b> option to let the dockable activate "
                                      "the top function in the current loaded dump.</li></ul></p>"));
#endif

    // default positions, will be adjusted automatically by stored state in config
    addDockWidget(Qt::LeftDockWidgetArea, _partDock );
    addDockWidget(Qt::LeftDockWidgetArea, _stackDock );
    addDockWidget(Qt::LeftDockWidgetArea, _functionDock );
    _stackDock->hide();
    _partDock->hide();

#if ENABLE_DUMPDOCK
    addDockWidget( Qt::LeftDockWidgetArea, _dumpDock );
    _dumpDock->hide();
#endif

    KConfigGroup dockConfig(KSharedConfig::openConfig(), "Docks");
    _forcePartDock = dockConfig.readEntry("ForcePartDockVisible", false);
}


TopLevel::~TopLevel()
{
    delete _data;
}


void TopLevel::saveProperties(KConfigGroup & c)
{
    if ( _data )
        c.writeEntry("TraceName", _data->traceName());
}

void TopLevel::readProperties(const KConfigGroup &c)
{
    QString traceName = c.readEntry("TraceName");
    if (!traceName.isEmpty()) {
        openDataFile(traceName);
    }
}

void TopLevel::createLayoutActions()
{
    QString hint;
    QAction* action;

    action = actionCollection()->addAction( QStringLiteral("layout_duplicate") );
    action->setText( i18n( "&Duplicate" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutDuplicate);
    KActionCollection::setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Plus));
    hint = i18n("<b>Duplicate Current Layout</b>"
                "<p>Make a copy of the current layout.</p>");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("layout_remove") );
    action->setText( i18n( "&Remove" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutRemove);
    hint = i18n("<b>Remove Current Layout</b>"
                "<p>Delete current layout and make the previous active.</p>");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("layout_next") );
    action->setText( i18n( "&Go to Next" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutNext);
    KActionCollection::setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Right));
    hint = i18n("Go to Next Layout");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("layout_previous") );
    action->setText( i18n( "&Go to Previous" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutPrevious);
    KActionCollection::setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Left));
    hint = i18n("Go to Previous Layout");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("layout_restore") );
    action->setText( i18n( "&Restore to Default" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutRestore);
    hint = i18n("Restore Layouts to Default");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("layout_save") );
    action->setText( i18n( "&Save as Default" ) );
    connect(action, &QAction::triggered, this, &TopLevel::layoutSave);
    hint = i18n("Save Layouts as Default");
    action->setWhatsThis( hint );
}

// TODO: split this up...
void TopLevel::createMiscActions()
{
    QString hint;
    QAction* action;

    action = KStandardAction::openNew(this, SLOT(newWindow()), actionCollection());
    hint = i18n("<b>New</b><p>Open new empty KCachegrind window.</p>");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("file_add") );
    action->setText( i18n( "&Add..." ) );
    connect(action, &QAction::triggered, this, qOverload<>(&TopLevel::add));
    hint = i18n("<b>Add Profile Data</b>"
                "<p>This opens an additional profile data file in the current window.</p>");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("reload") );
    action->setIcon( QIcon::fromTheme(QStringLiteral("view-refresh")) );
    action->setText( i18nc("Reload a document", "&Reload" ) );
    connect(action, &QAction::triggered, this, &TopLevel::reload);
    KActionCollection::setDefaultShortcuts(action, KStandardShortcut::reload());
    hint = i18n("<b>Reload Profile Data</b>"
                "<p>This loads any new created parts, too.</p>");
    action->setWhatsThis( hint );

    action = actionCollection()->addAction( QStringLiteral("export") );
    action->setText( i18n( "&Export Graph" ) );
    connect(action, &QAction::triggered, this, &TopLevel::exportGraph);

    hint = i18n("<b>Export Call Graph</b>"
                "<p>Generates a file with extension .dot for the tools "
                "of the GraphViz package.</p>");
    action->setWhatsThis( hint );


    _taDump = actionCollection()->add<KToggleAction>( QStringLiteral("dump") );
    _taDump->setIcon( QIcon::fromTheme(QStringLiteral("edit-redo")) );
    _taDump->setText( i18n( "&Force Dump" ) );
    connect(_taDump, &QAction::triggered, this, &TopLevel::forceTrace);
    KActionCollection::setDefaultShortcut(_taDump, QKeySequence::Undo);
    hint = i18n("<b>Force Dump</b>"
                "<p>This forces a dump for a Callgrind profile run "
                "in the current directory. This action is checked while "
                "KCachegrind looks for the dump. If the dump is "
                "finished, it automatically reloads the current trace. "
                "If this is the one from the running Callgrind, the new "
                "created trace part will be loaded, too.</p>"
                "<p>Force dump creates a file 'callgrind.cmd', and "
                "checks every second for its existence. A running "
                "Callgrind will detect this file, dump a trace part, "
                "and delete 'callgrind.cmd'. "
                "The deletion is detected by KCachegrind, "
                "and it does a Reload. If there is <em>no</em> Callgrind "
                "running, press 'Force Dump' again to cancel the dump "
                "request. This deletes 'callgrind.cmd' itself and "
                "stops polling for a new dump.</p>"
                "<p>Note: A Callgrind run <em>only</em> detects "
                "existence of 'callgrind.cmd' when actively running "
                "a few milliseconds, i.e. "
                "<em>not</em> sleeping. Tip: For a profiled GUI program, "
                "you can awake Callgrind e.g. by resizing a window "
                "of the program.</p>");
    _taDump->setWhatsThis( hint );

    action = KStandardAction::open(this, SLOT(load()), actionCollection());
    hint = i18n("<b>Open Profile Data</b>"
                "<p>This opens a profile data file, with possible multiple parts</p>");
    action->setToolTip( hint );
    action->setWhatsThis( hint );

    _openRecent = KStandardAction::openRecent(this, SLOT(load(QUrl)),
                                              actionCollection());

    KStandardAction::showStatusbar(this,
                                   SLOT(toggleStatusBar()), actionCollection());

    _partDockShown = actionCollection()->add<KToggleAction>(QStringLiteral("settings_show_partdock"));
    _partDockShown->setText(i18n("Parts Overview"));
    connect(_partDockShown, &QAction::triggered, this, &TopLevel::togglePartDock);

    hint = i18n("Show/Hide the Parts Overview Dockable");
    _partDockShown->setToolTip( hint );
    _partDockShown->setWhatsThis( hint );

    _stackDockShown = actionCollection()->add<KToggleAction>(QStringLiteral("settings_show_stackdock"));
    _stackDockShown->setText(i18n("Call Stack"));
    connect(_stackDockShown, &QAction::triggered, this, &TopLevel::toggleStackDock);

    hint = i18n("Show/Hide the Call Stack Dockable");
    _stackDockShown->setToolTip( hint );
    _stackDockShown->setWhatsThis( hint );

    _functionDockShown = actionCollection()->add<KToggleAction>(QStringLiteral("settings_show_profiledock"));
    _functionDockShown->setText(i18n("Function Profile"));
    connect(_functionDockShown, &QAction::triggered, this, &TopLevel::toggleFunctionDock);

    hint = i18n("Show/Hide the Function Profile Dockable");
    _functionDockShown->setToolTip( hint );
    _functionDockShown->setWhatsThis( hint );

#if ENABLE_DUMPDOCK
    _dumpDockShown = actionCollection()->add<KToggleAction>("settings_show_dumpdock",
                                                            this, SLOT(toggleDumpDock()));
    _dumpDockShown->setText(i18n("Profile Dumps"));
    hint = i18n("Show/Hide the Profile Dumps Dockable");
    _dumpDockShown->setToolTip( hint );
    _dumpDockShown->setWhatsThis( hint );
#endif

    _taPercentage = actionCollection()->add<KToggleAction>(QStringLiteral("view_percentage"));
    _taPercentage->setIcon(QIcon::fromTheme(QStringLiteral("percent")));
    _taPercentage->setText(i18n("Relative"));
    connect(_taPercentage, &QAction::triggered, this, &TopLevel::togglePercentage);
    hint = i18n("Show relative instead of absolute costs");
    _taPercentage->setToolTip( hint );
    _taPercentage->setWhatsThis( hint );

    _taExpanded = actionCollection()->add<KToggleAction>(QStringLiteral("view_expanded"));
    _taExpanded->setIcon(QIcon::fromTheme(QStringLiteral("move")));
    _taExpanded->setText(i18n("Relative to Parent"));
    connect(_taExpanded, &QAction::triggered, this, &TopLevel::toggleExpanded);

    hint = i18n("Show percentage costs relative to parent");
    _taExpanded->setToolTip( hint );
    _taExpanded->setWhatsThis( hint );

    hint = i18n("<b>Show percentage costs relative to parent</b>"
                "<p>If this is switched off, percentage costs are always shown "
                "relative to the total cost of the profile part(s) that are "
                "currently browsed. By turning on this option, percentage cost "
                "of shown cost items will be relative to the parent cost item.</p>"
                "<ul><table>"
                "<tr><td><b>Cost Type</b></td><td><b>Parent Cost</b></td></tr>"
                "<tr><td>Function Cumulative</td><td>Total</td></tr>"
                "<tr><td>Function Self</td><td>Function Group (*) / Total</td></tr>"
                "<tr><td>Call</td><td>Function Inclusive</td></tr>"
                "<tr><td>Source Line</td><td>Function Inclusive</td></tr>"
                "</table></ul>"
                "<p>(*) Only if function grouping is switched on (e.g. ELF object grouping).</p>");
    _taExpanded->setWhatsThis( hint );

    _taCycles = actionCollection()->add<KToggleAction>(QStringLiteral("view_cycles"));
    _taCycles->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
    _taCycles->setText(i18n( "Cycle Detection" ));
    connect(_taCycles, &QAction::triggered, this, &TopLevel::toggleCycles);

    hint = i18n("<b>Detect recursive cycles</b>"
                "<p>If this is switched off, the treemap drawing will show "
                "black areas when a recursive call is made instead of drawing the "
                "recursion ad infinitum. Note that "
                "the size of black areas often will be wrong, as inside recursive "
                "cycles the cost of calls cannot be determined; the error is small, "
                "however, for false cycles (see documentation).</p>"
                "<p>The correct handling for cycles is to detect them and collapse all "
                "functions of a cycle into an artificial function, which is done when this "
                "option is selected. Unfortunately, with GUI applications, this often will "
                "lead to huge false cycles, making the analysis impossible; therefore, there "
                "is the option to switch this off.</p>");
    _taCycles->setWhatsThis( hint );

    _taHideTemplates = actionCollection()->add<KToggleAction>(QStringLiteral("hide_templates"));
    _taHideTemplates->setIcon(QIcon::fromTheme(QStringLiteral("hidetemplates")));
    _taHideTemplates->setText(i18n( "Shorten Templates" ));
    connect(_taHideTemplates, &QAction::triggered, this, &TopLevel::toggleHideTemplates);
    _taHideTemplates->setToolTip(i18n( "Hide Template Parameters in C++ Symbols" ));
    hint = i18n("<b>Hide Template Parameters in C++ Symbols</b>"
                "<p>If this is switched on, every symbol displayed will have "
                "any C++ template parameters hidden, just showing &lt;&gt; "
                "instead of a potentially nested template parameter.</p>"
                "<p>In this mode, you can hover the mouse pointer over the "
                "activated symbol label to show a tooltip with the "
                "unabbreviated symbol.</p>");
    _taHideTemplates->setWhatsThis(hint);

    KStandardAction::quit(this, SLOT(close()), actionCollection());
    KStandardAction::preferences(this, SLOT(configure()), actionCollection());
    KStandardAction::keyBindings(this, SLOT(configureKeys()), actionCollection());
    KStandardAction::configureToolbars(this,SLOT(configureToolbars()),
                                       actionCollection());

    _paUp = new KToolBarPopupAction( QIcon::fromTheme( QStringLiteral("go-up") ),
                                     i18n( "&Up" ), this );
    connect( _paUp, &QAction::triggered,
             _stackSelection, &StackSelection::browserUp );
    actionCollection()->addAction( QStringLiteral("go_up"), _paUp );
    KActionCollection::setDefaultShortcut(_paUp, QKeySequence(Qt::ALT | Qt::Key_Up));
    connect( _paUp->popupMenu(), &QMenu::aboutToShow,
             this, &TopLevel::upAboutToShow );
    connect( _paUp->popupMenu(), &QMenu::triggered,
             this, &TopLevel::upTriggered );
    hint = i18n("<b>Go Up</b>"
                "<p>Go to last selected caller of current function. "
                "If no caller was visited, use that with highest cost.</p>");
    _paUp->setToolTip( hint );
    _paUp->setWhatsThis( hint );

    QPair< KGuiItem, KGuiItem > backForward = KStandardGuiItem::backAndForward();
    _paBack = new KToolBarPopupAction( backForward.first.icon(),
                                       backForward.first.text(), this );
    connect( _paBack, &QAction::triggered,
             _stackSelection, &StackSelection::browserBack );
    actionCollection()->addAction( QStringLiteral("go_back"), _paBack );
    KActionCollection::setDefaultShortcut(_paBack, QKeySequence(Qt::ALT | Qt::Key_Left));
    connect( _paBack->popupMenu(), &QMenu::aboutToShow,
             this, &TopLevel::backAboutToShow );
    connect( _paBack->popupMenu(), &QMenu::triggered,
             this, &TopLevel::backTriggered );
    hint = i18n("Go back in function selection history");
    _paBack->setToolTip( hint );
    _paBack->setWhatsThis( hint );

    _paForward = new KToolBarPopupAction( backForward.second.icon(),
                                          backForward.second.text(), this );
    connect( _paForward, &QAction::triggered,
             _stackSelection, &StackSelection::browserForward );
    actionCollection()->addAction( QStringLiteral("go_forward"), _paForward );
    KActionCollection::setDefaultShortcut(_paForward, QKeySequence(Qt::ALT | Qt::Key_Right));
    connect( _paForward->popupMenu(), &QMenu::aboutToShow,
             this, &TopLevel::forwardAboutToShow );
    connect( _paForward->popupMenu(), &QMenu::triggered,
             this, &TopLevel::forwardTriggered );
    hint = i18n("Go forward in function selection history");
    _paForward->setToolTip( hint );
    _paForward->setWhatsThis( hint );

    _saCost = actionCollection()->add<KSelectAction>(QStringLiteral("view_cost_type"));
    _saCost->setText(i18n("Primary Event Type"));
    hint = i18n("Select primary event type of costs");
    _saCost->setComboWidth(300);
    _saCost->setToolTip( hint );
    _saCost->setWhatsThis( hint );

    // This is needed because for KDE4, "_saCost->setComboWidth(300);" seems to
    // have no effect. Instead, list box entry widths are used to determine the
    // combobox width. However, at KCachegrind startup, we do not have yet
    // a list of event types, as this depends on the profile data.
    // In KDE 4.2, we used a translatable string, which did not really work as
    // the semantic is not known to translators. Instead, we use a
    // nontranslatable string now...
    QStringList dummyItems;
    dummyItems << QStringLiteral("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
    _saCost->setItems(dummyItems);

    // cost types are dependent on loaded data, thus KSelectAction
    // is filled in setData()
    connect( _saCost, &KSelectAction::textTriggered,
             this, &TopLevel::eventTypeSelected);

    _saCost2 = actionCollection()->add<KSelectAction>(QStringLiteral("view_cost_type2"));
    _saCost2->setText(i18n("Secondary Event Type"));
    hint = i18n("Select secondary event type for cost e.g. shown in annotations");
    _saCost2->setComboWidth(300);
    _saCost2->setToolTip( hint );
    _saCost2->setWhatsThis( hint );
    _saCost2->setItems(dummyItems);
    connect( _saCost2, &KSelectAction::textTriggered,
             this, &TopLevel::eventTypeSelected);

    saGroup = actionCollection()->add<KSelectAction>(QStringLiteral("view_group_type"));
    saGroup->setText(i18n("Grouping"));

    hint = i18n("Select how functions are grouped into higher level cost items");
    saGroup->setToolTip( hint );
    saGroup->setWhatsThis( hint );

    QStringList args;

    args << i18n("(No Grouping)")
         << ProfileContext::i18nTypeName(ProfileContext::Object)
         << ProfileContext::i18nTypeName(ProfileContext::File)
         << ProfileContext::i18nTypeName(ProfileContext::Class)
         << ProfileContext::i18nTypeName(ProfileContext::FunctionCycle);

    saGroup->setItems(args);
    connect( saGroup, &KSelectAction::indexTriggered,
             this, &TopLevel::groupTypeSelected);

    _taSplit = actionCollection()->add<KToggleAction>(QStringLiteral("view_split"));
    _taSplit->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
    _taSplit->setText(i18n("Split"));
    connect(_taSplit, &QAction::triggered, this, &TopLevel::splitSlot);

    hint = i18n("Show two information panels");
    _taSplit->setToolTip( hint );
    _taSplit->setWhatsThis( hint );

    _taSplitDir = actionCollection()->add<KToggleAction>(QStringLiteral("view_split_dir"));
    _taSplitDir->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right")));
    _taSplitDir->setText(i18n("Split Horizontal"));
    connect(_taSplitDir, &QAction::triggered, this, &TopLevel::splitDirSlot);

    hint = i18n("Change Split Orientation when main window is split.");
    _taSplitDir->setToolTip( hint );
    _taSplitDir->setWhatsThis( hint );
}

void TopLevel::createActions()
{
    createMiscActions();
    createLayoutActions();
}

void TopLevel::toggleStatusBar()
{
    if (statusBar()->isVisible())
        statusBar()->hide();
    else
        statusBar()->show();
}

void TopLevel::togglePartDock()
{
    if (!_partDock->isVisible())
        _partDock->show();
    else
        _partDock->hide();
}

void TopLevel::toggleStackDock()
{
    if (!_stackDock->isVisible())
        _stackDock->show();
    else
        _stackDock->hide();
}

void TopLevel::toggleDumpDock()
{
#if ENABLE_DUMPDOCK
    if (!_dumpDock->isVisible())
        _dumpDock->show();
    else
        _dumpDock->hide();
#endif
}

void TopLevel::toggleFunctionDock()
{
    if (!_functionDock->isVisible())
        _functionDock->show();
    else
        _functionDock->hide();
}

void TopLevel::togglePercentage()
{
    setPercentage(_taPercentage->isChecked());
}

void TopLevel::setAbsoluteCost()
{
    setPercentage(false);
}

void TopLevel::setRelativeCost()
{
    setPercentage(true);
}

void TopLevel::updateViewsOnChange(int change)
{
    _partSelection->notifyChange(change);
    _functionSelection->notifyChange(change);
    _multiView->notifyChange(change);
}

void TopLevel::setPercentage(bool show)
{
    if (_showPercentage == show) return;
    _showPercentage = show;
    if (_taPercentage->isChecked() != show)
        _taPercentage->setChecked(show);

    GlobalConfig::setShowPercentage(_showPercentage);

    _stackSelection->refresh();

    updateViewsOnChange(TraceItemView::configChanged);
}

void TopLevel::toggleExpanded()
{
    bool show = _taExpanded->isChecked();
    if (_showExpanded == show) return;
    _showExpanded = show;

    GlobalConfig::setShowExpanded(_showExpanded);

    _stackSelection->refresh();

    updateViewsOnChange(TraceItemView::configChanged);
}

void TopLevel::toggleCycles()
{
    bool show = _taCycles->isChecked();
    if (_showCycles == show) return;
    _showCycles = show;

    GlobalConfig::setShowCycles(_showCycles);

    if (!_data) return;

    _data->invalidateDynamicCost();
    _data->updateFunctionCycles();

    _stackSelection->rebuildStackList();

    updateViewsOnChange(TraceItemView::configChanged);
}

void TopLevel::toggleHideTemplates()
{
    bool b = _taHideTemplates->isChecked();
    if (_hideTemplates == b) return;
    _hideTemplates = b;

    GlobalConfig::setHideTemplates(_hideTemplates);

    _stackSelection->refresh();

    updateViewsOnChange(TraceItemView::configChanged);
}

void TopLevel::partVisibilityChanged(bool v)
{
    _partDockShown->setChecked(v);
}

void TopLevel::stackVisibilityChanged(bool v)
{
    _stackDockShown->setChecked(v);
}

#if ENABLE_DUMPDOCK
void TopLevel::dumpVisibilityChanged(bool v)
#else
void TopLevel::dumpVisibilityChanged(bool)
#endif
{
#if ENABLE_DUMPDOCK
    _dumpDockShown->setChecked(v);
#endif
}

void TopLevel::functionVisibilityChanged(bool v)
{
    _functionDockShown->setChecked(v);
    if (v)
        _functionSelection->updateView();
}


void TopLevel::querySlot()
{
    _functionSelection->query(queryLineEdit->text());
}

void TopLevel::configureKeys()
{
    KShortcutsDialog::showDialog(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this);
}


void TopLevel::configureToolbars()
{
    KEditToolBar *dlg = new KEditToolBar(guiFactory(), this);

    if (dlg->exec())
        createGUI();

    delete dlg;
}


void TopLevel::newWindow()
{
    TopLevel* t = new TopLevel();
    t->show();
}


void TopLevel::load()
{
    QUrl url = QFileDialog::getOpenFileUrl(this,
                                           i18n("Select Callgrind Profile Data"),
                                           QUrl(),
                                           i18n("Callgrind Profile Data (cachegrind.out* callgrind.out*);;All Files (*)"));

    load(url);
}

void TopLevel::load(const QUrl& url)
{
    if (url.isEmpty()) return;

    QString tmpFileName;
    QTemporaryFile tmpFile;
    if (url.isLocalFile()) {
        tmpFileName = url.toLocalFile();
    }
    else if (tmpFile.open()){
        // network transparency
        tmpFileName = tmpFile.fileName();
        KIO::FileCopyJob *job = KIO::file_copy(url,
                                               QUrl::fromLocalFile(tmpFileName));
        KJobWidgets::setWindow(job, this);
        job->exec();
    }
    if (!tmpFileName.isEmpty()) {
        _openRecent->addUrl(url);
        _openRecent->saveEntries( KConfigGroup( KSharedConfig::openConfig(), QString() ) );

        load(tmpFileName);
    } else {
        KMessageBox::error(this, i18n("Could not open the file \"%1\". "
                                      "Check it exists and you have enough "
                                      "permissions to read it.",
                                      url.toDisplayString()));
    }
}

/* if file name is ".": load first file found in current directory, but do
 * not show an error message if nothing could be loaded
 */
void TopLevel::load(QString file)
{
    if (file.isEmpty()) return;

    bool showError = true;
    if (file == QLatin1Char('.'))
        showError = false;

    if (_data && _data->parts().count()>0) {

        // In new window
        TopLevel* t = new TopLevel();
        t->show();
        t->loadDelayed(file);
        return;
    }

    bool loaded = openDataFile(file);
    if (!loaded && showError)
        KMessageBox::error(this, i18n("Could not open the file \"%1\". "
                                      "Check it exists and you have enough "
                                      "permissions to read it.", file));
}


void TopLevel::add()
{
    QUrl url = QFileDialog::getOpenFileUrl(this,
                                           i18n("Add Callgrind Profile Data"),
                                           QUrl(),
                                           i18n("Callgrind Profile Data (cachegrind.out* callgrind.out*);;All Files (*)"));

    add(url);
}

void TopLevel::add(const QUrl &url)
{
    if (url.isEmpty()) return;

    QString tmpFileName;
    QTemporaryFile tmpFile;
    if (url.isLocalFile()) {
        tmpFileName = url.toLocalFile();
    }
    else if (tmpFile.open()){
        // network transparency
        tmpFileName = tmpFile.fileName();
        KIO::FileCopyJob *job = KIO::file_copy(url,
                                               QUrl::fromLocalFile(tmpFileName));
        KJobWidgets::setWindow(job, this);
        job->exec();
    }
    if (!tmpFileName.isEmpty()) {
        _openRecent->addUrl(url);
        _openRecent->saveEntries( KSharedConfig::openConfig()->group( QString() ) );

        add(tmpFileName);
    }
}

void TopLevel::add(QString file)
{
    if (file.isEmpty()) return;

    if (_data) {
        _data->load(file);

        // GUI update for added data
        configChanged();
        return;
    }

    openDataFile(file);
}



void TopLevel::loadDelayed(QString file)
{
    _loadFilesDelayed << file;
    QTimer::singleShot(0, this, &TopLevel::loadTraceDelayed);
}

void TopLevel::loadDelayed(QStringList files)
{
    _loadFilesDelayed << files;
    QTimer::singleShot(0, this, &TopLevel::loadTraceDelayed);
}

void TopLevel::loadTraceDelayed()
{
    if (_loadFilesDelayed.isEmpty()) return;

    if (_loadFilesDelayed.count()>1) {
        // FIXME: we expect all files to be local and existing
        TraceData* d = new TraceData(this);
        d->load(_loadFilesDelayed);
        setData(d);
    }
    else {
        QString file = _loadFilesDelayed[0];

        // if URL scheme is missing (URL is relative), this is a local file
        QUrl u = QUrl::fromUserInput(file, QDir::currentPath(), QUrl::AssumeLocalFile);
        if (u.isLocalFile())
            load(file); // special case for local file: maybe just prefix
        else
            load(u);
    }
    _loadFilesDelayed.clear();
}


void TopLevel::reload()
{
    QString trace;
    if (!_data || _data->parts().isEmpty())
        trace = QStringLiteral("."); // open first trace found in dir
    else
        trace = _data->traceName();

    // this also keeps sure we have the same browsing position...
    openDataFile(trace);
}

void TopLevel::exportGraph()
{
    if (!_data || !_function) return;

    GraphExporter::savePrompt(this, _data, _function, _eventType, _groupType, nullptr);
}


void TopLevel::setEventType(QString s)
{
    EventType* ct;

    ct = (_data) ? _data->eventTypes()->type(s) : nullptr;

    // if costtype with given name not found, use first available
    if (!ct && _data) ct = _data->eventTypes()->type(0);

    setEventType(ct);
}

void TopLevel::setEventType2(QString s)
{
    EventType* ct;

    // Special type i18n("(Hidden)") gives 0
    ct = (_data) ? _data->eventTypes()->type(s) : nullptr;

    setEventType2(ct);
}

void TopLevel::eventTypeSelected(const QString& s)
{
    EventType* ct;

    ct = (_data) ? _data->eventTypes()->typeForLong(s) : nullptr;
    setEventType(ct);
}

void TopLevel::eventType2Selected(const QString& s)
{
    EventType* ct;

    ct = (_data) ? _data->eventTypes()->typeForLong(s) : nullptr;
    setEventType2(ct);
}

void TopLevel::setEventType(EventType* ct)
{
    if (_eventType == ct) return;

    _eventType = ct;

    if (ct) {
        QStringList l = _saCost->items();
        int idx = l.indexOf(ct->longName());
        if (idx >= 0)
            _saCost->setCurrentItem(idx);
    }

    _partSelection->setEventType(_eventType);
    _stackSelection->setEventType(_eventType);
    _functionSelection->setEventType(_eventType);
    _multiView->setEventType(_eventType);

    updateStatusBar();
}

void TopLevel::setEventType2(EventType* ct)
{
    if (_eventType2 == ct) return;

    _eventType2 = ct;

    QString longName = ct ? ct->longName() : i18n("(Hidden)");
    QStringList l = _saCost2->items();
    int idx = l.indexOf(longName);
    if (idx >= 0)
        _saCost2->setCurrentItem(idx);

    _partSelection->setEventType2(_eventType2);
    _stackSelection->setEventType2(_eventType2);
    _functionSelection->setEventType2(_eventType2);
    _multiView->setEventType2(_eventType2);

    updateStatusBar();
}


void TopLevel::groupTypeSelected(int cg)
{
    switch(cg) {
    case 0: setGroupType( ProfileContext::Function ); break;
    case 1: setGroupType( ProfileContext::Object ); break;
    case 2: setGroupType( ProfileContext::File ); break;
    case 3: setGroupType( ProfileContext::Class ); break;
    case 4: setGroupType( ProfileContext::FunctionCycle ); break;
    default: break;
    }
}

bool TopLevel::setGroupType(QString s)
{
    ProfileContext::Type gt;

    gt = ProfileContext::type(s);
    // only allow Function/Object/File/Class as grouptype
    switch(gt) {
    case ProfileContext::Object:
    case ProfileContext::File:
    case ProfileContext::Class:
    case ProfileContext::FunctionCycle:
        break;
    default:
        gt = ProfileContext::Function;
    }

    return setGroupType(gt);
}

bool TopLevel::setGroupType(ProfileContext::Type gt)
{
    if (_groupType == gt) return false;
    _groupType = gt;

    int idx = -1;
    switch(gt) {
    case ProfileContext::Function: idx = 0; break;
    case ProfileContext::Object: idx = 1; break;
    case ProfileContext::File: idx = 2; break;
    case ProfileContext::Class: idx = 3; break;
    case ProfileContext::FunctionCycle: idx = 4; break;
    default:
        break;
    }

    if (idx==-1) return false;

    if (saGroup->currentItem() != idx)
        saGroup->setCurrentItem(idx);

    _stackSelection->setGroupType(_groupType);
    _partSelection->set(_groupType);
    _functionSelection->set(_groupType);
    _multiView->set(_groupType);

    updateStatusBar();

    return true;
}

bool TopLevel::setGroup(QString s)
{
    TraceCostItem* ci = _functionSelection->group(s);
    if (!ci)
        return false;

    return setGroup(ci);
}


bool TopLevel::setGroup(TraceCostItem* g)
{
    if (_group == g) return false;
    _group = g;

    _functionSelection->setGroup(g);
    updateStatusBar();

    return true;
}

bool TopLevel::setFunction(QString s)
{
    if (!_data) return false;

    ProfileCostArray* f = _data->search(ProfileContext::Function, s, _eventType);
    if (!f) return false;

    return setFunction((TraceFunction*)f);
}

bool TopLevel::setFunction(TraceFunction* f)
{
    if (_function == f) return false;
    _function = f;

    _multiView->activate(f);
    _functionSelection->activate(f);
    _partSelection->activate(f);
    _stackSelection->setFunction(_function);

    StackBrowser* b = _stackSelection->browser();
    if (b) {
        // do not disable up: a press forces stack-up extending...
        _paForward->setEnabled(b->canGoForward());
        _paBack->setEnabled(b->canGoBack());
    }

#if TRACE_UPDATES
    qDebug("TopLevel::setFunction(%s), lastSender %s",
           f ? f->prettyName().ascii() : "0",
           _lastSender ? _lastSender->name() :"0" );
#endif

    return true;
}


/**
 * Delayed versions.
 * We always have a pair of slots: One receiver to start the
 * delay with a singleShot Timer. It stores the parameter into a
 * temporary variable. And one parameterless slot for
 * forwarding, using this temporary.
 */
void TopLevel::setEventTypeDelayed(EventType* ct)
{
    _eventTypeDelayed = ct;
    QTimer::singleShot (0, this, SLOT(setEventTypeDelayed()));
}

void TopLevel::setEventType2Delayed(EventType* ct)
{
    _eventType2Delayed = ct;
    QTimer::singleShot (0, this, SLOT(setEventType2Delayed()));
}

void TopLevel::setEventTypeDelayed()
{
    setEventType(_eventTypeDelayed);
}

void TopLevel::setEventType2Delayed()
{
    setEventType2(_eventType2Delayed);
}

void TopLevel::setGroupTypeDelayed(ProfileContext::Type gt)
{
    _groupTypeDelayed = gt;
    QTimer::singleShot (0, this, SLOT(setGroupTypeDelayed()));
}

void TopLevel::setGroupTypeDelayed()
{
    setGroupType(_groupTypeDelayed);
}

void TopLevel::setGroupDelayed(TraceCostItem* g)
{
#if TRACE_UPDATES
    qDebug("TopLevel::setGroupDelayed(%s), sender %s",
           g ? g->prettyName().ascii() : "0",
           _lastSender ? _lastSender->name() :"0" );
#endif

    _groupDelayed = g;
    QTimer::singleShot (0, this, SLOT(setGroupDelayed()));
}

void TopLevel::setGroupDelayed()
{
    setGroup(_groupDelayed);
}

void TopLevel::setDirectionDelayed(TraceItemView::Direction d)
{
    _directionDelayed = d;
    QTimer::singleShot (0, this, SLOT(setDirectionDelayed()));
}

void TopLevel::setDirectionDelayed()
{
    switch(_directionDelayed) {
    case TraceItemView::Back:
        _stackSelection->browserBack();
        break;

    case TraceItemView::Forward:
        _stackSelection->browserForward();
        break;

    case TraceItemView::Up:
    {
        StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
        HistoryItem* hi = b ? b->current() : nullptr;
        TraceFunction* f = hi ? hi->function() : nullptr;

        if (!f) break;
        f = hi->stack()->caller(f, false);
        if (f) setFunction(f);
    }
        break;

    default: break;
    }

    _directionDelayed = TraceItemView::None;
}


void TopLevel::setTraceItemDelayed(CostItem* i)
{
    // no need to select same item a 2nd time...
    if (_traceItemDelayed == i) return;
    _traceItemDelayed = i;
    _lastSender = sender();

    qDebug() << "Selected " << (i ? i->prettyName() : QStringLiteral("(none)"));

#if TRACE_UPDATES
    qDebug("TopLevel::setTraceItemDelayed(%s), sender %s",
           i ? i->prettyName().ascii() : "0",
           _lastSender ? _lastSender->name() :"0" );
#endif

    QTimer::singleShot (0, this, SLOT(setTraceItemDelayed()));
}

void TopLevel::setTraceItemDelayed()
{
    if (!_traceItemDelayed) return;

    switch(_traceItemDelayed->type()) {
    case ProfileContext::Function:
    case ProfileContext::FunctionCycle:
        setFunction((TraceFunction*)_traceItemDelayed);
        break;

    case ProfileContext::Object:
    case ProfileContext::File:
    case ProfileContext::Class:
        _multiView->activate(_traceItemDelayed);
        break;

#if 0
        // this conflicts with the selection policy of InstrView ?!?
    case ProfileContext::Instr:
    case ProfileContext::Line:
        // only for multiview
        _multiView->activate(_traceItemDelayed);
        break;
#endif

    default: break;
    }

    _traceItemDelayed = nullptr;
    _lastSender = nullptr;
}

/**
 * A TraceData object cannot be viewed many times in different
 * toplevel windows. Thus, this toplevel window takes ownership
 * of the TraceData object: on closing the window or opening
 * another trace, the object is destroyed.
 */
void TopLevel::setData(TraceData* data)
{
    if (data == _data) return;

    _lastSender = nullptr;

    saveTraceSettings();

    if (_data) {
        _partSelection->setData(nullptr);
        _stackSelection->setData(nullptr);
        _functionSelection->setData(nullptr);
        _multiView->setData(nullptr);
        _multiView->updateView(true);

        // we are the owner...
        delete _data;
    }

    // reset members
    resetState();

    _data = data;

    // fill cost type list
    QStringList types;

    if (_data) {
        /* add all supported virtual types */
        EventTypeSet* m = _data->eventTypes();
        m->addKnownDerivedTypes();

        /* first, fill selection list with available cost types */
        for (int i=0;i<m->realCount();i++)
            types << m->realType(i)->longName();
        for (int i=0;i<m->derivedCount();i++)
            types << m->derivedType(i)->longName();
    }
    _saCost->setItems(types);
    _saCost->setComboWidth(300);

    if (types.count()>0) {
        // second type list gets an additional "(Hidden)"
        types.prepend(i18n("(Hidden)"));
    }
    _saCost2->setItems(types);
    _saCost2->setComboWidth(300);
    // default is hidden
    if (types.count()>0)
        _saCost2->setCurrentItem(0);

    _partSelection->setData(_data);
    _stackSelection->setData(_data);
    _functionSelection->setData(_data);
    _multiView->setData(_data);
    // Force update of _data in all children of _multiView
    // This is needed to make restoring of activeItem work!
    _multiView->updateView(true);

    /* this is needed to let the other widgets know the types */
    restoreTraceTypes();

    restoreTraceSettings();

    QString caption;
    if (_data) {
        caption = _data->traceName();
        if (!_data->command().isEmpty())
            caption += " [" + _data->command() + ']';
    }
    setWindowTitle(caption);

    if (!_data || (!_forcePartDock && _data->parts().count()<2)) {
        _partDock->hide();
        _partDockShown->setChecked(false);
    }
    else {
        _partDock->show();
        _partDockShown->setChecked(true);
    }

    updateStatusBar();
}

void TopLevel::addEventTypeMenu(QMenu* popup, bool withCost2)
{
    if (_data) {
        QMenu *popup1, *popup2 = nullptr;
        QAction* action;

        popup1 = popup->addMenu(i18n("Primary Event Type"));
        connect(popup1, &QMenu::triggered,
                this, qOverload<QAction*>(&TopLevel::setEventType));

        if (withCost2) {
            popup2 = popup->addMenu(i18n("Secondary Event Type"));
            connect(popup2, &QMenu::triggered,
                    this, qOverload<QAction*>(&TopLevel::setEventType2));

            if (_eventType2) {
                action = popup2->addAction(i18n("Hide"));
                action->setData(199);
                popup2->addSeparator();
            }
        }

        EventTypeSet* m = _data->eventTypes();
        EventType* ct;
        for (int i=0;i<m->realCount();i++) {
            ct = m->realType(i);

            action = popup1->addAction(ct->longName());
            action->setCheckable(true);
            action->setData(100+i);
            if (_eventType == ct) action->setChecked(true);

            if (popup2) {
                action = popup2->addAction(ct->longName());
                action->setCheckable(true);
                action->setData(100+i);
                if (_eventType2 == ct) action->setChecked(true);
            }
        }

        for (int i=0;i<m->derivedCount();i++) {
            ct = m->derivedType(i);

            action = popup1->addAction(ct->longName());
            action->setCheckable(true);
            action->setData(200+i);
            if (_eventType == ct) action->setChecked(true);

            if (popup2) {
                action = popup2->addAction(ct->longName());
                action->setCheckable(true);
                action->setData(200+i);
                if (_eventType2 == ct) action->setChecked(true);
            }
        }
    }

    if (_showPercentage)
        popup->addAction(i18n("Show Absolute Cost"),
                         this, SLOT(setAbsoluteCost()));
    else
        popup->addAction(i18n("Show Relative Cost"),
                         this, SLOT(setRelativeCost()));
}

void TopLevel::setEventType(QAction* action)
{
    if (!_data) return;

    int id = action->data().toInt(nullptr);

    EventTypeSet* m = _data->eventTypes();
    EventType* ct=nullptr;
    if (id >=100 && id<199) ct = m->realType(id-100);
    if (id >=200 && id<299) ct = m->derivedType(id-200);

    if (ct)
        setEventType(ct);
}

void TopLevel::setEventType2(QAction* action)
{
    if (!_data) return;

    int id = action->data().toInt(nullptr);

    EventTypeSet* m = _data->eventTypes();
    EventType* ct=nullptr;
    if (id >=100 && id<199) ct = m->realType(id-100);
    if (id >=200 && id<299) ct = m->derivedType(id-200);

    setEventType2(ct);
}

void TopLevel::addGoMenu(QMenu* popup)
{
    popup->addAction(i18n("Go Back"), this, SLOT(goBack()));
    popup->addAction(i18n("Go Forward"), this, SLOT(goForward()));
    popup->addAction(i18n("Go Up"), this, SLOT(goUp()));
}

void TopLevel::goBack()
{
    setDirectionDelayed(TraceItemView::Back);
}

void TopLevel::goForward()
{
    setDirectionDelayed(TraceItemView::Forward);
}

void TopLevel::goUp()
{
    setDirectionDelayed(TraceItemView::Up);
}

QString TopLevel::traceKey()
{
    if (!_data || _data->command().isEmpty()) return QString();

    QString name = _data->command();
    QString key;
    for (int l=0;l<name.length();l++)
        if (name[l].isLetterOrNumber()) key += name[l];

    return QStringLiteral("-") + key;
}


void TopLevel::restoreTraceTypes()
{
    QString key = traceKey();
    QString groupType, eventType, eventType2;

    ConfigGroup* pConfig = ConfigStorage::group(QStringLiteral("TracePositions"));
    groupType = pConfig->value(QStringLiteral("GroupType%1").arg(key),QString()).toString();
    eventType = pConfig->value(QStringLiteral("EventType%1").arg(key),QString()).toString();
    eventType2 = pConfig->value(QStringLiteral("EventType2%1").arg(key),QString()).toString();
    delete pConfig;

    ConfigGroup* cConfig = ConfigStorage::group(QStringLiteral("CurrentState"));
    if (groupType.isEmpty())
        groupType = cConfig->value(QStringLiteral("GroupType"),QString()).toString();
    if (eventType.isEmpty())
        eventType = cConfig->value(QStringLiteral("EventType"),QString()).toString();
    if (eventType2.isEmpty())
        eventType2 = cConfig->value(QStringLiteral("EventType2"),QString()).toString();
    delete cConfig;

    setGroupType(groupType);
    setEventType(eventType);
    setEventType2(eventType2);

    // if still no cost type set, use first available
    if (!_eventType && !_saCost->items().isEmpty())
        eventTypeSelected(_saCost->items().first());

    ConfigGroup* aConfig = ConfigStorage::group(QStringLiteral("Layouts"));
    _layoutCount = aConfig->value(QStringLiteral("Count%1").arg(key), 0).toInt();
    _layoutCurrent = aConfig->value(QStringLiteral("Current%1").arg(key), 0).toInt();
    delete aConfig;

    if (_layoutCount == 0) layoutRestore();
    updateLayoutActions();
}


/**
 * This must be called after setting group/cost types in the function
 * selection widget, because the group/function choosing depends on
 * filled lists in the function selection widget
 */
void TopLevel::restoreTraceSettings()
{
    if (!_data) return;

    QString key = traceKey();

    restoreCurrentState(key);

    ConfigGroup* pConfig = ConfigStorage::group(QStringLiteral("TracePositions"));
    QString group = pConfig->value(QStringLiteral("Group%1").arg(key),QString()).toString();
    delete pConfig;
    if (!group.isEmpty()) setGroup(group);


    // restoreCurrentState() usually leads to a call to setTraceItemDelayed()
    // to restore last active item...
    if (!_traceItemDelayed) {
        // function not available any more.. try with "main"
        if (!setFunction(QStringLiteral("main")))
            _functionSelection->selectTopFunction();
    }
}


/* Layout */

void TopLevel::layoutDuplicate()
{
    // save current and allocate a new slot
    _multiView->saveLayout(QStringLiteral("Layout%1-MainView").arg(_layoutCurrent),
                           traceKey());
    _layoutCurrent = _layoutCount;
    _layoutCount++;

    updateLayoutActions();

    if (0) qDebug() << "TopLevel::layoutDuplicate: count " << _layoutCount;
}

void TopLevel::layoutRemove()
{
    if (_layoutCount <2) return;

    int from = _layoutCount-1;
    if (_layoutCurrent == from) { _layoutCurrent--; from--; }

    // restore from last and decrement count
    _multiView->restoreLayout(QStringLiteral("Layout%1-MainView").arg(from),
                              traceKey());
    _layoutCount--;

    updateLayoutActions();
}

void TopLevel::layoutNext()
{
    if (_layoutCount <2) return;

    QString key = traceKey();
    QString layoutPrefix = QStringLiteral("Layout%1-MainView");

    _multiView->saveLayout(layoutPrefix.arg(_layoutCurrent), key);
    _layoutCurrent++;
    if (_layoutCurrent == _layoutCount) _layoutCurrent = 0;
    _multiView->restoreLayout(layoutPrefix.arg(_layoutCurrent), key);

    if (0) qDebug() << "TopLevel::layoutNext: current "
                    << _layoutCurrent;
}

void TopLevel::layoutPrevious()
{
    if (_layoutCount <2) return;

    QString key = traceKey();
    QString layoutPrefix = QStringLiteral("Layout%1-MainView");

    _multiView->saveLayout(layoutPrefix.arg(_layoutCurrent), key);
    _layoutCurrent--;
    if (_layoutCurrent <0) _layoutCurrent = _layoutCount-1;
    _multiView->restoreLayout(layoutPrefix.arg(_layoutCurrent), key);

    if (0) qDebug() << "TopLevel::layoutPrevious: current "
                    << _layoutCurrent;
}

void TopLevel::layoutSave()
{
    QString key = traceKey();
    QString layoutPrefix = QStringLiteral("Layout%1-MainView");

    _multiView->saveLayout(layoutPrefix.arg(_layoutCurrent), key);

    // save all layouts as defaults (ie. without any group name postfix)
    for(int i=0;i<_layoutCount;i++) {
        _multiView->restoreLayout(layoutPrefix.arg(i), key);
        _multiView->saveLayout(layoutPrefix.arg(i), QString());
    }
    // restore the previously saved current layout
    _multiView->restoreLayout(layoutPrefix.arg(_layoutCurrent), key);

    ConfigGroup* layoutConfig = ConfigStorage::group(QStringLiteral("Layouts"));
    layoutConfig->setValue(QStringLiteral("DefaultCount"), _layoutCount);
    layoutConfig->setValue(QStringLiteral("DefaultCurrent"), _layoutCurrent);
    delete layoutConfig;
}

void TopLevel::layoutRestore()
{
    KConfig *config = KSharedConfig::openConfig().data();
    KConfigGroup aConfig(config, "Layouts");
    _layoutCount = aConfig.readEntry("DefaultCount", 0);
    _layoutCurrent = aConfig.readEntry("DefaultCurrent", 0);
    if (_layoutCount == 0) {
        _layoutCount++;
        return;
    }

    QString layoutPrefix = QStringLiteral("Layout%1-MainView");
    _multiView->restoreLayout( layoutPrefix.arg(_layoutCurrent), traceKey());

    updateLayoutActions();
}


void TopLevel::updateLayoutActions()
{
    QAction* ka;

    ka = actionCollection()->action(QStringLiteral("layout_next"));
    if (ka) ka->setEnabled(_layoutCount>1);

    ka = actionCollection()->action(QStringLiteral("layout_previous"));
    if (ka) ka->setEnabled(_layoutCount>1);

    ka = actionCollection()->action(QStringLiteral("layout_remove"));
    if (ka) ka->setEnabled(_layoutCount>1);

    _statusbar->showMessage(i18n("Layout Count: %1", _layoutCount), 1000);
}


void TopLevel::updateStatusBar()
{
    if (!_data || _data->parts().isEmpty()) {
        _statusLabel->setText(i18n("No profile data file loaded."));
        return;
    }

    QString status = QStringLiteral("%1 [%2] - ")
                     .arg(_data->shortTraceName())
                     .arg(_data->activePartRange());

    if (_eventType) {
        status += i18n("Total %1 Cost: %2",
                       _eventType->longName(),
                       _data->prettySubCost(_eventType));

        /* this gets too long...
        if (_eventType2 && (_eventType2 != _eventType))
          status += i18n(", %1 Cost: %2")
        .arg(_eventType2->longName())
        .arg(_data->prettySubCost(_eventType2));
        */
    }
    else
        status += i18n("No event type selected");

    /* Not working... should give group of selected function

    if (_groupType != ProfileContext::Function) {
      status += QString(" - %1 '%2'")
                .arg(ProfileContext::i18nTypeName(_groupType))
                .arg(_group ? _group->prettyName() : i18n("(None)"));
    }
    */

    _statusLabel->setText(status);
}

void TopLevel::configure()
{
    if (ConfigDlg::configure( GlobalGUIConfig::config(),
                              _data, this)) {
        GlobalGUIConfig::config()->saveOptions();

        configChanged();
    }
    else
        GlobalGUIConfig::config()->readOptions();
}

bool TopLevel::queryClose()
{
    saveTraceSettings();

    // save current toplevel options as defaults...
    GlobalConfig::setShowPercentage(_showPercentage);
    GlobalConfig::setShowExpanded(_showExpanded);
    GlobalConfig::setShowCycles(_showCycles);
    GlobalConfig::setHideTemplates(_hideTemplates);
    GlobalGUIConfig::config()->saveOptions();

    saveCurrentState(QString());

    // toolbar and dock positions are automatically stored

    // if part dock was chosen visible even for only 1 part loaded,
    // keep this choice...
    _forcePartDock = false;
    if (_data && (_data->parts().count()<2) && _partDock->isVisible())
        _forcePartDock=true;
    KConfigGroup dockConfig(KSharedConfig::openConfig(), "Docks");
    dockConfig.writeEntry("ForcePartDockVisible", _forcePartDock);

    return true;
}


void TopLevel::splitSlot()
{
    int count = _multiView->childCount();
    if (count<1) count = 1;
    if (count>2) count = 2;
    count = 3-count;
    _multiView->setChildCount(count);

    _taSplit->setChecked(count>1);
    _taSplitDir->setEnabled(count>1);
    _taSplitDir->setChecked(_multiView->orientation() == Qt::Horizontal);
}

void TopLevel::splitDirSlot()
{
    _multiView->setOrientation( _taSplitDir->isChecked() ?
                                    Qt::Horizontal : Qt::Vertical );
}



// this is called after a config change in the dialog
void TopLevel::configChanged()
{
    // Invalidate found/cached dirs of source files if we have TraceData loaded.
    if (_data) {
        _data->resetSourceDirs();
    }

    _stackSelection->refresh();

    updateViewsOnChange(TraceItemView::configChanged);
}

void TopLevel::dummySlot()
{
}

void TopLevel::activePartsChangedSlot(const TracePartList& list)
{
    if (!_data) return;

    if (!_data->activateParts(list)) {
        //    qDebug("TopLevel::activePartsChangedSlot: No Change!");
        return;
    }
    _activeParts = list;

    _partSelection->set(list);
    _multiView->set(list);
    _functionSelection->set(list);
    _stackSelection->refresh();

    updateStatusBar();
}

void TopLevel::partsHideSelectedSlotDelayed()
{
    QTimer::singleShot( 0, this, &TopLevel::partsHideSelectedSlot );
}

// this puts selected parts into hidden list,
// deselects them and makes the remaining parts selected
void TopLevel::partsHideSelectedSlot()
{
    if (!_data) return;

    TracePartList newHidden, newActive;
    foreach(TracePart* part, _data->parts()) {
        if (_activeParts.contains(part) ||
            _hiddenParts.contains(part))
            newHidden.append(part);
        else
            newActive.append(part);
    }

    _hiddenParts = newHidden;
    _partSelection->hiddenPartsChangedSlot(_hiddenParts);

#if 0
    _mainWidget1->hiddenPartsChangedSlot(_hiddenParts);
    _mainWidget2->hiddenPartsChangedSlot(_hiddenParts);
#endif

    activePartsChangedSlot(newActive);
}

void TopLevel::partsUnhideAllSlotDelayed()
{
    QTimer::singleShot( 0, this, &TopLevel::partsUnhideAllSlot );
}

// this unhides all hidden parts. Does NOT change selection
void TopLevel::partsUnhideAllSlot()
{
    if (!_data) return;

    _hiddenParts.clear();
    _partSelection->hiddenPartsChangedSlot(_hiddenParts);
#if 0
    _mainWidget1->hiddenPartsChangedSlot(_hiddenParts);
    _mainWidget2->hiddenPartsChangedSlot(_hiddenParts);
#endif
}

void TopLevel::forceTrace()
{
    if (_ccProcess) {
        // callgrind_control still running, cancel request
        qDebug("TopLevel::forceTrace: killing old callgrind_control");
        _ccProcess->kill();
        delete _ccProcess;
        _ccProcess = nullptr;
        _ccOutput = QString();
    }
    if (!_taDump->isChecked()) return;

    // get PID of first loaded part
    int pid = 0;
    TracePart* p = nullptr;
    TracePartList pl;
    if (_data) pl = _data->parts();
    if (!pl.isEmpty()) p = pl.first();
    if (p) pid = p->processID();
    if (pid == 0) {
        showMessage(i18n("Cannot determine receiver PID for dump request"),
                    5000);
        _taDump->setChecked(false);
        return;
    }

    qDebug("TopLevel::forceTrace: run 'callgrind_control -d %d'", pid);

    _ccProcess = new QProcess(this);
    connect(_ccProcess, &QProcess::readyReadStandardOutput,
            this, &TopLevel::ccReadOutput);
    connect(_ccProcess, &QProcess::errorOccurred,
            this, &TopLevel::ccError);
    connect(_ccProcess, &QProcess::finished,
            this, &TopLevel::ccExit);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    _ccProcess->start(QStringLiteral("callgrind_control -d %1").arg(pid),
                      QIODevice::ReadOnly);
#else
    _ccProcess->startCommand(QStringLiteral("callgrind_control -d %1").arg(pid),
                      QIODevice::ReadOnly);
#endif
}

void TopLevel::ccReadOutput()
{
    QProcess* p = qobject_cast<QProcess*>(sender());
    qDebug("TopLevel::ccReadOutput: QProcess %p", p);

    // signal from old/uninteresting process?
    if (!_ccProcess) return;
    if (p != _ccProcess) return;

    _ccOutput.append(QString::fromLocal8Bit(_ccProcess->readAllStandardOutput()));
}

void TopLevel::ccError(QProcess::ProcessError e)
{
    QProcess* p = qobject_cast<QProcess*>(sender());
    qDebug("TopLevel::ccError: Got %d from QProcess %p",
           e, p);

    // signal from old/uninteresting process?
    if (!_ccProcess) return;
    if (p != _ccProcess) return;

    showMessage(i18n("Error running callgrind_control"), 5000);

    _ccProcess->deleteLater();
    _ccProcess = nullptr;
}

void TopLevel::ccExit(int exitCode, QProcess::ExitStatus s)
{
    QProcess* p = qobject_cast<QProcess*>(sender());
    qDebug("TopLevel::ccExit: QProcess %p, exitCode %d",
           p, exitCode);

    // signal from old/uninteresting process?
    if (!_ccProcess) return;
    if (p != _ccProcess) return;
    _ccProcess->deleteLater();
    _ccProcess = nullptr;
    _taDump->setChecked(false);

    // if not successful no need to reload
    if ((s == QProcess::CrashExit) || (exitCode != 0))
        return;

    // FIXME: Are we sure that file is completely
    //        dumped after waiting one second?
    QTimer::singleShot( 1000, this, &TopLevel::reload );
}

void TopLevel::forwardAboutToShow()
{
    QMenu *popup = _paForward->popupMenu();

    popup->clear();
    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    HistoryItem* hi = b ? b->current() : nullptr;
    TraceFunction* f;
    QAction* action;

    if (!hi) {
        popup->addAction(i18n("(No Stack)"));
        return;
    }

    hi = hi->next();
    if (!hi) {
        popup->addAction(i18n("(No next function)"));
        return;
    }

    int count = 1;
    while (count<GlobalConfig::maxSymbolCount() && hi) {
        f = hi->function();
        if (!f) break;

        QString name = GlobalConfig::shortenSymbol(f->prettyName());

        //qDebug("forward: Adding %s", name.ascii());
        action = popup->addAction(name);
        action->setData(count);

        hi = hi->next();
        count++;
    }
}

void TopLevel::backAboutToShow()
{
    QMenu *popup = _paBack->popupMenu();

    popup->clear();
    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    HistoryItem* hi = b ? b->current() : nullptr;
    TraceFunction* f;
    QAction* action;

    if (!hi) {
        popup->addAction(i18n("(No Stack)"));
        return;
    }

    hi = hi->last();
    if (!hi) {
        popup->addAction(i18n("(No previous function)"));
        return;
    }

    int count = 1;
    while (count<GlobalConfig::maxSymbolCount() && hi) {
        f = hi->function();
        if (!f) break;

        QString name = GlobalConfig::shortenSymbol(f->prettyName());

        //qDebug("back: Adding %s", name.ascii());
        action = popup->addAction(name);
        action->setData(count);

        hi = hi->last();
        count++;
    }
}

void TopLevel::upAboutToShow()
{
    QMenu *popup = _paUp->popupMenu();

    popup->clear();
    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    HistoryItem* hi = b ? b->current() : nullptr;
    TraceFunction* f = hi ? hi->function() : nullptr;
    QAction* action;

    if (!f) {
        popup->addAction(i18n("(No Stack)"));
        return;
    }
    f = hi->stack()->caller(f, false);
    if (!f) {
        popup->addAction(i18n("(No Function Up)"));
        return;
    }

    int count = 1;
    while (count<GlobalConfig::maxSymbolCount() && f) {
        QString name = GlobalConfig::shortenSymbol(f->prettyName());

        action = popup->addAction(name);
        action->setData(count);

        f = hi->stack()->caller(f, false);
        count++;
    }

}

void TopLevel::forwardTriggered(QAction* action)
{
    int count = action->data().toInt(nullptr);
    //qDebug("forwardTriggered: %d", count);
    if( count <= 0)
        return;

    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    if (!b) return;

    while (count>1) {
        b->goForward();
        count--;
    }
    _stackSelection->browserForward();
}

void TopLevel::backTriggered(QAction* action)
{
    int count = action->data().toInt(nullptr);
    //qDebug("backTriggered: %d", count);
    if( count <= 0)
        return;

    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    if (!b) return;

    while (count>1) {
        b->goBack();
        count--;
    }
    _stackSelection->browserBack();
}

void TopLevel::upTriggered(QAction* action)
{
    int count = action->data().toInt(nullptr);
    //qDebug("upTriggered: %d", count);
    if( count <= 0)
        return;

    StackBrowser* b = _stackSelection ? _stackSelection->browser() : nullptr;
    HistoryItem* hi = b ? b->current() : nullptr;
    if (!hi) return;

    TraceFunction* f = hi->function();

    while (count>0 && f) {
        f = hi->stack()->caller(f, false);
        count--;
    }

    //qDebug("upActivated: %s", f ? f->prettyName().ascii() : "??" );
    if (f)
        setFunction(f);
}

void TopLevel::showMessage(const QString& msg, int ms)
{
    if (_statusbar)
        _statusbar->showMessage(msg, ms);
}

void TopLevel::showStatus(const QString& msg, int progress)
{
    static bool msgUpdateNeeded = true;

    if (!_statusbar) return;

    if (msg.isEmpty()) {
        //reset status
        if (_progressBar) {
            _statusbar->removeWidget(_progressBar);
            delete _progressBar;
            _progressBar = nullptr;
        }
        _statusbar->clearMessage();
        _progressMsg = msg;
        return;
    }

    if (_progressMsg.isEmpty())
        _progressStart.start();

    if (msg != _progressMsg) {
        _progressMsg = msg;
        msgUpdateNeeded = true;
    }

    // do nothing if last change was less than 0.5 seconds ago
    if (_progressStart.elapsed() < 500)
        return;

    if (!_progressBar) {
        _progressBar = new QProgressBar(_statusbar);
        _progressBar->setMaximumSize(200, _statusbar->height()-4);
        _statusbar->addPermanentWidget(_progressBar, 1);
        _progressBar->show();
        msgUpdateNeeded = true;
    }

    _progressStart.restart();

    if (msgUpdateNeeded) {
        _statusbar->showMessage(msg);
        msgUpdateNeeded = false;
    }
    _progressBar->setValue(progress);

    // let the progress bar update itself
    qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
}

void TopLevel::loadStart(const QString& filename)
{
    showStatus(i18n("Loading %1", filename), 0);
    Logger::_filename = filename;
}

void TopLevel::loadFinished(const QString& msg)
{
    showStatus(QString(), 0);
    if (!msg.isEmpty())
        showMessage(i18n("Error loading %1: %2", _filename, msg),
                    2000);
}

void TopLevel::loadProgress(int progress)
{
    showStatus(i18n("Loading %1", _filename), progress);
}

void TopLevel::loadError(int line, const QString& msg)
{
    qCritical() << "Loading" << _filename << ":" << line << ": " << msg;
}

void TopLevel::loadWarning(int line, const QString& msg)
{
    qWarning() << "Loading" << _filename << ":" << line << ": " << msg;
}

bool TopLevel::openDataFile(const QString& file)
{
    TraceData* d = new TraceData(this);
    int filesLoaded;

    // see whether this file is compressed, than take the direct route
    QMimeDatabase dataBase;
    QString mimeType = dataBase.mimeTypeForFile(file, QMimeDatabase::MatchContent).name();

    auto compressionType = KCompressionDevice::compressionTypeForMimeType(mimeType);
    KCompressionDevice* compressed;
    compressed = new KCompressionDevice(file,
                                        compressionType);
    if (compressed &&
        (compressed->compressionType() != KCompressionDevice::None)) {
        filesLoaded = d->load(compressed, file);
    } else {
        // else fallback to string based method that can also find multi-part callgrind data.
        filesLoaded = d->load(file);
    }
    if (filesLoaded > 0) {
        setData(d);
        return true;
    } else {
        return false;
    }
}

#include "moc_toplevel.cpp"
