summaryrefslogtreecommitdiffstats
path: root/application/pages/instance
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2018-04-12 01:44:51 +0200
committerPetr Mrázek <peterix@gmail.com>2018-04-23 02:05:22 +0200
commit72ff342d6325cab42cc3d8401b4fac5b2f7eff3b (patch)
treec779eaaf41a16be4c9990bec21e661ec6b53bafe /application/pages/instance
parent6284f070c19053ae2bcf26dd9bccac2e0d35477f (diff)
downloadMultiMC-72ff342d6325cab42cc3d8401b4fac5b2f7eff3b.tar
MultiMC-72ff342d6325cab42cc3d8401b4fac5b2f7eff3b.tar.gz
MultiMC-72ff342d6325cab42cc3d8401b4fac5b2f7eff3b.tar.lz
MultiMC-72ff342d6325cab42cc3d8401b4fac5b2f7eff3b.tar.xz
MultiMC-72ff342d6325cab42cc3d8401b4fac5b2f7eff3b.zip
GH-2053 basics of the servers.dat management
Diffstat (limited to 'application/pages/instance')
-rw-r--r--application/pages/instance/ServersPage.cpp743
-rw-r--r--application/pages/instance/ServersPage.h87
-rw-r--r--application/pages/instance/ServersPage.ui207
3 files changed, 1037 insertions, 0 deletions
diff --git a/application/pages/instance/ServersPage.cpp b/application/pages/instance/ServersPage.cpp
new file mode 100644
index 00000000..f7ddc7da
--- /dev/null
+++ b/application/pages/instance/ServersPage.cpp
@@ -0,0 +1,743 @@
+#include "ServersPage.h"
+#include "ui_ServersPage.h"
+
+#include <FileSystem.h>
+#include <sstream>
+#include <io/stream_reader.h>
+#include <tag_string.h>
+#include <tag_primitive.h>
+#include <tag_list.h>
+#include <tag_compound.h>
+#include <minecraft/MinecraftInstance.h>
+
+#include <QFileSystemWatcher>
+
+static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things.
+
+struct Server
+{
+ // Types
+ enum class AcceptsTextures : int
+ {
+ ASK = 0,
+ ALWAYS = 1,
+ NEVER = 2
+ };
+
+ // Methods
+ Server()
+ {
+ m_name = QObject::tr("Minecraft Server");
+ }
+ Server(const QString & name, const QString & address)
+ {
+ m_name = name;
+ m_address = address;
+ }
+ Server(nbt::tag_compound& server)
+ {
+ std::string addressStr(server["ip"]);
+ m_address = QString::fromUtf8(addressStr.c_str());
+
+ std::string nameStr(server["name"]);
+ m_name = QString::fromUtf8(nameStr.c_str());
+
+ if(server["icon"])
+ {
+ std::string base64str(server["icon"]);
+ m_icon = QByteArray::fromBase64(base64str.c_str());
+ }
+
+ if(server.has_key("acceptTextures", nbt::tag_type::Byte))
+ {
+ bool value = server["acceptTextures"].as<nbt::tag_byte>().get();
+ if(value)
+ {
+ m_acceptsTextures = AcceptsTextures::ALWAYS;
+ }
+ else
+ {
+ m_acceptsTextures = AcceptsTextures::NEVER;
+ }
+ }
+ }
+
+ void serialize(nbt::tag_compound& server)
+ {
+ server.insert("name", m_name.toUtf8().toStdString());
+ server.insert("ip", m_address.toUtf8().toStdString());
+ if(m_icon.size())
+ {
+ server.insert("icon", m_icon.toBase64().toStdString());
+ }
+ if(m_acceptsTextures != AcceptsTextures::ASK)
+ {
+ server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS));
+ }
+ }
+
+ // Data - persistent and user changeable
+ QString m_name;
+ QString m_address;
+ AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK;
+
+ // Data - persistent and automatically updated
+ QByteArray m_icon;
+
+ // Data - temporary
+ bool m_checked = false;
+ bool m_up = false;
+ QString m_motd; // https://mctools.org/motd-creator
+ int m_ping = 0;
+ int m_currentPlayers = 0;
+ int m_maxPlayers = 0;
+};
+
+static std::unique_ptr <nbt::tag_compound> parseServersDat(const QString& filename)
+{
+ try
+ {
+ QByteArray input = FS::read(filename);
+ std::istringstream foo(std::string(input.constData(), input.size()));
+ auto pair = nbt::io::read_compound(foo);
+
+ if(pair.first != "")
+ return nullptr;
+
+ if(pair.second == nullptr)
+ return nullptr;
+
+ return std::move(pair.second);
+ }
+ catch(...)
+ {
+ return nullptr;
+ }
+}
+
+static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo)
+{
+ try
+ {
+ std::ostringstream s;
+ nbt::io::write_tag("", *levelInfo, s);
+ QByteArray val(s.str().data(), (int) s.str().size() );
+ FS::write(filename, val);
+ return true;
+ }
+ catch(...)
+ {
+ return false;
+ }
+}
+
+class ServersModel: public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ enum Roles
+ {
+ ServerPtrRole = Qt::UserRole,
+ };
+ explicit ServersModel(const QString &path, QObject *parent = 0)
+ : QAbstractListModel(parent)
+ {
+ m_path = path;
+ m_watcher = new QFileSystemWatcher(this);
+ connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged);
+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged);
+ m_saveTimer.setSingleShot(true);
+ m_saveTimer.setInterval(5000);
+ connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal);
+ }
+ virtual ~ServersModel() {};
+
+ void observe()
+ {
+ if(m_observed)
+ {
+ return;
+ }
+ m_observed = true;
+
+ if(!m_loaded)
+ {
+ load();
+ }
+
+ updateFSObserver();
+ }
+
+ void unobserve()
+ {
+ if(!m_observed)
+ {
+ return;
+ }
+ m_observed = false;
+
+ updateFSObserver();
+ }
+
+ void lock()
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ saveNow();
+
+ m_locked = true;
+ updateFSObserver();
+ }
+
+ void unlock()
+ {
+ if(!m_locked)
+ {
+ return;
+ }
+ m_locked = false;
+
+ updateFSObserver();
+ }
+
+ int addEmptyRow(int position)
+ {
+ if(m_locked)
+ {
+ return -1;
+ }
+ if(position < 0 || position >= rowCount())
+ {
+ position = rowCount();
+ }
+ beginInsertRows(QModelIndex(), position, position);
+ m_servers.insert(position, Server());
+ endInsertRows();
+ scheduleSave();
+ return position;
+ }
+
+ bool removeRow(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row < 0 || row >= rowCount())
+ {
+ return false;
+ }
+ beginRemoveRows(QModelIndex(), row, row);
+ m_servers.removeAt(row);
+ endRemoveRows(); // does absolutely nothing, the selected server stays as the next line...
+ scheduleSave();
+ return true;
+ }
+
+ bool moveUp(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ if(row <= 0)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1);
+ m_servers.swap(row-1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ bool moveDown(int row)
+ {
+ if(m_locked)
+ {
+ return false;
+ }
+ int count = rowCount();
+ if(row + 1 >= count)
+ {
+ return false;
+ }
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2);
+ m_servers.swap(row+1, row);
+ endMoveRows();
+ scheduleSave();
+ return true;
+ }
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (section < 0 || section >= COLUMN_COUNT)
+ return QVariant();
+
+ if(role == Qt::DisplayRole)
+ {
+ switch(section)
+ {
+ case 0:
+ return tr("Name");
+ case 1:
+ return tr("Address");
+ case 2:
+ return tr("Latency");
+ }
+ }
+
+ return QAbstractListModel::headerData(section, orientation, role);
+ }
+
+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
+ {
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+ if(column < 0 || column >= COLUMN_COUNT)
+ return QVariant();
+
+ if (row < 0 || row >= m_servers.size())
+ return QVariant();
+
+ switch(column)
+ {
+ case 0:
+ switch (role)
+ {
+ case Qt::DecorationRole:
+ {
+ auto & bytes = m_servers[row].m_icon;
+ if(bytes.size())
+ {
+ QPixmap px;
+ if(px.loadFromData(bytes))
+ return QIcon(px);
+ }
+ return MMC->getThemedIcon("unknown_server");
+ }
+ case Qt::DisplayRole:
+ return m_servers[row].m_name;
+ case ServerPtrRole:
+ return QVariant::fromValue<void *>((void *)&m_servers[row]);
+ default:
+ return QVariant();
+ }
+ case 1:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_address;
+ default:
+ return QVariant();
+ }
+ case 2:
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return m_servers[row].m_ping;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ return m_servers.size();
+ }
+ int columnCount(const QModelIndex & parent) const override
+ {
+ return COLUMN_COUNT;
+ }
+
+ Server * at(int index)
+ {
+ if(index < 0 || index >= rowCount())
+ {
+ return nullptr;
+ }
+ return &m_servers[index];
+ }
+
+ void setName(int row, const QString & name)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_name == name)
+ {
+ return;
+ }
+ server->m_name = name;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAddress(int row, const QString & address)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_address == address)
+ {
+ return;
+ }
+ server->m_address = address;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void setAcceptsTextures(int row, Server::AcceptsTextures textures)
+ {
+ if(m_locked)
+ {
+ return;
+ }
+ auto server = at(row);
+ if(!server || server->m_acceptsTextures == textures)
+ {
+ return;
+ }
+ server->m_acceptsTextures = textures;
+ emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1));
+ scheduleSave();
+ }
+
+ void load()
+ {
+ cancelSave();
+ beginResetModel();
+ QList<Server> servers;
+ auto serversDat = parseServersDat(serversPath());
+ if(serversDat)
+ {
+ auto &serversList = serversDat->at("servers").as<nbt::tag_list>();
+ for(auto iter = serversList.begin(); iter != serversList.end(); iter++)
+ {
+ auto & serverTag = (*iter).as<nbt::tag_compound>();
+ Server s(serverTag);
+ servers.append(s);
+ }
+ }
+ m_servers.swap(servers);
+ m_loaded = true;
+ endResetModel();
+ }
+
+ void saveNow()
+ {
+ if(saveIsScheduled())
+ {
+ save_internal();
+ }
+ }
+
+
+public slots:
+ void dirChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ load();
+ }
+ void fileChanged(const QString& path)
+ {
+ qDebug() << "Changed:" << path;
+ }
+
+private slots:
+ void save_internal()
+ {
+ cancelSave();
+ qDebug() << "Server list save is performed for" << m_path;
+
+ nbt::tag_compound out;
+ nbt::tag_list list;
+ for(auto & server: m_servers)
+ {
+ nbt::tag_compound serverNbt;
+ server.serialize(serverNbt);
+ list.push_back(std::move(serverNbt));
+ }
+ out.insert("servers", nbt::value(std::move(list)));
+
+ if(!serializeServerDat(serversPath(), &out))
+ {
+ qDebug() << "Failed to save server list:" << m_path << "Will try again.";
+ scheduleSave();
+ }
+ }
+
+private:
+ void scheduleSave()
+ {
+ if(!m_loaded)
+ {
+ qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path;
+ return;
+ }
+ if(!m_dirty)
+ {
+ m_dirty = true;
+ qDebug() << "Server list save is scheduled for" << m_path;
+ }
+ m_saveTimer.start();
+ }
+
+ void cancelSave()
+ {
+ m_dirty = false;
+ m_saveTimer.stop();
+ }
+
+ bool saveIsScheduled() const
+ {
+ return m_dirty;
+ }
+
+ void updateFSObserver()
+ {
+ bool observingFS = m_watcher->directories().contains(m_path);
+ if(m_observed && m_locked)
+ {
+ if(!observingFS)
+ {
+ qWarning() << "Will watch" << m_path;
+ if(!m_watcher->addPath(m_path))
+ {
+ qWarning() << "Failed to start watching" << m_path;
+ }
+ }
+ }
+ else
+ {
+ if(observingFS)
+ {
+ qWarning() << "Will stop watching" << m_path;
+ if(!m_watcher->removePath(m_path))
+ {
+ qWarning() << "Failed to stop watching" << m_path;
+ }
+ }
+ }
+ }
+
+ QString serversPath()
+ {
+ QFileInfo foo(FS::PathCombine(m_path, "servers.dat"));
+ return foo.canonicalFilePath();
+ }
+
+private:
+ bool m_loaded = false;
+ bool m_locked = false;
+ bool m_observed = false;
+ bool m_dirty = false;
+ QString m_path;
+ QList<Server> m_servers;
+ QFileSystemWatcher *m_watcher = nullptr;
+ QTimer m_saveTimer;
+};
+
+ServersPage::ServersPage(MinecraftInstance * inst, QWidget* parent)
+ : QWidget(parent), ui(new Ui::ServersPage)
+{
+ ui->setupUi(this);
+ ui->tabWidget->tabBar()->hide();
+ m_inst = inst;
+ m_model = new ServersModel(inst->minecraftRoot(), this);
+ ui->serversView->setIconSize(QSize(64,64));
+ ui->serversView->setModel(m_model);
+ auto head = ui->serversView->header();
+ if(head->count())
+ {
+ head->setSectionResizeMode(0, QHeaderView::Stretch);
+ for(int i = 1; i < head->count(); i++)
+ {
+ head->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+ }
+ }
+
+ auto selectionModel = ui->serversView->selectionModel();
+ connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged);
+ connect(m_inst, &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed);
+ connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited);
+ connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited);
+ connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int)));
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved);
+
+ m_locked = m_inst->isRunning();
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+
+ updateState();
+}
+
+ServersPage::~ServersPage()
+{
+ m_model->saveNow();
+}
+
+void ServersPage::on_RunningState_changed(bool running)
+{
+ if(m_locked == running)
+ {
+ return;
+ }
+ m_locked = running;
+ if(m_locked)
+ {
+ m_model->lock();
+ }
+ else
+ {
+ m_model->unlock();
+ }
+ updateState();
+}
+
+void ServersPage::currentChanged(const QModelIndex &current, const QModelIndex &previous)
+{
+ int nextServer = -1;
+ if (!current.isValid())
+ {
+ nextServer = -1;
+ }
+ else
+ {
+ nextServer = current.row();
+ }
+ currentServer = nextServer;
+ updateState();
+}
+
+// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal.
+void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last)
+{
+ if(currentServer < first)
+ {
+ // current was before the removal
+ return;
+ }
+ else if(currentServer >= first && currentServer <= last)
+ {
+ // current got removed...
+ return;
+ }
+ else
+ {
+ // current was past the removal
+ int count = last - first + 1;
+ currentServer -= count;
+ }
+}
+
+void ServersPage::nameEdited(const QString& name)
+{
+ m_model->setName(currentServer, name);
+}
+
+void ServersPage::addressEdited(const QString& address)
+{
+ m_model->setAddress(currentServer, address);
+}
+
+void ServersPage::resourceIndexChanged(int index)
+{
+ auto acceptsTextures = Server::AcceptsTextures(index);
+ m_model->setAcceptsTextures(currentServer, acceptsTextures);
+}
+
+void ServersPage::updateState()
+{
+ auto server = m_model->at(currentServer);
+
+ bool serverEditEnabled = server && !m_locked;
+ ui->addressLine->setEnabled(serverEditEnabled);
+ ui->nameLine->setEnabled(serverEditEnabled);
+ ui->resourceComboBox->setEnabled(serverEditEnabled);
+ ui->moveDownBtn->setEnabled(serverEditEnabled);
+ ui->moveUpBtn->setEnabled(serverEditEnabled);
+ ui->removeBtn->setEnabled(serverEditEnabled);
+
+ if(server)
+ {
+ ui->addressLine->setText(server->m_address);
+ ui->nameLine->setText(server->m_name);
+ ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures));
+ }
+ else
+ {
+ ui->addressLine->setText(QString());
+ ui->nameLine->setText(QString());
+ ui->resourceComboBox->setCurrentIndex(0);
+ }
+
+ ui->addBtn->setDisabled(m_locked);
+}
+
+void ServersPage::openedImpl()
+{
+ m_model->observe();
+}
+
+void ServersPage::closedImpl()
+{
+ m_model->unobserve();
+}
+
+void ServersPage::on_addBtn_clicked()
+{
+ int position = m_model->addEmptyRow(currentServer + 1);
+ if(position < 0)
+ {
+ return;
+ }
+ // select the new row
+ ui->serversView->selectionModel()->setCurrentIndex(
+ m_model->index(position),
+ QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows
+ );
+ currentServer = position;
+}
+
+void ServersPage::on_removeBtn_clicked()
+{
+ m_model->removeRow(currentServer);
+}
+
+void ServersPage::on_moveUpBtn_clicked()
+{
+ if(m_model->moveUp(currentServer))
+ {
+ currentServer --;
+ }
+}
+
+void ServersPage::on_moveDownBtn_clicked()
+{
+ if(m_model->moveDown(currentServer))
+ {
+ currentServer ++;
+ }
+}
+
+void ServersPage::on_refreshBtn_clicked()
+{
+ m_model->load();
+}
+
+#include "ServersPage.moc"
diff --git a/application/pages/instance/ServersPage.h b/application/pages/instance/ServersPage.h
new file mode 100644
index 00000000..6c812bd9
--- /dev/null
+++ b/application/pages/instance/ServersPage.h
@@ -0,0 +1,87 @@
+/* Copyright 2013-2018 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 <QWidget>
+#include <QString>
+
+#include "pages/BasePage.h"
+#include <MultiMC.h>
+
+namespace Ui
+{
+class ServersPage;
+}
+
+struct Server;
+class ServersModel;
+class MinecraftInstance;
+
+class ServersPage : public QWidget, public BasePage
+{
+ Q_OBJECT
+
+public:
+ explicit ServersPage(MinecraftInstance *inst, QWidget *parent = 0);
+ virtual ~ServersPage();
+
+ void openedImpl() override;
+ void closedImpl() override;
+
+ virtual QString displayName() const override
+ {
+ return tr("Servers");
+ }
+ virtual QIcon icon() const override
+ {
+ return MMC->getThemedIcon("unknown_server");
+ }
+ virtual QString id() const override
+ {
+ return "servers";
+ }
+ virtual QString helpPage() const override
+ {
+ return "Servers-management";
+ }
+private:
+ void updateState();
+ void scheduleSave();
+ bool saveIsScheduled() const;
+
+private slots:
+ void currentChanged(const QModelIndex &current, const QModelIndex &previous);
+ void rowsRemoved(const QModelIndex &parent, int first, int last);
+
+ void on_addBtn_clicked();
+ void on_removeBtn_clicked();
+ void on_moveUpBtn_clicked();
+ void on_moveDownBtn_clicked();
+ void on_refreshBtn_clicked();
+ void on_RunningState_changed(bool running);
+
+ void nameEdited(const QString & name);
+ void addressEdited(const QString & address);
+ void resourceIndexChanged(int index);
+
+private: // data
+ int currentServer = -1;
+ bool m_locked = true;
+ Ui::ServersPage *ui = nullptr;
+ ServersModel * m_model = nullptr;
+ MinecraftInstance * m_inst = nullptr;
+};
+
diff --git a/application/pages/instance/ServersPage.ui b/application/pages/instance/ServersPage.ui
new file mode 100644
index 00000000..6d1a9bc0
--- /dev/null
+++ b/application/pages/instance/ServersPage.ui
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServersPage</class>
+ <widget class="QWidget" name="ServersPage">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>706</width>
+ <height>575</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <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>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <attribute name="title">
+ <string notr="true">Tab 1</string>
+ </attribute>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="3" column="1">
+ <widget class="QComboBox" name="resourceComboBox">
+ <item>
+ <property name="text">
+ <string>Ask to download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Always download</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Never download</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="resourcesLabel">
+ <property name="text">
+ <string>Reso&amp;urces</string>
+ </property>
+ <property name="buddy">
+ <cstring>resourceComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="addressLine"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="nameLabel">
+ <property name="text">
+ <string>&amp;Name</string>
+ </property>
+ <property name="buddy">
+ <cstring>nameLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="nameLine"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="addressLabel">
+ <property name="text">
+ <string>Address</string>
+ </property>
+ <property name="buddy">
+ <cstring>addressLine</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QTreeView" name="serversView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>64</width>
+ <height>64</height>
+ </size>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QPushButton" name="addBtn">
+ <property name="text">
+ <string>&amp;Add</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="removeBtn">
+ <property name="text">
+ <string>&amp;Remove</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="moveUpBtn">
+ <property name="text">
+ <string>Move Up</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="moveDownBtn">
+ <property name="text">
+ <string>Move Down</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshBtn">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>serversView</tabstop>
+ <tabstop>nameLine</tabstop>
+ <tabstop>addressLine</tabstop>
+ <tabstop>resourceComboBox</tabstop>
+ <tabstop>addBtn</tabstop>
+ <tabstop>removeBtn</tabstop>
+ <tabstop>moveUpBtn</tabstop>
+ <tabstop>moveDownBtn</tabstop>
+ <tabstop>refreshBtn</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>