Browse Source

Support for new fields in mod format including:
- screenshots, repository-only field, will be used by launcher to d/l
screeenshots
- screenshots will be viewed as thumbnails with switch to full view on
click
- changelog that is now visible in launcher
- somewhat better handling of submods by launcher

Ivan Savenko 11 years ago
parent
commit
9dda194ed3

+ 2 - 0
ChangeLog

@@ -12,6 +12,8 @@ SPELLS:
 * New configuration format: http://wiki.vcmi.eu/index.php?title=Spell_Format
 
 MODS:
+* Support for submods - mod may have their own "submods" located in <modname>/Mods directory
+* Mods may provide their own changelogs and screenshots that will be visible in Launcher
 * Mods cas now add new (offensive, buffs, debuffs) spells and change existing
 
 0.94 -> 0.95 (Mar 01 2014)

+ 25 - 1
config/schemas/mod.json

@@ -31,6 +31,16 @@
 			"description": "Author of the mod. Can be nickname, real name or name of team"
 		},
 
+		"licenseName" : {
+			"type":"string",
+			"description": "Name of the license, recommended is Creative Commons Attribution-ShareAlike"
+		},
+
+		"licenseURL" : {
+			"type":"string",
+			"description": "Url to license text, e.g. http://creativecommons.org/licenses/by-sa/4.0/deed"
+		},
+
 		"contact" : {
 			"type":"string",
 			"description": "Home page of mod or link to forum thread"
@@ -47,6 +57,11 @@
 			"items": { "type":"string" }
 		},
 
+		"keepDisabled" : {
+			"type":"boolean",
+			"description": "If set to true, mod will not be enabled automatically on install"
+		},
+
 		"artifacts": {
 			"type":"array",
 			"description": "List of configuration files for artifacts",
@@ -72,12 +87,21 @@
 			"description": "List of configuration files for heroes",
 			"items": { "type":"string", "format" : "textFile" }
 		},
-                "spells": {
+		"spells": {
 			"type":"array",
 			"description": "List of configuration files for spells",
 			"items": { "type":"string", "format" : "textFile" }
 		},
 
+		"changelog" : {
+			"type":"object",
+			"description": "List of changes/new features in each version",
+			"additionalProperties" : {
+				"type" : "array",
+				"items" : { "type":"string" }
+			}
+		},
+
 		"filesystem": {
 			"type":"object",
 			"description": "Optional, description on how files are organized in your mod. In most cases you do not need to use this field",

+ 2 - 0
launcher/CMakeLists.txt

@@ -10,6 +10,7 @@ set(launcher_modmanager_SRCS
 	modManager/cmodlistmodel_moc.cpp
 	modManager/cmodlistview_moc.cpp
 	modManager/cmodmanager.cpp
+	modManager/imageviewer.cpp
 )
 
 set(launcher_settingsview_SRCS
@@ -28,6 +29,7 @@ set(launcher_SRCS
 
 set(launcher_FORMS
 	modManager/cmodlistview_moc.ui
+	modManager/imageviewer.ui
 	settingsView/csettingsview_moc.ui
 	mainwindow_moc.ui
 )

+ 1 - 1
launcher/mainwindow_moc.ui

@@ -97,7 +97,7 @@
        <bool>false</bool>
       </property>
       <property name="currentRow">
-       <number>-1</number>
+       <number>0</number>
       </property>
       <item>
        <property name="text">

+ 10 - 0
launcher/modManager/cdownloadmanager_moc.cpp

@@ -113,3 +113,13 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte
 
 	emit downloadProgress(received, total);
 }
+
+bool CDownloadManager::downloadInProgress(const QUrl &url)
+{
+	for (auto & entry : currentDownloads)
+	{
+		if (entry.reply->url() == url)
+			return true;
+	}
+	return false;
+}

+ 13 - 0
launcher/modManager/cmodlist.cpp

@@ -271,3 +271,16 @@ QVector<QString> CModList::getModList() const
 	}
 	return modList;
 }
+
+QVector<QString> CModList::getChildren(QString parent) const
+{
+	QVector<QString> children;
+
+	int depth = parent.count('.') + 1;
+	for (const QString & mod : getModList())
+	{
+		if (mod.count('.') == depth && mod.startsWith(parent))
+			children.push_back(mod);
+	}
+	return children;
+}

+ 2 - 0
launcher/modManager/cmodlist.h

@@ -79,4 +79,6 @@ public:
 
 	// returns list of all available mods
 	QVector<QString> getModList() const;
+
+	QVector<QString> getChildren(QString parent) const;
 };

+ 118 - 11
launcher/modManager/cmodlistview_moc.cpp

@@ -1,6 +1,7 @@
 #include "StdInc.h"
 #include "cmodlistview_moc.h"
 #include "ui_cmodlistview_moc.h"
+#include "imageviewer.h"
 
 #include <QJsonArray>
 #include <QCryptographicHash>
@@ -68,7 +69,7 @@ CModListView::CModListView(QWidget *parent) :
 
 	ui->progressWidget->setVisible(false);
 	dlManager = nullptr;
-	//loadRepositories();
+	loadRepositories();
 	hideModInfo();
 }
 
@@ -106,6 +107,7 @@ void CModListView::showModInfo()
 {
 	ui->modInfoWidget->show();
 	ui->hideModInfoButton->setArrowType(Qt::RightArrow);
+	loadScreenshots();
 }
 
 void CModListView::hideModInfo()
@@ -135,25 +137,60 @@ static QString replaceIfNotEmpty(QStringList value, QString pattern)
 	return "";
 }
 
+QString CModListView::genChangelogText(CModEntry &mod)
+{
+	QString headerTemplate = "<p><span style=\" font-weight:600;\">%1: </span></p>";
+	QString entryBegin = "<p align=\"justify\"><ul>";
+	QString entryEnd = "</ul></p>";
+	QString entryLine = "<li>%1</li>";
+	//QString versionSeparator = "<hr/>";
+
+	QString result;
+
+	QVariantMap changelog = mod.getValue("changelog").toMap();
+	QList<QString> versions = changelog.keys();
+
+	std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater)
+	{
+		return !CModEntry::compareVersions(lesser, greater);
+	});
+
+	for (auto & version : versions)
+	{
+		result += headerTemplate.arg(version);
+		result += entryBegin;
+		for (auto & line : changelog.value(version).toStringList())
+			result += entryLine.arg(line);
+		result += entryEnd;
+	}
+	return result;
+}
+
 QString CModListView::genModInfoText(CModEntry &mod)
 {
 	QString prefix = "<p><span style=\" font-weight:600;\">%1: </span>"; // shared prefix
 	QString lineTemplate = prefix + "%2</p>";
-	QString urlTemplate  = prefix + "<a href=\"%2\"><span style=\" text-decoration: underline; color:#0000ff;\">%2</span></a></p>";
+	QString urlTemplate  = prefix + "<a href=\"%2\">%3</a></p>";
 	QString textTemplate = prefix + "</p><p align=\"justify\">%2</p>";
 	QString listTemplate = "<p align=\"justify\">%1: %2</p>";
 	QString noteTemplate = "<p align=\"justify\">%1</p>";
 
 	QString result;
 
-	result += "<html><body>";
 	result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg(tr("Mod name")));
 	result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version")));
 	result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version")));
-	if (mod.getValue("size").toDouble() != 0)
+
+	if (mod.getValue("size").isValid())
 		result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble()), lineTemplate.arg(tr("Download size")));
 	result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors")));
-	result += replaceIfNotEmpty(mod.getValue("contact"), urlTemplate.arg(tr("Home")));
+
+	if (mod.getValue("licenseURL").isValid())
+		result += urlTemplate.arg(tr("License")).arg(mod.getValue("licenseURL").toString()).arg(mod.getValue("licenseName").toString());
+
+	if (mod.getValue("contact").isValid())
+		result += urlTemplate.arg(tr("Home")).arg(mod.getValue("contact").toString()).arg(mod.getValue("contact").toString());
+
 	result += replaceIfNotEmpty(mod.getValue("depends"), lineTemplate.arg(tr("Required mods")));
 	result += replaceIfNotEmpty(mod.getValue("conflicts"), lineTemplate.arg(tr("Conflicting mods")));
 	result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description")));
@@ -181,7 +218,6 @@ QString CModListView::genModInfoText(CModEntry &mod)
 	if (notes.size())
 		result += textTemplate.arg(tr("Notes")).arg(notes);
 
-	result += "</body></html>";
 	return result;
 }
 
@@ -212,7 +248,8 @@ void CModListView::selectMod(const QModelIndex & index)
 	{
 		auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString());
 
-		ui->textBrowser->setHtml(genModInfoText(mod));
+		ui->modInfoBrowser->setHtml(genModInfoText(mod));
+		ui->changelogBrowser->setHtml(genChangelogText(mod));
 
 		bool hasInvalidDeps = !findInvalidDependencies(index.data(ModRoles::ModNameRole).toString()).empty();
 		bool hasBlockingMods = !findBlockingMods(index.data(ModRoles::ModNameRole).toString()).empty();
@@ -232,6 +269,8 @@ void CModListView::selectMod(const QModelIndex & index)
 		ui->installButton->setEnabled(!hasInvalidDeps);
 		ui->uninstallButton->setEnabled(!hasDependentMods);
 		ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods);
+
+		loadScreenshots();
 	}
 }
 
@@ -239,7 +278,7 @@ void CModListView::keyPressEvent(QKeyEvent * event)
 {
 	if (event->key() == Qt::Key_Escape && ui->modInfoWidget->isVisible() )
 	{
-		ui->modInfoWidget->hide();
+		hideModInfo();
 	}
 	else
 	{
@@ -480,17 +519,23 @@ void CModListView::hideProgressBar()
 void CModListView::installFiles(QStringList files)
 {
 	QStringList mods;
+	QStringList images;
 
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	for (QString filename : files)
 	{
-		if (filename.contains(".zip"))
+		if (filename.endsWith(".zip"))
 			mods.push_back(filename);
-		if (filename.contains(".json"))
+		if (filename.endsWith(".json"))
 			manager->loadRepository(filename);
+		if (filename.endsWith(".png"))
+			images.push_back(filename);
 	}
 	if (!mods.empty())
 		installMods(mods);
+
+	if (!images.empty())
+		loadScreenshots();
 }
 
 void CModListView::installMods(QStringList archives)
@@ -536,8 +581,23 @@ void CModListView::installMods(QStringList archives)
 	for (int i=0; i<modNames.size(); i++)
 		manager->installMod(modNames[i], archives[i]);
 
+	std::function<void(QString)> enableMod = [&](QString modName)
+	{
+		auto mod = modModel->getMod(modName);
+		if (mod.isInstalled() && !mod.getValue("keepDisabled").toBool())
+		{
+			if (manager->enableMod(modName))
+			{
+				for (QString child : modModel->getChildren(modName))
+					enableMod(child);
+			}
+		}
+	};
+
 	for (QString mod : modsToEnable)
-		manager->enableMod(mod);
+	{
+		enableMod(mod);
+	}
 
 	for (QString archive : archives)
 		QFile::remove(archive);
@@ -568,3 +628,50 @@ void CModListView::checkManagerErrors()
 		QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok );
 	}
 }
+
+void CModListView::on_tabWidget_currentChanged(int index)
+{
+	loadScreenshots();
+}
+
+void CModListView::loadScreenshots()
+{
+	if (ui->tabWidget->currentIndex() == 2 && ui->modInfoWidget->isVisible())
+	{
+		ui->screenshotsList->clear();
+		QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
+		assert(modModel->hasMod(modName)); //should be filtered out by check above
+
+		for (QString & url : modModel->getMod(modName).getValue("screenshots").toStringList())
+		{
+			// URL must be encoded to something else to get rid of symbols illegal in file names
+			auto hashed = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5);
+			auto hashedStr = QString::fromUtf8(hashed.toHex());
+
+			QString fullPath = CLauncherDirs::get().downloadsPath() + '/' + hashedStr + ".png";
+			QPixmap pixmap(fullPath);
+			if (pixmap.isNull())
+			{
+				// image file not exists or corrupted - try to redownload
+				downloadFile(hashedStr + ".png", url, "screenshots");
+			}
+			else
+			{
+				// managed to load cached image
+				QIcon icon(pixmap);
+				QListWidgetItem * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1));
+				ui->screenshotsList->addItem(item);
+			}
+		}
+	}
+}
+
+void CModListView::on_screenshotsList_clicked(const QModelIndex &index)
+{
+	if (index.isValid())
+	{
+		QIcon icon = ui->screenshotsList->item(index.row())->icon();
+		auto pixmap = icon.pixmap(icon.availableSizes()[0]);
+		ImageViewer::showPixmap(pixmap, this);
+	}
+}

+ 6 - 0
launcher/modManager/cmodlistview_moc.h

@@ -50,6 +50,7 @@ class CModListView : public QWidget
 	void installMods(QStringList archives);
 	void installFiles(QStringList mods);
 
+	QString genChangelogText(CModEntry & mod);
 	QString genModInfoText(CModEntry & mod);
 public:
 	explicit CModListView(QWidget *parent = 0);
@@ -57,6 +58,7 @@ public:
 	
 	void showModInfo();
 	void hideModInfo();
+	void loadScreenshots();
 
 	void enableModInfo();
 	void disableModInfo();
@@ -91,6 +93,10 @@ private slots:
 
 	void on_allModsView_activated(const QModelIndex &index);
 
+	void on_tabWidget_currentChanged(int index);
+
+	void on_screenshotsList_clicked(const QModelIndex &index);
+
 private:
 	Ui::CModListView *ui;
 };

+ 146 - 52
launcher/modManager/cmodlistview_moc.ui

@@ -202,50 +202,8 @@
       <property name="bottomMargin">
        <number>0</number>
       </property>
-      <item row="0" column="0" colspan="6">
-       <widget class="QTextBrowser" name="textBrowser">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
-          <horstretch>0</horstretch>
-          <verstretch>0</verstretch>
-         </sizepolicy>
-        </property>
-        <property name="readOnly">
-         <bool>true</bool>
-        </property>
-        <property name="html">
-         <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
-        </property>
-        <property name="openExternalLinks">
-         <bool>true</bool>
-        </property>
-        <property name="openLinks">
-         <bool>true</bool>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <spacer name="modButtonSpacer">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-        <property name="sizeType">
-         <enum>QSizePolicy::MinimumExpanding</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>0</width>
-          <height>20</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-      <item row="1" column="1">
-       <widget class="QPushButton" name="enableButton">
+      <item row="1" column="4">
+       <widget class="QPushButton" name="uninstallButton">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
           <horstretch>0</horstretch>
@@ -265,7 +223,7 @@ p, li { white-space: pre-wrap; }
          </size>
         </property>
         <property name="text">
-         <string>Enable</string>
+         <string>Uninstall</string>
         </property>
        </widget>
       </item>
@@ -319,8 +277,24 @@ p, li { white-space: pre-wrap; }
         </property>
        </widget>
       </item>
-      <item row="1" column="4">
-       <widget class="QPushButton" name="uninstallButton">
+      <item row="1" column="0">
+       <spacer name="modButtonSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeType">
+         <enum>QSizePolicy::MinimumExpanding</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>0</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="5">
+       <widget class="QPushButton" name="installButton">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
           <horstretch>0</horstretch>
@@ -340,12 +314,12 @@ p, li { white-space: pre-wrap; }
          </size>
         </property>
         <property name="text">
-         <string>Uninstall</string>
+         <string>Install</string>
         </property>
        </widget>
       </item>
-      <item row="1" column="5">
-       <widget class="QPushButton" name="installButton">
+      <item row="1" column="1">
+       <widget class="QPushButton" name="enableButton">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
           <horstretch>0</horstretch>
@@ -365,8 +339,126 @@ p, li { white-space: pre-wrap; }
          </size>
         </property>
         <property name="text">
-         <string>Install</string>
+         <string>Enable</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0" colspan="6">
+       <widget class="QTabWidget" name="tabWidget">
+        <property name="currentIndex">
+         <number>0</number>
         </property>
+        <widget class="QWidget" name="tabInfo">
+         <attribute name="title">
+          <string>Description</string>
+         </attribute>
+         <layout class="QGridLayout" name="gridLayout_4">
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>4</number>
+          </property>
+          <item row="0" column="0">
+           <widget class="QTextBrowser" name="modInfoBrowser">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="readOnly">
+             <bool>true</bool>
+            </property>
+            <property name="html">
+             <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+            </property>
+            <property name="openExternalLinks">
+             <bool>true</bool>
+            </property>
+            <property name="openLinks">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+        <widget class="QWidget" name="tabChangelog">
+         <attribute name="title">
+          <string>Changelog</string>
+         </attribute>
+         <layout class="QGridLayout" name="gridLayout_5">
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>4</number>
+          </property>
+          <item row="0" column="0">
+           <widget class="QTextBrowser" name="changelogBrowser"/>
+          </item>
+         </layout>
+        </widget>
+        <widget class="QWidget" name="tabScreenshots">
+         <attribute name="title">
+          <string>Screenshots</string>
+         </attribute>
+         <layout class="QGridLayout" name="gridLayout_6">
+          <property name="leftMargin">
+           <number>4</number>
+          </property>
+          <property name="topMargin">
+           <number>4</number>
+          </property>
+          <property name="rightMargin">
+           <number>4</number>
+          </property>
+          <property name="bottomMargin">
+           <number>4</number>
+          </property>
+          <item row="0" column="0">
+           <widget class="QListWidget" name="screenshotsList">
+            <property name="horizontalScrollBarPolicy">
+             <enum>Qt::ScrollBarAlwaysOff</enum>
+            </property>
+            <property name="selectionMode">
+             <enum>QAbstractItemView::NoSelection</enum>
+            </property>
+            <property name="selectionBehavior">
+             <enum>QAbstractItemView::SelectRows</enum>
+            </property>
+            <property name="iconSize">
+             <size>
+              <width>240</width>
+              <height>180</height>
+             </size>
+            </property>
+            <property name="viewMode">
+             <enum>QListView::IconMode</enum>
+            </property>
+            <property name="uniformItemSizes">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
        </widget>
       </item>
      </layout>
@@ -390,6 +482,9 @@ p, li { white-space: pre-wrap; }
       </size>
      </property>
      <layout class="QHBoxLayout" name="horizontalLayout">
+      <property name="leftMargin">
+       <number>9</number>
+      </property>
       <item>
        <widget class="QProgressBar" name="progressBar">
         <property name="sizePolicy">
@@ -437,7 +532,6 @@ p, li { white-space: pre-wrap; }
   <tabstop>lineEdit</tabstop>
   <tabstop>comboBox</tabstop>
   <tabstop>allModsView</tabstop>
-  <tabstop>textBrowser</tabstop>
   <tabstop>hideModInfoButton</tabstop>
   <tabstop>enableButton</tabstop>
   <tabstop>disableButton</tabstop>

+ 55 - 0
launcher/modManager/imageviewer.cpp

@@ -0,0 +1,55 @@
+#include "StdInc.h"
+
+#include <QDesktopWidget>
+
+#include "imageviewer.h"
+#include "ui_imageviewer.h"
+
+ImageViewer::ImageViewer(QWidget *parent) :
+    QDialog(parent),
+    ui(new Ui::ImageViewer)
+{
+	ui->setupUi(this);
+}
+
+ImageViewer::~ImageViewer()
+{
+	delete ui;
+}
+
+QSize ImageViewer::calculateWindowSize()
+{
+	QDesktopWidget desktop;
+	return desktop.availableGeometry(desktop.primaryScreen()).size() * 0.8;
+}
+
+void ImageViewer::showPixmap(QPixmap & pixmap, QWidget *parent)
+{
+	assert(!pixmap.isNull());
+
+	ImageViewer * iw = new ImageViewer(parent);
+
+	QSize size = pixmap.size();
+	size.scale(iw->calculateWindowSize(), Qt::KeepAspectRatio);
+	iw->resize(size);
+
+	iw->setPixmap(pixmap);
+	iw->setAttribute(Qt::WA_DeleteOnClose, true);
+	iw->setModal(Qt::WindowModal);
+	iw->show();
+}
+
+void ImageViewer::setPixmap(QPixmap & pixmap)
+{
+	ui->label->setPixmap(pixmap);
+}
+
+void ImageViewer::mousePressEvent(QMouseEvent * event)
+{
+	close();
+}
+
+void ImageViewer::keyPressEvent(QKeyEvent * event)
+{
+	close(); // FIXME: it also closes on pressing modifiers (e.g. Ctrl/Alt). Not exactly expected
+}

+ 31 - 0
launcher/modManager/imageviewer.h

@@ -0,0 +1,31 @@
+#ifndef IMAGEVIEWER_H
+#define IMAGEVIEWER_H
+
+#include <QDialog>
+
+namespace Ui {
+	class ImageViewer;
+}
+
+class ImageViewer : public QDialog
+{
+	Q_OBJECT
+	
+public:
+	explicit ImageViewer(QWidget *parent = 0);
+	~ImageViewer();
+
+	void setPixmap(QPixmap & pixmap);
+
+	static void showPixmap(QPixmap & pixmap, QWidget *parent = 0);
+protected:
+	void mousePressEvent(QMouseEvent * event);
+	void keyPressEvent(QKeyEvent * event);
+	QSize calculateWindowSize();
+
+
+private:
+	Ui::ImageViewer *ui;
+};
+
+#endif // IMAGEVIEWER_H

+ 58 - 0
launcher/modManager/imageviewer.ui

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ImageViewer</class>
+ <widget class="QDialog" name="ImageViewer">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>640</width>
+    <height>480</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Ignored" vsizetype="Ignored">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Image Viewer</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <property name="sizeConstraint">
+    <enum>QLayout::SetNoConstraint</enum>
+   </property>
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="scaledContents">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>