From 46f93311afc9f1e2afc306f63cee0e4f462758e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 11 Mar 2013 22:19:17 +0100 Subject: [PATCH] Instance view, model, delegate. --- CMakeLists.txt | 14 +- gui/instancedelegate.cpp | 222 +++ gui/instancedelegate.h | 12 + gui/instancemodel.cpp | 92 + gui/instancemodel.h | 38 + gui/mainwindow.cpp | 182 +- gui/mainwindow.h | 21 +- gui/mainwindow.ui | 16 - libgroupview/CMakeLists.txt | 41 + .../kcategorizedsortfilterproxymodel.h | 175 ++ libgroupview/include/kcategorizedview.h | 322 ++++ libgroupview/include/kcategorydrawer.h | 179 ++ libgroupview/include/libgroupview_config.h | 27 + .../src/kcategorizedsortfilterproxymodel.cpp | 168 ++ .../src/kcategorizedsortfilterproxymodel_p.h | 48 + libgroupview/src/kcategorizedview.cpp | 1696 +++++++++++++++++ libgroupview/src/kcategorizedview_p.h | 157 ++ libgroupview/src/kcategorydrawer.cpp | 231 +++ libmultimc/include/appsettings.h | 4 +- libmultimc/include/logintask.h | 8 +- libmultimc/src/appsettings.cpp | 3 +- libmultimc/src/instancelist.cpp | 2 +- libmultimc/src/logintask.cpp | 22 +- main.cpp | 16 +- 24 files changed, 3585 insertions(+), 111 deletions(-) create mode 100644 gui/instancedelegate.cpp create mode 100644 gui/instancedelegate.h create mode 100644 gui/instancemodel.cpp create mode 100644 gui/instancemodel.h create mode 100644 libgroupview/CMakeLists.txt create mode 100644 libgroupview/include/kcategorizedsortfilterproxymodel.h create mode 100644 libgroupview/include/kcategorizedview.h create mode 100644 libgroupview/include/kcategorydrawer.h create mode 100644 libgroupview/include/libgroupview_config.h create mode 100644 libgroupview/src/kcategorizedsortfilterproxymodel.cpp create mode 100644 libgroupview/src/kcategorizedsortfilterproxymodel_p.h create mode 100644 libgroupview/src/kcategorizedview.cpp create mode 100644 libgroupview/src/kcategorizedview_p.h create mode 100644 libgroupview/src/kcategorydrawer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9aa75f4d..cf6e1aab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,10 @@ include_directories(${LIBSETTINGS_INCLUDE_DIR}) add_subdirectory(libmultimc) include_directories(${LIBMULTIMC_INCLUDE_DIR}) +# Add the group view library. +add_subdirectory(libgroupview) +include_directories(${LIBGROUPVIEW_INCLUDE_DIR}) + # Add the stdinstance plugin. add_subdirectory(plugins/stdinstance) @@ -166,6 +170,8 @@ gui/taskdialog.h gui/browserdialog.h gui/aboutdialog.h gui/consolewindow.h +gui/instancemodel.h +gui/instancedelegate.h multimc_pragma.h @@ -192,6 +198,8 @@ gui/taskdialog.cpp gui/browserdialog.cpp gui/aboutdialog.cpp gui/consolewindow.cpp +gui/instancemodel.cpp +gui/instancedelegate.cpp java/javautils.cpp java/annotations.cpp @@ -222,7 +230,7 @@ ENDIF() # ICNS file for OS X IF(APPLE) - SET(MACOSX_BUNDLE_ICON_FILE MultiMC.icns) + SET(MACOSX_BUNDLE_ICON_FILE MultiMC.icns) SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_SOURCE_DIR}/MultiMC.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) SET(MULTIMC_SOURCES ${MULTIMC_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/MultiMC.icns) ENDIF(APPLE) @@ -248,9 +256,9 @@ ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 # Link QT5_USE_MODULES(MultiMC Widgets Network WebKitWidgets) TARGET_LINK_LIBRARIES(MultiMC quazip patchlib -libUtil libSettings libMultiMC +libUtil libSettings libMultiMC libGroupView ${MultiMC_LINK_ADDITIONAL_LIBS}) -ADD_DEPENDENCIES(MultiMC MultiMCLauncher libUtil libSettings libMultiMC) +ADD_DEPENDENCIES(MultiMC MultiMCLauncher libUtil libSettings libMultiMC libGroupView) ################################ INSTALLATION AND PACKAGING ################################ diff --git a/gui/instancedelegate.cpp b/gui/instancedelegate.cpp new file mode 100644 index 00000000..dac2dacb --- /dev/null +++ b/gui/instancedelegate.cpp @@ -0,0 +1,222 @@ +#include "instancedelegate.h" +#include +#include +#include +#include +#include + +// Origin: Qt +static void viewItemTextLayout ( QTextLayout &textLayout, int lineWidth, qreal &height, qreal &widthUsed ) +{ + height = 0; + widthUsed = 0; + textLayout.beginLayout(); + while ( true ) + { + QTextLine line = textLayout.createLine(); + if ( !line.isValid() ) + break; + line.setLineWidth ( lineWidth ); + line.setPosition ( QPointF ( 0, height ) ); + height += line.height(); + widthUsed = qMax ( widthUsed, line.naturalTextWidth() ); + } + textLayout.endLayout(); +} + +#define QFIXED_MAX (INT_MAX/256) + +ListViewDelegate::ListViewDelegate ( QObject* parent ) : QStyledItemDelegate ( parent ) +{ + +} + +void drawSelectionRect(QPainter *painter, const QStyleOptionViewItemV4 &option, const QRect &rect) +{ + if (!(option.state & QStyle::State_Selected)) + return; + painter->fillRect ( rect, option.palette.brush ( QPalette::Highlight ) ); +} + +void drawFocusRect(QPainter *painter, const QStyleOptionViewItemV4 &option, const QRect &rect) +{ + if (!(option.state & QStyle::State_HasFocus)) + return; + QStyleOptionFocusRect opt; + opt.direction = option.direction; + opt.fontMetrics = option.fontMetrics; + opt.palette = option.palette; + opt.rect = rect; + //opt.state = option.state | QStyle::State_KeyboardFocusChange | QStyle::State_Item; + auto col = option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base; + opt.backgroundColor = option.palette.color(col); + // Apparently some widget styles expect this hint to not be set + painter->setRenderHint(QPainter::Antialiasing, false); + + QStyle *style = option.widget ? option.widget->style() : QApplication::style(); + + style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget); + + painter->setRenderHint(QPainter::Antialiasing); +} + +static QSize viewItemTextSize ( const QStyleOptionViewItemV4 *option ) +{ + QStyle *style = option->widget ? option->widget->style() : QApplication::style(); + QTextOption textOption; + textOption.setWrapMode ( QTextOption::WrapAtWordBoundaryOrAnywhere ); + QTextLayout textLayout; + textLayout.setTextOption ( textOption ); + textLayout.setFont ( option->font ); + textLayout.setText ( option->text ); + const int textMargin = style->pixelMetric ( QStyle::PM_FocusFrameHMargin, option, option->widget ) + 1; + QRect bounds ( 0,0,100 - 2*textMargin,600 ); + qreal height = 0, widthUsed = 0; + viewItemTextLayout ( textLayout, bounds.width(), height, widthUsed ); + const QSize size ( qCeil ( widthUsed ), qCeil ( height ) ); + return QSize ( size.width() + 2 * textMargin, size.height() ); +} + +void ListViewDelegate::paint ( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const +{ + QStyleOptionViewItemV4 opt = option; + initStyleOption ( &opt, index ); + painter->save(); + painter->setClipRect ( opt.rect ); + + opt.features |= QStyleOptionViewItem::WrapText; + opt.text = index.data().toString(); + opt.textElideMode = Qt::ElideRight; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); + + //const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize); + const int iconSize = 48; + QRect iconbox = opt.rect; + const int textMargin = style->pixelMetric ( QStyle::PM_FocusFrameHMargin, 0, opt.widget ) + 1; + QRect textRect = opt.rect; + QRect textHighlightRect = textRect; + // clip the decoration on top, remove width padding + textRect.adjust ( textMargin,iconSize + textMargin + 5,-textMargin,0 ); + + textHighlightRect.adjust ( 0,iconSize + 5,0,0 ); + + // draw background + { + QSize textSize = viewItemTextSize ( &opt ); + QPalette::ColorGroup cg; + QStyleOptionViewItemV4 opt2(opt); + + if((opt.widget && opt.widget->isEnabled()) || (opt.state & QStyle::State_Enabled)) + { + if(! ( opt.state & QStyle::State_Active )) + cg = QPalette::Inactive; + else + cg = QPalette::Normal; + } + else + { + cg = QPalette::Disabled; + } + opt2.palette.setCurrentColorGroup(cg); + + // fill in background, if any + if ( opt.backgroundBrush.style() != Qt::NoBrush ) + { + QPointF oldBO = painter->brushOrigin(); + painter->setBrushOrigin ( opt.rect.topLeft() ); + painter->fillRect ( opt.rect, opt.backgroundBrush ); + painter->setBrushOrigin ( oldBO ); + } + + if ( opt.showDecorationSelected ) + { + drawSelectionRect(painter,opt2, opt.rect); + drawFocusRect(painter,opt2, opt.rect); + //painter->fillRect ( opt.rect, opt.palette.brush ( cg, QPalette::Highlight ) ); + } + else + { + + //if ( opt.state & QStyle::State_Selected ) + { + //QRect textRect = subElementRect ( QStyle::SE_ItemViewItemText, opt, opt.widget ); + //painter->fillRect ( textHighlightRect, opt.palette.brush ( cg, QPalette::Highlight ) ); + drawSelectionRect(painter,opt2, textHighlightRect); + drawFocusRect(painter,opt2, textHighlightRect); + } + } + } + + // draw the icon + { + QIcon::Mode mode = QIcon::Normal; + if ( ! ( opt.state & QStyle::State_Enabled ) ) + mode = QIcon::Disabled; + else if ( opt.state & QStyle::State_Selected ) + mode = QIcon::Selected; + QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off; + + iconbox.setHeight ( iconSize ); + opt.icon.paint ( painter, iconbox, Qt::AlignCenter, mode, state ); + } + // set the text colors + QPalette::ColorGroup cg = opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; + if ( cg == QPalette::Normal && ! ( opt.state & QStyle::State_Active ) ) + cg = QPalette::Inactive; + if ( opt.state & QStyle::State_Selected ) + { + painter->setPen ( opt.palette.color ( cg, QPalette::HighlightedText ) ); + } + else + { + painter->setPen ( opt.palette.color ( cg, QPalette::Text ) ); + } + + // draw the text + QTextOption textOption; + textOption.setWrapMode ( QTextOption::WrapAtWordBoundaryOrAnywhere ); + textOption.setTextDirection ( opt.direction ); + textOption.setAlignment ( QStyle::visualAlignment ( opt.direction, opt.displayAlignment ) ); + QTextLayout textLayout; + textLayout.setTextOption ( textOption ); + textLayout.setFont ( opt.font ); + textLayout.setText ( opt.text ); + + qreal width, height; + viewItemTextLayout ( textLayout, iconbox.width(), height, width ); + + const int lineCount = textLayout.lineCount(); + + const QRect layoutRect = QStyle::alignedRect ( opt.direction, opt.displayAlignment, QSize ( iconbox.width(), int ( height ) ), textRect ); + const QPointF position = layoutRect.topLeft(); + for ( int i = 0; i < lineCount; ++i ) + { + const QTextLine line = textLayout.lineAt ( i ); + line.draw ( painter, position ); + } + + painter->restore(); +} + + +QSize ListViewDelegate::sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const +{ + QStyleOptionViewItemV4 opt = option; + initStyleOption ( &opt, index ); + opt.features |= QStyleOptionViewItem::WrapText; + opt.text = index.data().toString(); + opt.textElideMode = Qt::ElideRight; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); + const int textMargin = style->pixelMetric ( QStyle::PM_FocusFrameHMargin, &option, opt.widget ) + 1; + int height = 48 + textMargin * 2 + 5; // TODO: turn constants into variables + QSize szz = viewItemTextSize ( &opt ); + height += szz.height(); + // FIXME: maybe the icon items could scale and keep proportions? + QSize sz ( 100,height ); + return sz; +} + diff --git a/gui/instancedelegate.h b/gui/instancedelegate.h new file mode 100644 index 00000000..c80f95a5 --- /dev/null +++ b/gui/instancedelegate.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class ListViewDelegate : public QStyledItemDelegate +{ +public: + explicit ListViewDelegate ( QObject* parent = 0 ); +protected: + void paint ( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const; + QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const; +}; \ No newline at end of file diff --git a/gui/instancemodel.cpp b/gui/instancemodel.cpp new file mode 100644 index 00000000..73d0dbc1 --- /dev/null +++ b/gui/instancemodel.cpp @@ -0,0 +1,92 @@ +#include "instancemodel.h" +#include +#include + +InstanceModel::InstanceModel ( const InstanceList& instances, QObject *parent ) + : QAbstractListModel ( parent ), m_instances ( &instances ) +{ + cachedIcon = QIcon(":/icons/multimc/scalable/apps/multimc.svg"); +} + +int InstanceModel::rowCount ( const QModelIndex& parent ) const +{ + Q_UNUSED ( parent ); + return m_instances->count(); +} + +QModelIndex InstanceModel::index ( int row, int column, const QModelIndex& parent ) const +{ + Q_UNUSED ( parent ); + if ( row < 0 || row >= m_instances->count() ) + return QModelIndex(); + return createIndex ( row, column, ( void* ) m_instances->at ( row ).data() ); +} + +QVariant InstanceModel::data ( const QModelIndex& index, int role ) const +{ + if ( !index.isValid() ) + { + return QVariant(); + } + Instance *pdata = static_cast ( index.internalPointer() ); + switch ( role ) + { + case InstancePointerRole: + { + QVariant v = qVariantFromValue((void *) pdata); + return v; + } + case Qt::DisplayRole: + { + return pdata->name(); + } + case Qt::ToolTipRole: + { + return pdata->rootDir(); + } + case Qt::DecorationRole: + { + // FIXME: replace with an icon cache + return cachedIcon; + } + // for now. + case KCategorizedSortFilterProxyModel::CategorySortRole: + case KCategorizedSortFilterProxyModel::CategoryDisplayRole: + { + return "IT'S A GROUP"; + } + default: + break; + } + return QVariant(); +} + +Qt::ItemFlags InstanceModel::flags ( const QModelIndex& index ) const +{ + Qt::ItemFlags f; + if ( index.isValid() ) + { + f |= ( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); + } + return f; +} + +InstanceProxyModel::InstanceProxyModel ( QObject *parent ) + : KCategorizedSortFilterProxyModel ( parent ) +{ + // disable since by default we are globally sorting by date: + setCategorizedModel(true); +} + +bool InstanceProxyModel::subSortLessThan ( + const QModelIndex& left, const QModelIndex& right ) const +{ + Instance *pdataLeft = static_cast ( left.internalPointer() ); + Instance *pdataRight = static_cast ( right.internalPointer() ); + //kDebug() << *pdataLeft << *pdataRight; + return QString::localeAwareCompare(pdataLeft->name(), pdataRight->name()) < 0; + //return pdataLeft->name() < pdataRight->name(); +} + +#include "instancemodel.moc" + diff --git a/gui/instancemodel.h b/gui/instancemodel.h new file mode 100644 index 00000000..995c51ec --- /dev/null +++ b/gui/instancemodel.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "kcategorizedsortfilterproxymodel.h" +#include "instancelist.h" +#include + +class InstanceModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum AdditionalRoles + { + InstancePointerRole = 0x34B1CB48 ///< Return pointer to real instance + }; + explicit InstanceModel ( const InstanceList& instances, + QObject *parent = 0 ); + + QModelIndex index ( int row, int column = 0, + const QModelIndex& parent = QModelIndex() ) const; + int rowCount ( const QModelIndex& parent = QModelIndex() ) const; + QVariant data ( const QModelIndex& index, int role ) const; + Qt::ItemFlags flags ( const QModelIndex& index ) const; + +private: + const InstanceList* m_instances; + QIcon cachedIcon; +}; + +class InstanceProxyModel : public KCategorizedSortFilterProxyModel +{ +public: + explicit InstanceProxyModel ( QObject *parent = 0 ); + +protected: + virtual bool subSortLessThan ( const QModelIndex& left, const QModelIndex& right ) const; +}; + diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 6866da28..e691c8c4 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -7,7 +7,7 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software @@ -40,45 +40,94 @@ #include "gui/browserdialog.h" #include "gui/aboutdialog.h" +#include "kcategorizedview.h" +#include "kcategorydrawer.h" + #include "instancelist.h" #include "appsettings.h" #include "version.h" #include "logintask.h" +#include + +#include "instancemodel.h" +#include "instancedelegate.h" // Opens the given file in the default application. // TODO: Move this somewhere. -void openInDefaultProgram(QString filename); +void openInDefaultProgram ( QString filename ); -MainWindow::MainWindow(QWidget *parent) : - QMainWindow(parent), - ui(new Ui::MainWindow), - instList(globalSettings->get("InstanceDir").toString()) +MainWindow::MainWindow ( QWidget *parent ) : + QMainWindow ( parent ), + ui ( new Ui::MainWindow ), + instList ( globalSettings->get ( "InstanceDir" ).toString() ) { - ui->setupUi(this); + ui->setupUi ( this ); + // Create the widget + instList.loadList(); - setWindowTitle(QString("MultiMC %1").arg(Version::current.toString())); + view = new KCategorizedView ( ui->centralWidget ); + drawer = new KCategoryDrawer ( view ); + + view->setSelectionMode ( QAbstractItemView::SingleSelection ); + //view->setSpacing( KDialog::spacingHint() ); + view->setCategoryDrawer ( drawer ); + view->setCollapsibleBlocks ( true ); + view->setViewMode ( QListView::IconMode ); + view->setFlow ( QListView::LeftToRight ); + view->setWordWrap(true); + view->setMouseTracking ( true ); + view->viewport()->setAttribute ( Qt::WA_Hover ); + auto delegate = new ListViewDelegate(); + view->setItemDelegate(delegate); + view->setSpacing(10); + + model = new InstanceModel ( instList,this ); + proxymodel = new InstanceProxyModel ( this ); + proxymodel->setSortRole ( KCategorizedSortFilterProxyModel::CategorySortRole ); + proxymodel->setFilterRole ( KCategorizedSortFilterProxyModel::CategorySortRole ); + //proxymodel->setDynamicSortFilter ( true ); + proxymodel->setSourceModel ( model ); + proxymodel->sort ( 0 ); + + view->setFrameShape ( QFrame::NoFrame ); + + ui->horizontalLayout->addWidget ( view ); + setWindowTitle ( QString ( "MultiMC %1" ).arg ( Version::current.toString() ) ); // TODO: Make this work with the new settings system. // restoreGeometry(settings->getConfig().value("MainWindowGeometry", saveGeometry()).toByteArray()); // restoreState(settings->getConfig().value("MainWindowState", saveState()).toByteArray()); - - instList.loadList(); + view->setModel ( proxymodel ); + connect(view, SIGNAL(doubleClicked(const QModelIndex &)), + this, SLOT(instanceActivated(const QModelIndex &))); + } MainWindow::~MainWindow() { delete ui; + delete proxymodel; + delete model; + delete drawer; +} + +void MainWindow::instanceActivated ( QModelIndex index ) +{ + if(!index.isValid()) + return; + Instance * inst = (Instance *) index.data(InstanceModel::InstancePointerRole).value(); + doLogin(inst->id()); } void MainWindow::on_actionAddInstance_triggered() { - NewInstanceDialog *newInstDlg = new NewInstanceDialog(this); + NewInstanceDialog *newInstDlg = new NewInstanceDialog ( this ); newInstDlg->exec(); } void MainWindow::on_actionViewInstanceFolder_triggered() { - openInDefaultProgram(globalSettings->get("InstanceDir").toString()); + openInDefaultProgram ( globalSettings->get ( "InstanceDir" ).toString() ); } void MainWindow::on_actionRefresh_triggered() @@ -88,115 +137,126 @@ void MainWindow::on_actionRefresh_triggered() void MainWindow::on_actionViewCentralModsFolder_triggered() { - openInDefaultProgram(globalSettings->get("CentralModsDir").toString()); + openInDefaultProgram ( globalSettings->get ( "CentralModsDir" ).toString() ); } void MainWindow::on_actionCheckUpdate_triggered() { - + } void MainWindow::on_actionSettings_triggered() { - SettingsDialog dialog(this); + SettingsDialog dialog ( this ); dialog.exec(); } void MainWindow::on_actionReportBug_triggered() { - //QDesktopServices::openUrl(QUrl("http://bugs.forkk.net/")); - openWebPage(QUrl("http://bugs.forkk.net/")); + //QDesktopServices::openUrl(QUrl("http://bugs.forkk.net/")); + openWebPage ( QUrl ( "http://bugs.forkk.net/" ) ); } void MainWindow::on_actionNews_triggered() { - //QDesktopServices::openUrl(QUrl("http://news.forkk.net/")); - openWebPage(QUrl("http://news.forkk.net/")); + //QDesktopServices::openUrl(QUrl("http://news.forkk.net/")); + openWebPage ( QUrl ( "http://news.forkk.net/" ) ); } void MainWindow::on_actionAbout_triggered() { - AboutDialog dialog(this); - dialog.exec(); + AboutDialog dialog ( this ); + dialog.exec(); } -void MainWindow::on_mainToolBar_visibilityChanged(bool) +void MainWindow::on_mainToolBar_visibilityChanged ( bool ) { // Don't allow hiding the main toolbar. // This is the only way I could find to prevent it... :/ - ui->mainToolBar->setVisible(true); + ui->mainToolBar->setVisible ( true ); } -void MainWindow::closeEvent(QCloseEvent *event) +void MainWindow::closeEvent ( QCloseEvent *event ) { // Save the window state and geometry. // TODO: Make this work with the new settings system. // settings->getConfig().setValue("MainWindowGeometry", saveGeometry()); // settings->getConfig().setValue("MainWindowState", saveState()); - QMainWindow::closeEvent(event); + QMainWindow::closeEvent ( event ); } -void MainWindow::on_instanceView_customContextMenuRequested(const QPoint &pos) +void MainWindow::on_instanceView_customContextMenuRequested ( const QPoint &pos ) { - QMenu *instContextMenu = new QMenu("Instance", this); - + QMenu *instContextMenu = new QMenu ( "Instance", this ); + // Add the actions from the toolbar to the context menu. - instContextMenu->addActions(ui->instanceToolBar->actions()); - - instContextMenu->exec(ui->instanceView->mapToGlobal(pos)); + instContextMenu->addActions ( ui->instanceToolBar->actions() ); + + instContextMenu->exec ( view->mapToGlobal ( pos ) ); } void MainWindow::on_actionLaunchInstance_triggered() { - doLogin(); -} - -void MainWindow::doLogin(const QString &errorMsg) -{ - LoginDialog* loginDlg = new LoginDialog(this, errorMsg); - if (loginDlg->exec()) + QModelIndex index = view->currentIndex(); + if(index.isValid()) { - UserInfo uInfo(loginDlg->getUsername(), loginDlg->getPassword()); - - TaskDialog* tDialog = new TaskDialog(this); - LoginTask* loginTask = new LoginTask(uInfo, tDialog); - connect(loginTask, SIGNAL(loginComplete(LoginResponse)), - SLOT(onLoginComplete(LoginResponse)), Qt::QueuedConnection); - connect(loginTask, SIGNAL(loginFailed(QString)), - SLOT(doLogin(QString)), Qt::QueuedConnection); - tDialog->exec(loginTask); + Instance * inst = (Instance *) index.data(InstanceModel::InstancePointerRole).value(); + doLogin(inst->id()); } } -void MainWindow::onLoginComplete(LoginResponse response) +void MainWindow::doLogin ( QString inst, const QString& errorMsg ) { - QMessageBox::information(this, "Login Successful", - QString("Logged in as %1 with session ID %2."). - arg(response.username(), response.sessionID())); + LoginDialog* loginDlg = new LoginDialog ( this, errorMsg ); + if ( loginDlg->exec() ) + { + UserInfo uInfo ( loginDlg->getUsername(), loginDlg->getPassword() ); + + TaskDialog* tDialog = new TaskDialog ( this ); + LoginTask* loginTask = new LoginTask ( uInfo, inst, tDialog ); + connect ( loginTask, SIGNAL ( loginComplete ( QString, LoginResponse ) ), + SLOT ( onLoginComplete ( QString, LoginResponse ) ), Qt::QueuedConnection ); + connect ( loginTask, SIGNAL ( loginFailed ( QString, QString ) ), + SLOT ( onLoginFailed( QString, QString ) ), Qt::QueuedConnection ); + tDialog->exec ( loginTask ); + } } +void MainWindow::onLoginComplete ( QString inst, LoginResponse response ) +{ + QMessageBox::information ( this, "Login Successful", + QString ( "Logged in as %1 with session ID %2. Instance: %3" ). + arg ( response.username(), response.sessionID(), inst ) ); +} + +void MainWindow::onLoginFailed ( QString inst, const QString& errorMsg ) +{ + doLogin(inst, errorMsg); +} + + // Create A Desktop Shortcut void MainWindow::on_actionMakeDesktopShortcut_triggered() { - QString name("Test"); - name = QInputDialog::getText(this, tr("MultiMC Shortcut"), tr("Enter a Shortcut Name."), QLineEdit::Normal, name); + QString name ( "Test" ); + name = QInputDialog::getText ( this, tr ( "MultiMC Shortcut" ), tr ( "Enter a Shortcut Name." ), QLineEdit::Normal, name ); - Util::createShortCut(Util::getDesktopDir(), QApplication::instance()->applicationFilePath(), QStringList() << "-dl" << QDir::currentPath() << "test", name, "application-x-octet-stream"); + Util::createShortCut ( Util::getDesktopDir(), QApplication::instance()->applicationFilePath(), QStringList() << "-dl" << QDir::currentPath() << "test", name, "application-x-octet-stream" ); - QMessageBox::warning(this, "Not useful", "A Dummy Shortcut was created. it will not do anything productive"); + QMessageBox::warning ( this, "Not useful", "A Dummy Shortcut was created. it will not do anything productive" ); } // BrowserDialog -void MainWindow::openWebPage(QUrl url) +void MainWindow::openWebPage ( QUrl url ) { - BrowserDialog *browser = new BrowserDialog(this); + BrowserDialog *browser = new BrowserDialog ( this ); - browser->load(url); - browser->exec(); + browser->load ( url ); + browser->exec(); } -void openInDefaultProgram(QString filename) +void openInDefaultProgram ( QString filename ) { - QDesktopServices::openUrl("file:///" + QFileInfo(filename).absolutePath()); + QDesktopServices::openUrl ( "file:///" + QFileInfo ( filename ).absolutePath() ); } diff --git a/gui/mainwindow.h b/gui/mainwindow.h index 5ea575a6..c2dedf74 100644 --- a/gui/mainwindow.h +++ b/gui/mainwindow.h @@ -20,6 +20,12 @@ #include "instancelist.h" #include "loginresponse.h" +#include "instance.h" + +class InstanceModel; +class InstanceProxyModel; +class KCategorizedView; +class KCategoryDrawer; namespace Ui { @@ -67,14 +73,21 @@ private slots: void on_actionMakeDesktopShortcut_triggered(); - void doLogin(const QString& errorMsg = ""); + void doLogin( QString inst, const QString& errorMsg = "" ); - void onLoginComplete(LoginResponse response); - + void onLoginComplete( QString inst, LoginResponse response ); + void onLoginFailed( QString inst, const QString& errorMsg ); + +public slots: + void instanceActivated ( QModelIndex ); + private: Ui::MainWindow *ui; - + KCategoryDrawer * drawer; + KCategorizedView * view; + InstanceModel * model; + InstanceProxyModel * proxymodel; InstanceList instList; }; diff --git a/gui/mainwindow.ui b/gui/mainwindow.ui index 137f7e97..fe6de6c9 100644 --- a/gui/mainwindow.ui +++ b/gui/mainwindow.ui @@ -33,22 +33,6 @@ 0 - - - - true - - - true - - - false - - - Qt::CustomContextMenu - - - diff --git a/libgroupview/CMakeLists.txt b/libgroupview/CMakeLists.txt new file mode 100644 index 00000000..9aa3013b --- /dev/null +++ b/libgroupview/CMakeLists.txt @@ -0,0 +1,41 @@ +project(libGroupView) + +set(CMAKE_AUTOMOC ON) + +# Find Qt +find_package(Qt5Core REQUIRED) +find_package(Qt5Gui REQUIRED) + +# Include Qt headers. +include_directories(${Qt5Base_INCLUDE_DIRS}) + +SET(LIBGROUPVIEW_HEADERS +include/libgroupview_config.h + +# Public headers +include/kcategorizedsortfilterproxymodel.h +include/kcategorizedview.h +include/kcategorydrawer.h + +# Private headers +src/kcategorizedsortfilterproxymodel_p.h +src/kcategorizedview_p.h +) + +SET(LIBGROUPVIEW_SOURCES +src/kcategorizedsortfilterproxymodel.cpp +src/kcategorizedview.cpp +src/kcategorydrawer.cpp +) + +# Set the include dir path. +SET(LIBGROUPVIEW_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include" PARENT_SCOPE) + +# Include self. +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) +include_directories(${CMAKE_BINARY_DIR}/include) + +add_definitions(-DLIBGROUPVIEW_LIBRARY) + +add_library(libGroupView SHARED ${LIBGROUPVIEW_SOURCES} ${LIBGROUPVIEW_HEADERS}) +qt5_use_modules(libGroupView Core Gui) diff --git a/libgroupview/include/kcategorizedsortfilterproxymodel.h b/libgroupview/include/kcategorizedsortfilterproxymodel.h new file mode 100644 index 00000000..be83f3be --- /dev/null +++ b/libgroupview/include/kcategorizedsortfilterproxymodel.h @@ -0,0 +1,175 @@ +/* + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDSORTFILTERPROXYMODEL_H +#define KCATEGORIZEDSORTFILTERPROXYMODEL_H + +#include + +#include + +class QItemSelection; + + +/** + * This class lets you categorize a view. It is meant to be used along with + * KCategorizedView class. + * + * In general terms all you need to do is to reimplement subSortLessThan() and + * compareCategories() methods. In order to make categorization work, you need + * to also call setCategorizedModel() class to enable it, since the categorization + * is disabled by default. + * + * @see KCategorizedView + * + * @author Rafael Fernández López + */ +class KDEUI_EXPORT KCategorizedSortFilterProxyModel + : public QSortFilterProxyModel +{ +public: + enum AdditionalRoles + { + // Note: use printf "0x%08X\n" $(($RANDOM*$RANDOM)) + // to define additional roles. + CategoryDisplayRole = 0x17CE990A, ///< This role is used for asking the category to a given index + + CategorySortRole = 0x27857E60 ///< This role is used for sorting categories. You can return a + ///< string or a long long value. Strings will be sorted alphabetically + ///< while long long will be sorted by their value. Please note that this + ///< value won't be shown on the view, is only for sorting purposes. What will + ///< be shown as "Category" on the view will be asked with the role + ///< CategoryDisplayRole. + }; + + KCategorizedSortFilterProxyModel ( QObject *parent = 0 ); + virtual ~KCategorizedSortFilterProxyModel(); + + /** + * Overridden from QSortFilterProxyModel. Sorts the source model using + * @p column for the given @p order. + */ + virtual void sort ( int column, Qt::SortOrder order = Qt::AscendingOrder ); + + /** + * @return whether the model is categorized or not. Disabled by default. + */ + bool isCategorizedModel() const; + + /** + * Enables or disables the categorization feature. + * + * @param categorizedModel whether to enable or disable the categorization feature. + */ + void setCategorizedModel ( bool categorizedModel ); + + /** + * @return the column being used for sorting. + */ + int sortColumn() const; + + /** + * @return the sort order being used for sorting. + */ + Qt::SortOrder sortOrder() const; + + /** + * Set if the sorting using CategorySortRole will use a natural comparison + * in the case that strings were returned. If enabled, QString::localeAwareCompare + * will be used for sorting. + * + * @param sortCategoriesByNaturalComparison whether to sort using a natural comparison or not. + */ + void setSortCategoriesByNaturalComparison ( bool sortCategoriesByNaturalComparison ); + + /** + * @return whether it is being used a natural comparison for sorting. Enabled by default. + */ + bool sortCategoriesByNaturalComparison() const; + +protected: + /** + * Overridden from QSortFilterProxyModel. If you are subclassing + * KCategorizedSortFilterProxyModel, you will probably not need to reimplement this + * method. + * + * It calls compareCategories() to sort by category. If the both items are in the + * same category (i.e. compareCategories returns 0), then subSortLessThan is called. + * + * @return Returns true if the item @p left is less than the item @p right when sorting. + * + * @warning You usually won't need to reimplement this method when subclassing + * from KCategorizedSortFilterProxyModel. + */ + virtual bool lessThan ( const QModelIndex &left, const QModelIndex &right ) const; + + /** + * This method has a similar purpose as lessThan() has on QSortFilterProxyModel. + * It is used for sorting items that are in the same category. + * + * @return Returns true if the item @p left is less than the item @p right when sorting. + */ + virtual bool subSortLessThan ( const QModelIndex &left, const QModelIndex &right ) const; + + /** + * This method compares the category of the @p left index with the category + * of the @p right index. + * + * Internally and if not reimplemented, this method will ask for @p left and + * @p right models for role CategorySortRole. In order to correctly sort + * categories, the data() metod of the model should return a qlonglong (or numeric) value, or + * a QString object. QString objects will be sorted with QString::localeAwareCompare if + * sortCategoriesByNaturalComparison() is true. + * + * @note Please have present that: + * QString(QChar(QChar::ObjectReplacementCharacter)) > + * QString(QChar(QChar::ReplacementCharacter)) > + * [ all possible strings ] > + * QString(); + * + * This means that QString() will be sorted the first one, while + * QString(QChar(QChar::ObjectReplacementCharacter)) and + * QString(QChar(QChar::ReplacementCharacter)) will be sorted in last + * position. + * + * @warning Please note that data() method of the model should return always + * information of the same type. If you return a QString for an index, + * you should return always QStrings for all indexes for role CategorySortRole + * in order to correctly sort categories. You can't mix by returning + * a QString for one index, and a qlonglong for other. + * + * @note If you need a more complex layout, you will have to reimplement this + * method. + * + * @return A negative value if the category of @p left should be placed before the + * category of @p right. 0 if @p left and @p right are on the same category, and + * a positive value if the category of @p left should be placed after the + * category of @p right. + */ + virtual int compareCategories ( const QModelIndex &left, const QModelIndex &right ) const; + +private: + class Private; + Private *const d; +}; + + +#endif // KCATEGORIZEDSORTFILTERPROXYMODEL_H diff --git a/libgroupview/include/kcategorizedview.h b/libgroupview/include/kcategorizedview.h new file mode 100644 index 00000000..04c2f5cc --- /dev/null +++ b/libgroupview/include/kcategorizedview.h @@ -0,0 +1,322 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDVIEW_H +#define KCATEGORIZEDVIEW_H + +#include + +#include + +class KCategoryDrawer; + +/** + * @short Item view for listing items in a categorized fashion optionally + * + * KCategorizedView basically has the same functionality as QListView, only that it also lets you + * layout items in a way that they are categorized visually. + * + * For it to work you will need to set a KCategorizedSortFilterProxyModel and a KCategoryDrawer + * with methods setModel() and setCategoryDrawer() respectively. Also, the model will need to be + * flagged as categorized with KCategorizedSortFilterProxyModel::setCategorizedModel(true). + * + * The way it works (if categorization enabled): + * + * - When sorting, it does more things than QListView does. It will ask the model for the + * special role CategorySortRole (@see KCategorizedSortFilterProxyModel). This can return + * a QString or an int in order to tell the view the order of categories. In this sense, for + * instance, if we are sorting by name ascending, "A" would be before than "B". If we are + * sorting by size ascending, 512 bytes would be before 1024 bytes. This way categories are + * also sorted. + * + * - When the view has to paint, it will ask the model with the role CategoryDisplayRole + * (@see KCategorizedSortFilterProxyModel). It will for instance return "F" for "foo.pdf" if + * we are sorting by name ascending, or "Small" if a certain item has 100 bytes, for example. + * + * For drawing categories, KCategoryDrawer will be used. You can inherit this class to do your own + * drawing. + * + * @note All examples cited before talk about filesystems and such, but have present that this + * is a completely generic class, and it can be used for whatever your purpose is. For + * instance when talking about animals, you can separate them by "Mammal" and "Oviparous". In + * this very case, for example, the CategorySortRole and the CategoryDisplayRole could be the + * same ("Mammal" and "Oviparous"). + * + * @note There is a really performance boost if CategorySortRole returns an int instead of a QString. + * Have present that this role is asked (n * log n) times when sorting and compared. Comparing + * ints is always faster than comparing strings, whithout mattering how fast the string + * comparison is. Consider thinking of a way of returning ints instead of QStrings if your + * model can contain a high number of items. + * + * @warning Note that for really drawing items in blocks you will need some things to be done: + * - The model set to this view has to be (or inherit if you want to do special stuff + * in it) KCategorizedSortFilterProxyModel. + * - This model needs to be set setCategorizedModel to true. + * - Set a category drawer by calling setCategoryDrawer. + * + * @see KCategorizedSortFilterProxyModel, KCategoryDrawer + * + * @author Rafael Fernández López + */ +class KDEUI_EXPORT KCategorizedView + : public QListView +{ + Q_OBJECT + Q_PROPERTY ( int categorySpacing READ categorySpacing WRITE setCategorySpacing ) + Q_PROPERTY ( bool alternatingBlockColors READ alternatingBlockColors WRITE setAlternatingBlockColors ) + Q_PROPERTY ( bool collapsibleBlocks READ collapsibleBlocks WRITE setCollapsibleBlocks ) + +public: + KCategorizedView ( QWidget *parent = 0 ); + + ~KCategorizedView(); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void setModel ( QAbstractItemModel *model ); + + /** + * Calls to setGridSizeOwn(). + */ + void setGridSize ( const QSize &size ); + + /** + * @warning note that setGridSize is not virtual in the base class (QListView), so if you are + * calling to this method, make sure you have a KCategorizedView pointer around. This + * means that something like: + * @code + * QListView *lv = new KCategorizedView(); + * lv->setGridSize(mySize); + * @endcode + * + * will not call to the expected setGridSize method. Instead do something like this: + * + * @code + * QListView *lv; + * ... + * KCategorizedView *cv = qobject_cast(lv); + * if (cv) { + * cv->setGridSizeOwn(mySize); + * } else { + * lv->setGridSize(mySize); + * } + * @endcode + * + * @note this method will call to QListView::setGridSize among other operations. + * + * @since 4.4 + */ + void setGridSizeOwn ( const QSize &size ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QRect visualRect ( const QModelIndex &index ) const; + + /** + * Returns the current category drawer. + */ + KCategoryDrawer *categoryDrawer() const; + + /** + * The category drawer that will be used for drawing categories. + */ + void setCategoryDrawer ( KCategoryDrawer *categoryDrawer ); + + /** + * @return Category spacing. The spacing between categories. + * + * @since 4.4 + */ + int categorySpacing() const; + + /** + * Stablishes the category spacing. This is the spacing between categories. + * + * @since 4.4 + */ + void setCategorySpacing ( int categorySpacing ); + + /** + * @return Whether blocks should be drawn with alternating colors. + * + * @since 4.4 + */ + bool alternatingBlockColors() const; + + /** + * Sets whether blocks should be drawn with alternating colors. + * + * @since 4.4 + */ + void setAlternatingBlockColors ( bool enable ); + + /** + * @return Whether blocks can be collapsed or not. + * + * @since 4.4 + */ + bool collapsibleBlocks() const; + + /** + * Sets whether blocks can be collapsed or not. + * + * @since 4.4 + */ + void setCollapsibleBlocks ( bool enable ); + + /** + * @return Block of indexes that are into @p category. + * + * @since 4.5 + */ + QModelIndexList block ( const QString &category ); + + /** + * @return Block of indexes that are represented by @p representative. + * + * @since 4.5 + */ + QModelIndexList block ( const QModelIndex &representative ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QModelIndex indexAt ( const QPoint &point ) const; + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void reset(); + +protected: + /** + * Reimplemented from QWidget. + */ + virtual void paintEvent ( QPaintEvent *event ); + + /** + * Reimplemented from QWidget. + */ + virtual void resizeEvent ( QResizeEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void setSelection ( const QRect &rect, + QItemSelectionModel::SelectionFlags flags ); + + /** + * Reimplemented from QWidget. + */ + virtual void mouseMoveEvent ( QMouseEvent *event ); + + /** + * Reimplemented from QWidget. + */ + virtual void mousePressEvent ( QMouseEvent *event ); + + /** + * Reimplemented from QWidget. + */ + virtual void mouseReleaseEvent ( QMouseEvent *event ); + + /** + * Reimplemented from QWidget. + */ + virtual void leaveEvent ( QEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void startDrag ( Qt::DropActions supportedActions ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragMoveEvent ( QDragMoveEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragEnterEvent ( QDragEnterEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dragLeaveEvent ( QDragLeaveEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dropEvent ( QDropEvent *event ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual QModelIndex moveCursor ( CursorAction cursorAction, + Qt::KeyboardModifiers modifiers ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void rowsAboutToBeRemoved ( const QModelIndex &parent, + int start, + int end ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void updateGeometries(); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void currentChanged ( const QModelIndex ¤t, + const QModelIndex &previous ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void dataChanged ( const QModelIndex &topLeft, + const QModelIndex &bottomRight ); + + /** + * Reimplemented from QAbstractItemView. + */ + virtual void rowsInserted ( const QModelIndex &parent, + int start, + int end ); + +protected Q_SLOTS: + /** + * @internal + * Reposition items as needed. + */ + virtual void slotLayoutChanged(); + virtual void slotCollapseOrExpandClicked ( QModelIndex ); + +private: + class Private; + Private *const d; +}; + +#endif // KCATEGORIZEDVIEW_H diff --git a/libgroupview/include/kcategorydrawer.h b/libgroupview/include/kcategorydrawer.h new file mode 100644 index 00000000..aa561f0d --- /dev/null +++ b/libgroupview/include/kcategorydrawer.h @@ -0,0 +1,179 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORYDRAWER_H +#define KCATEGORYDRAWER_H + +#include + +#include +#include + +class QPainter; +class QModelIndex; +class QStyleOption; +class KCategorizedView; + + +/** + * @since 4.5 + */ +class LIBGROUPVIEW_EXPORT KCategoryDrawer + : public QObject +{ + friend class KCategorizedView; + Q_OBJECT + + +public: + KCategoryDrawer ( KCategorizedView *view ); + virtual ~KCategoryDrawer(); + + /** + * @return The view this category drawer is associated with. + */ + KCategorizedView *view() const; + + /** + * This method purpose is to draw a category represented by the given + * @param index with the given @param sortRole sorting role + * + * @note This method will be called one time per category, always with the + * first element in that category + */ + virtual void drawCategory ( const QModelIndex &index, + int sortRole, + const QStyleOption &option, + QPainter *painter ) const; + + /** + * @return The category height for the category representated by index @p index with + * style options @p option. + */ + virtual int categoryHeight ( const QModelIndex &index, const QStyleOption &option ) const; + + //TODO KDE5: make virtual as leftMargin + /** + * @note 0 by default + * + * @since 4.4 + */ + int leftMargin() const; + + /** + * @note call to this method on the KCategoryDrawer constructor to set the left margin + * + * @since 4.4 + */ + void setLeftMargin ( int leftMargin ); + + //TODO KDE5: make virtual as rightMargin + /** + * @note 0 by default + * + * @since 4.4 + */ + int rightMargin() const; + + /** + * @note call to this method on the KCategoryDrawer constructor to set the right margin + * + * @since 4.4 + */ + void setRightMargin ( int rightMargin ); + + KCategoryDrawer &operator= ( const KCategoryDrawer &cd ); +protected: + /** + * Method called when the mouse button has been pressed. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonPressed ( const QModelIndex &index, const QRect &blockRect, QMouseEvent *event ); + + /** + * Method called when the mouse button has been released. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonReleased ( const QModelIndex &index, const QRect &blockRect, QMouseEvent *event ); + + /** + * Method called when the mouse has been moved. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + */ + virtual void mouseMoved ( const QModelIndex &index, const QRect &blockRect, QMouseEvent *event ); + + /** + * Method called when the mouse button has been double clicked. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + * @param event The mouse event. + * + * @warning You explicitly have to determine whether the event has been accepted or not. You + * have to call event->accept() or event->ignore() at all possible case branches in + * your code. + */ + virtual void mouseButtonDoubleClicked ( const QModelIndex &index, const QRect &blockRect, QMouseEvent *event ); + + /** + * Method called when the mouse button has left this block. + * + * @param index The representative index of the block of items. + * @param blockRect The rect occupied by the block of items. + */ + virtual void mouseLeft ( const QModelIndex &index, const QRect &blockRect ); + +private: + class Private; + Private *const d; +Q_SIGNALS: + /** + * This signal becomes emitted when collapse or expand has been clicked. + */ + void collapseOrExpandClicked ( const QModelIndex &index ); + + /** + * Emit this signal on your subclass implementation to notify that something happened. Usually + * this will be triggered when you have received an event, and its position matched some "hot spot". + * + * You give this action the integer you want, and having connected this signal to your code, + * the connected slot can perform the needed changes (view, model, selection model, delegate...) + */ + void actionRequested ( int action, const QModelIndex &index ); +}; + +#endif // KCATEGORYDRAWER_H diff --git a/libgroupview/include/libgroupview_config.h b/libgroupview/include/libgroupview_config.h new file mode 100644 index 00000000..86bed139 --- /dev/null +++ b/libgroupview/include/libgroupview_config.h @@ -0,0 +1,27 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//#ifndef LIBINSTANCE_CONFIG_H +//#define LIBINSTANCE_CONFIG_H + +#include + +#ifdef LIBGROUPVIEW_LIBRARY +# define LIBGROUPVIEW_EXPORT Q_DECL_EXPORT +#else +# define LIBGROUPVIEW_EXPORT Q_DECL_IMPORT +#endif + +//#endif // LIBINSTANCE_CONFIG_H diff --git a/libgroupview/src/kcategorizedsortfilterproxymodel.cpp b/libgroupview/src/kcategorizedsortfilterproxymodel.cpp new file mode 100644 index 00000000..46a845e0 --- /dev/null +++ b/libgroupview/src/kcategorizedsortfilterproxymodel.cpp @@ -0,0 +1,168 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kcategorizedsortfilterproxymodel.h" +#include "kcategorizedsortfilterproxymodel_p.h" + +#include + +#include +#include +#include + +KCategorizedSortFilterProxyModel::KCategorizedSortFilterProxyModel ( QObject *parent ) + : QSortFilterProxyModel ( parent ) + , d ( new Private() ) +{ +} + +KCategorizedSortFilterProxyModel::~KCategorizedSortFilterProxyModel() +{ + delete d; +} + +void KCategorizedSortFilterProxyModel::sort ( int column, Qt::SortOrder order ) +{ + d->sortColumn = column; + d->sortOrder = order; + + QSortFilterProxyModel::sort ( column, order ); +} + +bool KCategorizedSortFilterProxyModel::isCategorizedModel() const +{ + return d->categorizedModel; +} + +void KCategorizedSortFilterProxyModel::setCategorizedModel ( bool categorizedModel ) +{ + if ( categorizedModel == d->categorizedModel ) + { + return; + } + + d->categorizedModel = categorizedModel; + + invalidate(); +} + +int KCategorizedSortFilterProxyModel::sortColumn() const +{ + return d->sortColumn; +} + +Qt::SortOrder KCategorizedSortFilterProxyModel::sortOrder() const +{ + return d->sortOrder; +} + +void KCategorizedSortFilterProxyModel::setSortCategoriesByNaturalComparison ( bool sortCategoriesByNaturalComparison ) +{ + if ( sortCategoriesByNaturalComparison == d->sortCategoriesByNaturalComparison ) + { + return; + } + + d->sortCategoriesByNaturalComparison = sortCategoriesByNaturalComparison; + + invalidate(); +} + +bool KCategorizedSortFilterProxyModel::sortCategoriesByNaturalComparison() const +{ + return d->sortCategoriesByNaturalComparison; +} + +bool KCategorizedSortFilterProxyModel::lessThan ( const QModelIndex &left, const QModelIndex &right ) const +{ + if ( d->categorizedModel ) + { + int compare = compareCategories ( left, right ); + + if ( compare > 0 ) // left is greater than right + { + return false; + } + else if ( compare < 0 ) // left is less than right + { + return true; + } + } + + return subSortLessThan ( left, right ); +} + +bool KCategorizedSortFilterProxyModel::subSortLessThan ( const QModelIndex &left, const QModelIndex &right ) const +{ + return QSortFilterProxyModel::lessThan ( left, right ); +} + +int KCategorizedSortFilterProxyModel::compareCategories ( const QModelIndex &left, const QModelIndex &right ) const +{ + QVariant l = ( left.model() ? left.model()->data ( left, CategorySortRole ) : QVariant() ); + QVariant r = ( right.model() ? right.model()->data ( right, CategorySortRole ) : QVariant() ); + + Q_ASSERT ( l.isValid() ); + Q_ASSERT ( r.isValid() ); + Q_ASSERT ( l.type() == r.type() ); + + if ( l.type() == QVariant::String ) + { + QString lstr = l.toString(); + QString rstr = r.toString(); + + /* + if ( d->sortCategoriesByNaturalComparison ) + { + return KStringHandler::naturalCompare ( lstr, rstr ); + } + else + { + */ + if ( lstr < rstr ) + { + return -1; + } + + if ( lstr > rstr ) + { + return 1; + } + + return 0; + //} + } + + qlonglong lint = l.toLongLong(); + qlonglong rint = r.toLongLong(); + + if ( lint < rint ) + { + return -1; + } + + if ( lint > rint ) + { + return 1; + } + + return 0; +} diff --git a/libgroupview/src/kcategorizedsortfilterproxymodel_p.h b/libgroupview/src/kcategorizedsortfilterproxymodel_p.h new file mode 100644 index 00000000..d7e7c9a0 --- /dev/null +++ b/libgroupview/src/kcategorizedsortfilterproxymodel_p.h @@ -0,0 +1,48 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007 Rafael Fernández López + * Copyright (C) 2007 John Tapsell + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDSORTFILTERPROXYMODEL_P_H +#define KCATEGORIZEDSORTFILTERPROXYMODEL_P_H + +class KCategorizedSortFilterProxyModel; + +class KCategorizedSortFilterProxyModel::Private +{ +public: + Private() + : sortColumn ( 0 ) + , sortOrder ( Qt::AscendingOrder ) + , categorizedModel ( false ) + , sortCategoriesByNaturalComparison ( true ) + { + } + + ~Private() + { + } + + int sortColumn; + Qt::SortOrder sortOrder; + bool categorizedModel; + bool sortCategoriesByNaturalComparison; +}; + +#endif diff --git a/libgroupview/src/kcategorizedview.cpp b/libgroupview/src/kcategorizedview.cpp new file mode 100644 index 00000000..ba4ae2dc --- /dev/null +++ b/libgroupview/src/kcategorizedview.cpp @@ -0,0 +1,1696 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * IMPLEMENTATION NOTES: + * + * QListView::setRowHidden() and QListView::isRowHidden() are not taken into + * account. This methods should actually not exist. This effect should be handled + * by an hypothetical QSortFilterProxyModel which filters out the desired rows. + * + * In case this needs to be implemented, contact me, but I consider this a faulty + * design. + */ + +#include "kcategorizedview.h" +#include "kcategorizedview_p.h" + +#include // trunc on C99 compliant systems +#include // trunc for not C99 compliant systems + +#include +#include +#include + +#include "kcategorydrawer.h" +#include "kcategorizedsortfilterproxymodel.h" + +//BEGIN: Private part + +struct KCategorizedView::Private::Item +{ + Item() + : topLeft ( QPoint() ) + , size ( QSize() ) + { + } + + QPoint topLeft; + QSize size; +}; + +struct KCategorizedView::Private::Block +{ + Block() + : topLeft ( QPoint() ) + , height ( -1 ) + , firstIndex ( QModelIndex() ) + , quarantineStart ( QModelIndex() ) + , items ( QList() ) + , outOfQuarantine ( false ) + , alternate ( false ) + , collapsed ( false ) + { + } + + bool operator!= ( const Block &rhs ) const + { + return firstIndex != rhs.firstIndex; + } + + static bool lessThan ( const Block &left, const Block &right ) + { + Q_ASSERT ( left.firstIndex.isValid() ); + Q_ASSERT ( right.firstIndex.isValid() ); + return left.firstIndex.row() < right.firstIndex.row(); + } + + QPoint topLeft; + int height; + QPersistentModelIndex firstIndex; + // if we have n elements on this block, and we inserted an element at position i. The quarantine + // will start at index (i, column, parent). This means that for all elements j where i <= j <= n, the + // visual rect position of item j will have to be recomputed (cannot use the cached point). The quarantine + // will only affect the current block, since the rest of blocks can be affected only in the way + // that the whole block will have different offset, but items will keep the same relative position + // in terms of their parent blocks. + QPersistentModelIndex quarantineStart; + QList items; + + // this affects the whole block, not items separately. items contain the topLeft point relative + // to the block. Because of insertions or removals a whole block can be moved, so the whole block + // will enter in quarantine, what is faster than moving all items in absolute terms. + bool outOfQuarantine; + + // should we alternate its color ? is just a hint, could not be used + bool alternate; + bool collapsed; +}; + +KCategorizedView::Private::Private ( KCategorizedView *q ) + : q ( q ) + , proxyModel ( 0 ) + , categoryDrawer ( 0 ) + , categorySpacing ( 5 ) + , alternatingBlockColors ( false ) + , collapsibleBlocks ( false ) + , hoveredBlock ( new Block() ) + , hoveredIndex ( QModelIndex() ) + , pressedPosition ( QPoint() ) + , rubberBandRect ( QRect() ) +{ +} + +KCategorizedView::Private::~Private() +{ + delete hoveredBlock; +} + +bool KCategorizedView::Private::isCategorized() const +{ + return proxyModel && categoryDrawer && proxyModel->isCategorizedModel(); +} + +QStyleOptionViewItemV4 KCategorizedView::Private::blockRect ( const QModelIndex &representative ) +{ + QStyleOptionViewItemV4 option ( q->viewOptions() ); + const int height = categoryDrawer->categoryHeight ( representative, option ); + const QString categoryDisplay = representative.data ( KCategorizedSortFilterProxyModel::CategoryDisplayRole ).toString(); + QPoint pos = blockPosition ( categoryDisplay ); + pos.ry() -= height; + option.rect.setTopLeft ( pos ); + option.rect.setWidth ( viewportWidth() + categoryDrawer->leftMargin() + categoryDrawer->rightMargin() ); + option.rect.setHeight ( height + blockHeight ( categoryDisplay ) ); + option.rect = mapToViewport ( option.rect ); + + return option; +} + +QPair KCategorizedView::Private::intersectingIndexesWithRect ( const QRect &_rect ) const +{ + const int rowCount = proxyModel->rowCount(); + + const QRect rect = _rect.normalized(); + + // binary search to find out the top border + int bottom = 0; + int top = rowCount - 1; + while ( bottom <= top ) + { + const int middle = ( bottom + top ) / 2; + const QModelIndex index = proxyModel->index ( middle, q->modelColumn(), q->rootIndex() ); + QRect itemRect = q->visualRect ( index ); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if ( itemRect.bottomRight().y() <= rect.topLeft().y() ) + { + bottom = middle + 1; + } + else + { + top = middle - 1; + } + } + + const QModelIndex bottomIndex = proxyModel->index ( bottom, q->modelColumn(), q->rootIndex() ); + + // binary search to find out the bottom border + bottom = 0; + top = rowCount - 1; + while ( bottom <= top ) + { + const int middle = ( bottom + top ) / 2; + const QModelIndex index = proxyModel->index ( middle, q->modelColumn(), q->rootIndex() ); + QRect itemRect = q->visualRect ( index ); + const int verticalOff = q->verticalOffset(); + const int horizontalOff = q->horizontalOffset(); + itemRect.topLeft().ry() += verticalOff; + itemRect.topLeft().rx() += horizontalOff; + itemRect.bottomRight().ry() += verticalOff; + itemRect.bottomRight().rx() += horizontalOff; + if ( itemRect.topLeft().y() <= rect.bottomRight().y() ) + { + bottom = middle + 1; + } + else + { + top = middle - 1; + } + } + + const QModelIndex topIndex = proxyModel->index ( top, q->modelColumn(), q->rootIndex() ); + + return qMakePair ( bottomIndex, topIndex ); +} + +QPoint KCategorizedView::Private::blockPosition ( const QString &category ) +{ + Block &block = blocks[category]; + + if ( block.outOfQuarantine && !block.topLeft.isNull() ) + { + return block.topLeft; + } + + QPoint res ( categorySpacing, 0 ); + + const QModelIndex index = block.firstIndex; + + for ( QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it ) + { + Block &block = *it; + const QModelIndex categoryIndex = block.firstIndex; + if ( index.row() < categoryIndex.row() ) + { + continue; + } + res.ry() += categoryDrawer->categoryHeight ( categoryIndex, q->viewOptions() ) + categorySpacing; + if ( index.row() == categoryIndex.row() ) + { + continue; + } + res.ry() += blockHeight ( it.key() ); + } + + block.outOfQuarantine = true; + block.topLeft = res; + + return res; +} + +int KCategorizedView::Private::blockHeight ( const QString &category ) +{ + Block &block = blocks[category]; + + if ( block.collapsed ) + { + return 0; + } + + if ( block.height > -1 ) + { + return block.height; + } + + const QModelIndex firstIndex = block.firstIndex; + const QModelIndex lastIndex = proxyModel->index ( firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex() ); + const QRect topLeft = q->visualRect ( firstIndex ); + QRect bottomRight = q->visualRect ( lastIndex ); + + if ( hasGrid() ) + { + bottomRight.setHeight ( qMax ( bottomRight.height(), q->gridSize().height() ) ); + } + else + { + if ( !q->uniformItemSizes() ) + { + bottomRight.setHeight ( highestElementInLastRow ( block ) + q->spacing() * 2 ); + } + } + + const int height = bottomRight.bottomRight().y() - topLeft.topLeft().y() + 1; + block.height = height; + + return height; +} + +int KCategorizedView::Private::viewportWidth() const +{ + return q->viewport()->width() - categorySpacing * 2 - categoryDrawer->leftMargin() - categoryDrawer->rightMargin(); +} + +void KCategorizedView::Private::regenerateAllElements() +{ + for ( QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it ) + { + Block &block = *it; + block.outOfQuarantine = false; + block.quarantineStart = block.firstIndex; + block.height = -1; + } +} + +void KCategorizedView::Private::rowsInserted ( const QModelIndex &parent, int start, int end ) +{ + if ( !isCategorized() ) + { + return; + } + + for ( int i = start; i <= end; ++i ) + { + const QModelIndex index = proxyModel->index ( i, q->modelColumn(), parent ); + + Q_ASSERT ( index.isValid() ); + + const QString category = categoryForIndex ( index ); + + Block &block = blocks[category]; + + //BEGIN: update firstIndex + // save as firstIndex in block if + // - it forced the category creation (first element on this category) + // - it is before the first row on that category + const QModelIndex firstIndex = block.firstIndex; + if ( !firstIndex.isValid() || index.row() < firstIndex.row() ) + { + block.firstIndex = index; + } + //END: update firstIndex + + Q_ASSERT ( block.firstIndex.isValid() ); + + const int firstIndexRow = block.firstIndex.row(); + + block.items.insert ( index.row() - firstIndexRow, Private::Item() ); + block.height = -1; + + q->visualRect ( index ); + q->viewport()->update(); + } + + //BEGIN: update the items that are in quarantine in affected categories + { + const QModelIndex lastIndex = proxyModel->index ( end, q->modelColumn(), parent ); + const QString category = categoryForIndex ( lastIndex ); + Private::Block &block = blocks[category]; + block.quarantineStart = block.firstIndex; + } + //END: update the items that are in quarantine in affected categories + + //BEGIN: mark as in quarantine those categories that are under the affected ones + { + const QModelIndex firstIndex = proxyModel->index ( start, q->modelColumn(), parent ); + const QString category = categoryForIndex ( firstIndex ); + const QModelIndex firstAffectedCategory = blocks[category].firstIndex; + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = blocks.values(); + qSort ( blockList.begin(), blockList.end(), Block::lessThan ); + QList firstIndexesRows; + foreach ( const Block &block, blockList ) + { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for ( QHash::Iterator it = blocks.begin(); it != blocks.end(); ++it ) + { + Private::Block &block = *it; + if ( block.firstIndex.row() > firstAffectedCategory.row() ) + { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf ( block.firstIndex.row() ) % 2; + } + else if ( block.firstIndex.row() == firstAffectedCategory.row() ) + { + block.alternate = firstIndexesRows.indexOf ( block.firstIndex.row() ) % 2; + } + } + } + //END: mark as in quarantine those categories that are under the affected ones +} + +QRect KCategorizedView::Private::mapToViewport ( const QRect &rect ) const +{ + const int dx = -q->horizontalOffset(); + const int dy = -q->verticalOffset(); + return rect.adjusted ( dx, dy, dx, dy ); +} + +QRect KCategorizedView::Private::mapFromViewport ( const QRect &rect ) const +{ + const int dx = q->horizontalOffset(); + const int dy = q->verticalOffset(); + return rect.adjusted ( dx, dy, dx, dy ); +} + +int KCategorizedView::Private::highestElementInLastRow ( const Block &block ) const +{ + //Find the highest element in the last row + const QModelIndex lastIndex = proxyModel->index ( block.firstIndex.row() + block.items.count() - 1, q->modelColumn(), q->rootIndex() ); + const QRect prevRect = q->visualRect ( lastIndex ); + int res = prevRect.height(); + QModelIndex prevIndex = proxyModel->index ( lastIndex.row() - 1, q->modelColumn(), q->rootIndex() ); + if ( !prevIndex.isValid() ) + { + return res; + } + Q_FOREVER + { + const QRect tempRect = q->visualRect ( prevIndex ); + if ( tempRect.topLeft().y() < prevRect.topLeft().y() ) + { + break; + } + res = qMax ( res, tempRect.height() ); + if ( prevIndex == block.firstIndex ) + { + break; + } + prevIndex = proxyModel->index ( prevIndex.row() - 1, q->modelColumn(), q->rootIndex() ); + } + + return res; +} + +bool KCategorizedView::Private::hasGrid() const +{ + const QSize gridSize = q->gridSize(); + return gridSize.isValid() && !gridSize.isNull(); +} + +QString KCategorizedView::Private::categoryForIndex ( const QModelIndex &index ) const +{ + const QModelIndex categoryIndex = index.model()->index ( index.row(), proxyModel->sortColumn(), index.parent() ); + return categoryIndex.data ( KCategorizedSortFilterProxyModel::CategoryDisplayRole ).toString(); +} + +void KCategorizedView::Private::leftToRightVisualRect ( const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos ) const +{ + const int firstIndexRow = block.firstIndex.row(); + + if ( hasGrid() ) + { + const int relativeRow = index.row() - firstIndexRow; + const int maxItemsPerRow = qMax ( viewportWidth() / q->gridSize().width(), 1 ); + if ( q->layoutDirection() == Qt::LeftToRight ) + { + item.topLeft.rx() = ( relativeRow % maxItemsPerRow ) * q->gridSize().width() + blockPos.x() + categoryDrawer->leftMargin(); + } + else + { + item.topLeft.rx() = viewportWidth() - ( ( relativeRow % maxItemsPerRow ) + 1 ) * q->gridSize().width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = ( relativeRow / maxItemsPerRow ) * q->gridSize().height(); + } + else + { + if ( q->uniformItemSizes() ) + { + const int relativeRow = index.row() - firstIndexRow; + const QSize itemSize = q->sizeHintForIndex ( index ); + const int maxItemsPerRow = qMax ( ( viewportWidth() - q->spacing() ) / ( itemSize.width() + q->spacing() ), 1 ); + if ( q->layoutDirection() == Qt::LeftToRight ) + { + item.topLeft.rx() = ( relativeRow % maxItemsPerRow ) * itemSize.width() + blockPos.x() + categoryDrawer->leftMargin(); + } + else + { + item.topLeft.rx() = viewportWidth() - ( relativeRow % maxItemsPerRow ) * itemSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = ( relativeRow / maxItemsPerRow ) * itemSize.height(); + } + else + { + const QSize currSize = q->sizeHintForIndex ( index ); + if ( index != block.firstIndex ) + { + const int viewportW = viewportWidth() - q->spacing(); + QModelIndex prevIndex = proxyModel->index ( index.row() - 1, q->modelColumn(), q->rootIndex() ); + QRect prevRect = q->visualRect ( prevIndex ); + prevRect = mapFromViewport ( prevRect ); + if ( ( prevRect.bottomRight().x() + 1 ) + currSize.width() - blockPos.x() + q->spacing() > viewportW ) + { + // we have to check the whole previous row, and see which one was the + // highest. + Q_FOREVER + { + prevIndex = proxyModel->index ( prevIndex.row() - 1, q->modelColumn(), q->rootIndex() ); + const QRect tempRect = q->visualRect ( prevIndex ); + if ( tempRect.topLeft().y() < prevRect.topLeft().y() ) + { + break; + } + if ( tempRect.bottomRight().y() > prevRect.bottomRight().y() ) + { + prevRect = tempRect; + } + if ( prevIndex == block.firstIndex ) + { + break; + } + } + if ( q->layoutDirection() == Qt::LeftToRight ) + { + item.topLeft.rx() = categoryDrawer->leftMargin() + blockPos.x() + q->spacing(); + } + else + { + item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = ( prevRect.bottomRight().y() + 1 ) + q->spacing() - blockPos.y(); + } + else + { + if ( q->layoutDirection() == Qt::LeftToRight ) + { + item.topLeft.rx() = ( prevRect.bottomRight().x() + 1 ) + q->spacing(); + } + else + { + item.topLeft.rx() = ( prevRect.bottomLeft().x() - 1 ) - q->spacing() - item.size.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = prevRect.topLeft().y() - blockPos.y(); + } + } + else + { + if ( q->layoutDirection() == Qt::LeftToRight ) + { + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + } + else + { + item.topLeft.rx() = viewportWidth() - currSize.width() + categoryDrawer->leftMargin() + categorySpacing; + } + item.topLeft.ry() = q->spacing(); + } + } + } + item.size = q->sizeHintForIndex ( index ); +} + +void KCategorizedView::Private::topToBottomVisualRect ( const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos ) const +{ + const int firstIndexRow = block.firstIndex.row(); + + if ( hasGrid() ) + { + const int relativeRow = index.row() - firstIndexRow; + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin(); + item.topLeft.ry() = relativeRow * q->gridSize().height(); + } + else + { + if ( q->uniformItemSizes() ) + { + const int relativeRow = index.row() - firstIndexRow; + const QSize itemSize = q->sizeHintForIndex ( index ); + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin(); + item.topLeft.ry() = relativeRow * itemSize.height(); + } + else + { + if ( index != block.firstIndex ) + { + QModelIndex prevIndex = proxyModel->index ( index.row() - 1, q->modelColumn(), q->rootIndex() ); + QRect prevRect = q->visualRect ( prevIndex ); + prevRect = mapFromViewport ( prevRect ); + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + item.topLeft.ry() = ( prevRect.bottomRight().y() + 1 ) + q->spacing() - blockPos.y(); + } + else + { + item.topLeft.rx() = blockPos.x() + categoryDrawer->leftMargin() + q->spacing(); + item.topLeft.ry() = q->spacing(); + } + } + } + item.size = q->sizeHintForIndex ( index ); + item.size.setWidth ( viewportWidth() ); +} + +void KCategorizedView::Private::_k_slotCollapseOrExpandClicked ( QModelIndex ) +{ +} + +//END: Private part + +//BEGIN: Public part + +KCategorizedView::KCategorizedView ( QWidget *parent ) + : QListView ( parent ) + , d ( new Private ( this ) ) +{ +} + +KCategorizedView::~KCategorizedView() +{ + delete d; +} + +void KCategorizedView::setModel ( QAbstractItemModel *model ) +{ + if ( d->proxyModel == model ) + { + return; + } + + d->blocks.clear(); + + if ( d->proxyModel ) + { + disconnect ( d->proxyModel, SIGNAL ( layoutChanged() ), this, SLOT ( slotLayoutChanged() ) ); + } + + d->proxyModel = dynamic_cast ( model ); + + if ( d->proxyModel ) + { + connect ( d->proxyModel, SIGNAL ( layoutChanged() ), this, SLOT ( slotLayoutChanged() ) ); + } + + QListView::setModel ( model ); + + // if the model already had information inserted, update our data structures to it + if ( model->rowCount() ) + { + slotLayoutChanged(); + } +} + +void KCategorizedView::setGridSize ( const QSize &size ) +{ + setGridSizeOwn ( size ); +} + +void KCategorizedView::setGridSizeOwn ( const QSize &size ) +{ + d->regenerateAllElements(); + QListView::setGridSize ( size ); +} + +QRect KCategorizedView::visualRect ( const QModelIndex &index ) const +{ + if ( !d->isCategorized() ) + { + return QListView::visualRect ( index ); + } + + if ( !index.isValid() ) + { + return QRect(); + } + + const QString category = d->categoryForIndex ( index ); + + if ( !d->blocks.contains ( category ) ) + { + return QRect(); + } + + Private::Block &block = d->blocks[category]; + const int firstIndexRow = block.firstIndex.row(); + + Q_ASSERT ( block.firstIndex.isValid() ); + + if ( index.row() - firstIndexRow < 0 || index.row() - firstIndexRow >= block.items.count() ) + { + return QRect(); + } + + const QPoint blockPos = d->blockPosition ( category ); + + Private::Item &ritem = block.items[index.row() - firstIndexRow]; + + if ( ritem.topLeft.isNull() || ( block.quarantineStart.isValid() && + index.row() >= block.quarantineStart.row() ) ) + { + if ( flow() == LeftToRight ) + { + d->leftToRightVisualRect ( index, ritem, block, blockPos ); + } + else + { + d->topToBottomVisualRect ( index, ritem, block, blockPos ); + } + + //BEGIN: update the quarantine start + const bool wasLastIndex = ( index.row() == ( block.firstIndex.row() + block.items.count() - 1 ) ); + if ( index.row() == block.quarantineStart.row() ) + { + if ( wasLastIndex ) + { + block.quarantineStart = QModelIndex(); + } + else + { + const QModelIndex nextIndex = d->proxyModel->index ( index.row() + 1, modelColumn(), rootIndex() ); + block.quarantineStart = nextIndex; + } + } + //END: update the quarantine start + } + + // we get now the absolute position through the relative position of the parent block. do not + // save this on ritem, since this would override the item relative position in block terms. + Private::Item item ( ritem ); + item.topLeft.ry() += blockPos.y(); + + const QSize sizeHint = item.size; + + if ( d->hasGrid() ) + { + const QSize sizeGrid = gridSize(); + const QSize resultingSize = sizeHint.boundedTo ( sizeGrid ); + QRect res ( item.topLeft.x() + ( ( sizeGrid.width() - resultingSize.width() ) / 2 ), + item.topLeft.y(), resultingSize.width(), resultingSize.height() ); + if ( block.collapsed ) + { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft ( -resultingSize.width() ); + res.setHeight ( 0 ); + } + return d->mapToViewport ( res ); + } + + QRect res ( item.topLeft.x(), item.topLeft.y(), sizeHint.width(), sizeHint.height() ); + if ( block.collapsed ) + { + // we can still do binary search, while we "hide" items. We move those items in collapsed + // blocks to the left and set a 0 height. + res.setLeft ( -sizeHint.width() ); + res.setHeight ( 0 ); + } + return d->mapToViewport ( res ); +} + +KCategoryDrawer *KCategorizedView::categoryDrawer() const +{ + return d->categoryDrawer; +} + +void KCategorizedView::setCategoryDrawer ( KCategoryDrawer *categoryDrawer ) +{ + disconnect ( d->categoryDrawer, SIGNAL ( collapseOrExpandClicked ( QModelIndex ) ), + this, SLOT ( slotCollapseOrExpandClicked ( QModelIndex ) ) ); + d->categoryDrawer = categoryDrawer; + + connect ( d->categoryDrawer, SIGNAL ( collapseOrExpandClicked ( QModelIndex ) ), + this, SLOT ( slotCollapseOrExpandClicked ( QModelIndex ) ) ); +} + +int KCategorizedView::categorySpacing() const +{ + return d->categorySpacing; +} + +void KCategorizedView::setCategorySpacing ( int categorySpacing ) +{ + if ( d->categorySpacing == categorySpacing ) + { + return; + } + + d->categorySpacing = categorySpacing; + + for ( QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it ) + { + Private::Block &block = *it; + block.outOfQuarantine = false; + } +} + +bool KCategorizedView::alternatingBlockColors() const +{ + return d->alternatingBlockColors; +} + +void KCategorizedView::setAlternatingBlockColors ( bool enable ) +{ + d->alternatingBlockColors = enable; +} + +bool KCategorizedView::collapsibleBlocks() const +{ + return d->collapsibleBlocks; +} + +void KCategorizedView::setCollapsibleBlocks ( bool enable ) +{ + d->collapsibleBlocks = enable; +} + +QModelIndexList KCategorizedView::block ( const QString &category ) +{ + QModelIndexList res; + const Private::Block &block = d->blocks[category]; + if ( block.height == -1 ) + { + return res; + } + QModelIndex current = block.firstIndex; + const int first = current.row(); + for ( int i = 1; i <= block.items.count(); ++i ) + { + if ( current.isValid() ) + { + res << current; + } + current = d->proxyModel->index ( first + i, modelColumn(), rootIndex() ); + } + return res; +} + +QModelIndexList KCategorizedView::block ( const QModelIndex &representative ) +{ + return block ( representative.data ( KCategorizedSortFilterProxyModel::CategoryDisplayRole ).toString() ); +} + +QModelIndex KCategorizedView::indexAt ( const QPoint &point ) const +{ + if ( !d->isCategorized() ) + { + return QListView::indexAt ( point ); + } + + const int rowCount = d->proxyModel->rowCount(); + if ( !rowCount ) + { + return QModelIndex(); + } + + // Binary search that will try to spot if there is an index under point + int bottom = 0; + int top = rowCount - 1; + while ( bottom <= top ) + { + const int middle = ( bottom + top ) / 2; + const QModelIndex index = d->proxyModel->index ( middle, modelColumn(), rootIndex() ); + QRect rect = visualRect ( index ); + const int verticalOff = verticalOffset(); + int horizontalOff = horizontalOffset(); + if ( layoutDirection() == Qt::RightToLeft ) + { + horizontalOff *= -1; + } + rect.topLeft().ry() += verticalOff; + rect.topLeft().rx() += horizontalOff; + rect.bottomRight().ry() += verticalOff; + rect.bottomRight().rx() += horizontalOff; + if ( rect.contains ( point ) ) + { + if ( index.model()->flags ( index ) & Qt::ItemIsEnabled ) + { + return index; + } + return QModelIndex(); + } + bool directionCondition; + if ( layoutDirection() == Qt::LeftToRight ) + { + directionCondition = point.x() > rect.bottomRight().x(); + } + else + { + directionCondition = point.x() < rect.bottomLeft().x(); + } + if ( point.y() > rect.bottomRight().y() || + ( point.y() > rect.topLeft().y() && point.y() < rect.bottomRight().y() && directionCondition ) ) + { + bottom = middle + 1; + } + else + { + top = middle - 1; + } + } + return QModelIndex(); +} + +void KCategorizedView::reset() +{ + d->blocks.clear(); + QListView::reset(); +} + +void KCategorizedView::paintEvent ( QPaintEvent *event ) +{ + if ( !d->isCategorized() ) + { + QListView::paintEvent ( event ); + return; + } + + const QPair intersecting = d->intersectingIndexesWithRect ( viewport()->rect().intersected ( event->rect() ) ); + + QPainter p ( viewport() ); + p.save(); + + Q_ASSERT ( selectionModel()->model() == d->proxyModel ); + + //BEGIN: draw categories + QHash::ConstIterator it ( d->blocks.constBegin() ); + while ( it != d->blocks.constEnd() ) + { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index ( block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + QStyleOptionViewItemV4 option ( viewOptions() ); + option.features |= d->alternatingBlockColors && block.alternate ? QStyleOptionViewItemV4::Alternate + : QStyleOptionViewItemV4::None; + option.state |= !d->collapsibleBlocks || !block.collapsed ? QStyle::State_Open + : QStyle::State_None; + const int height = d->categoryDrawer->categoryHeight ( categoryIndex, option ); + QPoint pos = d->blockPosition ( it.key() ); + pos.ry() -= height; + option.rect.setTopLeft ( pos ); + option.rect.setWidth ( d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin() ); + option.rect.setHeight ( height + d->blockHeight ( it.key() ) ); + option.rect = d->mapToViewport ( option.rect ); + if ( !option.rect.intersects ( viewport()->rect() ) ) + { + ++it; + continue; + } + d->categoryDrawer->drawCategory ( categoryIndex, d->proxyModel->sortRole(), option, &p ); + ++it; + } + //END: draw categories + + if ( intersecting.first.isValid() && intersecting.second.isValid() ) + { + //BEGIN: draw items + int i = intersecting.first.row(); + int indexToCheckIfBlockCollapsed = i; + QModelIndex categoryIndex; + QString category; + Private::Block *block = 0; + while ( i <= intersecting.second.row() ) + { + //BEGIN: first check if the block is collapsed. if so, we have to skip the item painting + if ( i == indexToCheckIfBlockCollapsed ) + { + categoryIndex = d->proxyModel->index ( i, d->proxyModel->sortColumn(), rootIndex() ); + category = categoryIndex.data ( KCategorizedSortFilterProxyModel::CategoryDisplayRole ).toString(); + block = &d->blocks[category]; + indexToCheckIfBlockCollapsed = block->firstIndex.row() + block->items.count(); + if ( block->collapsed ) + { + i = indexToCheckIfBlockCollapsed; + continue; + } + } + //END: first check if the block is collapsed. if so, we have to skip the item painting + + Q_ASSERT ( block ); + + const bool alternateItem = ( i - block->firstIndex.row() ) % 2; + + const QModelIndex index = d->proxyModel->index ( i, modelColumn(), rootIndex() ); + const Qt::ItemFlags flags = d->proxyModel->flags ( index ); + QStyleOptionViewItemV4 option ( viewOptions() ); + option.rect = visualRect ( index ); + option.widget = this; + option.features |= wordWrap() ? QStyleOptionViewItemV2::WrapText + : QStyleOptionViewItemV2::None; + option.features |= alternatingRowColors() && alternateItem ? QStyleOptionViewItemV4::Alternate + : QStyleOptionViewItemV4::None; + if ( flags & Qt::ItemIsSelectable ) + { + option.state |= selectionModel()->isSelected ( index ) ? QStyle::State_Selected + : QStyle::State_None; + } + else + { + option.state &= ~QStyle::State_Selected; + } + option.state |= ( index == currentIndex() ) ? QStyle::State_HasFocus + : QStyle::State_None; + if ( ! ( flags & Qt::ItemIsEnabled ) ) + { + option.state &= ~QStyle::State_Enabled; + } + else + { + option.state |= ( index == d->hoveredIndex ) ? QStyle::State_MouseOver + : QStyle::State_None; + } + + itemDelegate ( index )->paint ( &p, option, index ); + ++i; + } + //END: draw items + } + + //BEGIN: draw selection rect + if ( isSelectionRectVisible() && d->rubberBandRect.isValid() ) + { + QStyleOptionRubberBand opt; + opt.initFrom ( this ); + opt.shape = QRubberBand::Rectangle; + opt.opaque = false; + opt.rect = d->mapToViewport ( d->rubberBandRect ).intersected ( viewport()->rect().adjusted ( -16, -16, 16, 16 ) ); + p.save(); + style()->drawControl ( QStyle::CE_RubberBand, &opt, &p ); + p.restore(); + } + //END: draw selection rect + + p.restore(); +} + +void KCategorizedView::resizeEvent ( QResizeEvent *event ) +{ + d->regenerateAllElements(); + QListView::resizeEvent ( event ); +} + +void KCategorizedView::setSelection ( const QRect &rect, + QItemSelectionModel::SelectionFlags flags ) +{ + if ( !d->isCategorized() ) + { + QListView::setSelection ( rect, flags ); + return; + } + + if ( rect.topLeft() == rect.bottomRight() ) + { + const QModelIndex index = indexAt ( rect.topLeft() ); + selectionModel()->select ( index, flags ); + return; + } + + const QPair intersecting = d->intersectingIndexesWithRect ( rect ); + + QItemSelection selection; + + //TODO: think of a faster implementation + QModelIndex firstIndex; + QModelIndex lastIndex; + for ( int i = intersecting.first.row(); i <= intersecting.second.row(); ++i ) + { + const QModelIndex index = d->proxyModel->index ( i, modelColumn(), rootIndex() ); + const bool visualRectIntersects = visualRect ( index ).intersects ( rect ); + if ( firstIndex.isValid() ) + { + if ( visualRectIntersects ) + { + lastIndex = index; + } + else + { + selection << QItemSelectionRange ( firstIndex, lastIndex ); + firstIndex = QModelIndex(); + } + } + else if ( visualRectIntersects ) + { + firstIndex = index; + lastIndex = index; + } + } + + if ( firstIndex.isValid() ) + { + selection << QItemSelectionRange ( firstIndex, lastIndex ); + } + + selectionModel()->select ( selection, flags ); +} + +void KCategorizedView::mouseMoveEvent ( QMouseEvent *event ) +{ + QListView::mouseMoveEvent ( event ); + d->hoveredIndex = indexAt ( event->pos() ); + const SelectionMode itemViewSelectionMode = selectionMode(); + if ( state() == DragSelectingState && isSelectionRectVisible() && itemViewSelectionMode != SingleSelection + && itemViewSelectionMode != NoSelection ) + { + QRect rect ( d->pressedPosition, event->pos() + QPoint ( horizontalOffset(), verticalOffset() ) ); + rect = rect.normalized(); + update ( rect.united ( d->rubberBandRect ) ); + d->rubberBandRect = rect; + } + QHash::ConstIterator it ( d->blocks.constBegin() ); + while ( it != d->blocks.constEnd() ) + { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index ( block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + QStyleOptionViewItemV4 option ( viewOptions() ); + const int height = d->categoryDrawer->categoryHeight ( categoryIndex, option ); + QPoint pos = d->blockPosition ( it.key() ); + pos.ry() -= height; + option.rect.setTopLeft ( pos ); + option.rect.setWidth ( d->viewportWidth() + d->categoryDrawer->leftMargin() + d->categoryDrawer->rightMargin() ); + option.rect.setHeight ( height + d->blockHeight ( it.key() ) ); + option.rect = d->mapToViewport ( option.rect ); + const QPoint mousePos = viewport()->mapFromGlobal ( QCursor::pos() ); + if ( option.rect.contains ( mousePos ) ) + { + if ( d->categoryDrawer && d->hoveredBlock->height != -1 && *d->hoveredBlock != block ) + { + const QModelIndex categoryIndex = d->proxyModel->index ( d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + const QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex ); + d->categoryDrawer->mouseLeft ( categoryIndex, option.rect ); + *d->hoveredBlock = block; + d->hoveredCategory = it.key(); + viewport()->update ( option.rect ); + } + else if ( d->hoveredBlock->height == -1 ) + { + *d->hoveredBlock = block; + d->hoveredCategory = it.key(); + } + else if ( d->categoryDrawer ) + { + d->categoryDrawer->mouseMoved ( categoryIndex, option.rect, event ); + } + viewport()->update ( option.rect ); + return; + } + ++it; + } + if ( d->categoryDrawer && d->hoveredBlock->height != -1 ) + { + const QModelIndex categoryIndex = d->proxyModel->index ( d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + const QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex ); + d->categoryDrawer->mouseLeft ( categoryIndex, option.rect ); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + viewport()->update ( option.rect ); + } +} + +void KCategorizedView::mousePressEvent ( QMouseEvent *event ) +{ + if ( event->button() == Qt::LeftButton ) + { + d->pressedPosition = event->pos(); + d->pressedPosition.rx() += horizontalOffset(); + d->pressedPosition.ry() += verticalOffset(); + } + if ( !d->categoryDrawer ) + { + QListView::mousePressEvent ( event ); + return; + } + QHash::ConstIterator it ( d->blocks.constBegin() ); + while ( it != d->blocks.constEnd() ) + { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index ( block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + const QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex ); + const QPoint mousePos = viewport()->mapFromGlobal ( QCursor::pos() ); + if ( option.rect.contains ( mousePos ) ) + { + if ( d->categoryDrawer ) + { + d->categoryDrawer->mouseButtonPressed ( categoryIndex, option.rect, event ); + } + viewport()->update ( option.rect ); + if ( !event->isAccepted() ) + { + QListView::mousePressEvent ( event ); + } + return; + } + ++it; + } + QListView::mousePressEvent ( event ); +} + +void KCategorizedView::mouseReleaseEvent ( QMouseEvent *event ) +{ + d->pressedPosition = QPoint(); + d->rubberBandRect = QRect(); + QHash::ConstIterator it ( d->blocks.constBegin() ); + while ( it != d->blocks.constEnd() ) + { + const Private::Block &block = *it; + const QModelIndex categoryIndex = d->proxyModel->index ( block.firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + const QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex ); + const QPoint mousePos = viewport()->mapFromGlobal ( QCursor::pos() ); + if ( option.rect.contains ( mousePos ) ) + { + if ( d->categoryDrawer ) + { + d->categoryDrawer->mouseButtonReleased ( categoryIndex, option.rect, event ); + } + viewport()->update ( option.rect ); + if ( !event->isAccepted() ) + { + QListView::mouseReleaseEvent ( event ); + } + return; + } + ++it; + } + QListView::mouseReleaseEvent ( event ); +} + +void KCategorizedView::leaveEvent ( QEvent *event ) +{ + QListView::leaveEvent ( event ); + if ( d->hoveredIndex.isValid() ) + { + viewport()->update ( visualRect ( d->hoveredIndex ) ); + d->hoveredIndex = QModelIndex(); + } + if ( d->categoryDrawer && d->hoveredBlock->height != -1 ) + { + const QModelIndex categoryIndex = d->proxyModel->index ( d->hoveredBlock->firstIndex.row(), d->proxyModel->sortColumn(), rootIndex() ); + const QStyleOptionViewItemV4 option = d->blockRect ( categoryIndex ); + d->categoryDrawer->mouseLeft ( categoryIndex, option.rect ); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + viewport()->update ( option.rect ); + } +} + +void KCategorizedView::startDrag ( Qt::DropActions supportedActions ) +{ + QListView::startDrag ( supportedActions ); +} + +void KCategorizedView::dragMoveEvent ( QDragMoveEvent *event ) +{ + QListView::dragMoveEvent ( event ); + d->hoveredIndex = indexAt ( event->pos() ); +} + +void KCategorizedView::dragEnterEvent ( QDragEnterEvent *event ) +{ + QListView::dragEnterEvent ( event ); +} + +void KCategorizedView::dragLeaveEvent ( QDragLeaveEvent *event ) +{ + QListView::dragLeaveEvent ( event ); +} + +void KCategorizedView::dropEvent ( QDropEvent *event ) +{ + QListView::dropEvent ( event ); +} + +//TODO: improve se we take into account collapsed blocks +//TODO: take into account when there is no grid and no uniformItemSizes +QModelIndex KCategorizedView::moveCursor ( CursorAction cursorAction, + Qt::KeyboardModifiers modifiers ) +{ + if ( !d->isCategorized() ) + { + return QListView::moveCursor ( cursorAction, modifiers ); + } + + const QModelIndex current = currentIndex(); + const QRect currentRect = visualRect ( current ); + if ( !current.isValid() ) + { + const int rowCount = d->proxyModel->rowCount ( rootIndex() ); + if ( !rowCount ) + { + return QModelIndex(); + } + return d->proxyModel->index ( 0, modelColumn(), rootIndex() ); + } + + switch ( cursorAction ) + { + case MoveLeft: + { + if ( !current.row() ) + { + return QModelIndex(); + } + const QModelIndex previous = d->proxyModel->index ( current.row() - 1, modelColumn(), rootIndex() ); + const QRect previousRect = visualRect ( previous ); + if ( previousRect.top() == currentRect.top() ) + { + return previous; + } + + return QModelIndex(); + } + case MoveRight: + { + if ( current.row() == d->proxyModel->rowCount() - 1 ) + { + return QModelIndex(); + } + const QModelIndex next = d->proxyModel->index ( current.row() + 1, modelColumn(), rootIndex() ); + const QRect nextRect = visualRect ( next ); + if ( nextRect.top() == currentRect.top() ) + { + return next; + } + + return QModelIndex(); + } + case MoveDown: + { + if ( d->hasGrid() || uniformItemSizes() ) + { + const QModelIndex current = currentIndex(); + const QSize itemSize = d->hasGrid() ? gridSize() + : sizeHintForIndex ( current ); + const Private::Block &block = d->blocks[d->categoryForIndex ( current )]; + const int maxItemsPerRow = qMax ( d->viewportWidth() / itemSize.width(), 1 ); + const bool canMove = current.row() + maxItemsPerRow < block.firstIndex.row() + + block.items.count(); + + if ( canMove ) + { + return d->proxyModel->index ( current.row() + maxItemsPerRow, modelColumn(), rootIndex() ); + } + + const int currentRelativePos = ( current.row() - block.firstIndex.row() ) % maxItemsPerRow; + const QModelIndex nextIndex = d->proxyModel->index ( block.firstIndex.row() + block.items.count(), modelColumn(), rootIndex() ); + + if ( !nextIndex.isValid() ) + { + return QModelIndex(); + } + + const Private::Block &nextBlock = d->blocks[d->categoryForIndex ( nextIndex )]; + + if ( nextBlock.items.count() <= currentRelativePos ) + { + return QModelIndex(); + } + + if ( currentRelativePos < ( block.items.count() % maxItemsPerRow ) ) + { + return d->proxyModel->index ( nextBlock.firstIndex.row() + currentRelativePos, modelColumn(), rootIndex() ); + } + + return QModelIndex(); + } + } + case MoveUp: + { + if ( d->hasGrid() || uniformItemSizes() ) + { + const QModelIndex current = currentIndex(); + const QSize itemSize = d->hasGrid() ? gridSize() + : sizeHintForIndex ( current ); + const Private::Block &block = d->blocks[d->categoryForIndex ( current )]; + const int maxItemsPerRow = qMax ( d->viewportWidth() / itemSize.width(), 1 ); + const bool canMove = current.row() - maxItemsPerRow >= block.firstIndex.row(); + + if ( canMove ) + { + return d->proxyModel->index ( current.row() - maxItemsPerRow, modelColumn(), rootIndex() ); + } + + const int currentRelativePos = ( current.row() - block.firstIndex.row() ) % maxItemsPerRow; + const QModelIndex prevIndex = d->proxyModel->index ( block.firstIndex.row() - 1, modelColumn(), rootIndex() ); + + if ( !prevIndex.isValid() ) + { + return QModelIndex(); + } + + const Private::Block &prevBlock = d->blocks[d->categoryForIndex ( prevIndex )]; + + if ( prevBlock.items.count() <= currentRelativePos ) + { + return QModelIndex(); + } + + const int remainder = prevBlock.items.count() % maxItemsPerRow; + if ( currentRelativePos < remainder ) + { + return d->proxyModel->index ( prevBlock.firstIndex.row() + prevBlock.items.count() - remainder + currentRelativePos, modelColumn(), rootIndex() ); + } + + return QModelIndex(); + } + } + default: + break; + } + + return QModelIndex(); +} + +void KCategorizedView::rowsAboutToBeRemoved ( const QModelIndex &parent, + int start, + int end ) +{ + if ( !d->isCategorized() ) + { + QListView::rowsAboutToBeRemoved ( parent, start, end ); + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + + if ( end - start + 1 == d->proxyModel->rowCount() ) + { + d->blocks.clear(); + QListView::rowsAboutToBeRemoved ( parent, start, end ); + return; + } + + // Removal feels a bit more complicated than insertion. Basically we can consider there are + // 3 different cases when going to remove items. (*) represents an item, Items between ([) and + // (]) are the ones which are marked for removal. + // + // - 1st case: + // ... * * * * * * [ * * * ... + // + // The items marked for removal are the last part of this category. No need to mark any item + // of this category as in quarantine, because no special offset will be pushed to items at + // the right because of any changes (since the removed items are those on the right most part + // of the category). + // + // - 2nd case: + // ... * * * * * * ] * * * ... + // + // The items marked for removal are the first part of this category. We have to mark as in + // quarantine all items in this category. Absolutely all. All items will have to be moved to + // the left (or moving up, because rows got a different offset). + // + // - 3rd case: + // ... * * [ * * * * ] * * ... + // + // The items marked for removal are in between of this category. We have to mark as in + // quarantine only those items that are at the right of the end of the removal interval, + // (starting on "]"). + // + // It hasn't been explicitly said, but when we remove, we have to mark all blocks that are + // located under the top most affected category as in quarantine (the block itself, as a whole), + // because such a change can force it to have a different offset (note that items themselves + // contain relative positions to the block, so marking the block as in quarantine is enough). + // + // Also note that removal implicitly means that we have to update correctly firstIndex of each + // block, and in general keep updated the internal information of elements. + + QStringList listOfCategoriesMarkedForRemoval; + + QString lastCategory; + int alreadyRemoved = 0; + for ( int i = start; i <= end; ++i ) + { + const QModelIndex index = d->proxyModel->index ( i, modelColumn(), parent ); + + Q_ASSERT ( index.isValid() ); + + const QString category = d->categoryForIndex ( index ); + + if ( lastCategory != category ) + { + lastCategory = category; + alreadyRemoved = 0; + } + + Private::Block &block = d->blocks[category]; + block.items.removeAt ( i - block.firstIndex.row() - alreadyRemoved ); + ++alreadyRemoved; + + if ( !block.items.count() ) + { + listOfCategoriesMarkedForRemoval << category; + } + + block.height = -1; + + viewport()->update(); + } + + //BEGIN: update the items that are in quarantine in affected categories + { + const QModelIndex lastIndex = d->proxyModel->index ( end, modelColumn(), parent ); + const QString category = d->categoryForIndex ( lastIndex ); + Private::Block &block = d->blocks[category]; + if ( block.items.count() && start <= block.firstIndex.row() && end >= block.firstIndex.row() ) + { + block.firstIndex = d->proxyModel->index ( end + 1, modelColumn(), parent ); + } + block.quarantineStart = block.firstIndex; + } + //END: update the items that are in quarantine in affected categories + + Q_FOREACH ( const QString &category, listOfCategoriesMarkedForRemoval ) + { + d->blocks.remove ( category ); + } + + //BEGIN: mark as in quarantine those categories that are under the affected ones + { + //BEGIN: order for marking as alternate those blocks that are alternate + QList blockList = d->blocks.values(); + qSort ( blockList.begin(), blockList.end(), Private::Block::lessThan ); + QList firstIndexesRows; + foreach ( const Private::Block &block, blockList ) + { + firstIndexesRows << block.firstIndex.row(); + } + //END: order for marking as alternate those blocks that are alternate + for ( QHash::Iterator it = d->blocks.begin(); it != d->blocks.end(); ++it ) + { + Private::Block &block = *it; + if ( block.firstIndex.row() > start ) + { + block.outOfQuarantine = false; + block.alternate = firstIndexesRows.indexOf ( block.firstIndex.row() ) % 2; + } + else if ( block.firstIndex.row() == start ) + { + block.alternate = firstIndexesRows.indexOf ( block.firstIndex.row() ) % 2; + } + } + } + //END: mark as in quarantine those categories that are under the affected ones + + QListView::rowsAboutToBeRemoved ( parent, start, end ); +} + +void KCategorizedView::updateGeometries() +{ + const int oldVerticalOffset = verticalOffset(); + const Qt::ScrollBarPolicy verticalP = verticalScrollBarPolicy(), horizontalP = horizontalScrollBarPolicy(); + + //BEGIN bugs 213068, 287847 ------------------------------------------------------------ + /* + * QListView::updateGeometries() has it's own opinion on whether the scrollbars should be visible (valid range) or not + * and triggers a (sometimes additionally timered) resize through ::layoutChildren() + * http://qt.gitorious.org/qt/qt/blobs/4.7/src/gui/itemviews/qlistview.cpp#line1499 + * (the comment above the main block isn't all accurate, layoutChldren is called regardless of the policy) + * + * As a result QListView and KCategorizedView occasionally started a race on the scrollbar visibility, effectively blocking the UI + * So we prevent QListView from having an own opinion on the scrollbar visibility by + * fixing it before calling the baseclass QListView::updateGeometries() + * + * Since the implicit show/hide by the followin range setting will cause further resizes if the policy is Qt::ScrollBarAsNeeded + * we keep it static until we're done, then restore the original value and ultimately change the scrollbar visibility ourself. + */ + if ( d->isCategorized() ) // important! - otherwise we'd pollute the setting if the view is initially not categorized + { + setVerticalScrollBarPolicy ( ( verticalP == Qt::ScrollBarAlwaysOn || verticalScrollBar()->isVisibleTo ( this ) ) ? + Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff ); + setHorizontalScrollBarPolicy ( ( horizontalP == Qt::ScrollBarAlwaysOn || horizontalScrollBar()->isVisibleTo ( this ) ) ? + Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff ); + } + //END bugs 213068, 287847 -------------------------------------------------------------- + + QListView::updateGeometries(); + + if ( !d->isCategorized() ) + { + return; + } + + const int rowCount = d->proxyModel->rowCount(); + if ( !rowCount ) + { + verticalScrollBar()->setRange ( 0, 0 ); + // unconditional, see function end todo + horizontalScrollBar()->setRange ( 0, 0 ); + return; + } + + const QModelIndex lastIndex = d->proxyModel->index ( rowCount - 1, modelColumn(), rootIndex() ); + Q_ASSERT ( lastIndex.isValid() ); + QRect lastItemRect = visualRect ( lastIndex ); + + if ( d->hasGrid() ) + { + lastItemRect.setSize ( lastItemRect.size().expandedTo ( gridSize() ) ); + } + else + { + if ( uniformItemSizes() ) + { + QSize itemSize = sizeHintForIndex ( lastIndex ); + itemSize.setHeight ( itemSize.height() + spacing() ); + lastItemRect.setSize ( itemSize ); + } + else + { + QSize itemSize = sizeHintForIndex ( lastIndex ); + const QString category = d->categoryForIndex ( lastIndex ); + itemSize.setHeight ( d->highestElementInLastRow ( d->blocks[category] ) + spacing() ); + lastItemRect.setSize ( itemSize ); + } + } + + const int bottomRange = lastItemRect.bottomRight().y() + verticalOffset() - viewport()->height(); + + if ( verticalScrollMode() == ScrollPerItem ) + { + verticalScrollBar()->setSingleStep ( lastItemRect.height() ); + const int rowsPerPage = qMax ( viewport()->height() / lastItemRect.height(), 1 ); + verticalScrollBar()->setPageStep ( rowsPerPage * lastItemRect.height() ); + } + + verticalScrollBar()->setRange ( 0, bottomRange ); + verticalScrollBar()->setValue ( oldVerticalOffset ); + + //TODO: also consider working with the horizontal scroll bar. since at this level I am not still + // supporting "top to bottom" flow, there is no real problem. If I support that someday + // (think how to draw categories), we would have to take care of the horizontal scroll bar too. + // In theory, as KCategorizedView has been designed, there is no need of horizontal scroll bar. + horizontalScrollBar()->setRange ( 0, 0 ); + + //BEGIN bugs 213068, 287847 ------------------------------------------------------------ + // restoring values from above ... + setVerticalScrollBarPolicy ( verticalP ); + setHorizontalScrollBarPolicy ( horizontalP ); + // ... and correct the visibility + bool validRange = verticalScrollBar()->maximum() != verticalScrollBar()->minimum(); + if ( verticalP == Qt::ScrollBarAsNeeded && ( verticalScrollBar()->isVisibleTo ( this ) != validRange ) ) + verticalScrollBar()->setVisible ( validRange ); + validRange = horizontalScrollBar()->maximum() > horizontalScrollBar()->minimum(); + if ( horizontalP == Qt::ScrollBarAsNeeded && ( horizontalScrollBar()->isVisibleTo ( this ) != validRange ) ) + horizontalScrollBar()->setVisible ( validRange ); + //END bugs 213068, 287847 -------------------------------------------------------------- +} + +void KCategorizedView::currentChanged ( const QModelIndex ¤t, + const QModelIndex &previous ) +{ + QListView::currentChanged ( current, previous ); +} + +void KCategorizedView::dataChanged ( const QModelIndex &topLeft, + const QModelIndex &bottomRight ) +{ + QListView::dataChanged ( topLeft, bottomRight ); + if ( !d->isCategorized() ) + { + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + + //BEGIN: since the model changed data, we need to reconsider item sizes + int i = topLeft.row(); + int indexToCheck = i; + QModelIndex categoryIndex; + QString category; + Private::Block *block; + while ( i <= bottomRight.row() ) + { + const QModelIndex currIndex = d->proxyModel->index ( i, modelColumn(), rootIndex() ); + if ( i == indexToCheck ) + { + categoryIndex = d->proxyModel->index ( i, d->proxyModel->sortColumn(), rootIndex() ); + category = categoryIndex.data ( KCategorizedSortFilterProxyModel::CategoryDisplayRole ).toString(); + block = &d->blocks[category]; + block->quarantineStart = currIndex; + indexToCheck = block->firstIndex.row() + block->items.count(); + } + visualRect ( currIndex ); + ++i; + } + //END: since the model changed data, we need to reconsider item sizes +} + +void KCategorizedView::rowsInserted ( const QModelIndex &parent, + int start, + int end ) +{ + QListView::rowsInserted ( parent, start, end ); + if ( !d->isCategorized() ) + { + return; + } + + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + d->rowsInserted ( parent, start, end ); +} + +void KCategorizedView::slotLayoutChanged() +{ + if ( !d->isCategorized() ) + { + return; + } + + d->blocks.clear(); + *d->hoveredBlock = Private::Block(); + d->hoveredCategory = QString(); + if ( d->proxyModel->rowCount() ) + { + d->rowsInserted ( rootIndex(), 0, d->proxyModel->rowCount() - 1 ); + } +} +//END: Public part + +void KCategorizedView::slotCollapseOrExpandClicked ( QModelIndex idx ) +{ + d->_k_slotCollapseOrExpandClicked ( idx ); +} + + +#include "kcategorizedview.moc" diff --git a/libgroupview/src/kcategorizedview_p.h b/libgroupview/src/kcategorizedview_p.h new file mode 100644 index 00000000..6dafa382 --- /dev/null +++ b/libgroupview/src/kcategorizedview_p.h @@ -0,0 +1,157 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef KCATEGORIZEDVIEW_P_H +#define KCATEGORIZEDVIEW_P_H + +class KCategorizedSortFilterProxyModel; +class KCategoryDrawer; +class KCategoryDrawerV2; +class KCategoryDrawerV3; + +/** + * @internal + */ +class KCategorizedView::Private +{ +public: + struct Block; + struct Item; + + Private(KCategorizedView *q); + ~Private(); + + /** + * @return whether this view has all required elements to be categorized. + */ + bool isCategorized() const; + + /** + * @return the block rect for the representative @p representative. + */ + QStyleOptionViewItemV4 blockRect(const QModelIndex &representative); + + /** + * Returns the first and last element that intersects with rect. + * + * @note see that here we cannot take out items between first and last (as we could + * do with the rubberband). + * + * Complexity: O(log(n)) where n is model()->rowCount(). + */ + QPair intersectingIndexesWithRect(const QRect &rect) const; + + /** + * Returns the position of the block of @p category. + * + * Complexity: O(n) where n is the number of different categories when the block has been + * marked as in quarantine. O(1) the rest of the times (the vast majority). + */ + QPoint blockPosition(const QString &category); + + /** + * Returns the height of the block determined by @p category. + */ + int blockHeight(const QString &category); + + /** + * Returns the actual viewport width. + */ + int viewportWidth() const; + + /** + * Marks all elements as in quarantine. + * + * Complexity: O(n) where n is model()->rowCount(). + * + * @warning this is an expensive operation + */ + void regenerateAllElements(); + + /** + * Update internal information, and keep sync with the real information that the model contains. + */ + void rowsInserted(const QModelIndex &parent, int start, int end); + + /** + * Returns @p rect in viewport terms, taking in count horizontal and vertical offsets. + */ + QRect mapToViewport(const QRect &rect) const; + + /** + * Returns @p rect in absolute terms, converted from viewport position. + */ + QRect mapFromViewport(const QRect &rect) const; + + /** + * Returns the height of the highest element in last row. This is only applicable if there is + * no grid set and uniformItemSizes is false. + * + * @param block in which block are we searching. Necessary to stop the search if we hit the + * first item in this block. + */ + int highestElementInLastRow(const Block &block) const; + + /** + * Returns whether the view has a valid grid size. + */ + bool hasGrid() const; + + /** + * Returns the category for the given index. + */ + QString categoryForIndex(const QModelIndex &index) const; + + /** + * Updates the visual rect for item when flow is LeftToRight. + */ + void leftToRightVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const; + + /** + * Updates the visual rect for item when flow is TopToBottom. + * @note we only support viewMode == ListMode in this case. + */ + void topToBottomVisualRect(const QModelIndex &index, Item &item, + const Block &block, const QPoint &blockPos) const; + + /** + * Called when expand or collapse has been clicked on the category drawer. + */ + void _k_slotCollapseOrExpandClicked(QModelIndex); + + KCategorizedView *q; + KCategorizedSortFilterProxyModel *proxyModel; + KCategoryDrawer *categoryDrawer; + int categorySpacing; + bool alternatingBlockColors; + bool collapsibleBlocks; + + Block *hoveredBlock; + QString hoveredCategory; + QModelIndex hoveredIndex; + + QPoint pressedPosition; + QRect rubberBandRect; + + QHash blocks; +}; + +#endif // KCATEGORIZEDVIEW_P_H diff --git a/libgroupview/src/kcategorydrawer.cpp b/libgroupview/src/kcategorydrawer.cpp new file mode 100644 index 00000000..951c1eb2 --- /dev/null +++ b/libgroupview/src/kcategorydrawer.cpp @@ -0,0 +1,231 @@ +/** + * This file is part of the KDE project + * Copyright (C) 2007, 2009 Rafael Fernández López + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kcategorydrawer.h" + +#include +#include +#include + +#include +#include + +#define HORIZONTAL_HINT 3 + +class KCategoryDrawer::Private +{ +public: + Private(KCategorizedView *view) + : view(view) + , leftMargin(0) + , rightMargin(0) + { + } + + ~Private() + { + } + int leftMargin; + int rightMargin; + KCategorizedView *view; +}; + +KCategoryDrawer::KCategoryDrawer(KCategorizedView *view) + : QObject(view) + , d(new Private(view)) +{ + setLeftMargin(2); + setRightMargin(2); +} + +KCategoryDrawer::~KCategoryDrawer() +{ + delete d; +} + + +void KCategoryDrawer::drawCategory(const QModelIndex &index, + int /*sortRole*/, + const QStyleOption &option, + QPainter *painter) const +{ + painter->setRenderHint(QPainter::Antialiasing); + + const QString category = index.model()->data(index, KCategorizedSortFilterProxyModel::CategoryDisplayRole).toString(); + const QRect optRect = option.rect; + QFont font(QApplication::font()); + font.setBold(true); + const QFontMetrics fontMetrics = QFontMetrics(font); + + QColor outlineColor = option.palette.text().color(); + outlineColor.setAlphaF(0.35); + + //BEGIN: top left corner + { + painter->save(); + painter->setPen(outlineColor); + const QPointF topLeft(optRect.topLeft()); + QRectF arc(topLeft, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 1440, 1440); + painter->restore(); + } + //END: top left corner + + //BEGIN: left vertical line + { + QPoint start(optRect.topLeft()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topLeft()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: left vertical line + + //BEGIN: horizontal line + { + QPoint start(optRect.topLeft()); + start.rx() += 3; + QPoint horizontalGradTop(optRect.topLeft()); + horizontalGradTop.rx() += optRect.width() - 6; + painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor); + } + //END: horizontal line + + //BEGIN: top right corner + { + painter->save(); + painter->setPen(outlineColor); + QPointF topRight(optRect.topRight()); + topRight.rx() -= 4; + QRectF arc(topRight, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 0, 1440); + painter->restore(); + } + //END: top right corner + + //BEGIN: right vertical line + { + QPoint start(optRect.topRight()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topRight()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: right vertical line + + //BEGIN: text + { + QRect textRect(option.rect); + textRect.setTop(textRect.top() + 7); + textRect.setLeft(textRect.left() + 7); + textRect.setHeight(fontMetrics.height()); + textRect.setRight(textRect.right() - 7); + + painter->save(); + painter->setFont(font); + QColor penColor(option.palette.text().color()); + penColor.setAlphaF(0.6); + painter->setPen(penColor); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, category); + painter->restore(); + } + //END: text +} + +int KCategoryDrawer::categoryHeight(const QModelIndex &index, const QStyleOption &option) const +{ + Q_UNUSED(index); + Q_UNUSED(option) + + QFont font(QApplication::font()); + font.setBold(true); + QFontMetrics fontMetrics(font); + + const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */ + + 11 /* top and bottom separation */; + return height; +} + +int KCategoryDrawer::leftMargin() const +{ + return d->leftMargin; +} + +void KCategoryDrawer::setLeftMargin(int leftMargin) +{ + d->leftMargin = leftMargin; +} + +int KCategoryDrawer::rightMargin() const +{ + return d->rightMargin; +} + +void KCategoryDrawer::setRightMargin(int rightMargin) +{ + d->rightMargin = rightMargin; +} + +KCategoryDrawer &KCategoryDrawer::operator=(const KCategoryDrawer &cd) +{ + d->leftMargin = cd.d->leftMargin; + d->rightMargin = cd.d->rightMargin; + d->view = cd.d->view; + return *this; +} + +KCategorizedView *KCategoryDrawer::view() const +{ + return d->view; +} + +void KCategoryDrawer::mouseButtonPressed(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawer::mouseButtonReleased(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawer::mouseMoved(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawer::mouseButtonDoubleClicked(const QModelIndex&, const QRect&, QMouseEvent *event) +{ + event->ignore(); +} + +void KCategoryDrawer::mouseLeft(const QModelIndex&, const QRect&) +{ +} + +#include "kcategorydrawer.moc" diff --git a/libmultimc/include/appsettings.h b/libmultimc/include/appsettings.h index 66ffb7b5..8447d58b 100644 --- a/libmultimc/include/appsettings.h +++ b/libmultimc/include/appsettings.h @@ -18,11 +18,11 @@ #include -#include +#include #include "libmmc_config.h" -class LIBMULTIMC_EXPORT AppSettings : public BasicSettingsObject +class LIBMULTIMC_EXPORT AppSettings : public INISettingsObject { Q_OBJECT public: diff --git a/libmultimc/include/logintask.h b/libmultimc/include/logintask.h index c1efb223..e00609ec 100644 --- a/libmultimc/include/logintask.h +++ b/libmultimc/include/logintask.h @@ -30,20 +30,20 @@ class LIBMULTIMC_EXPORT LoginTask : public Task { Q_OBJECT public: - explicit LoginTask(const UserInfo& uInfo, QObject *parent = 0); + explicit LoginTask(const UserInfo& uInfo, QString inst, QObject *parent = 0); public slots: void processNetReply(QNetworkReply* reply); signals: - void loginComplete(LoginResponse loginResponse); - void loginFailed(const QString& errorMsg); + void loginComplete(QString inst, LoginResponse loginResponse); + void loginFailed(QString inst, const QString& errorMsg); protected: void executeTask(); QNetworkReply* netReply; - + QString inst; UserInfo uInfo; }; diff --git a/libmultimc/src/appsettings.cpp b/libmultimc/src/appsettings.cpp index 68d2c015..7c886fdb 100644 --- a/libmultimc/src/appsettings.cpp +++ b/libmultimc/src/appsettings.cpp @@ -18,10 +18,11 @@ #include #include +#include //#include AppSettings::AppSettings(QObject *parent) : - BasicSettingsObject(parent) + INISettingsObject(QApplication::applicationDirPath() + "/multimc.cfg",parent) { // Updates registerSetting(new Setting("UseDevBuilds", false)); diff --git a/libmultimc/src/instancelist.cpp b/libmultimc/src/instancelist.cpp index 3b0b668f..78650634 100644 --- a/libmultimc/src/instancelist.cpp +++ b/libmultimc/src/instancelist.cpp @@ -28,7 +28,7 @@ InstanceList::InstanceList(const QString &instDir, QObject *parent) : - QObject(parent), m_instDir(instDir) + QObject(parent), m_instDir("instances") { } diff --git a/libmultimc/src/logintask.cpp b/libmultimc/src/logintask.cpp index 71075630..e042a93f 100644 --- a/libmultimc/src/logintask.cpp +++ b/libmultimc/src/logintask.cpp @@ -24,8 +24,8 @@ #include #include -LoginTask::LoginTask(const UserInfo &uInfo, QObject *parent) : - Task(parent), uInfo(uInfo) +LoginTask::LoginTask( const UserInfo& uInfo, QString inst, QObject* parent ) : + Task(parent), uInfo(uInfo), inst(inst) { } @@ -78,42 +78,42 @@ void LoginTask::processNetReply(QNetworkReply *reply) QString sessionID = strings[3]; LoginResponse response(username, sessionID, latestVersion); - emit loginComplete(response); + emit loginComplete(inst, response); } else { - emit loginFailed("Failed to parse Minecraft version string."); + emit loginFailed(inst, "Failed to parse Minecraft version string."); } } else { if (responseStr.toLower() == "bad login") - emit loginFailed("Invalid username or password."); + emit loginFailed(inst, "Invalid username or password."); else if (responseStr.toLower() == "old version") - emit loginFailed("Launcher outdated, please update."); + emit loginFailed(inst, "Launcher outdated, please update."); else - emit loginFailed("Login failed: " + responseStr); + emit loginFailed(inst, "Login failed: " + responseStr); } } else if (responseCode == 503) { - emit loginFailed("The login servers are currently unavailable. " + emit loginFailed(inst, "The login servers are currently unavailable. " "Check http://help.mojang.com/ for more info."); } else { - emit loginFailed(QString("Login failed: Unknown HTTP error %1 occurred."). + emit loginFailed(inst, QString("Login failed: Unknown HTTP error %1 occurred."). arg(QString::number(responseCode))); } break; } case QNetworkReply::OperationCanceledError: - emit loginFailed("Login canceled."); + emit loginFailed(inst, "Login canceled."); break; default: - emit loginFailed("Login failed: " + reply->errorString()); + emit loginFailed(inst, "Login failed: " + reply->errorString()); break; } diff --git a/main.cpp b/main.cpp index 465ee3c7..cf193ff0 100644 --- a/main.cpp +++ b/main.cpp @@ -81,7 +81,7 @@ private slots: QApplication::instance()->quit(); } - void onLoginComplete(LoginResponse response) + void onLoginComplete(QString instId, LoginResponse response) { // TODO: console console = new ConsoleWindow(); @@ -92,7 +92,7 @@ private slots: proc->launch(); } - void doLogin(const QString &errorMsg) + void doLogin(QString instId, const QString &errorMsg) { LoginDialog* loginDlg = new LoginDialog(nullptr, errorMsg); if (loginDlg->exec()) @@ -100,11 +100,11 @@ private slots: UserInfo uInfo(loginDlg->getUsername(), loginDlg->getPassword()); TaskDialog* tDialog = new TaskDialog(nullptr); - LoginTask* loginTask = new LoginTask(uInfo, tDialog); - connect(loginTask, SIGNAL(loginComplete(LoginResponse)), - SLOT(onLoginComplete(LoginResponse)), Qt::QueuedConnection); - connect(loginTask, SIGNAL(loginFailed(QString)), - SLOT(doLogin(QString)), Qt::QueuedConnection); + LoginTask* loginTask = new LoginTask(uInfo, instance.data()->id(), tDialog); + connect(loginTask, SIGNAL(loginComplete(QString, LoginResponse)), + SLOT(onLoginComplete(QString, LoginResponse)), Qt::QueuedConnection); + connect(loginTask, SIGNAL(loginFailed(QString, QString)), + SLOT(doLogin(QString, QString)), Qt::QueuedConnection); tDialog->exec(loginTask); } //onLoginComplete(LoginResponse("Offline","Offline", 1)); @@ -125,7 +125,7 @@ public: } std::cout << "Logging in..." << std::endl; - doLogin(""); + doLogin(instance->id(),""); return QApplication::instance()->exec(); }