diff options
84 files changed, 4442 insertions, 4023 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b276cf8..4e7dea67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ SET(MultiMC_NEWS_RSS_URL "http://multimc.org/rss.xml" CACHE STRING "URL to fetch ######## Set version numbers ######## SET(MultiMC_VERSION_MAJOR 0) SET(MultiMC_VERSION_MINOR 2) -SET(MultiMC_VERSION_HOTFIX 0) +SET(MultiMC_VERSION_HOTFIX 1) # Build number SET(MultiMC_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") @@ -186,7 +186,6 @@ configure_file("${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}/includ ADD_DEFINITIONS(-DQUAZIP_STATIC) ADD_DEFINITIONS(-DLIBSETTINGS_STATIC) ADD_DEFINITIONS(-DLIBUTIL_STATIC) -ADD_DEFINITIONS(-DLIBGROUPVIEW_STATIC) ######## Packaging/install paths setup ######## @@ -247,10 +246,6 @@ include_directories(${LIBUTIL_INCLUDE_DIR}) add_subdirectory(depends/settings) include_directories(${LIBSETTINGS_INCLUDE_DIR}) -# Add the group view library. -add_subdirectory(depends/groupview) -include_directories(${LIBGROUPVIEW_INCLUDE_DIR}) - # Add the updater add_subdirectory(mmc_updater) @@ -315,15 +310,27 @@ gui/dialogs/UpdateDialog.h gui/dialogs/UpdateDialog.cpp # GUI - widgets -gui/widgets/InstanceDelegate.h -gui/widgets/InstanceDelegate.cpp +gui/widgets/Common.h +gui/widgets/Common.cpp gui/widgets/ModListView.h gui/widgets/ModListView.cpp +gui/widgets/VersionListView.h +gui/widgets/VersionListView.cpp gui/widgets/LabeledToolButton.h gui/widgets/LabeledToolButton.cpp gui/widgets/MCModInfoFrame.h gui/widgets/MCModInfoFrame.cpp +# GUI - instance group view +gui/groupview/Group.cpp +gui/groupview/Group.h +gui/groupview/GroupedProxyModel.cpp +gui/groupview/GroupedProxyModel.h +gui/groupview/GroupView.cpp +gui/groupview/GroupView.h +gui/groupview/InstanceDelegate.cpp +gui/groupview/InstanceDelegate.h + # Base classes and infrastructure logic/BaseVersion.h logic/MinecraftVersion.h @@ -365,6 +372,8 @@ logic/net/PasteUpload.cpp logic/net/URLConstants.h # Yggdrasil login stuff +logic/auth/AuthSession.h +logic/auth/AuthSession.cpp logic/auth/MojangAccountList.h logic/auth/MojangAccountList.cpp logic/auth/MojangAccount.h @@ -405,10 +414,7 @@ logic/LegacyUpdate.cpp logic/LegacyForge.h logic/LegacyForge.cpp -# 1.6 instances -logic/OneSixInstance.h -logic/OneSixInstance.cpp -logic/OneSixInstance_p.h +# OneSix instances logic/OneSixUpdate.h logic/OneSixUpdate.cpp logic/OneSixVersion.h @@ -419,10 +425,17 @@ logic/OneSixRule.h logic/OneSixRule.cpp logic/OpSys.h logic/OpSys.cpp +logic/BaseInstaller.h +logic/BaseInstaller.cpp logic/ForgeInstaller.h logic/ForgeInstaller.cpp logic/LiteLoaderInstaller.h logic/LiteLoaderInstaller.cpp +logic/OneSixInstance.h +logic/OneSixInstance.cpp +logic/OneSixInstance_p.h +logic/OneSixVersionBuilder.h +logic/OneSixVersionBuilder.cpp # Nostalgia logic/NostalgiaInstance.h @@ -578,7 +591,7 @@ ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS}) # Link TARGET_LINK_LIBRARIES(MultiMC MultiMC_common) -TARGET_LINK_LIBRARIES(MultiMC_common xz-embedded unpack200 quazip libUtil libSettings libGroupView ${MultiMC_LINK_ADDITIONAL_LIBS}) +TARGET_LINK_LIBRARIES(MultiMC_common xz-embedded unpack200 quazip libUtil libSettings ${MultiMC_LINK_ADDITIONAL_LIBS}) QT5_USE_MODULES(MultiMC Core Widgets Network Xml Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) QT5_USE_MODULES(MultiMC_common Core Widgets Network Xml Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) diff --git a/changelog.yaml b/changelog.yaml index 01e11f52..8b96b76e 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -29,3 +29,5 @@ - Started using icon themes for the application icons, fixing many OSX graphical glitches. - Icon sources have been located, along with icon licenses. - Update to the German translation. +0.2.1: + - Hotfix - move the native library extraction into the onesix launcher part. diff --git a/depends/groupview/CMakeLists.txt b/depends/groupview/CMakeLists.txt deleted file mode 100644 index 417b1d3a..00000000 --- a/depends/groupview/CMakeLists.txt +++ /dev/null @@ -1,46 +0,0 @@ -project(libGroupView) - -set(CMAKE_AUTOMOC ON) - -# Find Qt -find_package(Qt5Core REQUIRED) -find_package(Qt5Widgets REQUIRED) - -# Include Qt headers. -include_directories(${Qt5Base_INCLUDE_DIRS}) - -SET(LIBGROUPVIEW_HEADERS -include/groupview_config.h - -# Public headers -include/categorizedsortfilterproxymodel.h -include/categorizedview.h -include/categorydrawer.h - -# Private headers -src/categorizedsortfilterproxymodel_p.h -src/categorizedview_p.h -) - -SET(LIBGROUPVIEW_SOURCES -src/categorizedsortfilterproxymodel.cpp -src/categorizedview.cpp -src/categorydrawer.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) - -# Static link! -ADD_DEFINITIONS(-DLIBGROUPVIEW_STATIC) - -add_definitions(-DLIBGROUPVIEW_LIBRARY) - -set(CMAKE_POSITION_INDEPENDENT_CODE ON) - -add_library(libGroupView STATIC ${LIBGROUPVIEW_SOURCES} ${LIBGROUPVIEW_HEADERS}) -qt5_use_modules(libGroupView Core Widgets) diff --git a/depends/groupview/include/categorizedsortfilterproxymodel.h b/depends/groupview/include/categorizedsortfilterproxymodel.h deleted file mode 100644 index d90fb254..00000000 --- a/depends/groupview/include/categorizedsortfilterproxymodel.h +++ /dev/null @@ -1,175 +0,0 @@ -/* - * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López <ereslibre@kde.org> - * Copyright (C) 2007 John Tapsell <tapsell@kde.org> - * - * 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 <QSortFilterProxyModel> - -#include <groupview_config.h> - -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 <ereslibre@kde.org> - */ -class LIBGROUPVIEW_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/depends/groupview/include/categorizedview.h b/depends/groupview/include/categorizedview.h deleted file mode 100644 index 81b1dbb1..00000000 --- a/depends/groupview/include/categorizedview.h +++ /dev/null @@ -1,332 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org> - * - * 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 <QListView> - -#include <groupview_config.h> - -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 <ereslibre@kde.org> - */ -class LIBGROUPVIEW_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<KCategorizedView*>(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(); - - /** - * Signify that all item delegates size hints return the same fixed size - */ - void setUniformItemWidths(bool enable); - - /** - * Do all item delegate size hints return the same fixed size? - */ - bool uniformItemWidths() const; - -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/depends/groupview/include/categorydrawer.h b/depends/groupview/include/categorydrawer.h deleted file mode 100644 index f37422ec..00000000 --- a/depends/groupview/include/categorydrawer.h +++ /dev/null @@ -1,179 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org> - * - * 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 <groupview_config.h> - -#include <QtCore/QObject> -#include <QtGui/QMouseEvent> - -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/depends/groupview/src/categorizedsortfilterproxymodel.cpp b/depends/groupview/src/categorizedsortfilterproxymodel.cpp deleted file mode 100644 index 09da9dd3..00000000 --- a/depends/groupview/src/categorizedsortfilterproxymodel.cpp +++ /dev/null @@ -1,168 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López <ereslibre@kde.org> - * Copyright (C) 2007 John Tapsell <tapsell@kde.org> - * - * 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 "categorizedsortfilterproxymodel.h" -#include "categorizedsortfilterproxymodel_p.h" - -#include <limits.h> - -#include <QItemSelection> -#include <QStringList> -#include <QSize> - -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/depends/groupview/src/categorizedsortfilterproxymodel_p.h b/depends/groupview/src/categorizedsortfilterproxymodel_p.h deleted file mode 100644 index d7e7c9a0..00000000 --- a/depends/groupview/src/categorizedsortfilterproxymodel_p.h +++ /dev/null @@ -1,48 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007 Rafael Fernández López <ereslibre@kde.org> - * Copyright (C) 2007 John Tapsell <tapsell@kde.org> - * - * 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/depends/groupview/src/categorizedview.cpp b/depends/groupview/src/categorizedview.cpp deleted file mode 100644 index 1345205c..00000000 --- a/depends/groupview/src/categorizedview.cpp +++ /dev/null @@ -1,1692 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org> - * - * 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 "categorizedview.h" -#include "categorizedview_p.h" - -#include <math.h> // trunc on C99 compliant systems -//#include <kdefakes.h> // trunc for not C99 compliant systems - -#include <QPainter> -#include <QScrollBar> -#include <QPaintEvent> - -#include "categorydrawer.h" -#include "categorizedsortfilterproxymodel.h" - -//BEGIN: Private part - -struct KCategorizedView::Private::Item -{ - Item() - : topLeft ( QPoint() ) - , size ( QSize() ) - { - } - - QPoint topLeft; - QSize size; -}; - -struct KCategorizedView::Private::Block -{ - 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 = -1; - 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<Item> 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 = false; - - // should we alternate its color ? is just a hint, could not be used - bool alternate = false; - bool collapsed = false; -}; - -KCategorizedView::Private::Private ( KCategorizedView *q ) - : q ( q ) - , hoveredBlock ( new Block() ) -{ -} - -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<QModelIndex, QModelIndex> 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<QString, Private::Block>::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<QString, Block>::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<Block> blockList = blocks.values(); - qSort ( blockList.begin(), blockList.end(), Block::lessThan ); - QList<int> firstIndexesRows; - foreach ( const Block &block, blockList ) - { - firstIndexesRows << block.firstIndex.row(); - } - //END: order for marking as alternate those blocks that are alternate - for ( QHash<QString, Private::Block>::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() /*|| q->uniformItemWidths()*/ ) - { - const int relativeRow = index.row() - firstIndexRow; - const QSize itemSize = q->sizeHintForIndex ( index ); - //HACK: Why is the -2 needed? - const int maxItemsPerRow = qMax ( ( viewportWidth() - q->spacing() - 2 ) / ( 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() ); - QRect tempRect = q->visualRect ( prevIndex ); - tempRect = mapFromViewport ( tempRect ); - 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<KCategorizedSortFilterProxyModel*> ( 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::setUniformItemWidths(bool enable) -{ - d->constantItemWidth = enable; -} - - -bool KCategorizedView::uniformItemWidths() const -{ - return d->constantItemWidth; -} - -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<QString, Private::Block>::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<QModelIndex, QModelIndex> intersecting = d->intersectingIndexesWithRect ( viewport()->rect().intersected ( event->rect() ) ); - - QPainter p ( viewport() ); - p.save(); - - Q_ASSERT ( selectionModel()->model() == d->proxyModel ); - - //BEGIN: draw categories - QHash<QString, Private::Block>::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<QModelIndex, QModelIndex> 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<QString, Private::Block>::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<QString, Private::Block>::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<QString, Private::Block>::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() || uniformItemWidths() ) - { - const QModelIndex current = currentIndex(); - const QSize itemSize = d->hasGrid() ? gridSize() - : sizeHintForIndex ( current ); - const Private::Block &block = d->blocks[d->categoryForIndex ( current )]; - //HACK: Why is the -2 needed? - const int maxItemsPerRow = qMax ( ( d->viewportWidth() - spacing() - 2 ) / ( itemSize.width() + spacing() ), 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() || uniformItemWidths() ) - { - const QModelIndex current = currentIndex(); - const QSize itemSize = d->hasGrid() ? gridSize() - : sizeHintForIndex ( current ); - const Private::Block &block = d->blocks[d->categoryForIndex ( current )]; - //HACK: Why is the -2 needed? - const int maxItemsPerRow = qMax ( ( d->viewportWidth() - spacing() - 2 ) / ( itemSize.width() + spacing() ), 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<Private::Block> blockList = d->blocks.values(); - qSort ( blockList.begin(), blockList.end(), Private::Block::lessThan ); - QList<int> firstIndexesRows; - foreach ( const Private::Block &block, blockList ) - { - firstIndexesRows << block.firstIndex.row(); - } - //END: order for marking as alternate those blocks that are alternate - for ( QHash<QString, Private::Block>::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 "categorizedview.moc" diff --git a/depends/groupview/src/categorizedview_p.h b/depends/groupview/src/categorizedview_p.h deleted file mode 100644 index 524bba3a..00000000 --- a/depends/groupview/src/categorizedview_p.h +++ /dev/null @@ -1,160 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org> - * - * 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<QModelIndex, QModelIndex> 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 = nullptr; - KCategorizedSortFilterProxyModel *proxyModel = nullptr; - KCategoryDrawer *categoryDrawer = nullptr; - int categorySpacing = 5; - bool alternatingBlockColors = false; - bool collapsibleBlocks = false; - bool constantItemWidth = false; - - // FIXME: this is some really weird logic. Investigate. - Block *hoveredBlock; - QString hoveredCategory; - QModelIndex hoveredIndex; - - QPoint pressedPosition; - QRect rubberBandRect; - - QHash<QString, Block> blocks; -}; - -#endif // KCATEGORIZEDVIEW_P_H - diff --git a/depends/groupview/src/categorydrawer.cpp b/depends/groupview/src/categorydrawer.cpp deleted file mode 100644 index 214ce3b2..00000000 --- a/depends/groupview/src/categorydrawer.cpp +++ /dev/null @@ -1,231 +0,0 @@ -/** - * This file is part of the KDE project - * Copyright (C) 2007, 2009 Rafael Fernández López <ereslibre@kde.org> - * - * 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 "categorydrawer.h" - -#include <QPainter> -#include <QStyleOption> -#include <QApplication> - -#include <categorizedview.h> -#include <categorizedsortfilterproxymodel.h> - -#define HORIZONTAL_HINT 3 - -class KCategoryDrawer::Private -{ -public: - Private(KCategorizedView *view) - : view(view) - , leftMargin(0) - , rightMargin(0) - { - } - - ~Private() - { - } - KCategorizedView *view; - int leftMargin; - int rightMargin; -}; - -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 "categorydrawer.moc" diff --git a/depends/util/CMakeLists.txt b/depends/util/CMakeLists.txt index db7d70e6..7f6573bd 100644 --- a/depends/util/CMakeLists.txt +++ b/depends/util/CMakeLists.txt @@ -35,6 +35,9 @@ src/userutils.cpp include/cmdutils.h src/cmdutils.cpp + +include/modutils.h +src/modutils.cpp ) # Set the include dir path. diff --git a/depends/util/include/modutils.h b/depends/util/include/modutils.h new file mode 100644 index 00000000..e04db66f --- /dev/null +++ b/depends/util/include/modutils.h @@ -0,0 +1,32 @@ +#pragma once + +#include <QString> +#include "libutil_config.h" + +class QUrl; + +namespace Util +{ +struct Version +{ + Version(const QString &str); + + bool operator<(const Version &other) const; + bool operator<=(const Version &other) const; + bool operator>(const Version &other) const; + bool operator==(const Version &other) const; + bool operator!=(const Version &other) const; + + QString toString() const + { + return m_string; + } + +private: + QString m_string; +}; + +LIBUTIL_EXPORT QUrl expandQMURL(const QString &in); +LIBUTIL_EXPORT bool versionIsInInterval(const QString &version, const QString &interval); +} + diff --git a/depends/util/src/modutils.cpp b/depends/util/src/modutils.cpp new file mode 100644 index 00000000..44a04b72 --- /dev/null +++ b/depends/util/src/modutils.cpp @@ -0,0 +1,216 @@ +#include "include/modutils.h" + +#include <QStringList> +#include <QUrl> +#include <QRegularExpression> +#include <QRegularExpressionMatch> + +Util::Version::Version(const QString &str) : m_string(str) +{ +} + +bool Util::Version::operator<(const Version &other) const +{ + QStringList parts1 = m_string.split('.'); + QStringList parts2 = other.m_string.split('.'); + + while (!parts1.isEmpty() && !parts2.isEmpty()) + { + QString part1 = parts1.isEmpty() ? "0" : parts1.takeFirst(); + QString part2 = parts2.isEmpty() ? "0" : parts2.takeFirst(); + bool ok1 = false; + bool ok2 = false; + int int1 = part1.toInt(&ok1); + int int2 = part2.toInt(&ok2); + if (ok1 && ok2) + { + if (int1 == int2) + { + continue; + } + else + { + return int1 < int2; + } + } + else + { + if (part1 == part2) + { + continue; + } + else + { + return part1 < part2; + } + } + } + + return false; +} +bool Util::Version::operator<=(const Util::Version &other) const +{ + return *this < other || *this == other; +} +bool Util::Version::operator>(const Version &other) const +{ + QStringList parts1 = m_string.split('.'); + QStringList parts2 = other.m_string.split('.'); + + while (!parts1.isEmpty() && !parts2.isEmpty()) + { + QString part1 = parts1.isEmpty() ? "0" : parts1.takeFirst(); + QString part2 = parts2.isEmpty() ? "0" : parts2.takeFirst(); + bool ok1 = false; + bool ok2 = false; + int int1 = part1.toInt(&ok1); + int int2 = part2.toInt(&ok2); + if (ok1 && ok2) + { + if (int1 == int2) + { + continue; + } + else + { + return int1 > int2; + } + } + else + { + if (part1 == part2) + { + continue; + } + else + { + return part1 > part2; + } + } + } + + return false; +} +bool Util::Version::operator==(const Version &other) const +{ + QStringList parts1 = m_string.split('.'); + QStringList parts2 = other.m_string.split('.'); + + while (!parts1.isEmpty() && !parts2.isEmpty()) + { + QString part1 = parts1.isEmpty() ? "0" : parts1.takeFirst(); + QString part2 = parts2.isEmpty() ? "0" : parts2.takeFirst(); + bool ok1 = false; + bool ok2 = false; + int int1 = part1.toInt(&ok1); + int int2 = part2.toInt(&ok2); + if (ok1 && ok2) + { + if (int1 == int2) + { + continue; + } + else + { + return false; + } + } + else + { + if (part1 == part2) + { + continue; + } + else + { + return false; + } + } + } + + return true; +} +bool Util::Version::operator!=(const Version &other) const +{ + return !operator==(other); +} + +QUrl Util::expandQMURL(const QString &in) +{ + QUrl inUrl(in); + if (inUrl.scheme() == "github") + { + // needed because QUrl makes the host all lower cases + const QString repo = in.mid(in.indexOf(inUrl.host(), 0, Qt::CaseInsensitive), inUrl.host().size()); + QUrl out; + out.setScheme("https"); + out.setHost("raw.github.com"); + out.setPath(QString("/%1/%2/%3%4") + .arg(inUrl.userInfo(), repo, + inUrl.fragment().isEmpty() ? "master" : inUrl.fragment(), inUrl.path())); + return out; + } + else if (inUrl.scheme() == "mcf") + { + QUrl out; + out.setScheme("http"); + out.setHost("www.minecraftforum.net"); + out.setPath(QString("/topic/%1-").arg(inUrl.path())); + return out; + } + else + { + return in; + } +} + +bool Util::versionIsInInterval(const QString &version, const QString &interval) +{ + if (interval.isEmpty() || version == interval) + { + return true; + } + + // Interval notation is used + QRegularExpression exp( + "(?<start>[\\[\\]\\(\\)])(?<bottom>.*?)(,(?<top>.*?))?(?<end>[\\[\\]\\(\\)])"); + QRegularExpressionMatch match = exp.match(interval); + if (match.hasMatch()) + { + const QChar start = match.captured("start").at(0); + const QChar end = match.captured("end").at(0); + const QString bottom = match.captured("bottom"); + const QString top = match.captured("top"); + + // check if in range (bottom) + if (!bottom.isEmpty()) + { + if ((start == '[') && !(version >= bottom)) + { + return false; + } + else if ((start == '(') && !(version > bottom)) + { + return false; + } + } + + // check if in range (top) + if (!top.isEmpty()) + { + if ((end == ']') && !(version <= top)) + { + return false; + } + else if ((end == ')') && !(version < top)) + { + return false; + } + } + + return true; + } + + return false; +} + diff --git a/gui/ConsoleWindow.cpp b/gui/ConsoleWindow.cpp index dc36a8ff..ccc037f2 100644 --- a/gui/ConsoleWindow.cpp +++ b/gui/ConsoleWindow.cpp @@ -84,7 +84,7 @@ void ConsoleWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) } } -void ConsoleWindow::writeColor(QString text, const char *color) +void ConsoleWindow::writeColor(QString text, const char *color, const char * background) { // append a paragraph QString newtext; @@ -92,6 +92,8 @@ void ConsoleWindow::writeColor(QString text, const char *color) { if (color) newtext += QString("color:") + color + ";"; + if (background) + newtext += QString("background-color:") + background + ";"; newtext += "font-family: monospace;"; } newtext += "\">"; @@ -127,26 +129,26 @@ void ConsoleWindow::write(QString data, MessageLevel::Enum mode) QListIterator<QString> iter(paragraphs); if (mode == MessageLevel::MultiMC) while (iter.hasNext()) - writeColor(iter.next(), "blue"); + writeColor(iter.next(), "blue", 0); else if (mode == MessageLevel::Error) while (iter.hasNext()) - writeColor(iter.next(), "red"); + writeColor(iter.next(), "red", 0); else if (mode == MessageLevel::Warning) while (iter.hasNext()) - writeColor(iter.next(), "orange"); + writeColor(iter.next(), "orange", 0); else if (mode == MessageLevel::Fatal) while (iter.hasNext()) - writeColor(iter.next(), "pink"); + writeColor(iter.next(), "red", "black"); else if (mode == MessageLevel::Debug) while (iter.hasNext()) - writeColor(iter.next(), "green"); + writeColor(iter.next(), "green", 0); else if (mode == MessageLevel::PrePost) while (iter.hasNext()) - writeColor(iter.next(), "grey"); + writeColor(iter.next(), "grey", 0); // TODO: implement other MessageLevels else while (iter.hasNext()) - writeColor(iter.next()); + writeColor(iter.next(), 0, 0); if(isVisible()) { if (m_scroll_active) diff --git a/gui/ConsoleWindow.h b/gui/ConsoleWindow.h index 9291320e..7fe90c52 100644 --- a/gui/ConsoleWindow.h +++ b/gui/ConsoleWindow.h @@ -47,7 +47,7 @@ private: * this will only insert a single paragraph. * \n are ignored. a real \n is always appended. */ - void writeColor(QString data, const char *color = nullptr); + void writeColor(QString text, const char *color, const char *background); signals: void isClosing(); diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index ee9c3fad..29f7c8e8 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -26,6 +26,7 @@ #include <QInputDialog> #include <QDesktopServices> +#include <QKeyEvent> #include <QUrl> #include <QDir> #include <QFileInfo> @@ -37,12 +38,12 @@ #include "userutils.h" #include "pathutils.h" -#include "categorizedview.h" -#include "categorydrawer.h" +#include "gui/groupview/GroupView.h" +#include "gui/groupview/InstanceDelegate.h" #include "gui/Platform.h" -#include "gui/widgets/InstanceDelegate.h" + #include "gui/widgets/LabeledToolButton.h" #include "gui/dialogs/SettingsDialog.h" @@ -71,7 +72,6 @@ #include "logic/auth/flows/AuthenticateTask.h" #include "logic/auth/flows/RefreshTask.h" -#include "logic/auth/flows/ValidateTask.h" #include "logic/updater/DownloadUpdateTask.h" @@ -132,28 +132,30 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); - QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); - QObject::connect(MMC->newsChecker().get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); + QObject::connect(newsLabel, &QAbstractButton::clicked, this, + &MainWindow::newsButtonClicked); + QObject::connect(MMC->newsChecker().get(), &NewsChecker::newsLoaded, this, + &MainWindow::updateNewsLabel); updateNewsLabel(); } // Create the instance list widget { - view = new KCategorizedView(ui->centralWidget); - drawer = new KCategoryDrawer(view); + view = new GroupView(ui->centralWidget); + view->setSelectionMode(QAbstractItemView::SingleSelection); - 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); + // 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); - view->setUniformItemWidths(true); + // view->setSpacing(10); + // view->setUniformItemWidths(true); // do not show ugly blue border on the mac view->setAttribute(Qt::WA_MacShowFocusRect, false); @@ -173,8 +175,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi view->setModel(proxymodel); view->setContextMenuPolicy(Qt::CustomContextMenu); - connect(view, SIGNAL(customContextMenuRequested(const QPoint&)), - this, SLOT(showInstanceContextMenu(const QPoint&))); + connect(view, SIGNAL(customContextMenuRequested(const QPoint &)), this, + SLOT(showInstanceContextMenu(const QPoint &))); ui->horizontalLayout->addWidget(view); } @@ -213,8 +215,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // Start status checker { - connect(MMC->statusChecker().get(), &StatusChecker::statusLoaded, this, &MainWindow::updateStatusUI); - connect(MMC->statusChecker().get(), &StatusChecker::statusLoadingFailed, this, &MainWindow::updateStatusFailedUI); + connect(MMC->statusChecker().get(), &StatusChecker::statusLoaded, this, + &MainWindow::updateStatusUI); + connect(MMC->statusChecker().get(), &StatusChecker::statusLoadingFailed, this, + &MainWindow::updateStatusFailedUI); connect(m_statusRefresh, &QAbstractButton::clicked, this, &MainWindow::reloadStatus); connect(&statusTimer, &QTimer::timeout, this, &MainWindow::reloadStatus); @@ -313,8 +317,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi if (MMC->settings()->get("AutoUpdate").toBool()) on_actionCheckUpdate_triggered(); - connect(MMC->notificationChecker().get(), &NotificationChecker::notificationCheckFinished, - this, &MainWindow::notificationsChanged); + connect(MMC->notificationChecker().get(), + &NotificationChecker::notificationCheckFinished, this, + &MainWindow::notificationsChanged); } setSelectedInstanceById(MMC->settings()->get("SelectedInstance").toString()); @@ -327,12 +332,11 @@ MainWindow::~MainWindow() { delete ui; delete proxymodel; - delete drawer; } -void MainWindow::showInstanceContextMenu(const QPoint& pos) +void MainWindow::showInstanceContextMenu(const QPoint &pos) { - if(!view->indexAt(pos).isValid()) + if (!view->indexAt(pos).isValid()) { return; } @@ -522,9 +526,12 @@ static QString convertStatus(const QString &status) { QString ret = "?"; - if(status == "green") ret = "↑"; - else if(status == "yellow") ret = "-"; - else if(status == "red") ret="↓"; + if (status == "green") + ret = "↑"; + else if (status == "yellow") + ret = "-"; + else if (status == "red") + ret = "↓"; return "<span style=\"font-size:11pt; font-weight:600;\">" + ret + "</span>"; } @@ -533,7 +540,7 @@ void MainWindow::reloadStatus() { m_statusRefresh->setChecked(true); MMC->statusChecker()->reloadStatus(); - //updateStatusUI(); + // updateStatusUI(); } static QString makeStatusString(const QMap<QString, QString> statuses) @@ -632,7 +639,8 @@ void MainWindow::notificationsChanged() } QMessageBox box(icon, tr("Notification"), entry.message, QMessageBox::Close, this); - QPushButton *dontShowAgainButton = box.addButton(tr("Don't show again"), QMessageBox::AcceptRole); + QPushButton *dontShowAgainButton = + box.addButton(tr("Don't show again"), QMessageBox::AcceptRole); box.setDefaultButton(QMessageBox::Close); box.exec(); if (box.clickedButton() == dontShowAgainButton) @@ -657,9 +665,9 @@ void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit if (updateDlg.exec(&updateTask)) { UpdateFlags baseFlags = None; - #ifdef MultiMC_UPDATER_DRY_RUN - baseFlags |= DryRun; - #endif +#ifdef MultiMC_UPDATER_DRY_RUN + baseFlags |= DryRun; +#endif if (installOnExit) MMC->installUpdates(updateTask.updateFilesDir(), baseFlags | OnExit); else @@ -677,7 +685,7 @@ void MainWindow::setCatBackground(bool enabled) { if (enabled) { - view->setStyleSheet("QListView" + view->setStyleSheet("GroupView" "{" "background-image: url(:/backgrounds/kitteh);" "background-attachment: fixed;" @@ -751,7 +759,7 @@ void MainWindow::on_actionAddInstance_triggered() if (MMC->accounts()->anyAccountIsValid()) { ProgressDialog loadDialog(this); - auto update = newInstance->doUpdate(false); + auto update = newInstance->doUpdate(); connect(update.get(), &Task::failed, [this](QString reason) { QString error = QString("Instance load failed: %1").arg(reason); @@ -837,7 +845,7 @@ void MainWindow::on_actionChangeInstIcon_triggered() void MainWindow::iconUpdated(QString icon) { - if(icon == m_currentInstIcon) + if (icon == m_currentInstIcon) { ui->actionChangeInstIcon->setIcon(MMC->icons()->getBigIcon(m_currentInstIcon)); } @@ -860,7 +868,8 @@ void MainWindow::setSelectedInstanceById(const QString &id) selectionIndex = proxymodel->mapFromSource(index); } } - view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); + view->selectionModel()->setCurrentIndex(selectionIndex, + QItemSelectionModel::ClearAndSelect); } void MainWindow::on_actionChangeInstGroup_triggered() @@ -1060,7 +1069,16 @@ void MainWindow::on_actionLaunchInstance_triggered() } } -void MainWindow::doLaunch() +void MainWindow::on_actionLaunchInstanceOffline_triggered() +{ + if (m_selectedInstance) + { + NagUtils::checkJVMArgs(m_selectedInstance->settings().get("JvmArgs").toString(), this); + doLaunch(false); + } +} + +void MainWindow::doLaunch(bool online) { if (!m_selectedInstance) return; @@ -1104,89 +1122,111 @@ void MainWindow::doLaunch() if (!account.get()) return; + // we try empty password first :) + QString password; + // we loop until the user succeeds in logging in or gives up + bool tryagain = true; + // the failure. the default failure. QString failReason = tr("Your account is currently not logged in. Please enter " "your password to log in again."); - // do the login. if the account has an access token, try to refresh it first. - if (account->accountStatus() != NotVerified) + + while (tryagain) { - // We'll need to validate the access token to make sure the account is still logged in. - ProgressDialog progDialog(this); - progDialog.setSkipButton(true, tr("Play Offline")); - auto task = account->login(); - progDialog.exec(task.get()); - - auto status = account->accountStatus(); - if (status != NotVerified) - { - updateInstance(m_selectedInstance, account); - } - else + AuthSessionPtr session(new AuthSession()); + session->wants_online = online; + auto task = account->login(session, password); + if (task) { + // We'll need to validate the access token to make sure the account + // is still logged in. + ProgressDialog progDialog(this); + if (online) + progDialog.setSkipButton(true, tr("Play Offline")); + progDialog.exec(task.get()); if (!task->successful()) { failReason = task->failReason(); } - if (loginWithPassword(account, failReason)) - updateInstance(m_selectedInstance, account); } - // in any case, revert from online to verified. - account->downgrade(); - } - else - { - if (loginWithPassword(account, failReason)) + switch (session->status) + { + case AuthSession::Undetermined: { - updateInstance(m_selectedInstance, account); - account->downgrade(); + QLOG_ERROR() << "Received undetermined session status during login. Bye."; + tryagain = false; + break; } - // in any case, revert from online to verified. - account->downgrade(); - } -} - -bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString &errorMsg) -{ - EditAccountDialog passDialog(errorMsg, this, EditAccountDialog::PasswordField); - if (passDialog.exec() == QDialog::Accepted) - { - // To refresh the token, we just create an authenticate task with the given account and - // the user's password. - ProgressDialog progDialog(this); - auto task = account->login(passDialog.password()); - progDialog.exec(task.get()); - if (task->successful()) - return true; - else + case AuthSession::RequiresPassword: { - // If the authentication task failed, recurse with the task's error message. - return loginWithPassword(account, task->failReason()); + EditAccountDialog passDialog(failReason, this, EditAccountDialog::PasswordField); + if (passDialog.exec() == QDialog::Accepted) + { + password = passDialog.password(); + } + else + { + tryagain = false; + } + break; + } + case AuthSession::PlayableOffline: + { + // we ask the user for a player name + bool ok = false; + QString usedname = session->player_name; + QString name = QInputDialog::getText(this, tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, session->player_name, &ok); + if (!ok) + { + tryagain = false; + break; + } + if (name.length()) + { + usedname = name; + } + session->MakeOffline(usedname); + // offline flavored game from here :3 + } + case AuthSession::PlayableOnline: + { + // update first if the server actually responded + if (session->auth_server_online) + { + updateInstance(m_selectedInstance, session); + } + else + { + launchInstance(m_selectedInstance, session); + } + tryagain = false; + } } } - return false; } -void MainWindow::updateInstance(BaseInstance *instance, MojangAccountPtr account) +void MainWindow::updateInstance(BaseInstance *instance, AuthSessionPtr session) { - bool only_prepare = account->accountStatus() != Online; - auto updateTask = instance->doUpdate(only_prepare); + auto updateTask = instance->doUpdate(); if (!updateTask) { - launchInstance(instance, account); + launchInstance(instance, session); return; } ProgressDialog tDialog(this); - connect(updateTask.get(), &Task::succeeded, [this, instance, account] - { launchInstance(instance, account); }); + connect(updateTask.get(), &Task::succeeded, [this, instance, session] + { launchInstance(instance, session); }); connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); tDialog.exec(updateTask.get()); } -void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account) +void MainWindow::launchInstance(BaseInstance *instance, AuthSessionPtr session) { Q_ASSERT_X(instance != NULL, "launchInstance", "instance is NULL"); - Q_ASSERT_X(account.get() != nullptr, "launchInstance", "account is NULL"); + Q_ASSERT_X(session.get() != nullptr, "launchInstance", "session is NULL"); - proc = instance->prepareForLaunch(account); + proc = instance->prepareForLaunch(session); if (!proc) return; @@ -1195,7 +1235,7 @@ void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account console = new ConsoleWindow(proc); connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded())); - proc->setLogin(account); + proc->setLogin(session); proc->launch(); } @@ -1258,7 +1298,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() VersionSelectDialog vselect(m_selectedInstance->versionList().get(), tr("Change Minecraft version"), this); vselect.setFilter(1, "OneSix"); - if(!vselect.exec() || !vselect.selectedVersion()) + if (!vselect.exec() || !vselect.selectedVersion()) return; if (!MMC->accounts()->anyAccountIsValid()) @@ -1276,7 +1316,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() auto result = CustomMessageBox::selectable( this, tr("Are you sure?"), tr("This will remove any library/version customization you did previously. " - "This includes things like Forge install and similar."), + "This includes things like Forge install and similar."), QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort, QMessageBox::Abort)->exec(); @@ -1285,7 +1325,7 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() } m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor()); - auto updateTask = m_selectedInstance->doUpdate(false); + auto updateTask = m_selectedInstance->doUpdate(); if (!updateTask) { return; @@ -1384,7 +1424,7 @@ void MainWindow::instanceEnded() void MainWindow::checkMigrateLegacyAssets() { int legacyAssets = AssetsUtils::findLegacyAssets(); - if(legacyAssets > 0) + if (legacyAssets > 0) { ProgressDialog migrateDlg(this); AssetsMigrateTask migrateTask(legacyAssets, &migrateDlg); diff --git a/gui/MainWindow.h b/gui/MainWindow.h index eb478776..4d9e165d 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -27,9 +27,6 @@ class QToolButton; class LabeledToolButton; class QLabel; -class InstanceProxyModel; -class KCategorizedView; -class KCategoryDrawer; class MinecraftProcess; class ConsoleWindow; @@ -96,6 +93,8 @@ slots: void on_actionLaunchInstance_triggered(); + void on_actionLaunchInstanceOffline_triggered(); + void on_actionDeleteInstance_triggered(); void on_actionRenameInstance_triggered(); @@ -112,25 +111,18 @@ slots: * Launches the currently selected instance with the default account. * If no default account is selected, prompts the user to pick an account. */ - void doLaunch(); - - /*! - * Opens an input dialog, allowing the user to input their password and refresh its access token. - * This function will execute the proper Yggdrasil task to refresh the access token. - * Returns true if successful. False if the user cancelled. - */ - bool loginWithPassword(MojangAccountPtr account, const QString& errorMsg=""); + void doLaunch(bool online = true); /*! * Launches the given instance with the given account. * This function assumes that the given account has a valid, usable access token. */ - void launchInstance(BaseInstance* instance, MojangAccountPtr account); + void launchInstance(BaseInstance *instance, AuthSessionPtr session); /*! * Prepares the given instance for launch with the given account. */ - void updateInstance(BaseInstance* instance, MojangAccountPtr account); + void updateInstance(BaseInstance *instance, AuthSessionPtr account); void onGameUpdateError(QString error); @@ -190,8 +182,7 @@ protected: private: Ui::MainWindow *ui; - KCategoryDrawer *drawer; - KCategorizedView *view; + class GroupView *view; InstanceProxyModel *proxymodel; MinecraftProcess *proc; ConsoleWindow *console; diff --git a/gui/MainWindow.ui b/gui/MainWindow.ui index 25af6f60..8cf26d18 100644 --- a/gui/MainWindow.ui +++ b/gui/MainWindow.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>688</width> - <height>460</height> + <width>694</width> + <height>563</height> </rect> </property> <property name="windowTitle"> @@ -107,6 +107,7 @@ </attribute> <addaction name="actionChangeInstIcon"/> <addaction name="actionLaunchInstance"/> + <addaction name="actionLaunchInstanceOffline"/> <addaction name="separator"/> <addaction name="actionEditInstNotes"/> <addaction name="actionChangeInstGroup"/> @@ -152,7 +153,9 @@ </widget> <action name="actionAddInstance"> <property name="icon"> - <iconset theme="new"/> + <iconset theme="new"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Add Instance</string> @@ -166,7 +169,9 @@ </action> <action name="actionViewInstanceFolder"> <property name="icon"> - <iconset theme="viewfolder"/> + <iconset theme="viewfolder"> + <normaloff/> + </iconset> </property> <property name="text"> <string>View Instance Folder</string> @@ -180,7 +185,9 @@ </action> <action name="actionRefresh"> <property name="icon"> - <iconset theme="refresh"/> + <iconset theme="refresh"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Refresh</string> @@ -194,7 +201,9 @@ </action> <action name="actionViewCentralModsFolder"> <property name="icon"> - <iconset theme="centralmods"/> + <iconset theme="centralmods"> + <normaloff/> + </iconset> </property> <property name="text"> <string>View Central Mods Folder</string> @@ -208,7 +217,9 @@ </action> <action name="actionCheckUpdate"> <property name="icon"> - <iconset theme="checkupdate"/> + <iconset theme="checkupdate"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Check for Updates</string> @@ -222,7 +233,9 @@ </action> <action name="actionSettings"> <property name="icon"> - <iconset theme="settings"/> + <iconset theme="settings"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Settings</string> @@ -239,7 +252,9 @@ </action> <action name="actionReportBug"> <property name="icon"> - <iconset theme="bug"/> + <iconset theme="bug"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Report a Bug</string> @@ -253,7 +268,9 @@ </action> <action name="actionMoreNews"> <property name="icon"> - <iconset theme="news"/> + <iconset theme="news"> + <normaloff/> + </iconset> </property> <property name="text"> <string>More News</string> @@ -270,7 +287,9 @@ </action> <action name="actionAbout"> <property name="icon"> - <iconset theme="about"/> + <iconset theme="about"> + <normaloff/> + </iconset> </property> <property name="text"> <string>About MultiMC</string> @@ -463,7 +482,9 @@ <bool>true</bool> </property> <property name="icon"> - <iconset theme="cat"/> + <iconset theme="cat"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Meow</string> @@ -474,7 +495,9 @@ </action> <action name="actionCopyInstance"> <property name="icon"> - <iconset theme="copy"/> + <iconset theme="copy"> + <normaloff/> + </iconset> </property> <property name="text"> <string>Copy Instance</string> @@ -494,6 +517,17 @@ <string>Manage your Mojang or Minecraft accounts.</string> </property> </action> + <action name="actionLaunchInstanceOffline"> + <property name="text"> + <string>Play Offline</string> + </property> + <property name="toolTip"> + <string>Launch the selected instance in offline mode.</string> + </property> + <property name="statusTip"> + <string>Launch the selected instance.</string> + </property> + </action> </widget> <layoutdefault spacing="6" margin="11"/> <resources> diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp index 1712e352..a38035a6 100644 --- a/gui/dialogs/AccountListDialog.cpp +++ b/gui/dialogs/AccountListDialog.cpp @@ -126,7 +126,7 @@ void AccountListDialog::addAccount(const QString& errMsg) MojangAccountPtr account = MojangAccount::createFromUsername(username); ProgressDialog progDialog(this); - auto task = account->login(password); + auto task = account->login(nullptr, password); progDialog.exec(task.get()); if(task->successful()) { diff --git a/gui/dialogs/IconPickerDialog.cpp b/gui/dialogs/IconPickerDialog.cpp index f7970b37..9b1c26ff 100644 --- a/gui/dialogs/IconPickerDialog.cpp +++ b/gui/dialogs/IconPickerDialog.cpp @@ -23,7 +23,7 @@ #include "ui_IconPickerDialog.h" #include "gui/Platform.h" -#include "gui/widgets/InstanceDelegate.h" +#include "gui/groupview/InstanceDelegate.h" #include "logic/icons/IconList.h" diff --git a/gui/dialogs/OneSixModEditDialog.cpp b/gui/dialogs/OneSixModEditDialog.cpp index 3982f17d..9e585de5 100644 --- a/gui/dialogs/OneSixModEditDialog.cpp +++ b/gui/dialogs/OneSixModEditDialog.cpp @@ -39,6 +39,18 @@ #include "logic/lists/ForgeVersionList.h" #include "logic/ForgeInstaller.h" #include "logic/LiteLoaderInstaller.h" +#include "logic/OneSixVersionBuilder.h" + +template<typename A, typename B> +QMap<A, B> invert(const QMap<B, A> &in) +{ + QMap<A, B> out; + for (auto it = in.begin(); it != in.end(); ++it) + { + out.insert(it.value(), it.key()); + } + return out; +} OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent) : QDialog(parent), ui(new Ui::OneSixModEditDialog), m_inst(inst) @@ -55,7 +67,8 @@ OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent) main_model->setSourceModel(m_version.get()); ui->libraryTreeView->setModel(main_model); ui->libraryTreeView->installEventFilter(this); - ui->mainClassEdit->setText(m_version->mainClass); + connect(ui->libraryTreeView->selectionModel(), &QItemSelectionModel::currentChanged, + this, &OneSixModEditDialog::versionCurrent); updateVersionControls(); } else @@ -81,6 +94,8 @@ OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent) ui->resPackTreeView->installEventFilter(this); m_resourcepacks->startWatching(); } + + connect(m_inst, &OneSixInstance::versionReloaded, this, &OneSixModEditDialog::updateVersionControls); } OneSixModEditDialog::~OneSixModEditDialog() @@ -92,95 +107,138 @@ OneSixModEditDialog::~OneSixModEditDialog() void OneSixModEditDialog::updateVersionControls() { - bool customVersion = m_inst->versionIsCustom(); - ui->customizeBtn->setEnabled(!customVersion); - ui->revertBtn->setEnabled(customVersion); ui->forgeBtn->setEnabled(true); - ui->liteloaderBtn->setEnabled(LiteLoaderInstaller(m_inst->intendedVersionId()).canApply()); - ui->customEditorBtn->setEnabled(customVersion); + ui->liteloaderBtn->setEnabled(LiteLoaderInstaller().canApply(m_inst)); + ui->mainClassEdit->setText(m_version->mainClass); } void OneSixModEditDialog::disableVersionControls() { - ui->customizeBtn->setEnabled(false); - ui->revertBtn->setEnabled(false); ui->forgeBtn->setEnabled(false); ui->liteloaderBtn->setEnabled(false); - ui->customEditorBtn->setEnabled(false); + ui->reloadLibrariesBtn->setEnabled(false); + ui->removeLibraryBtn->setEnabled(false); + ui->mainClassEdit->setText(""); } -void OneSixModEditDialog::on_customizeBtn_clicked() +void OneSixModEditDialog::on_reloadLibrariesBtn_clicked() { - if (m_inst->customizeVersion()) - { - m_version = m_inst->getFullVersion(); - main_model->setSourceModel(m_version.get()); - updateVersionControls(); - } + m_inst->reloadVersion(this); } -void OneSixModEditDialog::on_revertBtn_clicked() +void OneSixModEditDialog::on_removeLibraryBtn_clicked() { - auto response = CustomMessageBox::selectable( - this, tr("Revert?"), tr("Do you want to revert the " - "version of this instance to its original configuration?"), - QMessageBox::Question, QMessageBox::Yes | QMessageBox::No)->exec(); - if (response == QMessageBox::Yes) + if (ui->libraryTreeView->currentIndex().isValid()) { - if (m_inst->revertCustomVersion()) + if (!m_version->remove(ui->libraryTreeView->currentIndex().row())) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); + } + else { - m_version = m_inst->getFullVersion(); - main_model->setSourceModel(m_version.get()); - updateVersionControls(); + m_inst->reloadVersion(this); } } } -void OneSixModEditDialog::on_customEditorBtn_clicked() +void OneSixModEditDialog::on_resetLibraryOrderBtn_clicked() { - if (m_inst->versionIsCustom()) + QDir(m_inst->instanceRoot()).remove("order.json"); + m_inst->reloadVersion(this); +} +void OneSixModEditDialog::on_moveLibraryUpBtn_clicked() +{ + + QMap<QString, int> order = getExistingOrder(); + if (order.size() < 2 || ui->libraryTreeView->selectionModel()->selectedIndexes().isEmpty()) { - if (!MMC->openJsonEditor(m_inst->instanceRoot() + "/custom.json")) - { - QMessageBox::warning(this, tr("Error"), tr("Unable to open custom.json, check the settings")); - } + return; + } + const int ourRow = ui->libraryTreeView->selectionModel()->selectedIndexes().first().row(); + const QString ourId = m_version->versionFileId(ourRow); + const int ourOrder = order[ourId]; + if (ourId.isNull() || ourId.startsWith("org.multimc.")) + { + return; + } + + QMap<int, QString> sortedOrder = invert(order); + + QList<int> sortedOrders = sortedOrder.keys(); + const int ourIndex = sortedOrders.indexOf(ourOrder); + if (ourIndex <= 0) + { + return; + } + const int ourNewOrder = sortedOrders.at(ourIndex - 1); + order[ourId] = ourNewOrder; + order[sortedOrder[sortedOrders[ourIndex - 1]]] = ourOrder; + + if (!OneSixVersionBuilder::writeOverrideOrders(order, m_inst)) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't save the new order")); + } + else + { + m_inst->reloadVersion(this); + ui->libraryTreeView->selectionModel()->select(m_version->index(ourRow - 1), QItemSelectionModel::SelectCurrent); + } +} +void OneSixModEditDialog::on_moveLibraryDownBtn_clicked() +{ + QMap<QString, int> order = getExistingOrder(); + if (order.size() < 2 || ui->libraryTreeView->selectionModel()->selectedIndexes().isEmpty()) + { + return; + } + const int ourRow = ui->libraryTreeView->selectionModel()->selectedIndexes().first().row(); + const QString ourId = m_version->versionFileId(ourRow); + const int ourOrder = order[ourId]; + if (ourId.isNull() || ourId.startsWith("org.multimc.")) + { + return; + } + + QMap<int, QString> sortedOrder = invert(order); + + QList<int> sortedOrders = sortedOrder.keys(); + const int ourIndex = sortedOrders.indexOf(ourOrder); + if ((ourIndex + 1) >= sortedOrders.size()) + { + return; + } + const int ourNewOrder = sortedOrders.at(ourIndex + 1); + order[ourId] = ourNewOrder; + order[sortedOrder[sortedOrders[ourIndex + 1]]] = ourOrder; + + if (!OneSixVersionBuilder::writeOverrideOrders(order, m_inst)) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't save the new order")); + } + else + { + m_inst->reloadVersion(this); + ui->libraryTreeView->selectionModel()->select(m_version->index(ourRow + 1), QItemSelectionModel::SelectCurrent); } } void OneSixModEditDialog::on_forgeBtn_clicked() { + if (QDir(m_inst->instanceRoot()).exists("custom.json")) + { + if (QMessageBox::question(this, tr("Revert?"), tr("This action will remove your custom.json. Continue?")) != QMessageBox::Yes) + { + return; + } + QDir(m_inst->instanceRoot()).remove("custom.json"); + m_inst->reloadVersion(this); + } VersionSelectDialog vselect(MMC->forgelist().get(), tr("Select Forge version"), this); vselect.setFilter(1, m_inst->currentVersionId()); + vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + + m_inst->currentVersionId()); if (vselect.exec() && vselect.selectedVersion()) { - if (m_inst->versionIsCustom()) - { - auto reply = QMessageBox::question( - this, tr("Revert?"), - tr("This will revert any " - "changes you did to the version up to this point. Is that " - "OK?"), - QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::Yes) - { - m_inst->revertCustomVersion(); - m_inst->customizeVersion(); - { - m_version = m_inst->getFullVersion(); - main_model->setSourceModel(m_version.get()); - updateVersionControls(); - } - } - else - return; - } - else - { - m_inst->customizeVersion(); - m_version = m_inst->getFullVersion(); - main_model->setSourceModel(m_version.get()); - updateVersionControls(); - } ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(vselect.selectedVersion()); if (!forgeVersion) @@ -197,9 +255,9 @@ void OneSixModEditDialog::on_forgeBtn_clicked() // install QString forgePath = entry->getFullPath(); ForgeInstaller forge(forgePath, forgeVersion->universal_url); - if (!forge.apply(m_version)) + if (!forge.add(m_inst)) { - // failure notice + QLOG_ERROR() << "Failure installing forge"; } } else @@ -212,18 +270,28 @@ void OneSixModEditDialog::on_forgeBtn_clicked() // install QString forgePath = entry->getFullPath(); ForgeInstaller forge(forgePath, forgeVersion->universal_url); - if (!forge.apply(m_version)) + if (!forge.add(m_inst)) { - // failure notice + QLOG_ERROR() << "Failure installing forge"; } } } + m_inst->reloadVersion(this); } void OneSixModEditDialog::on_liteloaderBtn_clicked() { - LiteLoaderInstaller liteloader(m_inst->intendedVersionId()); - if (!liteloader.canApply()) + if (QDir(m_inst->instanceRoot()).exists("custom.json")) + { + if (QMessageBox::question(this, tr("Revert?"), tr("This action will remove your custom.json. Continue?")) != QMessageBox::Yes) + { + return; + } + QDir(m_inst->instanceRoot()).remove("custom.json"); + m_inst->reloadVersion(this); + } + LiteLoaderInstaller liteloader; + if (!liteloader.canApply(m_inst)) { QMessageBox::critical( this, tr("LiteLoader"), @@ -231,18 +299,15 @@ void OneSixModEditDialog::on_liteloaderBtn_clicked() "into this version of Minecraft")); return; } - if (!m_inst->versionIsCustom()) + if (!liteloader.add(m_inst)) { - m_inst->customizeVersion(); - m_version = m_inst->getFullVersion(); - main_model->setSourceModel(m_version.get()); - updateVersionControls(); + QMessageBox::critical(this, tr("LiteLoader"), + tr("For reasons unknown, the LiteLoader installation failed. " + "Check your MultiMC log files for details.")); } - if (!liteloader.apply(m_version)) + else { - QMessageBox::critical( - this, tr("LiteLoader"), - tr("For reasons unknown, the LiteLoader installation failed. Check your MultiMC log files for details.")); + m_inst->reloadVersion(this); } } @@ -278,6 +343,35 @@ bool OneSixModEditDialog::resourcePackListFilter(QKeyEvent *keyEvent) return QDialog::eventFilter(ui->resPackTreeView, keyEvent); } +QMap<QString, int> OneSixModEditDialog::getExistingOrder() const +{ + + QMap<QString, int> order; + // default + { + for (OneSixVersion::VersionFile file : m_version->versionFiles) + { + if (file.id.startsWith("org.multimc.")) + { + continue; + } + order.insert(file.id, file.order); + } + } + // overriden + { + QMap<QString, int> overridenOrder = OneSixVersionBuilder::readOverrideOrders(m_inst); + for (auto id : order.keys()) + { + if (overridenOrder.contains(id)) + { + order[id] = overridenOrder[id]; + } + } + } + return order; +} + bool OneSixModEditDialog::eventFilter(QObject *obj, QEvent *ev) { if (ev->type() != QEvent::KeyPress) @@ -362,3 +456,15 @@ void OneSixModEditDialog::loaderCurrent(QModelIndex current, QModelIndex previou Mod &m = m_mods->operator[](row); ui->frame->updateWithMod(m); } + +void OneSixModEditDialog::versionCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + ui->removeLibraryBtn->setDisabled(true); + } + else + { + ui->removeLibraryBtn->setEnabled(m_version->canRemove(current.row())); + } +} diff --git a/gui/dialogs/OneSixModEditDialog.h b/gui/dialogs/OneSixModEditDialog.h index 2510c59c..f44b336b 100644 --- a/gui/dialogs/OneSixModEditDialog.h +++ b/gui/dialogs/OneSixModEditDialog.h @@ -45,9 +45,11 @@ slots: void on_buttonBox_rejected(); void on_forgeBtn_clicked(); void on_liteloaderBtn_clicked(); - void on_customizeBtn_clicked(); - void on_revertBtn_clicked(); - void on_customEditorBtn_clicked(); + void on_reloadLibrariesBtn_clicked(); + void on_removeLibraryBtn_clicked(); + void on_resetLibraryOrderBtn_clicked(); + void on_moveLibraryUpBtn_clicked(); + void on_moveLibraryDownBtn_clicked(); void updateVersionControls(); void disableVersionControls(); @@ -63,7 +65,11 @@ private: std::shared_ptr<ModList> m_resourcepacks; EnabledItemFilter *main_model; OneSixInstance *m_inst; + + QMap<QString, int> getExistingOrder() const; + public slots: void loaderCurrent(QModelIndex current, QModelIndex previous); + void versionCurrent(const QModelIndex ¤t, const QModelIndex &previous); }; diff --git a/gui/dialogs/OneSixModEditDialog.ui b/gui/dialogs/OneSixModEditDialog.ui index 899e0cbf..eaf8f7fd 100644 --- a/gui/dialogs/OneSixModEditDialog.ui +++ b/gui/dialogs/OneSixModEditDialog.ui @@ -26,7 +26,7 @@ </size> </property> <property name="currentIndex"> - <number>1</number> + <number>0</number> </property> <widget class="QWidget" name="libTab"> <attribute name="title"> @@ -43,6 +43,9 @@ <property name="horizontalScrollBarPolicy"> <enum>Qt::ScrollBarAlwaysOff</enum> </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> </widget> </item> <item> @@ -85,61 +88,30 @@ </widget> </item> <item> - <widget class="QPushButton" name="customizeBtn"> - <property name="toolTip"> - <string>Create an customized copy of the base version</string> - </property> - <property name="text"> - <string>Customize</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="revertBtn"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="toolTip"> - <string>Revert to original base version</string> - </property> - <property name="text"> - <string>Revert</string> - </property> - </widget> - </item> - <item> <widget class="Line" name="line"> - <property name="frameShadow"> - <enum>QFrame::Sunken</enum> - </property> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> </widget> </item> <item> - <widget class="QPushButton" name="addLibraryBtn"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="toolTip"> - <string>Add new libraries</string> - </property> + <widget class="QPushButton" name="reloadLibrariesBtn"> <property name="text"> - <string>&Add</string> + <string>Reload</string> </property> </widget> </item> <item> <widget class="QPushButton" name="removeLibraryBtn"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="toolTip"> - <string>Remove selected libraries</string> + <property name="text"> + <string>Remove</string> </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="resetLibraryOrderBtn"> <property name="text"> - <string>&Remove</string> + <string>Reset order</string> </property> </widget> </item> @@ -151,9 +123,16 @@ </widget> </item> <item> - <widget class="QPushButton" name="customEditorBtn"> + <widget class="QPushButton" name="moveLibraryUpBtn"> + <property name="text"> + <string>Move up</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="moveLibraryDownBtn"> <property name="text"> - <string>Open custom.json</string> + <string>Move down</string> </property> </widget> </item> diff --git a/gui/dialogs/VersionSelectDialog.cpp b/gui/dialogs/VersionSelectDialog.cpp index d6efe3c0..0f379f56 100644 --- a/gui/dialogs/VersionSelectDialog.cpp +++ b/gui/dialogs/VersionSelectDialog.cpp @@ -51,6 +51,11 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, } } +void VersionSelectDialog::setEmptyString(QString emptyString) +{ + ui->listView->setEmptyString(emptyString); +} + VersionSelectDialog::~VersionSelectDialog() { delete ui; diff --git a/gui/dialogs/VersionSelectDialog.h b/gui/dialogs/VersionSelectDialog.h index e36341db..61fa8ab6 100644 --- a/gui/dialogs/VersionSelectDialog.h +++ b/gui/dialogs/VersionSelectDialog.h @@ -44,6 +44,7 @@ public: BaseVersionPtr selectedVersion() const; void setFilter(int column, QString filter); + void setEmptyString(QString emptyString); void setResizeOn(int column); private diff --git a/gui/dialogs/VersionSelectDialog.ui b/gui/dialogs/VersionSelectDialog.ui index 58264f24..07e9e73e 100644 --- a/gui/dialogs/VersionSelectDialog.ui +++ b/gui/dialogs/VersionSelectDialog.ui @@ -15,7 +15,7 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QTreeView" name="listView"> + <widget class="VersionListView" name="listView"> <property name="horizontalScrollBarPolicy"> <enum>Qt::ScrollBarAlwaysOff</enum> </property> @@ -65,6 +65,13 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>VersionListView</class> + <extends>QTreeView</extends> + <header>gui/widgets/VersionListView.h</header> + </customwidget> + </customwidgets> <resources/> <connections> <connection> diff --git a/gui/groupview/Group.cpp b/gui/groupview/Group.cpp new file mode 100644 index 00000000..51aa6658 --- /dev/null +++ b/gui/groupview/Group.cpp @@ -0,0 +1,269 @@ +#include "Group.h" + +#include <QModelIndex> +#include <QPainter> +#include <QtMath> +#include <QApplication> + +#include "GroupView.h" + +Group::Group(const QString &text, GroupView *view) : view(view), text(text), collapsed(false) +{ +} + +Group::Group(const Group *other) + : view(other->view), text(other->text), collapsed(other->collapsed) +{ +} + +void Group::update() +{ + firstItemIndex = firstItem().row(); + + rowHeights = QVector<int>(numRows()); + for (int i = 0; i < numRows(); ++i) + { + rowHeights[i] = view->categoryRowHeight( + view->model()->index(i * view->itemsPerRow() + firstItemIndex, 0)); + } +} + +Group::HitResults Group::hitScan(const QPoint &pos) const +{ + Group::HitResults results = Group::NoHit; + int y_start = verticalPosition(); + int body_start = y_start + headerHeight(); + int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5? + int y = pos.y(); + // int x = pos.x(); + if (y < y_start) + { + results = Group::NoHit; + } + else if (y < body_start) + { + results = Group::HeaderHit; + int collapseSize = headerHeight() - 4; + + // the icon + QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize); + if (iconRect.contains(pos)) + { + results |= Group::CheckboxHit; + } + } + else if (y < body_end) + { + results |= Group::BodyHit; + } + return results; +} + +void Group::drawHeader(QPainter *painter, const QStyleOptionViewItem &option) +{ + painter->setRenderHint(QPainter::Antialiasing); + + 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: checkboxy thing + { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, false); + painter->setFont(font); + QColor penColor(option.palette.text().color()); + penColor.setAlphaF(0.6); + painter->setPen(penColor); + QRect iconSubRect(option.rect); + iconSubRect.setTop(iconSubRect.top() + 7); + iconSubRect.setLeft(iconSubRect.left() + 7); + + int sizing = fontMetrics.height(); + int even = ( (sizing - 1) % 2 ); + + iconSubRect.setHeight(sizing - even); + iconSubRect.setWidth(sizing - even); + painter->drawRect(iconSubRect); + + + /* + if(collapsed) + painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+"); + else + painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-"); + */ + painter->setBrush(option.palette.text()); + painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2, + iconSubRect.width(), 2, penColor); + if (collapsed) + { + painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2, + iconSubRect.height(), penColor); + } + + painter->restore(); + } + //END: checkboxy thing + + //BEGIN: text + { + QRect textRect(option.rect); + textRect.setTop(textRect.top() + 7); + textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 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, text); + painter->restore(); + } + //END: text +} + +int Group::totalHeight() const +{ + return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'? +} + +int Group::headerHeight() const +{ + 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 raw = view->viewport()->fontMetrics().height() + 4; + // add english. maybe. depends on font height. + if (raw % 2 == 0) + raw++; + return std::min(raw, 25); + */ +} + +int Group::contentHeight() const +{ + if (collapsed) + { + return 0; + } + int result = 0; + for (int i = 0; i < rowHeights.size(); ++i) + { + result += rowHeights[i]; + } + return result; +} + +int Group::numRows() const +{ + return qMax(1, qCeil((qreal)numItems() / (qreal)view->itemsPerRow())); +} + +int Group::verticalPosition() const +{ + return m_verticalPosition; +} + +QList<QModelIndex> Group::items() const +{ + QList<QModelIndex> indices; + for (int i = 0; i < view->model()->rowCount(); ++i) + { + const QModelIndex index = view->model()->index(i, 0); + if (index.data(GroupViewRoles::GroupRole).toString() == text) + { + indices.append(index); + } + } + return indices; +} + +int Group::numItems() const +{ + return items().size(); +} + +QModelIndex Group::firstItem() const +{ + QList<QModelIndex> indices = items(); + return indices.isEmpty() ? QModelIndex() : indices.first(); +} + +QModelIndex Group::lastItem() const +{ + QList<QModelIndex> indices = items(); + return indices.isEmpty() ? QModelIndex() : indices.last(); +} diff --git a/gui/groupview/Group.h b/gui/groupview/Group.h new file mode 100644 index 00000000..3b797f4c --- /dev/null +++ b/gui/groupview/Group.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QString> +#include <QRect> +#include <QVector> +#include <QStyleOption> + +class GroupView; +class QPainter; +class QModelIndex; + +struct Group +{ +/* constructors */ + Group(const QString &text, GroupView *view); + Group(const Group *other); + +/* data */ + GroupView *view = nullptr; + QString text; + bool collapsed = false; + QVector<int> rowHeights; + int firstItemIndex = 0; + int m_verticalPosition = 0; + +/* logic */ + /// do stuff. and things. TODO: redo. + void update(); + + /// draw the header at y-position. + void drawHeader(QPainter *painter, const QStyleOptionViewItem &option); + + /// height of the group, in total. includes a small bit of padding. + int totalHeight() const; + + /// height of the group header, in pixels + int headerHeight() const; + + /// height of the group content, in pixels + int contentHeight() const; + + /// the number of visual rows this group has + int numRows() const; + + /// the height at which this group starts, in pixels + int verticalPosition() const; + + enum HitResult + { + NoHit = 0x0, + TextHit = 0x1, + CheckboxHit = 0x2, + HeaderHit = 0x4, + BodyHit = 0x8 + }; + Q_DECLARE_FLAGS(HitResults, HitResult) + + /// shoot! BANG! what did we hit? + HitResults hitScan (const QPoint &pos) const; + + /// super derpy thing. + QList<QModelIndex> items() const; + /// I don't even + int numItems() const; + QModelIndex firstItem() const; + QModelIndex lastItem() const; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Group::HitResults) diff --git a/gui/groupview/GroupView.cpp b/gui/groupview/GroupView.cpp new file mode 100644 index 00000000..25042d02 --- /dev/null +++ b/gui/groupview/GroupView.cpp @@ -0,0 +1,931 @@ +#include "GroupView.h" + +#include <QPainter> +#include <QApplication> +#include <QtMath> +#include <QDebug> +#include <QMouseEvent> +#include <QListView> +#include <QPersistentModelIndex> +#include <QDrag> +#include <QMimeData> +#include <QScrollBar> + +#include "Group.h" + +template <typename T> bool listsIntersect(const QList<T> &l1, const QList<T> t2) +{ + for (auto &item : l1) + { + if (t2.contains(item)) + { + return true; + } + } + return false; +} + +GroupView::GroupView(QWidget *parent) + : QAbstractItemView(parent), m_leftMargin(5), m_rightMargin(5), m_bottomMargin(5), + m_categoryMargin(5) //, m_updatesDisabled(false), m_categoryEditor(0), m_editedCategory(0) +{ + // setViewMode(IconMode); + // setMovement(Snap); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + // setWordWrap(true); + // setDragDropMode(QListView::InternalMove); + setAcceptDrops(true); + m_spacing = 5; +} + +GroupView::~GroupView() +{ + qDeleteAll(m_groups); + m_groups.clear(); +} + +void GroupView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QVector<int> &roles) +{ + scheduleDelayedItemsLayout(); +} +void GroupView::rowsInserted(const QModelIndex &parent, int start, int end) +{ + scheduleDelayedItemsLayout(); +} + +void GroupView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + scheduleDelayedItemsLayout(); +} + +void GroupView::updateGeometries() +{ + int previousScroll = verticalScrollBar()->value(); + + QMap<QString, Group *> cats; + + for (int i = 0; i < model()->rowCount(); ++i) + { + const QString groupName = + model()->index(i, 0).data(GroupViewRoles::GroupRole).toString(); + if (!cats.contains(groupName)) + { + Group *old = this->category(groupName); + if (old) + { + cats.insert(groupName, new Group(old)); + } + else + { + cats.insert(groupName, new Group(groupName, this)); + } + } + } + + /*if (m_editedCategory) + { + m_editedCategory = cats[m_editedCategory->text]; + }*/ + + qDeleteAll(m_groups); + m_groups = cats.values(); + + for (auto cat : m_groups) + { + cat->update(); + } + + if (m_groups.isEmpty()) + { + verticalScrollBar()->setRange(0, 0); + } + else + { + int totalHeight = 0; + // top margin + totalHeight += m_categoryMargin; + int itemScroll = 0; + for (auto category : m_groups) + { + category->m_verticalPosition = totalHeight; + totalHeight += category->totalHeight() + m_categoryMargin; + if(!itemScroll && category->totalHeight() != 0) + { + itemScroll = category->contentHeight() / category->numRows(); + } + } + // do not divide by zero + if(itemScroll == 0) + itemScroll = 64; + + totalHeight += m_bottomMargin; + verticalScrollBar()->setSingleStep ( itemScroll ); + const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 ); + verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll ); + + verticalScrollBar()->setRange(0, totalHeight - height()); + } + + verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum())); + + viewport()->update(); +} + +bool GroupView::isIndexHidden(const QModelIndex &index) const +{ + Group *cat = category(index); + if (cat) + { + return cat->collapsed; + } + else + { + return false; + } +} + +Group *GroupView::category(const QModelIndex &index) const +{ + return category(index.data(GroupViewRoles::GroupRole).toString()); +} + +Group *GroupView::category(const QString &cat) const +{ + for (auto group : m_groups) + { + if (group->text == cat) + { + return group; + } + } + return nullptr; +} + +Group *GroupView::categoryAt(const QPoint &pos) const +{ + for (auto group : m_groups) + { + if(group->hitScan(pos) & Group::CheckboxHit) + { + return group; + } + } + return nullptr; +} + +int GroupView::itemsPerRow() const +{ + return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing)); +} + +int GroupView::contentWidth() const +{ + return width() - m_leftMargin - m_rightMargin; +} + +int GroupView::itemWidth() const +{ + return itemDelegate() + ->sizeHint(viewOptions(), model()->index(model()->rowCount() - 1, 0)) + .width(); +} + +int GroupView::categoryRowHeight(const QModelIndex &index) const +{ + QModelIndexList indices; + int internalRow = categoryInternalPosition(index).second; + for (auto &i : category(index)->items()) + { + if (categoryInternalPosition(i).second == internalRow) + { + indices.append(i); + } + } + + int largestHeight = 0; + for (auto &i : indices) + { + largestHeight = + qMax(largestHeight, itemDelegate()->sizeHint(viewOptions(), i).height()); + } + return largestHeight + m_spacing; +} + +QPair<int, int> GroupView::categoryInternalPosition(const QModelIndex &index) const +{ + QList<QModelIndex> indices = category(index)->items(); + int x = 0; + int y = 0; + const int perRow = itemsPerRow(); + for (int i = 0; i < indices.size(); ++i) + { + if (indices.at(i) == index) + { + break; + } + ++x; + if (x == perRow) + { + x = 0; + ++y; + } + } + return qMakePair(x, y); +} + +int GroupView::categoryInternalRowTop(const QModelIndex &index) const +{ + Group *cat = category(index); + int categoryInternalRow = categoryInternalPosition(index).second; + int result = 0; + for (int i = 0; i < categoryInternalRow; ++i) + { + result += cat->rowHeights.at(i); + } + return result; +} + +int GroupView::itemHeightForCategoryRow(const Group *category, const int internalRow) const +{ + for (auto &i : category->items()) + { + QPair<int, int> pos = categoryInternalPosition(i); + if (pos.second == internalRow) + { + return categoryRowHeight(i); + } + } + return -1; +} + +void GroupView::mousePressEvent(QMouseEvent *event) +{ + // endCategoryEditor(); + + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + + QPersistentModelIndex index = indexAt(visualPos); + + m_pressedIndex = index; + m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); + QItemSelectionModel::SelectionFlags selection_flags = selectionCommand(index, event); + m_pressedPosition = geometryPos; + + m_pressedCategory = categoryAt(geometryPos); + if (m_pressedCategory) + { + setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); + event->accept(); + return; + } + + if (index.isValid() && (index.flags() & Qt::ItemIsEnabled)) + { + // we disable scrollTo for mouse press so the item doesn't change position + // when the user is interacting with it (ie. clicking on it) + bool autoScroll = hasAutoScroll(); + setAutoScroll(false); + selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + setAutoScroll(autoScroll); + QRect rect(geometryPos, geometryPos); + setSelection(rect, QItemSelectionModel::ClearAndSelect); + + // signal handlers may change the model + emit pressed(index); + } + else + { + // Forces a finalize() even if mouse is pressed, but not on a item + selectionModel()->select(QModelIndex(), QItemSelectionModel::Select); + } +} + +void GroupView::mouseMoveEvent(QMouseEvent *event) +{ + QPoint topLeft; + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + + if (state() == ExpandingState || state() == CollapsingState) + { + return; + } + + if (state() == DraggingState) + { + topLeft = m_pressedPosition - offset(); + if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance()) + { + m_pressedIndex = QModelIndex(); + startDrag(model()->supportedDragActions()); + setState(NoState); + stopAutoScroll(); + } + return; + } + + if (selectionMode() != SingleSelection) + { + topLeft = m_pressedPosition - offset(); + } + else + { + topLeft = geometryPos; + } + + if (m_pressedIndex.isValid() && (state() != DragSelectingState) && + (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty()) + { + setState(DraggingState); + return; + } + + if ((event->buttons() & Qt::LeftButton) && selectionModel()) + { + setState(DragSelectingState); + + setSelection(QRect(geometryPos, geometryPos), QItemSelectionModel::ClearAndSelect); + QModelIndex index = indexAt(visualPos); + + // set at the end because it might scroll the view + if (index.isValid() && (index != selectionModel()->currentIndex()) && + (index.flags() & Qt::ItemIsEnabled)) + { + selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + } +} + +void GroupView::mouseReleaseEvent(QMouseEvent *event) +{ + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + QPersistentModelIndex index = indexAt(visualPos); + + bool click = (index == m_pressedIndex && index.isValid()) || + (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos)); + + if (click && m_pressedCategory) + { + if (state() == ExpandingState) + { + m_pressedCategory->collapsed = false; + updateGeometries(); + viewport()->update(); + event->accept(); + return; + } + else if (state() == CollapsingState) + { + m_pressedCategory->collapsed = true; + updateGeometries(); + viewport()->update(); + event->accept(); + return; + } + } + + m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate; + + setState(NoState); + + if (click) + { + if (event->button() == Qt::LeftButton) + { + emit clicked(index); + } + QStyleOptionViewItem option = viewOptions(); + if (m_pressedAlreadySelected) + { + option.state |= QStyle::State_Selected; + } + if ((model()->flags(index) & Qt::ItemIsEnabled) && + style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) + { + emit activated(index); + } + } +} + +void GroupView::mouseDoubleClickEvent(QMouseEvent *event) +{ + QModelIndex index = indexAt(event->pos()); + if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) + { + QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(), + event->screenPos(), event->button(), event->buttons(), + event->modifiers()); + mousePressEvent(&me); + return; + } + // signal handlers may change the model + QPersistentModelIndex persistent = index; + emit doubleClicked(persistent); +} + +void GroupView::paintEvent(QPaintEvent *event) +{ + QPainter painter(this->viewport()); + + QStyleOptionViewItemV4 option(viewOptions()); + option.widget = this; + + int wpWidth = viewport()->width(); + option.rect.setWidth(wpWidth); + for (int i = 0; i < m_groups.size(); ++i) + { + Group *category = m_groups.at(i); + int y = category->verticalPosition(); + y -= verticalOffset(); + QRect backup = option.rect; + int height = category->totalHeight(); + option.rect.setTop(y); + option.rect.setHeight(height); + option.rect.setLeft(m_leftMargin); + option.rect.setRight(wpWidth - m_rightMargin); + category->drawHeader(&painter, option); + y += category->totalHeight() + m_categoryMargin; + option.rect = backup; + } + + for (int i = 0; i < model()->rowCount(); ++i) + { + const QModelIndex index = model()->index(i, 0); + if (isIndexHidden(index)) + { + continue; + } + Qt::ItemFlags flags = index.flags(); + option.rect = visualRect(index); + option.features |= + QStyleOptionViewItemV2::WrapText; // FIXME: what is the meaning of this anyway? + if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index)) + { + 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; + } + itemDelegate()->paint(&painter, option, index); + } + + /* + * Drop indicators for manual reordering... + */ +#if 0 + if (!m_lastDragPosition.isNull()) + { + QPair<Group *, int> pair = rowDropPos(m_lastDragPosition); + Group *category = pair.first; + int row = pair.second; + if (category) + { + int internalRow = row - category->firstItemIndex; + QLine line; + if (internalRow >= category->numItems()) + { + QRect toTheRightOfRect = visualRect(category->lastItem()); + line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight()); + } + else + { + QRect toTheLeftOfRect = visualRect(model()->index(row, 0)); + line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft()); + } + painter.save(); + painter.setPen(QPen(Qt::black, 3)); + painter.drawLine(line); + painter.restore(); + } + } +#endif +} + +void GroupView::resizeEvent(QResizeEvent *event) +{ + // QListView::resizeEvent(event); + + // if (m_categoryEditor) + // { + // m_categoryEditor->resize(qMax(contentWidth() / 2, + // m_editedCategory->textRect.width()), + // m_categoryEditor->height()); + // } + + updateGeometries(); +} + +void GroupView::dragEnterEvent(QDragEnterEvent *event) +{ + if (!isDragEventAccepted(event)) + { + return; + } + m_lastDragPosition = event->pos() + offset(); + viewport()->update(); + event->accept(); +} + +void GroupView::dragMoveEvent(QDragMoveEvent *event) +{ + if (!isDragEventAccepted(event)) + { + return; + } + m_lastDragPosition = event->pos() + offset(); + viewport()->update(); + event->accept(); +} + +void GroupView::dragLeaveEvent(QDragLeaveEvent *event) +{ + m_lastDragPosition = QPoint(); + viewport()->update(); +} + +void GroupView::dropEvent(QDropEvent *event) +{ + m_lastDragPosition = QPoint(); + + stopAutoScroll(); + setState(NoState); + + if (event->source() != this || !(event->possibleActions() & Qt::MoveAction)) + { + return; + } + + QPair<Group *, int> dropPos = rowDropPos(event->pos() + offset()); + const Group *category = dropPos.first; + const int row = dropPos.second; + + if (row == -1) + { + viewport()->update(); + return; + } + + const QString categoryText = category->text; + if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex())) + { + model()->setData(model()->index(row, 0), categoryText, + GroupViewRoles::GroupRole); + event->setDropAction(Qt::MoveAction); + event->accept(); + } + updateGeometries(); + viewport()->update(); +} + +void GroupView::startDrag(Qt::DropActions supportedActions) +{ + QModelIndexList indexes = selectionModel()->selectedIndexes(); + if (indexes.count() > 0) + { + QMimeData *data = model()->mimeData(indexes); + if (!data) + { + return; + } + QRect rect; + QPixmap pixmap = renderToPixmap(indexes, &rect); + //rect.translate(offset()); + // rect.adjust(horizontalOffset(), verticalOffset(), 0, 0); + QDrag *drag = new QDrag(this); + drag->setPixmap(pixmap); + drag->setMimeData(data); + Qt::DropAction defaultDropAction = Qt::IgnoreAction; + if (this->defaultDropAction() != Qt::IgnoreAction && + (supportedActions & this->defaultDropAction())) + { + defaultDropAction = this->defaultDropAction(); + } + if (drag->exec(supportedActions, defaultDropAction) == Qt::MoveAction) + { + const QItemSelection selection = selectionModel()->selection(); + + for (auto it = selection.constBegin(); it != selection.constEnd(); ++it) + { + QModelIndex parent = (*it).parent(); + if ((*it).left() != 0) + { + continue; + } + if ((*it).right() != (model()->columnCount(parent) - 1)) + { + continue; + } + int count = (*it).bottom() - (*it).top() + 1; + model()->removeRows((*it).top(), count, parent); + } + } + } +} + +QRect GroupView::visualRect(const QModelIndex &index) const +{ + return geometryRect(index).translated(-offset()); +} + +QRect GroupView::geometryRect(const QModelIndex &index) const +{ + if (!index.isValid() || isIndexHidden(index) || index.column() > 0) + { + return QRect(); + } + + const Group *cat = category(index); + QPair<int, int> pos = categoryInternalPosition(index); + int x = pos.first; + // int y = pos.second; + + QRect out; + out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + categoryInternalRowTop(index)); + out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); + out.setSize(itemDelegate()->sizeHint(viewOptions(), index)); + + return out; +} + +/* +void CategorizedView::startCategoryEditor(Category *category) +{ + if (m_categoryEditor != 0) + { + return; + } + m_editedCategory = category; + m_categoryEditor = new QLineEdit(m_editedCategory->text, this); + QRect rect = m_editedCategory->textRect; + rect.setWidth(qMax(contentWidth() / 2, rect.width())); + m_categoryEditor->setGeometry(rect); + m_categoryEditor->show(); + m_categoryEditor->setFocus(); + connect(m_categoryEditor, &QLineEdit::returnPressed, this, +&CategorizedView::endCategoryEditor); +} + +void CategorizedView::endCategoryEditor() +{ + if (m_categoryEditor == 0) + { + return; + } + m_editedCategory->text = m_categoryEditor->text(); + m_updatesDisabled = true; + foreach (const QModelIndex &index, itemsForCategory(m_editedCategory)) + { + const_cast<QAbstractItemModel *>(index.model())->setData(index, +m_categoryEditor->text(), CategoryRole); + } + m_updatesDisabled = false; + delete m_categoryEditor; + m_categoryEditor = 0; + m_editedCategory = 0; + updateGeometries(); +} +*/ + +QModelIndex GroupView::indexAt(const QPoint &point) const +{ + for (int i = 0; i < model()->rowCount(); ++i) + { + QModelIndex index = model()->index(i, 0); + if (visualRect(index).contains(point)) + { + return index; + } + } + return QModelIndex(); +} + +// FIXME: is rect supposed to be geometry or visual coords? +void GroupView::setSelection(const QRect &rect, + const QItemSelectionModel::SelectionFlags commands) +{ + for (int i = 0; i < model()->rowCount(); ++i) + { + QModelIndex index = model()->index(i, 0); + QRect itemRect = geometryRect(index); + if (itemRect.intersects(rect)) + { + selectionModel()->select(index, commands); + update(itemRect.translated(-offset())); + } + } + +} + +QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) const +{ + Q_ASSERT(r); + auto paintPairs = draggablePaintPairs(indices, r); + if (paintPairs.isEmpty()) + { + return QPixmap(); + } + QPixmap pixmap(r->size()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + QStyleOptionViewItem option = viewOptions(); + option.state |= QStyle::State_Selected; + for (int j = 0; j < paintPairs.count(); ++j) + { + option.rect = paintPairs.at(j).first.translated(-r->topLeft()); + const QModelIndex ¤t = paintPairs.at(j).second; + itemDelegate()->paint(&painter, option, current); + } + return pixmap; +} + +QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices, + QRect *r) const +{ + Q_ASSERT(r); + QRect &rect = *r; + QList<QPair<QRect, QModelIndex>> ret; + for (int i = 0; i < indices.count(); ++i) + { + const QModelIndex &index = indices.at(i); + const QRect current = geometryRect(index); + ret += qMakePair(current, index); + rect |= current; + } + return ret; +} + +bool GroupView::isDragEventAccepted(QDropEvent *event) +{ + if (event->source() != this) + { + return false; + } + if (!listsIntersect(event->mimeData()->formats(), model()->mimeTypes())) + { + return false; + } + if (!model()->canDropMimeData(event->mimeData(), event->dropAction(), + rowDropPos(event->pos()).second, 0, QModelIndex())) + { + return false; + } + return true; +} + +QPair<Group *, int> GroupView::rowDropPos(const QPoint &pos) +{ + // check that we aren't on a category header and calculate which category we're in + Group *category = 0; + { + int y = 0; + for (auto cat : m_groups) + { + if (pos.y() > y && pos.y() < (y + cat->headerHeight())) + { + return qMakePair<Group*, int>(nullptr, -1); + } + y += cat->totalHeight() + m_categoryMargin; + if (pos.y() < y) + { + category = cat; + break; + } + } + if (category == 0) + { + return qMakePair<Group*, int>(nullptr, -1); + } + } + + QList<QModelIndex> indices = category->items(); + + // calculate the internal column + int internalColumn = -1; + { + const int itemWidth = this->itemWidth(); + if (pos.x() >= (itemWidth * itemsPerRow())) + { + internalColumn = itemsPerRow(); + } + else + { + for (int i = 0, c = 0; i < contentWidth(); i += itemWidth + 10 /*spacing()*/, ++c) + { + if (pos.x() > (i - itemWidth / 2) && pos.x() <= (i + itemWidth / 2)) + { + internalColumn = c; + break; + } + } + } + if (internalColumn == -1) + { + return qMakePair<Group*, int>(nullptr, -1); + } + } + + // calculate the internal row + int internalRow = -1; + { + // FIXME rework the drag and drop code + const int top = category->verticalPosition(); + for (int r = 0, h = top; r < category->numRows(); + h += itemHeightForCategoryRow(category, r), ++r) + { + if (pos.y() > h && pos.y() < (h + itemHeightForCategoryRow(category, r))) + { + internalRow = r; + break; + } + } + if (internalRow == -1) + { + return qMakePair<Group*, int>(nullptr, -1); + } + // this happens if we're in the margin between a one category and another + // categories header + if (internalRow > (indices.size() / itemsPerRow())) + { + return qMakePair<Group*, int>(nullptr, -1); + } + } + + // flaten the internalColumn/internalRow to one row + int categoryRow = internalRow * itemsPerRow() + internalColumn; + + // this is used if we're past the last item + if (categoryRow >= indices.size()) + { + return qMakePair(category, indices.last().row() + 1); + } + + return qMakePair(category, indices.at(categoryRow).row()); +} + +QPoint GroupView::offset() const +{ + return QPoint(horizontalOffset(), verticalOffset()); +} + +QRegion GroupView::visualRegionForSelection(const QItemSelection &selection) const +{ + QRegion region; + for (auto &range : selection) + { + int start_row = range.top(); + int end_row = range.bottom(); + for (int row = start_row; row <= end_row; ++row) + { + int start_column = range.left(); + int end_column = range.right(); + for (int column = start_column; column <= end_column; ++column) + { + QModelIndex index = model()->index(row, column, rootIndex()); + region += visualRect(index); // OK + } + } + } + return region; +} +QModelIndex GroupView::moveCursor(QAbstractItemView::CursorAction cursorAction, + Qt::KeyboardModifiers modifiers) +{ + auto current = currentIndex(); + if(!current.isValid()) + { + qDebug() << "model row: invalid"; + return current; + } + qDebug() << "model row: " << current.row(); + auto cat = category(current); + int i = m_groups.indexOf(cat); + if(i >= 0) + { + // this is a pile of something foul + auto real_group = m_groups[i]; + int beginning_row = 0; + for(auto group: m_groups) + { + if(group == real_group) + break; + beginning_row += group->numRows(); + } + qDebug() << "category: " << real_group->text; + QPair<int, int> pos = categoryInternalPosition(current); + int row = beginning_row + pos.second; + qDebug() << "row: " << row; + qDebug() << "column: " << pos.first; + } + return current; +} diff --git a/gui/groupview/GroupView.h b/gui/groupview/GroupView.h new file mode 100644 index 00000000..e8f9107c --- /dev/null +++ b/gui/groupview/GroupView.h @@ -0,0 +1,143 @@ +#pragma once + +#include <QListView> +#include <QLineEdit> +#include <QScrollBar> + +struct GroupViewRoles +{ + enum + { + GroupRole = Qt::UserRole, + ProgressValueRole, + ProgressMaximumRole + }; +}; + +struct Group; + +class GroupView : public QAbstractItemView +{ + Q_OBJECT + +public: + GroupView(QWidget *parent = 0); + ~GroupView(); + + /// return geometry rectangle occupied by the specified model item + QRect geometryRect(const QModelIndex &index) const; + /// return visual rectangle occupied by the specified model item + virtual QRect visualRect(const QModelIndex &index) const override; + /// get the model index at the specified visual point + virtual QModelIndex indexAt(const QPoint &point) const override; + void setSelection(const QRect &rect, + const QItemSelectionModel::SelectionFlags commands) override; + + virtual int horizontalOffset() const override + { + return horizontalScrollBar()->value(); + } + + virtual int verticalOffset() const override + { + return verticalScrollBar()->value(); + } + + virtual void scrollContentsBy(int dx, int dy) override + { + scrollDirtyRegion(dx, dy); + viewport()->scroll(dx, dy); + } + + /* + * TODO! + */ + virtual void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override + { + return; + } + + virtual QModelIndex moveCursor(CursorAction cursorAction, + Qt::KeyboardModifiers modifiers) override; + + virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override; + +protected +slots: + virtual void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, + const QVector<int> &roles) override; + virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; + virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override; + virtual void updateGeometries() override; + +protected: + virtual bool isIndexHidden(const QModelIndex &index) const override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + void startDrag(Qt::DropActions supportedActions) override; + +private: + friend struct Group; + + QList<Group *> m_groups; + + int m_leftMargin; + int m_rightMargin; + int m_bottomMargin; + int m_categoryMargin; + + // bool m_updatesDisabled; + + Group *category(const QModelIndex &index) const; + Group *category(const QString &cat) const; + Group *categoryAt(const QPoint &pos) const; + + int itemsPerRow() const; + int contentWidth() const; + +private: + int itemWidth() const; + int categoryRowHeight(const QModelIndex &index) const; + + /*QLineEdit *m_categoryEditor; + Category *m_editedCategory; + void startCategoryEditor(Category *category); + +private slots: + void endCategoryEditor();*/ + +private: /* variables */ + /// point where the currently active mouse action started in geometry coordinates + QPoint m_pressedPosition; + QPersistentModelIndex m_pressedIndex; + bool m_pressedAlreadySelected; + Group *m_pressedCategory; + QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag; + QPoint m_lastDragPosition; + int m_spacing = 5; + +private: /* methods */ + QPair<int, int> categoryInternalPosition(const QModelIndex &index) const; + int categoryInternalRowTop(const QModelIndex &index) const; + int itemHeightForCategoryRow(const Group *category, const int internalRow) const; + + QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const; + QList<QPair<QRect, QModelIndex>> draggablePaintPairs(const QModelIndexList &indices, + QRect *r) const; + + bool isDragEventAccepted(QDropEvent *event); + + QPair<Group *, int> rowDropPos(const QPoint &pos); + + QPoint offset() const; +}; diff --git a/gui/groupview/GroupedProxyModel.cpp b/gui/groupview/GroupedProxyModel.cpp new file mode 100644 index 00000000..d9d6ac78 --- /dev/null +++ b/gui/groupview/GroupedProxyModel.cpp @@ -0,0 +1,26 @@ +#include "GroupedProxyModel.h" + +#include "GroupView.h" + +GroupedProxyModel::GroupedProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ +} + +bool GroupedProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + const QString leftCategory = left.data(GroupViewRoles::GroupRole).toString(); + const QString rightCategory = right.data(GroupViewRoles::GroupRole).toString(); + if (leftCategory == rightCategory) + { + return subSortLessThan(left, right); + } + else + { + return leftCategory < rightCategory; + } +} + +bool GroupedProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const +{ + return left.row() < right.row(); +} diff --git a/gui/groupview/GroupedProxyModel.h b/gui/groupview/GroupedProxyModel.h new file mode 100644 index 00000000..12edee0f --- /dev/null +++ b/gui/groupview/GroupedProxyModel.h @@ -0,0 +1,15 @@ +#pragma once + +#include <QSortFilterProxyModel> + +class GroupedProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + GroupedProxyModel(QObject *parent = 0); + +protected: + virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + virtual bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const; +}; diff --git a/gui/widgets/InstanceDelegate.cpp b/gui/groupview/InstanceDelegate.cpp index 5020b8b6..8a273758 100644 --- a/gui/widgets/InstanceDelegate.cpp +++ b/gui/groupview/InstanceDelegate.cpp @@ -20,6 +20,8 @@ #include <QApplication> #include <QtCore/qmath.h> +#include "GroupView.h" + // Origin: Qt static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, qreal &widthUsed) @@ -85,6 +87,27 @@ void drawFocusRect(QPainter *painter, const QStyleOptionViewItemV4 &option, cons painter->setRenderHint(QPainter::Antialiasing); } +// TODO this can be made a lot prettier +void drawProgressOverlay(QPainter *painter, const QStyleOptionViewItemV4 &option, + const int value, const int maximum) +{ + if (maximum == 0 || value == maximum) + { + return; + } + + painter->save(); + + qreal percent = (qreal)value / (qreal)maximum; + QColor color = option.palette.color(QPalette::Dark); + color.setAlphaF(0.70f); + painter->setBrush(color); + painter->setPen(QPen(QBrush(), 0)); + painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16); + + painter->restore(); +} + static QSize viewItemTextSize(const QStyleOptionViewItemV4 *option) { QStyle *style = option->widget ? option->widget->style() : QApplication::style(); @@ -150,6 +173,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti opt2.palette.setCurrentColorGroup(cg); // fill in background, if any + if (opt.backgroundBrush.style() != Qt::NoBrush) { QPointF oldBO = painter->brushOrigin(); @@ -158,6 +182,9 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti painter->setBrushOrigin(oldBO); } + drawSelectionRect(painter, opt2, textHighlightRect); + + /* if (opt.showDecorationSelected) { drawSelectionRect(painter, opt2, opt.rect); @@ -177,6 +204,7 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti drawFocusRect(painter, opt2, textHighlightRect); } } + */ } // draw the icon @@ -229,6 +257,10 @@ void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti line.draw(painter, position); } + drawProgressOverlay(painter, opt, + index.data(GroupViewRoles::ProgressValueRole).toInt(), + index.data(GroupViewRoles::ProgressMaximumRole).toInt()); + painter->restore(); } diff --git a/gui/widgets/InstanceDelegate.h b/gui/groupview/InstanceDelegate.h index 6f924405..de2f429b 100644 --- a/gui/widgets/InstanceDelegate.h +++ b/gui/groupview/InstanceDelegate.h @@ -20,8 +20,10 @@ class ListViewDelegate : public QStyledItemDelegate { public: - explicit ListViewDelegate ( QObject* parent = 0 ); + 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; + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; }; diff --git a/gui/widgets/Common.cpp b/gui/widgets/Common.cpp new file mode 100644 index 00000000..9b730d6c --- /dev/null +++ b/gui/widgets/Common.cpp @@ -0,0 +1,27 @@ +#include "Common.h" + +// Origin: Qt +QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, + qreal &widthUsed) +{ + QStringList lines; + height = 0; + widthUsed = 0; + textLayout.beginLayout(); + QString str = textLayout.text(); + while (true) + { + QTextLine line = textLayout.createLine(); + if (!line.isValid()) + break; + if (line.textLength() == 0) + break; + line.setLineWidth(lineWidth); + line.setPosition(QPointF(0, height)); + height += line.height(); + lines.append(str.mid(line.textStart(), line.textLength())); + widthUsed = qMax(widthUsed, line.naturalTextWidth()); + } + textLayout.endLayout(); + return lines; +} diff --git a/gui/widgets/Common.h b/gui/widgets/Common.h new file mode 100644 index 00000000..fc46e08f --- /dev/null +++ b/gui/widgets/Common.h @@ -0,0 +1,6 @@ +#pragma once +#include <QStringList> +#include <QTextLayout> + +QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, + qreal &widthUsed);
\ No newline at end of file diff --git a/gui/widgets/VersionListView.cpp b/gui/widgets/VersionListView.cpp new file mode 100644 index 00000000..b7f45f27 --- /dev/null +++ b/gui/widgets/VersionListView.cpp @@ -0,0 +1,150 @@ +/* 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. + */ + +#include <QHeaderView> +#include <QApplication> +#include <QMouseEvent> +#include <QDrag> +#include <QPainter> +#include "VersionListView.h" +#include "Common.h" + +VersionListView::VersionListView(QWidget *parent) + :QTreeView ( parent ) +{ + m_emptyString = tr("No versions are currently available."); +} + +void VersionListView::rowsInserted(const QModelIndex &parent, int start, int end) +{ + if(!m_itemCount) + viewport()->update(); + m_itemCount += end-start+1; + QTreeView::rowsInserted(parent, start, end); +} + + +void VersionListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + m_itemCount -= end-start+1; + if(!m_itemCount) + viewport()->update(); + QTreeView::rowsInserted(parent, start, end); +} + +void VersionListView::setModel(QAbstractItemModel *model) +{ + m_itemCount = model->rowCount(); + if(!m_itemCount) + viewport()->update(); + QTreeView::setModel(model); +} + +void VersionListView::reset() +{ + if(model()) + { + m_itemCount = model()->rowCount(); + } + viewport()->update(); + QTreeView::reset(); +} + +void VersionListView::setEmptyString(QString emptyString) +{ + m_emptyString = emptyString; + if(!m_itemCount) + { + viewport()->update(); + } +} + +void VersionListView::paintEvent(QPaintEvent *event) +{ + if(m_itemCount) + { + QTreeView::paintEvent(event); + } + else + { + paintInfoLabel(event); + } +} + +void VersionListView::paintInfoLabel(QPaintEvent *event) +{ + int scrollInterval = 500; + + //calculate the rect for the overlay + QPainter painter(viewport()); + painter.setRenderHint(QPainter::Antialiasing, true); + const QChar letter = 'Q'; + QFont font("sans", 20); + font.setBold(true); + + QRect bounds = viewport()->geometry(); + bounds.moveTop(0); + QTextLayout layout(m_emptyString, font); + qreal height = 0.0; + qreal widthUsed = 0.0; + QStringList lines = viewItemTextLayout(layout, bounds.width() - 20, height, widthUsed); + QRect rect (0,0, widthUsed, height); + rect.setWidth(rect.width()+20); + rect.setHeight(rect.height()+20); + rect.moveCenter(bounds.center()); + //check if we are allowed to draw in our area + if (!event->rect().intersects(rect)) { + return; + } + //draw the letter of the topmost item semitransparent in the middle + QColor background = QApplication::palette().color(QPalette::Foreground); + QColor foreground = QApplication::palette().color(QPalette::Base); + /* + background.setAlpha(128 - scrollFade); + foreground.setAlpha(128 - scrollFade); + */ + painter.setBrush(QBrush(background)); + painter.setPen(foreground); + painter.drawRoundedRect(rect, 5.0, 5.0); + foreground.setAlpha(190); + painter.setPen(foreground); + painter.setFont(font); + painter.drawText(rect, Qt::AlignCenter, lines.join("\n")); + +} + +/* +void ModListView::setModel ( QAbstractItemModel* model ) +{ + QTreeView::setModel ( model ); + auto head = header(); + head->setStretchLastSection(false); + // HACK: this is true for the checkbox column of mod lists + auto string = model->headerData(0,head->orientation()).toString(); + if(!string.size()) + { + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + head->setSectionResizeMode(1, QHeaderView::Stretch); + for(int i = 2; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + else + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } +} +*/
\ No newline at end of file diff --git a/gui/widgets/VersionListView.h b/gui/widgets/VersionListView.h new file mode 100644 index 00000000..af9b1f6a --- /dev/null +++ b/gui/widgets/VersionListView.h @@ -0,0 +1,43 @@ +/* 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. + */ + +#pragma once +#include <QTreeView> + +class Mod; + +class VersionListView : public QTreeView +{ + Q_OBJECT +public: + explicit VersionListView(QWidget *parent = 0); + virtual void paintEvent(QPaintEvent *event) override; + void setEmptyString(QString emptyString); + virtual void setModel ( QAbstractItemModel* model ); + +public slots: + virtual void reset() override; + +protected slots: + virtual void rowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) override; + virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; + +private: /* methods */ + void paintInfoLabel(QPaintEvent *event); + +private: /* variables */ + int m_itemCount = 0; + QString m_emptyString; +}; diff --git a/logic/BaseInstaller.cpp b/logic/BaseInstaller.cpp new file mode 100644 index 00000000..92aa0c92 --- /dev/null +++ b/logic/BaseInstaller.cpp @@ -0,0 +1,66 @@ +/* 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. + */ + +#include "BaseInstaller.h" + +#include <QFile> + +#include "OneSixVersion.h" +#include "OneSixLibrary.h" +#include "OneSixInstance.h" + +#include "cmdutils.h" + +BaseInstaller::BaseInstaller() +{ + +} + +bool BaseInstaller::isApplied(OneSixInstance *on) +{ + return QFile::exists(filename(on->instanceRoot())); +} + +bool BaseInstaller::add(OneSixInstance *to) +{ + if (!patchesDir(to->instanceRoot()).exists()) + { + QDir(to->instanceRoot()).mkdir("patches"); + } + + if (isApplied(to)) + { + if (!remove(to)) + { + return false; + } + } + + return true; +} + +bool BaseInstaller::remove(OneSixInstance *from) +{ + return QFile::remove(filename(from->instanceRoot())); +} + +QString BaseInstaller::filename(const QString &root) const +{ + return patchesDir(root).absoluteFilePath(id() + ".json"); +} +QDir BaseInstaller::patchesDir(const QString &root) const +{ + return QDir(root + "/patches/"); +} diff --git a/depends/groupview/include/groupview_config.h b/logic/BaseInstaller.h index c63acbde..df7eab89 100644 --- a/depends/groupview/include/groupview_config.h +++ b/logic/BaseInstaller.h @@ -15,14 +15,25 @@ #pragma once -#include <QtCore/QtGlobal> +#include <memory> -#ifdef LIBGROUPVIEW_STATIC - #define LIBGROUPVIEW_EXPORT -#else - #ifdef LIBGROUPVIEW_LIBRARY - #define LIBGROUPVIEW_EXPORT Q_DECL_EXPORT - #else - #define LIBGROUPVIEW_EXPORT Q_DECL_IMPORT - #endif -#endif +class OneSixInstance; +class QDir; +class QString; + +class BaseInstaller +{ +public: + BaseInstaller(); + + virtual bool canApply(OneSixInstance *instance) const { return true; } + bool isApplied(OneSixInstance *on); + + virtual bool add(OneSixInstance *to); + virtual bool remove(OneSixInstance *from); + +protected: + virtual QString id() const = 0; + QString filename(const QString &root) const; + QDir patchesDir(const QString &root) const; +}; diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index a861e9b2..cd49f99b 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -155,10 +155,10 @@ public: virtual SettingsObject &settings() const; /// returns a valid update task - virtual std::shared_ptr<Task> doUpdate(bool only_prepare) = 0; + virtual std::shared_ptr<Task> doUpdate() = 0; /// returns a valid minecraft process, ready for launch with the given account. - virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0; + virtual MinecraftProcess *prepareForLaunch(AuthSessionPtr account) = 0; /// do any necessary cleanups after the instance finishes. also runs before /// 'prepareForLaunch' diff --git a/logic/ForgeInstaller.cpp b/logic/ForgeInstaller.cpp index 8d4c5b41..3e18d17f 100644 --- a/logic/ForgeInstaller.cpp +++ b/logic/ForgeInstaller.cpp @@ -21,7 +21,15 @@ #include <quazipfile.h> #include <pathutils.h> #include <QStringList> +#include <QRegularExpression> +#include <QRegularExpressionMatch> #include "MultiMC.h" +#include "OneSixInstance.h" + +#include <QJsonDocument> +#include <QJsonArray> +#include <QSaveFile> +#include <QCryptographicHash> ForgeInstaller::ForgeInstaller(QString filename, QString universal_url) { @@ -66,6 +74,7 @@ ForgeInstaller::ForgeInstaller(QString filename, QString universal_url) QJsonObject installObj = installVal.toObject(); QString libraryName = installObj.value("path").toString(); internalPath = installObj.value("filePath").toString(); + m_forgeVersionString = installObj.value("version").toString().remove("Forge").trimmed(); // where do we put the library? decode the mojang path OneSixLibrary lib(libraryName); @@ -103,13 +112,22 @@ ForgeInstaller::ForgeInstaller(QString filename, QString universal_url) realVersionId = m_forge_version->id = installObj.value("minecraft").toString(); } -bool ForgeInstaller::apply(std::shared_ptr<OneSixVersion> to) +bool ForgeInstaller::add(OneSixInstance *to) { + if (!BaseInstaller::add(to)) + { + return false; + } + + QJsonObject obj; + obj.insert("order", 5); + if (!m_forge_version) return false; - to->externalUpdateStart(); int sliding_insert_window = 0; { + QJsonArray librariesPlus; + // for each library in the version we are adding (except for the blacklisted) QSet<QString> blacklist{"lwjgl", "lwjgl_util", "lwjgl-platform"}; for (auto lib : m_forge_version->libraries) @@ -128,28 +146,83 @@ bool ForgeInstaller::apply(std::shared_ptr<OneSixVersion> to) if (blacklist.contains(libName)) continue; - // find an entry that matches this one + QJsonObject libObj = lib->toJson(); + bool found = false; - for (auto tolib : to->libraries) + bool equals = false; + // find an entry that matches this one + for (auto tolib : to->getVanillaVersion()->libraries) { if (tolib->name() != libName) continue; found = true; + if (tolib->toJson() == libObj) + { + equals = true; + } // replace lib - tolib = lib; + libObj.insert("insert", QString("replace")); break; } + if (equals) + { + continue; + } if (!found) { // add lib - to->libraries.insert(sliding_insert_window, lib); + libObj.insert("insert", QString("prepend")); + if (lib->name() == "minecraftforge") + { + libObj.insert("MMC-depend", QString("hard")); + } sliding_insert_window++; } + librariesPlus.prepend(libObj); + } + obj.insert("+libraries", librariesPlus); + obj.insert("mainClass", m_forge_version->mainClass); + QString args = m_forge_version->minecraftArguments; + QStringList tweakers; + { + QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)"); + QRegularExpressionMatch match = expression.match(args); + while (match.hasMatch()) + { + tweakers.append(match.captured(1)); + args.remove(match.capturedStart(), match.capturedLength()); + match = expression.match(args); + } + } + if (!args.isEmpty() && args != to->getVanillaVersion()->minecraftArguments) + { + obj.insert("minecraftArguments", args); + } + if (!tweakers.isEmpty()) + { + obj.insert("+tweakers", QJsonArray::fromStringList(tweakers)); + } + if (!m_forge_version->processArguments.isEmpty() && + m_forge_version->processArguments != to->getVanillaVersion()->processArguments) + { + obj.insert("processArguments", m_forge_version->processArguments); } - to->mainClass = m_forge_version->mainClass; - to->minecraftArguments = m_forge_version->minecraftArguments; - to->processArguments = m_forge_version->processArguments; } - to->externalUpdateFinish(); - return to->toOriginalFile(); + + obj.insert("name", QString("Forge")); + obj.insert("fileId", id()); + obj.insert("version", m_forgeVersionString); + obj.insert("mcVersion", to->intendedVersionId()); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) + { + QLOG_ERROR() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(QJsonDocument(obj).toJson()); + file.close(); + + return true; } diff --git a/logic/ForgeInstaller.h b/logic/ForgeInstaller.h index 0b9f9c77..c5052092 100644 --- a/logic/ForgeInstaller.h +++ b/logic/ForgeInstaller.h @@ -14,17 +14,22 @@ */ #pragma once + +#include "BaseInstaller.h" + #include <QString> #include <memory> class OneSixVersion; -class ForgeInstaller +class ForgeInstaller : public BaseInstaller { public: ForgeInstaller(QString filename, QString universal_url); - bool apply(std::shared_ptr<OneSixVersion> to); + bool add(OneSixInstance *to) override; + + QString id() const override { return "net.minecraftforge"; } private: // the version, read from the installer @@ -32,5 +37,6 @@ private: QString internalPath; QString finalPath; QString realVersionId; + QString m_forgeVersionString; QString m_universal_url; }; diff --git a/logic/InstanceFactory.cpp b/logic/InstanceFactory.cpp index 1f1a5879..807bccd0 100644 --- a/logic/InstanceFactory.cpp +++ b/logic/InstanceFactory.cpp @@ -24,6 +24,7 @@ #include "OneSixInstance.h" #include "OneSixFTBInstance.h" #include "NostalgiaInstance.h" +#include "OneSixInstance.h" #include "BaseVersion.h" #include "MinecraftVersion.h" @@ -50,13 +51,13 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst QString inst_type = m_settings->get("InstanceType").toString(); // FIXME: replace with a map lookup, where instance classes register their types - if (inst_type == "Legacy") + if (inst_type == "OneSix") { - inst = new LegacyInstance(instDir, m_settings, this); + inst = new OneSixInstance(instDir, m_settings, this); } - else if (inst_type == "OneSix") + else if (inst_type == "Legacy") { - inst = new OneSixInstance(instDir, m_settings, this); + inst = new LegacyInstance(instDir, m_settings, this); } else if (inst_type == "Nostalgia") { @@ -101,6 +102,7 @@ InstanceFactory::InstCreateError InstanceFactory::createInstance(BaseInstance *& switch (mcVer->type) { case MinecraftVersion::Legacy: + // TODO new instance type m_settings->set("InstanceType", "Legacy"); inst = new LegacyInstance(instDir, m_settings, this); inst->setIntendedVersionId(version->descriptor()); diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp index 2828bcbf..a9f0d112 100644 --- a/logic/LegacyInstance.cpp +++ b/logic/LegacyInstance.cpp @@ -42,15 +42,15 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings, settings->registerSetting("IntendedJarVersion", ""); } -std::shared_ptr<Task> LegacyInstance::doUpdate(bool only_prepare) +std::shared_ptr<Task> LegacyInstance::doUpdate() { // make sure the jar mods list is initialized by asking for it. auto list = jarModList(); // create an update task - return std::shared_ptr<Task>(new LegacyUpdate(this, only_prepare, this)); + return std::shared_ptr<Task>(new LegacyUpdate(this, this)); } -MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) +MinecraftProcess *LegacyInstance::prepareForLaunch(AuthSessionPtr account) { MinecraftProcess *proc = new MinecraftProcess(this); @@ -66,13 +66,14 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) if (settings().get("LaunchMaximized").toBool()) windowParams = "max"; else - windowParams = QString("%1x%2").arg(settings().get("MinecraftWinWidth").toInt()).arg( - settings().get("MinecraftWinHeight").toInt()); + windowParams = QString("%1x%2") + .arg(settings().get("MinecraftWinWidth").toInt()) + .arg(settings().get("MinecraftWinHeight").toInt()); QString lwjgl = QDir(MMC->settings()->get("LWJGLDir").toString() + "/" + lwjglVersion()) .absolutePath(); - launchScript += "userName " + account->currentProfile()->name + "\n"; - launchScript += "sessionId " + account->sessionId() + "\n"; + launchScript += "userName " + account->player_name + "\n"; + launchScript += "sessionId " + account->session + "\n"; launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; launchScript += "lwjgl " + lwjgl + "\n"; diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h index 1e7d9eb6..636addeb 100644 --- a/logic/LegacyInstance.h +++ b/logic/LegacyInstance.h @@ -76,9 +76,9 @@ public: virtual bool shouldUpdate() const override; virtual void setShouldUpdate(bool val) override; - virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override; + virtual std::shared_ptr<Task> doUpdate() override; - virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; + virtual MinecraftProcess *prepareForLaunch(AuthSessionPtr account) override; virtual void cleanupAfterRun() override; virtual QDialog *createModEditDialog(QWidget *parent) override; diff --git a/logic/LegacyUpdate.cpp b/logic/LegacyUpdate.cpp index cb3598a7..5d82a76b 100644 --- a/logic/LegacyUpdate.cpp +++ b/logic/LegacyUpdate.cpp @@ -27,13 +27,13 @@ #include "logger/QsLog.h" #include "logic/net/URLConstants.h" -LegacyUpdate::LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent) - : Task(parent), m_inst(inst), m_only_prepare(only_prepare) +LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst) { } void LegacyUpdate::executeTask() { + /* if(m_only_prepare) { // FIXME: think this through some more. @@ -49,8 +49,9 @@ void LegacyUpdate::executeTask() } else { - lwjglStart(); - } + */ + lwjglStart(); + //} } void LegacyUpdate::lwjglStart() @@ -268,7 +269,6 @@ void LegacyUpdate::jarStart() auto dljob = new NetJob("Minecraft.jar for version " + version_id); - auto metacache = MMC->metacache(); auto entry = metacache->resolveEntry("versions", localPath); dljob->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); @@ -425,7 +425,7 @@ void LegacyUpdate::ModTheJar() auto &mod = modList->operator[](i); // do not merge disabled mods. - if(!mod.enabled()) + if (!mod.enabled()) continue; if (mod.type() == Mod::MOD_ZIPFILE) diff --git a/logic/LegacyUpdate.h b/logic/LegacyUpdate.h index 0b573ca5..613eb1f9 100644 --- a/logic/LegacyUpdate.h +++ b/logic/LegacyUpdate.h @@ -31,7 +31,7 @@ class LegacyUpdate : public Task { Q_OBJECT public: - explicit LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent = 0); + explicit LegacyUpdate(BaseInstance *inst, QObject *parent = 0); virtual void executeTask(); private @@ -72,5 +72,4 @@ private: private: NetJobPtr legacyDownloadJob; BaseInstance *m_inst = nullptr; - bool m_only_prepare = false; }; diff --git a/logic/LiteLoaderInstaller.cpp b/logic/LiteLoaderInstaller.cpp index 07fffff3..c363cad6 100644 --- a/logic/LiteLoaderInstaller.cpp +++ b/logic/LiteLoaderInstaller.cpp @@ -15,12 +15,19 @@ #include "LiteLoaderInstaller.h" +#include <QJsonArray> +#include <QJsonDocument> + +#include "logger/QsLog.h" + #include "OneSixVersion.h" #include "OneSixLibrary.h" +#include "OneSixInstance.h" QMap<QString, QString> LiteLoaderInstaller::m_launcherWrapperVersionMapping; -LiteLoaderInstaller::LiteLoaderInstaller(const QString &mcVersion) : m_mcVersion(mcVersion) +LiteLoaderInstaller::LiteLoaderInstaller() + : BaseInstaller() { if (m_launcherWrapperVersionMapping.isEmpty()) { @@ -31,72 +38,60 @@ LiteLoaderInstaller::LiteLoaderInstaller(const QString &mcVersion) : m_mcVersion } } -bool LiteLoaderInstaller::canApply() const +bool LiteLoaderInstaller::canApply(OneSixInstance *instance) const { - return m_launcherWrapperVersionMapping.contains(m_mcVersion); + return m_launcherWrapperVersionMapping.contains(instance->intendedVersionId()); } -bool LiteLoaderInstaller::apply(std::shared_ptr<OneSixVersion> to) +bool LiteLoaderInstaller::add(OneSixInstance *to) { - to->externalUpdateStart(); - - applyLaunchwrapper(to); - applyLiteLoader(to); - - to->mainClass = "net.minecraft.launchwrapper.Launch"; - if (!to->minecraftArguments.contains( - " --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker")) + if (!BaseInstaller::add(to)) { - to->minecraftArguments.append( - " --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker"); + return false; } - to->externalUpdateFinish(); - return to->toOriginalFile(); -} + QJsonObject obj; -void LiteLoaderInstaller::applyLaunchwrapper(std::shared_ptr<OneSixVersion> to) -{ - const QString intendedVersion = m_launcherWrapperVersionMapping[m_mcVersion]; + obj.insert("mainClass", QString("net.minecraft.launchwrapper.Launch")); + obj.insert("+tweakers", QJsonArray::fromStringList(QStringList() << "com.mumfrey.liteloader.launch.LiteLoaderTweaker")); + obj.insert("order", 10); - QMutableListIterator<std::shared_ptr<OneSixLibrary>> it(to->libraries); - while (it.hasNext()) + QJsonArray libraries; + + // launchwrapper { - it.next(); - if (it.value()->rawName().startsWith("net.minecraft:launchwrapper:")) - { - if (it.value()->version() >= intendedVersion) - { - return; - } - else - { - it.remove(); - } - } + OneSixLibrary launchwrapperLib("net.minecraft:launchwrapper:" + m_launcherWrapperVersionMapping[to->intendedVersionId()]); + launchwrapperLib.finalize(); + QJsonObject lwLibObj = launchwrapperLib.toJson(); + lwLibObj.insert("insert", QString("prepend")); + libraries.append(lwLibObj); } - std::shared_ptr<OneSixLibrary> lib(new OneSixLibrary( - "net.minecraft:launchwrapper:" + m_launcherWrapperVersionMapping[m_mcVersion])); - lib->finalize(); - to->libraries.prepend(lib); -} + // liteloader + { + OneSixLibrary liteloaderLib("com.mumfrey:liteloader:" + to->intendedVersionId()); + liteloaderLib.setBaseUrl("http://dl.liteloader.com/versions/"); + liteloaderLib.finalize(); + QJsonObject llLibObj = liteloaderLib.toJson(); + llLibObj.insert("insert", QString("prepend")); + llLibObj.insert("MMC-depend", QString("hard")); + libraries.append(llLibObj); + } -void LiteLoaderInstaller::applyLiteLoader(std::shared_ptr<OneSixVersion> to) -{ - QMutableListIterator<std::shared_ptr<OneSixLibrary>> it(to->libraries); - while (it.hasNext()) + obj.insert("+libraries", libraries); + obj.insert("name", QString("LiteLoader")); + obj.insert("fileId", id()); + obj.insert("version", to->intendedVersionId()); + obj.insert("mcVersion", to->intendedVersionId()); + + QFile file(filename(to->instanceRoot())); + if (!file.open(QFile::WriteOnly)) { - it.next(); - if (it.value()->rawName().startsWith("com.mumfrey:liteloader:")) - { - it.remove(); - } + QLOG_ERROR() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; } + file.write(QJsonDocument(obj).toJson()); + file.close(); - std::shared_ptr<OneSixLibrary> lib( - new OneSixLibrary("com.mumfrey:liteloader:" + m_mcVersion)); - lib->setBaseUrl("http://dl.liteloader.com/versions/"); - lib->finalize(); - to->libraries.prepend(lib); + return true; } diff --git a/logic/LiteLoaderInstaller.h b/logic/LiteLoaderInstaller.h index 44b306d6..5e01b16c 100644 --- a/logic/LiteLoaderInstaller.h +++ b/logic/LiteLoaderInstaller.h @@ -14,26 +14,22 @@ */ #pragma once + +#include "BaseInstaller.h" + #include <QString> #include <QMap> -#include <memory> - -class OneSixVersion; -class LiteLoaderInstaller +class LiteLoaderInstaller : public BaseInstaller { public: - LiteLoaderInstaller(const QString &mcVersion); + LiteLoaderInstaller(); - bool canApply() const; - - bool apply(std::shared_ptr<OneSixVersion> to); + bool canApply(OneSixInstance *instance) const override; + bool add(OneSixInstance *to) override; private: - QString m_mcVersion; - - void applyLaunchwrapper(std::shared_ptr<OneSixVersion> to); - void applyLiteLoader(std::shared_ptr<OneSixVersion> to); + virtual QString id() const override { return "com.mumfrey.liteloader"; } static QMap<QString, QString> m_launcherWrapperVersionMapping; }; diff --git a/logic/MinecraftProcess.cpp b/logic/MinecraftProcess.cpp index 84610021..70a9d55f 100644 --- a/logic/MinecraftProcess.cpp +++ b/logic/MinecraftProcess.cpp @@ -79,26 +79,18 @@ void MinecraftProcess::setWorkdir(QString path) QString MinecraftProcess::censorPrivateInfo(QString in) { - if(!m_account) + if(!m_session) return in; - QString sessionId = m_account->sessionId(); - QString accessToken = m_account->accessToken(); - QString clientToken = m_account->clientToken(); - in.replace(sessionId, "<SESSION ID>"); - in.replace(accessToken, "<ACCESS TOKEN>"); - in.replace(clientToken, "<CLIENT TOKEN>"); - auto profile = m_account->currentProfile(); - if(profile) - { - QString profileId = profile->id; - QString profileName = profile->name; - in.replace(profileId, "<PROFILE ID>"); - in.replace(profileName, "<PROFILE NAME>"); - } + if(m_session->session != "-") + in.replace(m_session->session, "<SESSION ID>"); + in.replace(m_session->access_token, "<ACCESS TOKEN>"); + in.replace(m_session->client_token, "<CLIENT TOKEN>"); + in.replace(m_session->uuid, "<PROFILE ID>"); + in.replace(m_session->player_name, "<PROFILE NAME>"); - auto i = m_account->user().properties.begin(); - while (i != m_account->user().properties.end()) + auto i = m_session->u.properties.begin(); + while (i != m_session->u.properties.end()) { in.replace(i.value(), "<" + i.key().toUpper() + ">"); ++i; @@ -121,6 +113,8 @@ MessageLevel::Enum MinecraftProcess::guessLevel(const QString &line, MessageLeve level = MessageLevel::Fatal; if (line.contains("[DEBUG]")) level = MessageLevel::Debug; + if(line.contains("overwriting existing")) + level = MessageLevel::Fatal; return level; } diff --git a/logic/MinecraftProcess.h b/logic/MinecraftProcess.h index 70e5df52..26214026 100644 --- a/logic/MinecraftProcess.h +++ b/logic/MinecraftProcess.h @@ -78,9 +78,9 @@ public: void killMinecraft(); - inline void setLogin(MojangAccountPtr account) + inline void setLogin(AuthSessionPtr session) { - m_account = account; + m_session = session; } signals: @@ -117,7 +117,7 @@ protected: QString m_out_leftover; QProcess m_prepostlaunchprocess; bool killed = false; - MojangAccountPtr m_account; + AuthSessionPtr m_session; QString launchScript; QString m_nativeFolder; diff --git a/logic/Mod.cpp b/logic/Mod.cpp index 6732446d..22ac36c8 100644 --- a/logic/Mod.cpp +++ b/logic/Mod.cpp @@ -164,6 +164,16 @@ void Mod::ReadMCModInfo(QByteArray contents) m_name = firstObj.value("name").toString(); m_version = firstObj.value("version").toString(); m_homeurl = firstObj.value("url").toString(); + m_homeurl = m_homeurl.trimmed(); + if(!m_homeurl.isEmpty()) + { + // fix up url. + if (!m_homeurl.startsWith("http://") && !m_homeurl.startsWith("https://") && + !m_homeurl.startsWith("ftp://")) + { + m_homeurl.prepend("http://"); + } + } m_description = firstObj.value("description").toString(); QJsonArray authors = firstObj.value("authors").toArray(); if (authors.size() == 0) @@ -178,7 +188,8 @@ void Mod::ReadMCModInfo(QByteArray contents) } m_credits = firstObj.value("credits").toString(); return; - }; + } + ; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); // this is the very old format that had just the array @@ -227,17 +238,17 @@ void Mod::ReadLiteModInfo(QByteArray contents) QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); - if(object.contains("name")) + if (object.contains("name")) { m_mod_id = m_name = object.value("name").toString(); } - if(object.contains("version")) + if (object.contains("version")) { - m_version=object.value("version").toString(""); + m_version = object.value("version").toString(""); } else { - m_version=object.value("revision").toString(""); + m_version = object.value("revision").toString(""); } m_mcversion = object.value("mcversion").toString(); m_authors = object.value("author").toString(); diff --git a/logic/ModList.cpp b/logic/ModList.cpp index 499623bf..79b56986 100644 --- a/logic/ModList.cpp +++ b/logic/ModList.cpp @@ -62,6 +62,19 @@ void ModList::stopWatching() } } +void ModList::internalSort(QList<Mod> &what) +{ + auto predicate = [](const Mod & left, const Mod & right) + { + if (left.name() == right.name()) + { + return left.mmc_id().localeAwareCompare(right.mmc_id()) <= 0; + } + return left.name().localeAwareCompare(right.name()) <= 0; + }; + std::sort(what.begin(), what.end(), predicate); +} + bool ModList::update() { if (!isValid()) @@ -98,7 +111,7 @@ bool ModList::update() isEnabled = idxEnabled >= 0; } int idx = isEnabled ? idxEnabled : idxDisabled; - QFileInfo & info = isEnabled ? infoEnabled : infoDisabled; + QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; // if the file from the index file exists if (idx != -1) { @@ -122,8 +135,7 @@ bool ModList::update() { newMods.append(Mod(entry)); } - std::sort(newMods.begin(), newMods.end(), [](const Mod & left, const Mod & right) - { return left.name().localeAwareCompare(right.name()) <= 0; }); + internalSort(newMods); orderedMods.append(newMods); orderOrStateChanged = true; } @@ -236,8 +248,8 @@ bool ModList::installMod(const QFileInfo &filename, int index) int idx = mods.indexOf(m); if (idx != -1) { - int idx2 = mods.indexOf(m,idx+1); - if(idx2 != -1) + int idx2 = mods.indexOf(m, idx + 1); + if (idx2 != -1) return false; if (mods[idx].replace(m)) { @@ -416,7 +428,7 @@ QVariant ModList::data(const QModelIndex &index, int role) const switch (index.column()) { case ActiveColumn: - return mods[row].enabled() ? Qt::Checked: Qt::Unchecked; + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -567,8 +579,7 @@ bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row if (m_list_file.isEmpty()) { beginResetModel(); - std::sort(mods.begin(), mods.end(), [](const Mod & left, const Mod & right) - { return left.name().localeAwareCompare(right.name()) <= 0; }); + internalSort(mods); endResetModel(); } } diff --git a/logic/ModList.h b/logic/ModList.h index 0d6507fb..95f52061 100644 --- a/logic/ModList.h +++ b/logic/ModList.h @@ -127,6 +127,7 @@ public: } private: + void internalSort(QList<Mod> & what); struct OrderItem { QString id; diff --git a/logic/OneSixFTBInstance.cpp b/logic/OneSixFTBInstance.cpp index e50a5b53..ca88142a 100644 --- a/logic/OneSixFTBInstance.cpp +++ b/logic/OneSixFTBInstance.cpp @@ -55,15 +55,13 @@ slots: setStatus(tr("Installing Forge...")); QString forgePath = entry->getFullPath(); ForgeInstaller forge(forgePath, forgeVersion->universal_url); - if (!instance->reloadFullVersion()) + if (!instance->reloadVersion()) { emitFailed(tr("Couldn't load the version config")); return; } - instance->revertCustomVersion(); - instance->customizeVersion(); auto version = instance->getFullVersion(); - if (!forge.apply(version)) + if (!forge.add(instance)) { emitFailed(tr("Couldn't install Forge")); return; @@ -106,7 +104,7 @@ bool OneSixFTBInstance::menuActionEnabled(QString action_name) const return false; } -std::shared_ptr<Task> OneSixFTBInstance::doUpdate(bool only_prepare) +std::shared_ptr<Task> OneSixFTBInstance::doUpdate() { std::shared_ptr<SequentialTask> task; task.reset(new SequentialTask(this)); @@ -114,11 +112,11 @@ std::shared_ptr<Task> OneSixFTBInstance::doUpdate(bool only_prepare) { task->addTask(std::shared_ptr<Task>(MMC->forgelist()->getLoadTask())); } - task->addTask(OneSixInstance::doUpdate(only_prepare)); + task->addTask(OneSixInstance::doUpdate()); task->addTask(std::shared_ptr<Task>(new OneSixFTBInstanceForge(m_forge->version(), this, this))); //FIXME: yes. this may appear dumb. but the previous step can change the list, so we do it all again. //TODO: Add a graph task. Construct graphs of tasks so we may capture the logic properly. - task->addTask(OneSixInstance::doUpdate(only_prepare)); + task->addTask(OneSixInstance::doUpdate()); return task; } diff --git a/logic/OneSixFTBInstance.h b/logic/OneSixFTBInstance.h index dc028819..bc543aeb 100644 --- a/logic/OneSixFTBInstance.h +++ b/logic/OneSixFTBInstance.h @@ -13,7 +13,7 @@ public: virtual QString getStatusbarDescription(); virtual bool menuActionEnabled(QString action_name) const; - virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override; + virtual std::shared_ptr<Task> doUpdate() override; virtual QString id() const; diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp index ab87a1db..ae172f21 100644 --- a/logic/OneSixInstance.cpp +++ b/logic/OneSixInstance.cpp @@ -13,37 +13,42 @@ * limitations under the License. */ -#include "MultiMC.h" #include "OneSixInstance.h" + +#include <QIcon> + #include "OneSixInstance_p.h" #include "OneSixUpdate.h" -#include "MinecraftProcess.h" #include "OneSixVersion.h" -#include "JavaChecker.h" -#include "logic/icons/IconList.h" - -#include <setting.h> -#include <pathutils.h> -#include <cmdutils.h> -#include <JlCompress.h> -#include "gui/dialogs/OneSixModEditDialog.h" +#include "pathutils.h" #include "logger/QsLog.h" -#include "logic/assets/AssetsUtils.h" -#include <QIcon> +#include "assets/AssetsUtils.h" +#include "MultiMC.h" +#include "icons/IconList.h" +#include "MinecraftProcess.h" +#include "gui/dialogs/OneSixModEditDialog.h" -OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_obj, - QObject *parent) - : BaseInstance(new OneSixInstancePrivate(), rootDir, setting_obj, parent) +OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) + : BaseInstance(new OneSixInstancePrivate(), rootDir, settings, parent) { I_D(OneSixInstance); d->m_settings->registerSetting("IntendedVersion", ""); d->m_settings->registerSetting("ShouldUpdate", false); - reloadFullVersion(); + d->version.reset(new OneSixVersion(this, this)); + d->vanillaVersion.reset(new OneSixVersion(this, this)); + if (QDir(instanceRoot()).exists("version.json")) + { + reloadVersion(); + } + else + { + clearVersion(); + } } -std::shared_ptr<Task> OneSixInstance::doUpdate(bool only_prepare) +std::shared_ptr<Task> OneSixInstance::doUpdate() { - return std::shared_ptr<Task>(new OneSixUpdate(this, only_prepare)); + return std::shared_ptr<Task>(new OneSixUpdate(this)); } QString replaceTokensIn(QString text, QMap<QString, QString> with) @@ -130,25 +135,23 @@ QDir OneSixInstance::reconstructAssets(std::shared_ptr<OneSixVersion> version) return virtualRoot; } -QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) +QStringList OneSixInstance::processMinecraftArgs(AuthSessionPtr session) { I_D(OneSixInstance); auto version = d->version; QString args_pattern = version->minecraftArguments; + for (auto tweaker : version->tweakers) + { + args_pattern += " --tweakClass " + tweaker; + } QMap<QString, QString> token_mapping; // yggdrasil! - token_mapping["auth_username"] = account->username(); - token_mapping["auth_session"] = account->sessionId(); - token_mapping["auth_access_token"] = account->accessToken(); - token_mapping["auth_player_name"] = account->currentProfile()->name; - token_mapping["auth_uuid"] = account->currentProfile()->id; - - // this is for offline?: - /* - map["auth_player_name"] = "Player"; - map["auth_player_name"] = "00000000-0000-0000-0000-000000000000"; - */ + token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; // these do nothing and are stupid. token_mapping["profile_name"] = name(); @@ -159,17 +162,8 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) QString absAssetsDir = QDir("assets/").absolutePath(); token_mapping["game_assets"] = reconstructAssets(d->version).absolutePath(); - auto user = account->user(); - QJsonObject userAttrs; - for (auto key : user.properties.keys()) - { - auto array = QJsonArray::fromStringList(user.properties.values(key)); - userAttrs.insert(key, array); - } - QJsonDocument value(userAttrs); - - token_mapping["user_properties"] = value.toJson(QJsonDocument::Compact); - token_mapping["user_type"] = account->currentProfile()->legacy ? "legacy" : "mojang"; + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; // 1.7.3+ assets tokens token_mapping["assets_root"] = absAssetsDir; token_mapping["assets_index_name"] = version->assets; @@ -182,7 +176,7 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) return parts; } -MinecraftProcess *OneSixInstance::prepareForLaunch(MojangAccountPtr account) +MinecraftProcess *OneSixInstance::prepareForLaunch(AuthSessionPtr session) { I_D(OneSixInstance); @@ -207,7 +201,7 @@ MinecraftProcess *OneSixInstance::prepareForLaunch(MojangAccountPtr account) } launchScript += "mainClass " + version->mainClass + "\n"; - for (auto param : processMinecraftArgs(account)) + for (auto param : processMinecraftArgs(session)) { launchScript += "param " + param + "\n"; } @@ -282,11 +276,8 @@ bool OneSixInstance::setIntendedVersionId(QString version) { settings().set("IntendedVersion", version); setShouldUpdate(true); - auto pathCustom = PathCombine(instanceRoot(), "custom.json"); - auto pathOrig = PathCombine(instanceRoot(), "version.json"); - QFile::remove(pathCustom); - QFile::remove(pathOrig); - reloadFullVersion(); + QFile::remove(PathCombine(instanceRoot(), "version.json")); + clearVersion(); return true; } @@ -312,9 +303,10 @@ bool OneSixInstance::shouldUpdate() const bool OneSixInstance::versionIsCustom() { - QString verpath_custom = PathCombine(instanceRoot(), "custom.json"); - QFile versionfile(verpath_custom); - return versionfile.exists(); + QDir patches(PathCombine(instanceRoot(), "patches/")); + return (patches.exists() && patches.count() >= 0) + || QFile::exists(PathCombine(instanceRoot(), "custom.json")) + || QFile::exists(PathCombine(instanceRoot(), "user.json")); } QString OneSixInstance::currentVersionId() const @@ -322,62 +314,39 @@ QString OneSixInstance::currentVersionId() const return intendedVersionId(); } -bool OneSixInstance::customizeVersion() +bool OneSixInstance::reloadVersion(QWidget *widgetParent) { - if (!versionIsCustom()) - { - auto pathCustom = PathCombine(instanceRoot(), "custom.json"); - auto pathOrig = PathCombine(instanceRoot(), "version.json"); - QFile::copy(pathOrig, pathCustom); - return reloadFullVersion(); - } - else - return true; -} + I_D(OneSixInstance); -bool OneSixInstance::revertCustomVersion() -{ - if (versionIsCustom()) + bool ret = d->version->reload(widgetParent); + if (ret) { - auto path = PathCombine(instanceRoot(), "custom.json"); - QFile::remove(path); - return reloadFullVersion(); + ret = d->vanillaVersion->reload(widgetParent, true); } - else - return true; + emit versionReloaded(); + return ret; } -bool OneSixInstance::reloadFullVersion() +void OneSixInstance::clearVersion() { I_D(OneSixInstance); - - QString verpath = PathCombine(instanceRoot(), "version.json"); - { - QString verpath_custom = PathCombine(instanceRoot(), "custom.json"); - QFile versionfile(verpath_custom); - if (versionfile.exists()) - verpath = verpath_custom; - } - - auto version = OneSixVersion::fromFile(verpath); - if (version) - { - d->version = version; - return true; - } - else - { - d->version.reset(); - return false; - } + d->version->clear(); + d->vanillaVersion->clear(); + emit versionReloaded(); } -std::shared_ptr<OneSixVersion> OneSixInstance::getFullVersion() +std::shared_ptr<OneSixVersion> OneSixInstance::getFullVersion() const { - I_D(OneSixInstance); + I_D(const OneSixInstance); return d->version; } +std::shared_ptr<OneSixVersion> OneSixInstance::getVanillaVersion() const +{ + I_D(const OneSixInstance); + return d->vanillaVersion; +} + QString OneSixInstance::defaultBaseJar() const { return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; @@ -397,7 +366,7 @@ bool OneSixInstance::menuActionEnabled(QString action_name) const QString OneSixInstance::getStatusbarDescription() { - QString descr = "One Six : " + intendedVersionId(); + QString descr = "OneSix : " + intendedVersionId(); if (versionIsCustom()) { descr + " (custom)"; diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h index f869e345..ae95eab1 100644 --- a/logic/OneSixInstance.h +++ b/logic/OneSixInstance.h @@ -15,21 +15,17 @@ #pragma once -#include <QStringList> -#include <QDir> - #include "BaseInstance.h" -class OneSixVersion; -class Task; -class ModList; +#include "OneSixVersion.h" +#include "ModList.h" class OneSixInstance : public BaseInstance { Q_OBJECT public: explicit OneSixInstance(const QString &rootDir, SettingsObject *settings, - QObject *parent = 0); + QObject *parent = 0); ////// Mod Lists ////// std::shared_ptr<ModList> loaderModList(); @@ -40,8 +36,8 @@ public: QString loaderModsDir() const; virtual QString instanceConfigFolder() const override; - virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override; - virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; + virtual std::shared_ptr<Task> doUpdate() override; + virtual MinecraftProcess *prepareForLaunch(AuthSessionPtr session) override; virtual void cleanupAfterRun() override; @@ -55,14 +51,14 @@ public: virtual QDialog *createModEditDialog(QWidget *parent) override; - /// reload the full version json file. return true on success! - bool reloadFullVersion(); + /// reload the full version json files. return true on success! + bool reloadVersion(QWidget *widgetParent = 0); + /// clears all version information in preparation for an update + void clearVersion(); /// get the current full version info - std::shared_ptr<OneSixVersion> getFullVersion(); - /// revert the current custom version back to base - bool revertCustomVersion(); - /// customize the current base version - bool customizeVersion(); + std::shared_ptr<OneSixVersion> getFullVersion() const; + /// gets the current version info, but only for version.json + std::shared_ptr<OneSixVersion> getVanillaVersion() const; /// is the current version original, or custom? virtual bool versionIsCustom() override; @@ -72,7 +68,10 @@ public: virtual bool menuActionEnabled(QString action_name) const override; virtual QString getStatusbarDescription() override; +signals: + void versionReloaded(); + private: - QStringList processMinecraftArgs(MojangAccountPtr account); + QStringList processMinecraftArgs(AuthSessionPtr account); QDir reconstructAssets(std::shared_ptr<OneSixVersion> version); }; diff --git a/logic/OneSixInstance_p.h b/logic/OneSixInstance_p.h index 6b7ea431..0cc46f33 100644 --- a/logic/OneSixInstance_p.h +++ b/logic/OneSixInstance_p.h @@ -15,16 +15,14 @@ #pragma once -#include <memory> - -#include "logic/BaseInstance_p.h" -#include "logic/OneSixVersion.h" -#include "logic/OneSixLibrary.h" -#include "logic/ModList.h" +#include "BaseInstance_p.h" +#include "OneSixVersion.h" +#include "ModList.h" struct OneSixInstancePrivate : public BaseInstancePrivate { std::shared_ptr<OneSixVersion> version; + std::shared_ptr<OneSixVersion> vanillaVersion; std::shared_ptr<ModList> loader_mod_list; std::shared_ptr<ModList> resource_pack_list; -};
\ No newline at end of file +}; diff --git a/logic/OneSixLibrary.cpp b/logic/OneSixLibrary.cpp index 7b80d5e7..c78679d1 100644 --- a/logic/OneSixLibrary.cpp +++ b/logic/OneSixLibrary.cpp @@ -46,7 +46,7 @@ void OneSixLibrary::finalize() } m_decentname = parts[1]; - m_decentversion = parts[2]; + m_decentversion = minVersion = parts[2]; m_storage_path = relative; m_download_url = m_base_url + relative; @@ -76,11 +76,11 @@ void OneSixLibrary::finalize() } } -void OneSixLibrary::setName(QString name) +void OneSixLibrary::setName(const QString &name) { m_name = name; } -void OneSixLibrary::setBaseUrl(QString base_url) +void OneSixLibrary::setBaseUrl(const QString &base_url) { m_base_url = base_url; } @@ -88,50 +88,54 @@ void OneSixLibrary::setIsNative() { m_is_native = true; } -void OneSixLibrary::addNative(OpSys os, QString suffix) +void OneSixLibrary::addNative(OpSys os, const QString &suffix) { m_is_native = true; m_native_suffixes[os] = suffix; } +void OneSixLibrary::clearSuffixes() +{ + m_native_suffixes.clear(); +} void OneSixLibrary::setRules(QList<std::shared_ptr<Rule>> rules) { m_rules = rules; } -bool OneSixLibrary::isActive() +bool OneSixLibrary::isActive() const { return m_is_active; } -bool OneSixLibrary::isNative() +bool OneSixLibrary::isNative() const { return m_is_native; } -QString OneSixLibrary::downloadUrl() +QString OneSixLibrary::downloadUrl() const { if (m_absolute_url.size()) return m_absolute_url; return m_download_url; } -QString OneSixLibrary::storagePath() +QString OneSixLibrary::storagePath() const { return m_storage_path; } -void OneSixLibrary::setAbsoluteUrl(QString absolute_url) +void OneSixLibrary::setAbsoluteUrl(const QString &absolute_url) { m_absolute_url = absolute_url; } -QString OneSixLibrary::absoluteUrl() +QString OneSixLibrary::absoluteUrl() const { return m_absolute_url; } -void OneSixLibrary::setHint(QString hint) +void OneSixLibrary::setHint(const QString &hint) { m_hint = hint; } -QString OneSixLibrary::hint() +QString OneSixLibrary::hint() const { return m_hint; } @@ -176,7 +180,7 @@ bool OneSixLibrary::extractTo(QString target_dir) cooked_storage.replace("${arch}", "32"); QString origin = PathCombine("libraries", cooked_storage); QString target_dir_cooked = PathCombine(target_dir, "32"); - if(!ensureFolderPathExists(target_dir_cooked)) + if (!ensureFolderPathExists(target_dir_cooked)) { QLOG_ERROR() << "Couldn't create folder " + target_dir_cooked; return false; @@ -191,7 +195,7 @@ bool OneSixLibrary::extractTo(QString target_dir) cooked_storage.replace("${arch}", "64"); origin = PathCombine("libraries", cooked_storage); target_dir_cooked = PathCombine(target_dir, "64"); - if(!ensureFolderPathExists(target_dir_cooked)) + if (!ensureFolderPathExists(target_dir_cooked)) { QLOG_ERROR() << "Couldn't create folder " + target_dir_cooked; return false; @@ -205,7 +209,7 @@ bool OneSixLibrary::extractTo(QString target_dir) } else { - if(!ensureFolderPathExists(target_dir)) + if (!ensureFolderPathExists(target_dir)) { QLOG_ERROR() << "Couldn't create folder " + target_dir; return false; @@ -230,8 +234,10 @@ QJsonObject OneSixLibrary::toJson() libRoot.insert("MMC-hint", m_hint); if (m_base_url != "http://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && m_base_url != "https://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && - m_base_url != "https://" + URLConstants::LIBRARY_BASE) + m_base_url != "https://" + URLConstants::LIBRARY_BASE && !m_base_url.isEmpty()) + { libRoot.insert("url", m_base_url); + } if (isNative() && m_native_suffixes.size()) { QJsonObject nativeList; diff --git a/logic/OneSixLibrary.h b/logic/OneSixLibrary.h index 227cdbef..371ca6f4 100644 --- a/logic/OneSixLibrary.h +++ b/logic/OneSixLibrary.h @@ -60,12 +60,21 @@ private: public: QStringList extract_excludes; + QString minVersion; + + enum DependType + { + Soft, + Hard + }; + DependType dependType; public: /// Constructor - OneSixLibrary(QString name) + OneSixLibrary(const QString &name, const DependType type = Soft) { m_name = name; + dependType = type; } /// Returns the raw name field @@ -84,48 +93,50 @@ public: void finalize(); /// Set the library composite name - void setName(QString name); + void setName(const QString &name); /// get a decent-looking name - QString name() + QString name() const { return m_decentname; } /// get a decent-looking version - QString version() + QString version() const { return m_decentversion; } /// what kind of library is it? (for display) - QString type() + QString type() const { return m_decenttype; } /// Set the url base for downloads - void setBaseUrl(QString base_url); + void setBaseUrl(const QString &base_url); /// Call this to mark the library as 'native' (it's a zip archive with DLLs) void setIsNative(); /// Attach a name suffix to the specified OS native - void addNative(OpSys os, QString suffix); + void addNative(OpSys os, const QString &suffix); + /// Clears all suffixes + void clearSuffixes(); /// Set the load rules void setRules(QList<std::shared_ptr<Rule>> rules); /// Returns true if the library should be loaded (or extracted, in case of natives) - bool isActive(); + bool isActive() const; /// Returns true if the library is native - bool isNative(); + bool isNative() const; /// Get the URL to download the library from - QString downloadUrl(); + QString downloadUrl() const; /// Get the relative path where the library should be saved - QString storagePath(); + QString storagePath() const; /// set an absolute URL for the library. This is an MMC extension. - void setAbsoluteUrl(QString absolute_url); - QString absoluteUrl(); + void setAbsoluteUrl(const QString &absolute_url); + QString absoluteUrl() const; /// set a hint about how to treat the library. This is an MMC extension. - void setHint(QString hint); - QString hint(); + void setHint(const QString &hint); + QString hint() const; bool extractTo(QString target_dir); bool filesExist(); diff --git a/logic/OneSixRule.cpp b/logic/OneSixRule.cpp index 392b1dd1..d8d13b50 100644 --- a/logic/OneSixRule.cpp +++ b/logic/OneSixRule.cpp @@ -18,7 +18,7 @@ #include "OneSixRule.h" -QList<std::shared_ptr<Rule>> rulesFromJsonV4(QJsonObject &objectWithRules) +QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules) { QList<std::shared_ptr<Rule>> rules; auto rulesVal = objectWithRules.value("rules"); @@ -86,4 +86,4 @@ RuleAction RuleAction_fromString(QString name) if (name == "disallow") return Disallow; return Defer; -}
\ No newline at end of file +} diff --git a/logic/OneSixRule.h b/logic/OneSixRule.h index 5a13cbd9..426e2886 100644 --- a/logic/OneSixRule.h +++ b/logic/OneSixRule.h @@ -27,7 +27,7 @@ enum RuleAction }; RuleAction RuleAction_fromString(QString); -QList<std::shared_ptr<Rule>> rulesFromJsonV4(QJsonObject &objectWithRules); +QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject &objectWithRules); class Rule { diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp index ae647bfe..d3ac80c2 100644 --- a/logic/OneSixUpdate.cpp +++ b/logic/OneSixUpdate.cpp @@ -35,8 +35,8 @@ #include "pathutils.h" #include <JlCompress.h> -OneSixUpdate::OneSixUpdate(BaseInstance *inst, bool only_prepare, QObject *parent) - : Task(parent), m_inst(inst), m_only_prepare(only_prepare) +OneSixUpdate::OneSixUpdate(BaseInstance *inst, QObject *parent) + : Task(parent), m_inst(inst) { } @@ -52,12 +52,6 @@ void OneSixUpdate::executeTask() return; } - if (m_only_prepare) - { - prepareForLaunch(); - return; - } - if (m_inst->shouldUpdate()) { // Get a pointer to the version object that corresponds to the instance's version. @@ -137,7 +131,7 @@ void OneSixUpdate::versionFileFinished() { finfo.remove(); } - inst->reloadFullVersion(); + inst->reloadVersion(); jarlibStart(); } @@ -222,7 +216,7 @@ void OneSixUpdate::assetIndexFailed() void OneSixUpdate::assetsFinished() { - prepareForLaunch(); + emitSucceeded(); } void OneSixUpdate::assetsFailed() @@ -235,7 +229,7 @@ void OneSixUpdate::jarlibStart() setStatus(tr("Getting the library files from Mojang...")); QLOG_INFO() << m_inst->name() << ": downloading libraries"; OneSixInstance *inst = (OneSixInstance *)m_inst; - bool successful = inst->reloadFullVersion(); + bool successful = inst->reloadVersion(); if (!successful) { emitFailed("Failed to load the version description file. It might be " @@ -330,43 +324,3 @@ void OneSixUpdate::jarlibFailed() emitFailed("Failed to download the following files:\n" + failed_all + "\n\nPlease try again."); } - -void OneSixUpdate::prepareForLaunch() -{ - setStatus(tr("Preparing for launch...")); - QLOG_INFO() << m_inst->name() << ": preparing for launch"; - auto onesix_inst = (OneSixInstance *)m_inst; - - // delete any leftovers, if they are present. - onesix_inst->cleanupAfterRun(); - - QString natives_dir_raw = PathCombine(onesix_inst->instanceRoot(), "natives/"); - auto version = onesix_inst->getFullVersion(); - if (!version) - { - emitFailed("The version information for this instance is not complete. Try re-creating " - "it or changing the version."); - return; - } - /* - for (auto lib : version->getActiveNativeLibs()) - { - if (!lib->filesExist()) - { - emitFailed("Native library is missing some files:\n" + lib->storagePath() + - "\n\nRun the instance at least once in online mode to get all the " - "required files."); - return; - } - if (!lib->extractTo(natives_dir_raw)) - { - emitFailed("Could not extract the native library:\n" + lib->storagePath() + " to " + - natives_dir_raw + - "\n\nMake sure MultiMC has appropriate permissions and there is enough " - "space on the storage device."); - return; - } - } -*/ - emitSucceeded(); -} diff --git a/logic/OneSixUpdate.h b/logic/OneSixUpdate.h index bc717a94..3c18211e 100644 --- a/logic/OneSixUpdate.h +++ b/logic/OneSixUpdate.h @@ -29,7 +29,7 @@ class OneSixUpdate : public Task { Q_OBJECT public: - explicit OneSixUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent = 0); + explicit OneSixUpdate(BaseInstance *inst, QObject *parent = 0); virtual void executeTask(); private @@ -49,9 +49,6 @@ slots: void assetsFinished(); void assetsFailed(); - // extract the appropriate libraries - void prepareForLaunch(); - private: NetJobPtr specificVersionDownloadJob; NetJobPtr jarlibDownloadJob; @@ -59,5 +56,4 @@ private: // target version, determined during this task std::shared_ptr<MinecraftVersion> targetVersion; BaseInstance *m_inst = nullptr; - bool m_only_prepare = false; }; diff --git a/logic/OneSixVersion.cpp b/logic/OneSixVersion.cpp index 8ae685f0..fb32f3a8 100644 --- a/logic/OneSixVersion.cpp +++ b/logic/OneSixVersion.cpp @@ -13,228 +13,95 @@ * limitations under the License. */ -#include "logic/OneSixVersion.h" -#include "logic/OneSixLibrary.h" -#include "logic/OneSixRule.h" +#include "OneSixVersion.h" -#include "logger/QsLog.h" +#include <QDebug> +#include <QFile> -std::shared_ptr<OneSixVersion> fromJsonV4(QJsonObject root, - std::shared_ptr<OneSixVersion> fullVersion) -{ - fullVersion->id = root.value("id").toString(); - - fullVersion->mainClass = root.value("mainClass").toString(); - auto procArgsValue = root.value("processArguments"); - if (procArgsValue.isString()) - { - fullVersion->processArguments = procArgsValue.toString(); - QString toCompare = fullVersion->processArguments.toLower(); - if (toCompare == "legacy") - { - fullVersion->minecraftArguments = " ${auth_player_name} ${auth_session}"; - } - else if (toCompare == "username_session") - { - fullVersion->minecraftArguments = - "--username ${auth_player_name} --session ${auth_session}"; - } - else if (toCompare == "username_session_version") - { - fullVersion->minecraftArguments = "--username ${auth_player_name} " - "--session ${auth_session} " - "--version ${profile_name}"; - } - } - - auto minecraftArgsValue = root.value("minecraftArguments"); - if (minecraftArgsValue.isString()) - { - fullVersion->minecraftArguments = minecraftArgsValue.toString(); - } +#include "OneSixVersionBuilder.h" - auto minecraftTypeValue = root.value("type"); - if (minecraftTypeValue.isString()) - { - fullVersion->type = minecraftTypeValue.toString(); - } - - fullVersion->releaseTime = root.value("releaseTime").toString(); - fullVersion->time = root.value("time").toString(); - - auto assetsID = root.value("assets"); - if (assetsID.isString()) - { - fullVersion->assets = assetsID.toString(); - } - else - { - fullVersion->assets = "legacy"; - } - - QLOG_DEBUG() << "Assets version:" << fullVersion->assets; - - // Iterate through the list, if it's a list. - auto librariesValue = root.value("libraries"); - if (!librariesValue.isArray()) - return fullVersion; - - QJsonArray libList = root.value("libraries").toArray(); - for (auto libVal : libList) - { - if (!libVal.isObject()) - { - continue; - } - - QJsonObject libObj = libVal.toObject(); - - // Library name - auto nameVal = libObj.value("name"); - if (!nameVal.isString()) - continue; - std::shared_ptr<OneSixLibrary> library(new OneSixLibrary(nameVal.toString())); - - auto urlVal = libObj.value("url"); - if (urlVal.isString()) - { - library->setBaseUrl(urlVal.toString()); - } - auto hintVal = libObj.value("MMC-hint"); - if (hintVal.isString()) - { - library->setHint(hintVal.toString()); - } - auto urlAbsVal = libObj.value("MMC-absoluteUrl"); - auto urlAbsuVal = libObj.value("MMC-absulute_url"); // compatibility - if (urlAbsVal.isString()) - { - library->setAbsoluteUrl(urlAbsVal.toString()); - } - else if (urlAbsuVal.isString()) - { - library->setAbsoluteUrl(urlAbsuVal.toString()); - } - // Extract excludes (if any) - auto extractVal = libObj.value("extract"); - if (extractVal.isObject()) - { - QStringList excludes; - auto extractObj = extractVal.toObject(); - auto excludesVal = extractObj.value("exclude"); - if (excludesVal.isArray()) - { - auto excludesList = excludesVal.toArray(); - for (auto excludeVal : excludesList) - { - if (excludeVal.isString()) - excludes.append(excludeVal.toString()); - } - library->extract_excludes = excludes; - } - } - - auto nativesVal = libObj.value("natives"); - if (nativesVal.isObject()) - { - library->setIsNative(); - auto nativesObj = nativesVal.toObject(); - auto iter = nativesObj.begin(); - while (iter != nativesObj.end()) - { - auto osType = OpSys_fromString(iter.key()); - if (osType == Os_Other) - continue; - if (!iter.value().isString()) - continue; - library->addNative(osType, iter.value().toString()); - iter++; - } - } - library->setRules(rulesFromJsonV4(libObj)); - library->finalize(); - fullVersion->libraries.append(library); - } - return fullVersion; +OneSixVersion::OneSixVersion(OneSixInstance *instance, QObject *parent) + : QAbstractListModel(parent), m_instance(instance) +{ + clear(); } -std::shared_ptr<OneSixVersion> OneSixVersion::fromJson(QJsonObject root) +bool OneSixVersion::reload(QWidget *widgetParent, const bool onlyVanilla) { - std::shared_ptr<OneSixVersion> readVersion(new OneSixVersion()); - int launcher_ver = readVersion->minimumLauncherVersion = - root.value("minimumLauncherVersion").toDouble(); + beginResetModel(); + bool ret = OneSixVersionBuilder::build(this, m_instance, widgetParent, onlyVanilla); + endResetModel(); + return ret; +} - // ADD MORE HERE :D - if (launcher_ver > 0 && launcher_ver <= 13) - return fromJsonV4(root, readVersion); - else - { - return std::shared_ptr<OneSixVersion>(); - } +void OneSixVersion::clear() +{ + beginResetModel(); + id.clear(); + time.clear(); + releaseTime.clear(); + type.clear(); + assets.clear(); + processArguments.clear(); + minecraftArguments.clear(); + minimumLauncherVersion = 0xDEADBEAF; + mainClass.clear(); + libraries.clear(); + tweakers.clear(); + versionFiles.clear(); + endResetModel(); } -std::shared_ptr<OneSixVersion> OneSixVersion::fromFile(QString filepath) +void OneSixVersion::dump() const { - QFile file(filepath); - if (!file.open(QIODevice::ReadOnly)) + qDebug().nospace() << "OneSixVersion(" + << "\n\tid=" << id + << "\n\ttime=" << time + << "\n\treleaseTime=" << releaseTime + << "\n\ttype=" << type + << "\n\tassets=" << assets + << "\n\tprocessArguments=" << processArguments + << "\n\tminecraftArguments=" << minecraftArguments + << "\n\tminimumLauncherVersion=" << minimumLauncherVersion + << "\n\tmainClass=" << mainClass + << "\n\tlibraries="; + for (auto lib : libraries) { - return std::shared_ptr<OneSixVersion>(); + qDebug().nospace() << "\n\t\t" << lib.get(); } + qDebug().nospace() << "\n)"; +} - auto data = file.readAll(); - QJsonParseError jsonError; - QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); - - if (jsonError.error != QJsonParseError::NoError) +bool OneSixVersion::canRemove(const int index) const +{ + if (index < versionFiles.size()) { - return std::shared_ptr<OneSixVersion>(); + return versionFiles.at(index).id != "org.multimc.version.json"; } + return false; +} - if (!jsonDoc.isObject()) +QString OneSixVersion::versionFileId(const int index) const +{ + if (index < 0 || index >= versionFiles.size()) { - return std::shared_ptr<OneSixVersion>(); + return QString(); } - QJsonObject root = jsonDoc.object(); - auto version = fromJson(root); - if (version) - version->original_file = filepath; - return version; + return versionFiles.at(index).id; } -bool OneSixVersion::toOriginalFile() +bool OneSixVersion::remove(const int index) { - if (original_file.isEmpty()) - return false; - QSaveFile file(original_file); - if (!file.open(QIODevice::WriteOnly)) - { - return false; - } - // serialize base attributes (those we care about anyway) - QJsonObject root; - root.insert("minecraftArguments", minecraftArguments); - root.insert("mainClass", mainClass); - root.insert("minimumLauncherVersion", minimumLauncherVersion); - root.insert("time", time); - root.insert("id", id); - root.insert("type", type); - // screw processArguments - root.insert("releaseTime", releaseTime); - QJsonArray libarray; - for (const auto &lib : libraries) + if (canRemove(index)) { - libarray.append(lib->toJson()); + return QFile::remove(versionFiles.at(index).filename); } - if (libarray.count()) - root.insert("libraries", libarray); - QJsonDocument doc(root); - file.write(doc.toJson()); - return file.commit(); + return false; } -QList<std::shared_ptr<OneSixLibrary>> OneSixVersion::getActiveNormalLibs() +QList<std::shared_ptr<OneSixLibrary> > OneSixVersion::getActiveNormalLibs() { - QList<std::shared_ptr<OneSixLibrary>> output; + QList<std::shared_ptr<OneSixLibrary> > output; for (auto lib : libraries) { if (lib->isActive() && !lib->isNative()) @@ -245,9 +112,9 @@ QList<std::shared_ptr<OneSixLibrary>> OneSixVersion::getActiveNormalLibs() return output; } -QList<std::shared_ptr<OneSixLibrary>> OneSixVersion::getActiveNativeLibs() +QList<std::shared_ptr<OneSixLibrary> > OneSixVersion::getActiveNativeLibs() { - QList<std::shared_ptr<OneSixLibrary>> output; + QList<std::shared_ptr<OneSixLibrary> > output; for (auto lib : libraries) { if (lib->isActive() && lib->isNative()) @@ -258,14 +125,14 @@ QList<std::shared_ptr<OneSixLibrary>> OneSixVersion::getActiveNativeLibs() return output; } -void OneSixVersion::externalUpdateStart() -{ - beginResetModel(); -} - -void OneSixVersion::externalUpdateFinish() +std::shared_ptr<OneSixVersion> OneSixVersion::fromJson(const QJsonObject &obj) { - endResetModel(); + std::shared_ptr<OneSixVersion> version(new OneSixVersion(0)); + if (OneSixVersionBuilder::read(version.get(), obj)) + { + return version; + } + return 0; } QVariant OneSixVersion::data(const QModelIndex &index, int role) const @@ -276,7 +143,7 @@ QVariant OneSixVersion::data(const QModelIndex &index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= libraries.size()) + if (row < 0 || row >= versionFiles.size()) return QVariant(); if (role == Qt::DisplayRole) @@ -284,11 +151,9 @@ QVariant OneSixVersion::data(const QModelIndex &index, int role) const switch (column) { case 0: - return libraries[row]->name(); + return versionFiles.at(row).name; case 1: - return libraries[row]->type(); - case 2: - return libraries[row]->version(); + return versionFiles.at(row).version; default: return QVariant(); } @@ -296,45 +161,61 @@ QVariant OneSixVersion::data(const QModelIndex &index, int role) const return QVariant(); } -Qt::ItemFlags OneSixVersion::flags(const QModelIndex &index) const +QVariant OneSixVersion::headerData(int section, Qt::Orientation orientation, int role) const { - if (!index.isValid()) - return Qt::NoItemFlags; - int row = index.row(); - if (libraries[row]->isActive()) - { - return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren; - } - else + if (orientation == Qt::Horizontal) { - return Qt::ItemNeverHasChildren; + if (role == Qt::DisplayRole) + { + switch (section) + { + case 0: + return tr("Name"); + case 1: + return tr("Version"); + default: + return QVariant(); + } + } } - // return QAbstractListModel::flags(index); + return QVariant(); } -QVariant OneSixVersion::headerData(int section, Qt::Orientation orientation, int role) const +Qt::ItemFlags OneSixVersion::flags(const QModelIndex &index) const { - if (role != Qt::DisplayRole || orientation != Qt::Horizontal) - return QVariant(); - switch (section) - { - case 0: - return QString("Name"); - case 1: - return QString("Type"); - case 2: - return QString("Version"); - default: - return QString(); - } + if (!index.isValid()) + return Qt::NoItemFlags; + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } int OneSixVersion::rowCount(const QModelIndex &parent) const { - return libraries.size(); + return versionFiles.size(); } int OneSixVersion::columnCount(const QModelIndex &parent) const { - return 3; + return 2; +} + +QDebug operator<<(QDebug &dbg, const OneSixVersion *version) +{ + version->dump(); + return dbg.maybeSpace(); +} +QDebug operator<<(QDebug &dbg, const OneSixLibrary *library) +{ + dbg.nospace() << "OneSixLibrary(" + << "\n\t\t\trawName=" << library->rawName() + << "\n\t\t\tname=" << library->name() + << "\n\t\t\tversion=" << library->version() + << "\n\t\t\ttype=" << library->type() + << "\n\t\t\tisActive=" << library->isActive() + << "\n\t\t\tisNative=" << library->isNative() + << "\n\t\t\tdownloadUrl=" << library->downloadUrl() + << "\n\t\t\tstoragePath=" << library->storagePath() + << "\n\t\t\tabsolutePath=" << library->absoluteUrl() + << "\n\t\t\thint=" << library->hint(); + dbg.nospace() << "\n\t\t)"; + return dbg.maybeSpace(); } diff --git a/logic/OneSixVersion.h b/logic/OneSixVersion.h index 036f3d53..ba7695d5 100644 --- a/logic/OneSixVersion.h +++ b/logic/OneSixVersion.h @@ -14,40 +14,50 @@ */ #pragma once -#include <QtCore> + +#include <QAbstractListModel> + +#include <QString> +#include <QList> #include <memory> -class OneSixLibrary; +#include "OneSixLibrary.h" + +class OneSixInstance; class OneSixVersion : public QAbstractListModel { - // Things required to implement the Qt list model + Q_OBJECT public: + explicit OneSixVersion(OneSixInstance *instance, QObject *parent = 0); + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; - virtual QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex &parent) const; virtual Qt::ItemFlags flags(const QModelIndex &index) const; - // serialization/deserialization -public: - bool toOriginalFile(); - static std::shared_ptr<OneSixVersion> fromJson(QJsonObject root); - static std::shared_ptr<OneSixVersion> fromFile(QString filepath); + bool reload(QWidget *widgetParent, const bool onlyVanilla = false); + void clear(); + + void dump() const; + + bool canRemove(const int index) const; + + QString versionFileId(const int index) const; + +public +slots: + bool remove(const int index); public: QList<std::shared_ptr<OneSixLibrary>> getActiveNormalLibs(); QList<std::shared_ptr<OneSixLibrary>> getActiveNativeLibs(); - // called when something starts/stops messing with the object - // FIXME: these are ugly in every possible way. - void externalUpdateStart(); - void externalUpdateFinish(); + + static std::shared_ptr<OneSixVersion> fromJson(const QJsonObject &obj); // data members public: - /// file this was read from. blank, if none - QString original_file; /// the ID - determines which jar to use! ACTUALLY IMPORTANT! QString id; /// Last updated time - as a string @@ -76,6 +86,10 @@ public: */ int minimumLauncherVersion = 0xDEADBEEF; /** + * A list of all tweaker classes + */ + QStringList tweakers; + /** * The main class to load first */ QString mainClass; @@ -103,4 +117,21 @@ public: } */ // QList<Rule> rules; + + struct VersionFile + { + QString name; + QString id; + QString version; + QString mcVersion; + QString filename; + int order; + }; + QList<VersionFile> versionFiles; + +private: + OneSixInstance *m_instance; }; + +QDebug operator<<(QDebug &dbg, const OneSixVersion *version); +QDebug operator<<(QDebug &dbg, const OneSixLibrary *library); diff --git a/logic/OneSixVersionBuilder.cpp b/logic/OneSixVersionBuilder.cpp new file mode 100644 index 00000000..bbd33ddc --- /dev/null +++ b/logic/OneSixVersionBuilder.cpp @@ -0,0 +1,1077 @@ +/* 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. + */ + +#include "OneSixVersionBuilder.h" + +#include <QList> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonDocument> +#include <QFile> +#include <QFileInfo> +#include <QMessageBox> +#include <QObject> +#include <QDir> +#include <QDebug> + +#include "OneSixVersion.h" +#include "OneSixInstance.h" +#include "OneSixRule.h" +#include "modutils.h" +#include "logger/QsLog.h" + +struct VersionFile +{ + int order; + QString name; + QString fileId; + QString version; + // TODO use the mcVersion to determine if a version file should be removed on update + QString mcVersion; + QString filename; + // TODO requirements + // QMap<QString, QString> requirements; + QString id; + QString mainClass; + QString overwriteMinecraftArguments; + QString addMinecraftArguments; + QString removeMinecraftArguments; + QString processArguments; + QString type; + QString releaseTime; + QString time; + QString assets; + int minimumLauncherVersion = -1; + + bool shouldOverwriteTweakers = false; + QStringList overwriteTweakers; + QStringList addTweakers; + QStringList removeTweakers; + + struct Library + { + QString name; + QString url; + QString hint; + QString absoluteUrl; + bool applyExcludes = false; + QStringList excludes; + bool applyNatives = false; + QList<QPair<OpSys, QString>> natives; + bool applyRules = false; + QList<std::shared_ptr<Rule>> rules; + + // user for '+' libraries + enum InsertType + { + Apply, + Append, + Prepend, + Replace + }; + InsertType insertType = Append; + QString insertData; + enum DependType + { + Soft, + Hard + }; + DependType dependType = Soft; + }; + bool shouldOverwriteLibs = false; + QList<Library> overwriteLibs; + QList<Library> addLibs; + QList<QString> removeLibs; + + static Library fromLibraryJson(const QJsonObject &libObj, const QString &filename, + bool &isError) + { + isError = true; + Library out; + if (!libObj.contains("name")) + { + QLOG_ERROR() << filename << "contains a library that doesn't have a 'name' field"; + return out; + } + out.name = libObj.value("name").toString(); + + auto readString = [libObj, filename](const QString &key, QString &variable) + { + if (libObj.contains(key)) + { + QJsonValue val = libObj.value(key); + if (!val.isString()) + { + QLOG_WARN() << key << "is not a string in" << filename << "(skipping)"; + } + else + { + variable = val.toString(); + } + } + }; + + readString("url", out.url); + readString("MMC-hint", out.hint); + readString("MMC-absulute_url", out.absoluteUrl); + readString("MMC-absoluteUrl", out.absoluteUrl); + if (libObj.contains("extract")) + { + if (!libObj.value("extract").isObject()) + { + QLOG_ERROR() + << filename + << "contains a library with an 'extract' field that's not an object"; + return out; + } + QJsonObject extractObj = libObj.value("extract").toObject(); + if (!extractObj.contains("exclude") || !extractObj.value("exclude").isArray()) + { + QLOG_ERROR() << filename + << "contains a library with an invalid 'extract' field"; + return out; + } + out.applyExcludes = true; + QJsonArray excludeArray = extractObj.value("exclude").toArray(); + for (auto excludeVal : excludeArray) + { + if (!excludeVal.isString()) + { + QLOG_WARN() << filename << "contains a library that contains an 'extract' " + "field that contains an invalid 'exclude' entry " + "(skipping)"; + } + else + { + out.excludes.append(excludeVal.toString()); + } + } + } + if (libObj.contains("natives")) + { + if (!libObj.value("natives").isObject()) + { + QLOG_ERROR() + << filename + << "contains a library with a 'natives' field that's not an object"; + return out; + } + out.applyNatives = true; + QJsonObject nativesObj = libObj.value("natives").toObject(); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + QLOG_WARN() << filename << "contains an invalid native (skipping)"; + } + OpSys opSys = OpSys_fromString(it.key()); + if (opSys != Os_Other) + { + out.natives.append(qMakePair(opSys, it.value().toString())); + } + } + } + if (libObj.contains("rules")) + { + out.applyRules = true; + out.rules = rulesFromJsonV4(libObj); + } + isError = false; + return out; + } + static VersionFile fromJson(const QJsonDocument &doc, const QString &filename, + const bool requireOrder, bool &isError) + { + VersionFile out; + isError = true; + if (doc.isEmpty() || doc.isNull()) + { + QLOG_ERROR() << filename << "is empty or null"; + return out; + } + if (!doc.isObject()) + { + QLOG_ERROR() << "The root of" << filename << "is not an object"; + return out; + } + + QJsonObject root = doc.object(); + + if (requireOrder) + { + if (root.contains("order")) + { + if (root.value("order").isDouble()) + { + out.order = root.value("order").toDouble(); + } + else + { + QLOG_ERROR() << "'order' field contains an invalid value in" << filename; + return out; + } + } + else + { + QLOG_ERROR() << filename << "doesn't contain an order field"; + } + } + + out.name = root.value("name").toString(); + out.fileId = root.value("fileId").toString(); + out.version = root.value("version").toString(); + out.mcVersion = root.value("mcVersion").toString(); + out.filename = filename; + + auto readString = [root, filename](const QString &key, QString &variable) + { + if (root.contains(key)) + { + QJsonValue val = root.value(key); + if (!val.isString()) + { + QLOG_WARN() << key << "is not a string in" << filename << "(skipping)"; + } + else + { + variable = val.toString(); + } + } + }; + + readString("id", out.id); + readString("mainClass", out.mainClass); + readString("processArguments", out.processArguments); + readString("minecraftArguments", out.overwriteMinecraftArguments); + readString("+minecraftArguments", out.addMinecraftArguments); + readString("-minecraftArguments", out.removeMinecraftArguments); + readString("type", out.type); + readString("releaseTime", out.releaseTime); + readString("time", out.time); + readString("assets", out.assets); + if (root.contains("minimumLauncherVersion")) + { + QJsonValue val = root.value("minimumLauncherVersion"); + if (!val.isDouble()) + { + QLOG_WARN() << "minimumLauncherVersion is not an int in" << filename + << "(skipping)"; + } + else + { + out.minimumLauncherVersion = val.toDouble(); + } + } + + if (root.contains("tweakers")) + { + QJsonValue tweakersVal = root.value("tweakers"); + if (!tweakersVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a 'tweakers' field, but it's not an array"; + return out; + } + out.shouldOverwriteTweakers = true; + QJsonArray tweakers = root.value("tweakers").toArray(); + for (auto tweakerVal : tweakers) + { + if (!tweakerVal.isString()) + { + QLOG_ERROR() << filename + << "contains a 'tweakers' field entry that's not a string"; + return out; + } + out.overwriteTweakers.append(tweakerVal.toString()); + } + } + if (root.contains("+tweakers")) + { + QJsonValue tweakersVal = root.value("+tweakers"); + if (!tweakersVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a '+tweakers' field, but it's not an array"; + return out; + } + QJsonArray tweakers = root.value("+tweakers").toArray(); + for (auto tweakerVal : tweakers) + { + if (!tweakerVal.isString()) + { + QLOG_ERROR() << filename + << "contains a '+tweakers' field entry that's not a string"; + return out; + } + out.addTweakers.append(tweakerVal.toString()); + } + } + if (root.contains("-tweakers")) + { + QJsonValue tweakersVal = root.value("-tweakers"); + if (!tweakersVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a '-tweakers' field, but it's not an array"; + return out; + } + out.shouldOverwriteTweakers = true; + QJsonArray tweakers = root.value("-tweakers").toArray(); + for (auto tweakerVal : tweakers) + { + if (!tweakerVal.isString()) + { + QLOG_ERROR() << filename + << "contains a '-tweakers' field entry that's not a string"; + return out; + } + out.removeTweakers.append(tweakerVal.toString()); + } + } + + if (root.contains("libraries")) + { + out.shouldOverwriteLibs = true; + QJsonValue librariesVal = root.value("libraries"); + if (!librariesVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a 'libraries' field, but its not an array"; + return out; + } + QJsonArray librariesArray = librariesVal.toArray(); + for (auto libVal : librariesArray) + { + if (!libVal.isObject()) + { + QLOG_ERROR() << filename << "contains a library that's not an object"; + return out; + } + QJsonObject libObj = libVal.toObject(); + bool error; + Library lib = fromLibraryJson(libObj, filename, error); + if (error) + { + QLOG_ERROR() << "Error while reading a library entry in" << filename; + return out; + } + out.overwriteLibs.append(lib); + } + } + if (root.contains("+libraries")) + { + QJsonValue librariesVal = root.value("+libraries"); + if (!librariesVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a '+libraries' field, but its not an array"; + return out; + } + QJsonArray librariesArray = librariesVal.toArray(); + for (auto libVal : librariesArray) + { + if (!libVal.isObject()) + { + QLOG_ERROR() << filename << "contains a library that's not an object"; + return out; + } + QJsonObject libObj = libVal.toObject(); + bool error; + Library lib = fromLibraryJson(libObj, filename, error); + if (error) + { + QLOG_ERROR() << "Error while reading a library entry in" << filename; + return out; + } + if (!libObj.contains("insert")) + { + QLOG_ERROR() << "Missing 'insert' field in '+libraries' field in" + << filename; + return out; + } + QJsonValue insertVal = libObj.value("insert"); + QString insertString; + { + if (insertVal.isString()) + { + insertString = insertVal.toString(); + } + else if (insertVal.isObject()) + { + QJsonObject insertObj = insertVal.toObject(); + if (insertObj.isEmpty()) + { + QLOG_ERROR() << "One library has an empty insert object in" + << filename; + return out; + } + insertString = insertObj.keys().first(); + lib.insertData = insertObj.value(insertString).toString(); + } + } + if (insertString == "apply") + { + lib.insertType = Library::Apply; + } + else if (insertString == "prepend") + { + lib.insertType = Library::Prepend; + } + else if (insertString == "append") + { + lib.insertType = Library::Prepend; + } + else if (insertString == "replace") + { + lib.insertType = Library::Replace; + } + else + { + QLOG_ERROR() << "A '+' library in" << filename + << "contains an invalid insert type"; + return out; + } + if (libObj.contains("MMC-depend") && libObj.value("MMC-depend").isString()) + { + const QString dependString = libObj.value("MMC-depend").toString(); + if (dependString == "hard") + { + lib.dependType = Library::Hard; + } + else if (dependString == "soft") + { + lib.dependType = Library::Soft; + } + else + { + QLOG_ERROR() << "A '+' library in" << filename + << "contains an invalid depend type"; + return out; + } + } + out.addLibs.append(lib); + } + } + if (root.contains("-libraries")) + { + QJsonValue librariesVal = root.value("-libraries"); + if (!librariesVal.isArray()) + { + QLOG_ERROR() << filename + << "contains a '-libraries' field, but its not an array"; + return out; + } + QJsonArray librariesArray = librariesVal.toArray(); + for (auto libVal : librariesArray) + { + if (!libVal.isObject()) + { + QLOG_ERROR() << filename << "contains a library that's not an object"; + return out; + } + QJsonObject libObj = libVal.toObject(); + if (!libObj.contains("name")) + { + QLOG_ERROR() << filename << "contains a library without a name"; + return out; + } + if (!libObj.value("name").isString()) + { + QLOG_ERROR() << filename + << "contains a library without a valid 'name' field"; + return out; + } + out.removeLibs.append(libObj.value("name").toString()); + } + } + + isError = false; + return out; + } + + static std::shared_ptr<OneSixLibrary> createLibrary(const Library &lib) + { + std::shared_ptr<OneSixLibrary> out(new OneSixLibrary(lib.name)); + if (!lib.url.isEmpty()) + { + out->setBaseUrl(lib.url); + } + out->setHint(lib.hint); + if (!lib.absoluteUrl.isEmpty()) + { + out->setAbsoluteUrl(lib.absoluteUrl); + } + out->setAbsoluteUrl(lib.absoluteUrl); + out->extract_excludes = lib.excludes; + for (auto native : lib.natives) + { + out->addNative(native.first, native.second); + } + out->setRules(lib.rules); + out->finalize(); + return out; + } + int findLibrary(QList<std::shared_ptr<OneSixLibrary>> haystack, const QString &needle) + { + for (int i = 0; i < haystack.size(); ++i) + { + if (QRegExp(needle, Qt::CaseSensitive, QRegExp::WildcardUnix) + .indexIn(haystack.at(i)->rawName()) != -1) + { + return i; + } + } + return -1; + } + void applyTo(OneSixVersion *version, bool &isError) + { + isError = true; + if (!version->id.isNull() && !mcVersion.isNull()) + { + if (QRegExp(mcVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(version->id) == -1) + { + QLOG_ERROR() << filename << "is for a different version of Minecraft"; + return; + } + } + + if (!id.isNull()) + { + version->id = id; + } + if (!mainClass.isNull()) + { + version->mainClass = mainClass; + } + if (!processArguments.isNull()) + { + version->processArguments = processArguments; + } + if (!type.isNull()) + { + version->type = type; + } + if (!releaseTime.isNull()) + { + version->releaseTime = releaseTime; + } + if (!time.isNull()) + { + version->time = time; + } + if (!assets.isNull()) + { + version->assets = assets; + } + if (minimumLauncherVersion >= 0) + { + version->minimumLauncherVersion = minimumLauncherVersion; + } + if (!overwriteMinecraftArguments.isNull()) + { + version->minecraftArguments = overwriteMinecraftArguments; + } + if (!addMinecraftArguments.isNull()) + { + version->minecraftArguments += addMinecraftArguments; + } + if (!removeMinecraftArguments.isNull()) + { + version->minecraftArguments.remove(removeMinecraftArguments); + } + if (shouldOverwriteTweakers) + { + version->tweakers = overwriteTweakers; + } + for (auto tweaker : addTweakers) + { + version->tweakers += tweaker; + } + for (auto tweaker : removeTweakers) + { + version->tweakers.removeAll(tweaker); + } + if (shouldOverwriteLibs) + { + version->libraries.clear(); + for (auto lib : overwriteLibs) + { + version->libraries.append(createLibrary(lib)); + } + } + for (auto lib : addLibs) + { + switch (lib.insertType) + { + case Library::Apply: + { + + int index = findLibrary(version->libraries, lib.name); + if (index >= 0) + { + auto library = version->libraries[index]; + if (!lib.url.isNull()) + { + library->setBaseUrl(lib.url); + } + if (!lib.hint.isNull()) + { + library->setHint(lib.hint); + } + if (!lib.absoluteUrl.isNull()) + { + library->setAbsoluteUrl(lib.absoluteUrl); + } + if (lib.applyExcludes) + { + library->extract_excludes = lib.excludes; + } + if (lib.applyNatives) + { + library->clearSuffixes(); + for (auto native : lib.natives) + { + library->addNative(native.first, native.second); + } + } + if (lib.applyRules) + { + library->setRules(lib.rules); + } + library->finalize(); + } + else + { + QLOG_WARN() << "Couldn't find" << lib.insertData << "(skipping)"; + } + break; + } + case Library::Append: + case Library::Prepend: + { + + const int startOfVersion = lib.name.lastIndexOf(':') + 1; + const int index = + findLibrary(version->libraries, + QString(lib.name).replace(startOfVersion, INT_MAX, '*')); + if (index < 0) + { + if (lib.insertType == Library::Append) + { + version->libraries.append(createLibrary(lib)); + } + else + { + version->libraries.prepend(createLibrary(lib)); + } + } + else + { + auto otherLib = version->libraries.at(index); + const Util::Version ourVersion = lib.name.mid(startOfVersion, INT_MAX); + const Util::Version otherVersion = otherLib->version(); + // if the existing version is a hard dependency we can either use it or + // fail, but we can't change it + if (otherLib->dependType == OneSixLibrary::Hard) + { + // we need a higher version, or we're hard to and the versions aren't + // equal + if (ourVersion > otherVersion || + (lib.dependType == Library::Hard && ourVersion != otherVersion)) + { + QLOG_ERROR() << "Error resolving library dependencies between" + << otherLib->rawName() << "and" << lib.name << "in" + << filename; + return; + } + else + { + // the library is already existing, so we don't have to do anything + } + } + else if (otherLib->dependType == OneSixLibrary::Soft) + { + // if we are higher it means we should update + if (ourVersion > otherVersion) + { + auto library = createLibrary(lib); + if (Util::Version(otherLib->minVersion) < ourVersion) + { + library->minVersion = ourVersion.toString(); + } + version->libraries.replace(index, library); + } + else + { + // our version is smaller than the existing version, but we require + // it: fail + if (lib.dependType == Library::Hard) + { + QLOG_ERROR() << "Error resolving library dependencies between" + << otherLib->rawName() << "and" << lib.name << "in" + << filename; + return; + } + } + } + } + break; + } + case Library::Replace: + { + int index = findLibrary(version->libraries, lib.insertData); + if (index >= 0) + { + version->libraries.replace(index, createLibrary(lib)); + } + else + { + QLOG_WARN() << "Couldn't find" << lib.insertData << "(skipping)"; + } + break; + } + } + } + for (auto lib : removeLibs) + { + int index = findLibrary(version->libraries, lib); + if (index >= 0) + { + version->libraries.removeAt(index); + } + else + { + QLOG_WARN() << "Couldn't find" << lib << "(skipping)"; + } + } + + OneSixVersion::VersionFile versionFile; + versionFile.name = name; + versionFile.id = fileId; + versionFile.version = this->version; + versionFile.mcVersion = mcVersion; + versionFile.filename = filename; + versionFile.order = order; + version->versionFiles.append(versionFile); + + isError = false; + } +}; + +OneSixVersionBuilder::OneSixVersionBuilder() +{ +} + +bool OneSixVersionBuilder::build(OneSixVersion *version, OneSixInstance *instance, + QWidget *widgetParent, const bool onlyVanilla) +{ + OneSixVersionBuilder builder; + builder.m_version = version; + builder.m_instance = instance; + builder.m_widgetParent = widgetParent; + return builder.build(onlyVanilla); +} + +bool OneSixVersionBuilder::read(OneSixVersion *version, const QJsonObject &obj) +{ + OneSixVersionBuilder builder; + builder.m_version = version; + builder.m_instance = 0; + builder.m_widgetParent = 0; + return builder.read(obj); +} + +bool OneSixVersionBuilder::build(const bool onlyVanilla) +{ + m_version->clear(); + + QDir root(m_instance->instanceRoot()); + QDir patches(root.absoluteFilePath("patches/")); + + if (QFile::exists(root.absoluteFilePath("custom.json"))) + { + QLOG_INFO() << "Reading custom.json"; + VersionFile file; + if (!read(QFileInfo(root.absoluteFilePath("custom.json")), false, &file)) + { + return false; + } + file.name = "custom.json"; + file.filename = "custom.json"; + file.fileId = "org.multimc.custom.json"; + file.version = QString(); + bool isError = false; + file.applyTo(m_version, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr( + "Error while applying %1. Please check MultiMC-0.log for more info.") + .arg(root.absoluteFilePath("custom.json"))); + return false; + } + } + else + { + // version.json -> patches/*.json -> user.json + + // version.json + { + QLOG_INFO() << "Reading version.json"; + VersionFile file; + if (!read(QFileInfo(root.absoluteFilePath("version.json")), false, &file)) + { + return false; + } + file.name = "version.json"; + file.fileId = "org.multimc.version.json"; + file.version = m_instance->intendedVersionId(); + file.mcVersion = m_instance->intendedVersionId(); + bool isError = false; + file.applyTo(m_version, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr( + "Error while applying %1. Please check MultiMC-0.log for more info.") + .arg(root.absoluteFilePath("version.json"))); + return false; + } + } + + if (!onlyVanilla) + { + + // patches/ + { + // load all, put into map for ordering, apply in the right order + QMap<QString, int> overrideOrder = readOverrideOrders(m_instance); + + QMap<int, QPair<QString, VersionFile>> files; + for (auto info : patches.entryInfoList(QStringList() << "*.json", QDir::Files)) + { + QLOG_INFO() << "Reading" << info.fileName(); + VersionFile file; + if (!read(info, true, &file)) + { + return false; + } + if (overrideOrder.contains(file.fileId)) + { + file.order = overrideOrder.value(file.fileId); + } + if (files.contains(file.order)) + { + QLOG_ERROR() << file.fileId << "has the same order as" << files[file.order].second.fileId; + return false; + } + files.insert(file.order, qMakePair(info.fileName(), file)); + } + for (auto order : files.keys()) + { + QLOG_DEBUG() << "Applying file with order" << order; + auto filePair = files[order]; + bool isError = false; + filePair.second.applyTo(m_version, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr("Error while applying %1. Please check MultiMC-0.log " + "for more info.").arg(filePair.first)); + return false; + } + } + } + +#if 0 + // user.json + { + if (QFile::exists(root.absoluteFilePath("user.json"))) + { + QLOG_INFO() << "Reading user.json"; + VersionFile file; + if (!read(QFileInfo(root.absoluteFilePath("user.json")), false, &file)) + { + return false; + } + file.name = "user.json"; + file.fileId = "org.multimc.user.json"; + file.version = QString(); + file.mcVersion = QString(); + bool isError = false; + file.applyTo(m_version, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr( + "Error while applying %1. Please check MultiMC-0.log for more info.") + .arg(root.absoluteFilePath("user.json"))); + return false; + } + } + } +#endif + } + } + + // some final touches + { + if (m_version->assets.isEmpty()) + { + m_version->assets = "legacy"; + } + if (m_version->minecraftArguments.isEmpty()) + { + QString toCompare = m_version->processArguments.toLower(); + if (toCompare == "legacy") + { + m_version->minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if (toCompare == "username_session") + { + m_version->minecraftArguments = + "--username ${auth_player_name} --session ${auth_session}"; + } + else if (toCompare == "username_session_version") + { + m_version->minecraftArguments = "--username ${auth_player_name} " + "--session ${auth_session} " + "--version ${profile_name}"; + } + } + } + + return true; +} + +bool OneSixVersionBuilder::read(const QJsonObject &obj) +{ + m_version->clear(); + + bool isError = false; + VersionFile file = VersionFile::fromJson(QJsonDocument(obj), QString(), false, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr("Error while reading. Please check MultiMC-0.log for more info.")); + return false; + } + file.applyTo(m_version, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr("Error while applying. Please check MultiMC-0.log for more info.")); + return false; + } + + return true; +} + +bool OneSixVersionBuilder::read(const QFileInfo &fileInfo, const bool requireOrder, + VersionFile *out) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr("Unable to open %1: %2").arg(file.fileName(), file.errorString())); + return false; + } + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + QMessageBox::critical(m_widgetParent, QObject::tr("Error"), + QObject::tr("Unable to parse %1: %2 at %3") + .arg(file.fileName(), error.errorString()) + .arg(error.offset)); + return false; + } + bool isError = false; + *out = VersionFile::fromJson(doc, file.fileName(), requireOrder, isError); + if (isError) + { + QMessageBox::critical( + m_widgetParent, QObject::tr("Error"), + QObject::tr("Error while reading %1. Please check MultiMC-0.log for more info.") + .arg(file.fileName())); + ; + } + return true; +} + +QMap<QString, int> OneSixVersionBuilder::readOverrideOrders(OneSixInstance *instance) +{ + QMap<QString, int> out; + if (QDir(instance->instanceRoot()).exists("order.json")) + { + QFile orderFile(instance->instanceRoot() + "/order.json"); + if (!orderFile.open(QFile::ReadOnly)) + { + QLOG_ERROR() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); + QLOG_WARN() << "Ignoring overriden order"; + } + else + { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError || !doc.isObject()) + { + QLOG_ERROR() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + QLOG_WARN() << "Ignoring overriden order"; + } + else + { + QJsonObject obj = doc.object(); + for (auto it = obj.begin(); it != obj.end(); ++it) + { + if (it.key().startsWith("org.multimc.")) + { + continue; + } + out.insert(it.key(), it.value().toDouble()); + } + } + } + } + return out; +} +bool OneSixVersionBuilder::writeOverrideOrders(const QMap<QString, int> &order, OneSixInstance *instance) +{ + QJsonObject obj; + for (auto it = order.cbegin(); it != order.cend(); ++it) + { + if (it.key().startsWith("org.multimc.")) + { + continue; + } + obj.insert(it.key(), it.value()); + } + QFile orderFile(instance->instanceRoot() + "/order.json"); + if (!orderFile.open(QFile::WriteOnly)) + { + QLOG_ERROR() << "Couldn't open" << orderFile.fileName() << "for writing:" << orderFile.errorString(); + return false; + } + orderFile.write(QJsonDocument(obj).toJson(QJsonDocument::Indented)); + return true; +} diff --git a/logic/OneSixVersionBuilder.h b/logic/OneSixVersionBuilder.h new file mode 100644 index 00000000..ab0966df --- /dev/null +++ b/logic/OneSixVersionBuilder.h @@ -0,0 +1,47 @@ +/* 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. + */ + +#pragma once + +#include <QString> +#include <QMap> + +class OneSixVersion; +class OneSixInstance; +class QWidget; +class QJsonObject; +class QFileInfo; +class VersionFile; + +class OneSixVersionBuilder +{ + OneSixVersionBuilder(); +public: + static bool build(OneSixVersion *version, OneSixInstance *instance, QWidget *widgetParent, const bool onlyVanilla); + static bool read(OneSixVersion *version, const QJsonObject &obj); + static QMap<QString, int> readOverrideOrders(OneSixInstance *instance); + static bool writeOverrideOrders(const QMap<QString, int> &order, OneSixInstance *instance); + +private: + OneSixVersion *m_version; + OneSixInstance *m_instance; + QWidget *m_widgetParent; + + bool build(const bool onlyVanilla); + bool read(const QJsonObject &obj); + + bool read(const QFileInfo &fileInfo, const bool requireOrder, VersionFile *out); + +}; diff --git a/logic/auth/AuthSession.cpp b/logic/auth/AuthSession.cpp new file mode 100644 index 00000000..8758bfbd --- /dev/null +++ b/logic/auth/AuthSession.cpp @@ -0,0 +1,30 @@ +#include "AuthSession.h" +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonDocument> +#include <QStringList> + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); + +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + if (status != PlayableOffline && status != PlayableOnline) + { + return false; + } + session = "-"; + player_name = offline_playername; + status = PlayableOffline; + return true; +} diff --git a/logic/auth/AuthSession.h b/logic/auth/AuthSession.h new file mode 100644 index 00000000..2ac170fa --- /dev/null +++ b/logic/auth/AuthSession.h @@ -0,0 +1,49 @@ +#pragma once + +#include <QString> +#include <QMultiMap> +#include <memory> + +struct User +{ + QString id; + QMultiMap<QString, QString> properties; +}; + +struct AuthSession +{ + bool MakeOffline(QString offline_playername); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresPassword, + PlayableOffline, + PlayableOnline + } status = Undetermined; + + User u; + + // client token + QString client_token; + // account user name + QString username; + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; +}; + +typedef std::shared_ptr<AuthSession> AuthSessionPtr; diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp index f41985ec..6c937ef1 100644 --- a/logic/auth/MojangAccount.cpp +++ b/logic/auth/MojangAccount.cpp @@ -24,6 +24,7 @@ #include <QJsonArray> #include <QRegExp> #include <QStringList> +#include <QJsonDocument> #include <logger/QsLog.h> @@ -165,15 +166,26 @@ AccountStatus MojangAccount::accountStatus() const { if (m_accessToken.isEmpty()) return NotVerified; - if (!m_online) + else return Verified; - return Online; } -std::shared_ptr<YggdrasilTask> MojangAccount::login(QString password) +std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session, + QString password) { - if (m_currentTask) - return m_currentTask; + Q_ASSERT(m_currentTask.get() == nullptr); + + // take care of the true offline status + if (accountStatus() == NotVerified && password.isEmpty()) + { + if (session) + { + session->status = AuthSession::RequiresPassword; + fillSession(session); + } + return nullptr; + } + if (password.isEmpty()) { m_currentTask.reset(new RefreshTask(this)); @@ -182,6 +194,8 @@ std::shared_ptr<YggdrasilTask> MojangAccount::login(QString password) { m_currentTask.reset(new AuthenticateTask(this, password)); } + m_currentTask->assignSession(session); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); return m_currentTask; @@ -189,24 +203,76 @@ std::shared_ptr<YggdrasilTask> MojangAccount::login(QString password) void MojangAccount::authSucceeded() { - m_online = true; + auto session = m_currentTask->getAssignedSession(); + if (session) + { + session->status = + session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline; + fillSession(session); + session->auth_server_online = true; + } m_currentTask.reset(); emit changed(); } void MojangAccount::authFailed(QString reason) { + auto session = m_currentTask->getAssignedSession(); // This is emitted when the yggdrasil tasks time out or are cancelled. // -> we treat the error as no-op if (reason == "Yggdrasil task cancelled.") { - // do nothing + if (session) + { + session->status = accountStatus() == Verified ? AuthSession::PlayableOffline + : AuthSession::RequiresPassword; + session->auth_server_online = false; + fillSession(session); + } } else { - m_online = false; m_accessToken = QString(); emit changed(); + if (session) + { + session->status = AuthSession::RequiresPassword; + session->auth_server_online = true; + fillSession(session); + } } m_currentTask.reset(); } + +void MojangAccount::fillSession(AuthSessionPtr session) +{ + // the user name. you have to have an user name + session->username = m_username; + // volatile auth token + session->access_token = m_accessToken; + // the semi-permanent client token + session->client_token = m_clientToken; + if (currentProfile()) + { + // profile name + session->player_name = currentProfile()->name; + // profile ID + session->uuid = currentProfile()->id; + // 'legacy' or 'mojang', depending on account type + session->user_type = currentProfile()->legacy ? "legacy" : "mojang"; + if (!session->access_token.isEmpty()) + { + session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + } + else + { + session->session = "-"; + } + } + else + { + session->player_name = "Player"; + session->session = "-"; + } + session->u = user(); +} diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h index dd5d54ae..a0565e2c 100644 --- a/logic/auth/MojangAccount.h +++ b/logic/auth/MojangAccount.h @@ -23,6 +23,7 @@ #include <QMap> #include <memory> +#include "AuthSession.h" class Task; class YggdrasilTask; @@ -45,17 +46,10 @@ struct AccountProfile bool legacy; }; -struct User -{ - QString id; - QMultiMap<QString,QString> properties; -}; - enum AccountStatus { NotVerified, - Verified, - Online + Verified }; /** @@ -84,7 +78,7 @@ public: /* construction */ QJsonObject saveToJson() const; public: /* manipulation */ - /** + /** * Sets the currently selected profile to the profile with the given ID string. * If profileId is not in the list of available profiles, the function will simply return * false. @@ -95,12 +89,9 @@ public: /* manipulation */ * Attempt to login. Empty password means we use the token. * If the attempt fails because we already are performing some task, it returns false. */ - std::shared_ptr<YggdrasilTask> login(QString password = QString()); + std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session, + QString password = QString()); - void downgrade() - { - m_online = false; - } public: /* queries */ const QString &username() const { @@ -122,19 +113,11 @@ public: /* queries */ return m_profiles; } - const User & user() + const User &user() { return m_user; } - //! Get the session ID required for legacy Minecraft versions - QString sessionId() const - { - if (m_currentProfile != -1 && !m_accessToken.isEmpty()) - return "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; - return "-"; - } - //! Returns the currently selected profile (if none, returns nullptr) const AccountProfile *currentProfile() const; @@ -169,16 +152,17 @@ protected: /* variables */ // the user structure, whatever it is. User m_user; - // true when the account is verified - bool m_online = false; - // current task we are executing here std::shared_ptr<YggdrasilTask> m_currentTask; -private slots: +private +slots: void authSucceeded(); void authFailed(QString reason); +private: + void fillSession(AuthSessionPtr session); + public: friend class YggdrasilTask; friend class AuthenticateTask; diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h index 85f5a1e1..4a87067a 100644 --- a/logic/auth/YggdrasilTask.h +++ b/logic/auth/YggdrasilTask.h @@ -36,6 +36,21 @@ public: explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); /** + * assign a session to this task. the session will be filled with required infomration + * upon completion + */ + void assignSession(AuthSessionPtr session) + { + m_session = session; + } + + /// get the assigned session for filling with information. + AuthSessionPtr getAssignedSession() + { + return m_session; + } + + /** * Class describing a Yggdrasil error response. */ struct Error @@ -117,4 +132,6 @@ protected: const int timeout_max = 10000; const int time_step = 50; + + AuthSessionPtr m_session; }; diff --git a/logic/auth/flows/RefreshTask.cpp b/logic/auth/flows/RefreshTask.cpp index f63c736e..5a55ed91 100644 --- a/logic/auth/flows/RefreshTask.cpp +++ b/logic/auth/flows/RefreshTask.cpp @@ -25,8 +25,7 @@ #include "logger/QsLog.h" -RefreshTask::RefreshTask(MojangAccount *account, QObject *parent) - : YggdrasilTask(account, parent) +RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account) { } @@ -126,7 +125,6 @@ bool RefreshTask::processResponse(QJsonObject responseData) m_account->m_user = u; } - // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. QLOG_DEBUG() << "Finished reading refresh response."; diff --git a/logic/auth/flows/RefreshTask.h b/logic/auth/flows/RefreshTask.h index 2fd50c60..0dadc025 100644 --- a/logic/auth/flows/RefreshTask.h +++ b/logic/auth/flows/RefreshTask.h @@ -30,7 +30,7 @@ class RefreshTask : public YggdrasilTask { Q_OBJECT public: - RefreshTask(MojangAccount * account, QObject *parent = 0); + RefreshTask(MojangAccount * account); protected: virtual QJsonObject getRequestContent() const; @@ -41,3 +41,4 @@ protected: QString getStateMessage(const YggdrasilTask::State state) const; }; + diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp index 0d4eab95..cd59e6d6 100644 --- a/logic/lists/InstanceList.cpp +++ b/logic/lists/InstanceList.cpp @@ -33,6 +33,7 @@ #include "logic/BaseInstance.h" #include "logic/InstanceFactory.h" #include "logger/QsLog.h" +#include <gui/groupview/GroupView.h> const static int GROUP_FILE_FORMAT_VERSION = 1; @@ -46,6 +47,13 @@ InstanceList::InstanceList(const QString &instDir, QObject *parent) QDir::current().mkpath(m_instDir); } + /* + * FIXME HACK: instances sometimes need to be created at launch. They need the versions for + * that. + * + * Remove this. it has no business of reloading the whole list. The instances which + * need it should track such events themselves and CHANGE THEIR DATA ONLY! + */ connect(MMC->minecraftlist().get(), &MinecraftVersionList::modelReset, this, &InstanceList::loadList); } @@ -96,8 +104,7 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const return MMC->icons()->getIcon(key); } // for now. - case KCategorizedSortFilterProxyModel::CategorySortRole: - case KCategorizedSortFilterProxyModel::CategoryDisplayRole: + case GroupViewRoles::GroupRole: { return pdata->group(); } @@ -282,16 +289,7 @@ void InstanceList::loadGroupList(QMap<QString, QString> &groupMap) } } -struct FTBRecord -{ - QString dir; - QString name; - QString logo; - QString mcVersion; - QString description; -}; - -void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) +QList<FTBRecord> InstanceList::discoverFTBInstances() { QList<FTBRecord> records; QDir dir = QDir(MMC->settings()->get("FTBLauncherRoot").toString()); @@ -300,18 +298,18 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) { QLOG_INFO() << "The FTB launcher directory specified does not exist. Please check your " "settings."; - return; + return records; } else if (!dataDir.exists()) { QLOG_INFO() << "The FTB directory specified does not exist. Please check your settings"; - return; + return records; } dir.cd("ModPacks"); auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name); - for(auto filename: allFiles) + for (auto filename : allFiles) { - if(!filename.endsWith(".xml")) + if (!filename.endsWith(".xml")) continue; auto fpath = dir.absoluteFilePath(filename); QFile f(fpath); @@ -331,11 +329,15 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) { QXmlStreamAttributes attrs = reader.attributes(); FTBRecord record; - record.dir = attrs.value("dir").toString(); - QDir test(dataDir.absoluteFilePath(record.dir)); - if(!test.exists()) + record.dirName = attrs.value("dir").toString(); + record.instanceDir = dataDir.absoluteFilePath(record.dirName); + record.templateDir = dir.absoluteFilePath(record.dirName); + QDir test(record.instanceDir); + if (!test.exists()) continue; record.name = attrs.value("name").toString(); + if(record.name.contains("voxel", Qt::CaseInsensitive)) + continue; record.logo = attrs.value("logo").toString(); record.mcVersion = attrs.value("mcVersion").toString(); record.description = attrs.value("description").toString(); @@ -353,8 +355,14 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) } f.close(); } + return records; +} - if(!records.size()) +void InstanceList::loadFTBInstances(QMap<QString, QString> &groupMap, + QList<InstancePtr> &tempList) +{ + auto records = discoverFTBInstances(); + if (!records.size()) { QLOG_INFO() << "No FTB instances to load."; return; @@ -363,20 +371,13 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) // process the records we acquired. for (auto record : records) { - auto instanceDir = dataDir.absoluteFilePath(record.dir); - QLOG_INFO() << "Loading FTB instance from " << instanceDir; - auto templateDir = dir.absoluteFilePath(record.dir); - if (!QFileInfo(instanceDir).exists()) - { - continue; - } - + QLOG_INFO() << "Loading FTB instance from " << record.instanceDir; QString iconKey = record.logo; iconKey.remove(QRegularExpression("\\..*")); - MMC->icons()->addIcon(iconKey, iconKey, PathCombine(templateDir, record.logo), + MMC->icons()->addIcon(iconKey, iconKey, PathCombine(record.templateDir, record.logo), MMCIcon::Transient); - if (!QFileInfo(PathCombine(instanceDir, "instance.cfg")).exists()) + if (!QFileInfo(PathCombine(record.instanceDir, "instance.cfg")).exists()) { QLOG_INFO() << "Converting " << record.name << " as new."; BaseInstance *instPtr = NULL; @@ -384,12 +385,12 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) auto version = MMC->minecraftlist()->findVersion(record.mcVersion); if (!version) { - QLOG_ERROR() << "Can't load instance " << instanceDir + QLOG_ERROR() << "Can't load instance " << record.instanceDir << " because minecraft version " << record.mcVersion << " can't be resolved."; continue; } - auto error = factory.createInstance(instPtr, version, instanceDir, + auto error = factory.createInstance(instPtr, version, record.instanceDir, InstanceFactory::FTBInstance); if (!instPtr || error != InstanceFactory::NoCreateError) @@ -400,13 +401,15 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) instPtr->setIconKey(iconKey); instPtr->setIntendedVersionId(record.mcVersion); instPtr->setNotes(record.description); - continueProcessInstance(instPtr, error, instanceDir, groupMap); + if(!continueProcessInstance(instPtr, error, record.instanceDir, groupMap)) + continue; + tempList.append(InstancePtr(instPtr)); } else { QLOG_INFO() << "Loading existing " << record.name; BaseInstance *instPtr = NULL; - auto error = InstanceFactory::get().loadInstance(instPtr, instanceDir); + auto error = InstanceFactory::get().loadInstance(instPtr, record.instanceDir); if (!instPtr || error != InstanceFactory::NoCreateError) continue; instPtr->setGroupInitial("FTB"); @@ -415,7 +418,9 @@ void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap) if (instPtr->intendedVersionId() != record.mcVersion) instPtr->setIntendedVersionId(record.mcVersion); instPtr->setNotes(record.description); - continueProcessInstance(instPtr, error, instanceDir, groupMap); + if(!continueProcessInstance(instPtr, error, record.instanceDir, groupMap)) + continue; + tempList.append(InstancePtr(instPtr)); } } } @@ -426,10 +431,7 @@ InstanceList::InstListError InstanceList::loadList() QMap<QString, QString> groupMap; loadGroupList(groupMap); - beginResetModel(); - - m_instances.clear(); - + QList<InstancePtr> tempList; { QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, QDirIterator::FollowSymlinks); @@ -441,15 +443,28 @@ InstanceList::InstListError InstanceList::loadList() QLOG_INFO() << "Loading MultiMC instance from " << subDir; BaseInstance *instPtr = NULL; auto error = InstanceFactory::get().loadInstance(instPtr, subDir); - continueProcessInstance(instPtr, error, subDir, groupMap); + if(!continueProcessInstance(instPtr, error, subDir, groupMap)) + continue; + tempList.append(InstancePtr(instPtr)); } } if (MMC->settings()->get("TrackFTBInstances").toBool()) { - loadForgeInstances(groupMap); + loadFTBInstances(groupMap, tempList); + } + beginResetModel(); + m_instances.clear(); + for(auto inst: tempList) + { + inst->setParent(this); + connect(inst.get(), SIGNAL(propertiesChanged(BaseInstance *)), this, + SLOT(propertiesChanged(BaseInstance *))); + connect(inst.get(), SIGNAL(groupChanged()), this, SLOT(groupChanged())); + connect(inst.get(), SIGNAL(nuked(BaseInstance *)), this, + SLOT(instanceNuked(BaseInstance *))); + m_instances.append(inst); } - endResetModel(); emit dataIsInvalid(); return NoError; @@ -523,7 +538,7 @@ int InstanceList::getInstIndex(BaseInstance *inst) const return -1; } -void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int error, +bool InstanceList::continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir, QMap<QString, QString> &groupMap) { if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance) @@ -539,12 +554,14 @@ void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int erro break; } QLOG_ERROR() << errorMsg.toUtf8(); + return false; } else if (!instPtr) { QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.") .arg(QFileInfo(dir.absolutePath()).baseName()) .toUtf8(); + return false; } else { @@ -554,13 +571,7 @@ void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int erro instPtr->setGroupInitial((*iter)); } QLOG_INFO() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath(); - instPtr->setParent(this); - m_instances.append(std::shared_ptr<BaseInstance>(instPtr)); - connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this, - SLOT(propertiesChanged(BaseInstance *))); - connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged())); - connect(instPtr, SIGNAL(nuked(BaseInstance *)), this, - SLOT(instanceNuked(BaseInstance *))); + return true; } } @@ -584,11 +595,8 @@ void InstanceList::propertiesChanged(BaseInstance *inst) } } -InstanceProxyModel::InstanceProxyModel(QObject *parent) - : KCategorizedSortFilterProxyModel(parent) +InstanceProxyModel::InstanceProxyModel(QObject *parent) : GroupedProxyModel(parent) { - // disable since by default we are globally sorting by date: - setCategorizedModel(true); } bool InstanceProxyModel::subSortLessThan(const QModelIndex &left, diff --git a/logic/lists/InstanceList.h b/logic/lists/InstanceList.h index 0ce808e5..ebe3e051 100644 --- a/logic/lists/InstanceList.h +++ b/logic/lists/InstanceList.h @@ -18,7 +18,7 @@ #include <QObject> #include <QAbstractListModel> #include <QSet> -#include "categorizedsortfilterproxymodel.h" +#include <gui/groupview/GroupedProxyModel.h> #include <QIcon> #include "logic/BaseInstance.h" @@ -27,11 +27,24 @@ class BaseInstance; class QDir; +struct FTBRecord +{ + QString dirName; + QString name; + QString logo; + QString mcVersion; + QString description; + QString instanceDir; + QString templateDir; +}; + class InstanceList : public QAbstractListModel { Q_OBJECT private: void loadGroupList(QMap<QString, QString> &groupList); + QList<FTBRecord> discoverFTBInstances(); + void loadFTBInstances(QMap<QString, QString> &groupMap, QList<InstancePtr> & tempList); private slots: @@ -109,7 +122,6 @@ slots: * \brief Loads the instance list. Triggers notifications. */ InstListError loadList(); - void loadForgeInstances(QMap<QString, QString> groupMap); private slots: @@ -120,7 +132,7 @@ slots: private: int getInstIndex(BaseInstance *inst) const; - void continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir, + bool continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir, QMap<QString, QString> &groupMap); protected: @@ -129,7 +141,7 @@ protected: QSet<QString> m_groups; }; -class InstanceProxyModel : public KCategorizedSortFilterProxyModel +class InstanceProxyModel : public GroupedProxyModel { public: explicit InstanceProxyModel(QObject *parent = 0); diff --git a/logic/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp index 91f86df0..ece31e3d 100644 --- a/logic/lists/MinecraftVersionList.cpp +++ b/logic/lists/MinecraftVersionList.cpp @@ -60,10 +60,15 @@ bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second) return left->timestamp > right->timestamp; } +void MinecraftVersionList::sortInternal() +{ + qSort(m_vlist.begin(), m_vlist.end(), cmpVersions); +} + void MinecraftVersionList::sort() { beginResetModel(); - qSort(m_vlist.begin(), m_vlist.end(), cmpVersions); + sortInternal(); endResetModel(); } @@ -85,9 +90,8 @@ void MinecraftVersionList::updateListData(QList<BaseVersionPtr> versions) beginResetModel(); m_vlist = versions; m_loaded = true; + sortInternal(); endResetModel(); - // NOW SORT!! - sort(); } inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) diff --git a/logic/lists/MinecraftVersionList.h b/logic/lists/MinecraftVersionList.h index 82af1009..167f4d11 100644 --- a/logic/lists/MinecraftVersionList.h +++ b/logic/lists/MinecraftVersionList.h @@ -29,6 +29,8 @@ class QNetworkReply; class MinecraftVersionList : public BaseVersionList { Q_OBJECT +private: + void sortInternal(); public: friend class MCVListLoadTask; diff --git a/logic/net/NetJob.cpp b/logic/net/NetJob.cpp index 8b79bc54..9e800d13 100644 --- a/logic/net/NetJob.cpp +++ b/logic/net/NetJob.cpp @@ -31,7 +31,6 @@ void NetJob::partSucceeded(int index) num_succeeded++; QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_succeeded << "/" << downloads.size(); - emit filesProgress(num_succeeded, num_failed, downloads.size()); if (num_failed + num_succeeded == downloads.size()) { @@ -55,7 +54,6 @@ void NetJob::partFailed(int index) { QLOG_ERROR() << "Part" << index << "failed 3 times (" << downloads[index]->m_url << ")"; num_failed++; - emit filesProgress(num_succeeded, num_failed, downloads.size()); if (num_failed + num_succeeded == downloads.size()) { QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed."; diff --git a/logic/net/NetJob.h b/logic/net/NetJob.h index 68c4c408..03d6a36e 100644 --- a/logic/net/NetJob.h +++ b/logic/net/NetJob.h @@ -84,7 +84,6 @@ public: { return m_job_name; } - ; virtual bool isRunning() const { return m_running; @@ -94,7 +93,6 @@ public: signals: void started(); void progress(qint64 current, qint64 total); - void filesProgress(int, int, int); void succeeded(); void failed(); public |