blob: 294588e0bfbc318f9577596a121a5d57e6d90a57 [file] [log] [blame]
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/**
* Copyright (c) 2013-2017, Regents of the University of California.
*
* This file is part of ChronoShare, a decentralized file sharing application over NDN.
*
* ChronoShare is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* ChronoShare 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 General Public License for more details.
*
* You should have received copies of the GNU General Public License along with
* ChronoShare, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
*
* See AUTHORS.md for complete list of ChronoShare authors and contributors.
*/
#include "chronosharegui.hpp"
#include "core/logging.hpp"
#include <QDesktopServices>
#include <QDir>
#include <QFileInfo>
#include <QValidator>
#include <boost/throw_exception.hpp>
#include <thread>
namespace ndn {
namespace chronoshare {
namespace fs = boost::filesystem;
static const std::string HTTP_SERVER_ADDRESS = "localhost";
static const std::string HTTP_SERVER_PORT = "9001";
#ifdef _DEBUG
static const std::string DOC_ROOT = "gui/html";
#else
static const std::string DOC_ROOT = ":/html";
#endif
static const QString ICON_BIG_FILE(":/images/chronoshare-big.png");
static const QString ICON_TRAY_FILE(":/images/" TRAY_ICON);
_LOG_INIT(Gui);
ChronoShareGui::ChronoShareGui(QWidget* parent)
: QDialog(parent)
, m_httpServer(0)
#ifdef AUTOUPDATE
, m_sparkle(CHRONOSHARE_APPCAST)
#endif
{
setWindowTitle("Settings");
setMinimumWidth(600);
labelUsername = new QLabel("Username(hint: /<username>)");
labelSharedFolder = new QLabel("Shared Folder Name");
labelSharedFolderPath = new QLabel("Shared Folder Path");
QRegExp regex("(/[^/]+)+$");
QValidator* prefixValidator = new QRegExpValidator(regex, this);
editUsername = new QLineEdit();
editUsername->setValidator(prefixValidator);
QRegExp noPureWhiteSpace("^\\S+.*$");
QValidator* wsValidator = new QRegExpValidator(noPureWhiteSpace, this);
editSharedFolder = new QLineEdit();
editSharedFolder->setValidator(wsValidator);
editSharedFolderPath = new QLineEdit();
editSharedFolderPath->setReadOnly(true);
QPalette pal = editSharedFolderPath->palette();
pal.setColor(QPalette::Active, QPalette::Base, pal.color(QPalette::Disabled, QPalette::Base));
editSharedFolderPath->setPalette(pal);
button = new QPushButton("Save and apply settings");
QString versionString = QString("Version: ChronoShare v%1").arg(CHRONOSHARE_VERSION);
label = new QLabel(versionString, this);
connect(button, SIGNAL(clicked()), this, SLOT(changeSettings()));
mainLayout = new QVBoxLayout; // vertically
mainLayout->addWidget(labelUsername);
mainLayout->addWidget(editUsername);
mainLayout->addWidget(labelSharedFolder);
mainLayout->addWidget(editSharedFolder);
mainLayout->addWidget(labelSharedFolderPath);
mainLayout->addWidget(editSharedFolderPath);
mainLayout->addWidget(button);
mainLayout->addWidget(label);
setLayout(mainLayout);
// create actions that result from clicking a menu option
createActionsAndMenu();
// create tray icon
createTrayIcon();
// set icon image
setIcon();
// show tray icon
m_trayIcon->show();
// load settings
if (!loadSettings() || m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() ||
m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") {
// prompt user to choose folder
_LOG_DEBUG("First Time Setup");
openMessageBox("First Time Setup",
"Please enter a username, shared folder name and choose the shared folder path on your local filesystem.");
viewSettings();
openFileDialog();
viewSettings();
}
else {
startBackend();
}
}
ChronoShareGui::ChronoShareGui(QString dirPath, QString username, QString sharedFolderName, QWidget* parent)
: QDialog(parent)
, m_dirPath(dirPath)
, m_username(username)
, m_sharedFolderName(sharedFolderName)
#ifdef AUTOUPDATE
, m_sparkle(CHRONOSHARE_APPCAST)
#endif
{
if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() ||
m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") {
// prompt user to choose folder
BOOST_THROW_EXCEPTION(Error("Some error with init info"));
}
else {
startBackend();
}
}
void
ChronoShareGui::startBackend(bool restart /*=false*/)
{
if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() ||
m_sharedFolderName == "" || m_dirPath.isNull() || m_dirPath == "") {
_LOG_DEBUG("Don't start backend, because settings are in progress or incomplete");
return;
}
if (m_watcher != 0 && m_dispatcher != 0) {
if (!restart) {
return;
}
_LOG_DEBUG("Restarting Dispatcher and FileWatcher for the new location or new username");
m_watcher.reset(); // stop filewatching ASAP
m_dispatcher.reset(); // stop dispatcher ASAP, but after watcher(to prevent triggering callbacks
// on deleted object)
m_ioServiceWork.reset();
m_ioServiceManager->handle_stop();
m_NetworkThread.join();
delete m_ioServiceManager;
m_face.reset();
}
fs::path realPathToFolder(m_dirPath.toStdString());
realPathToFolder /= m_sharedFolderName.toStdString();
_LOG_DEBUG("m_dispatcher username:" << m_username.toStdString()
<< " m_sharedFolderName:" << m_sharedFolderName.toStdString()
<< " realPathToFolder: " << realPathToFolder);
m_ioService.reset(new boost::asio::io_service());
m_face.reset(new Face(*m_ioService));
m_dispatcher.reset(new Dispatcher(m_username.toStdString(), m_sharedFolderName.toStdString(),
realPathToFolder, *m_face));
// Alex: this **must** be here, otherwise m_dirPath will be uninitialized
m_watcher.reset(new FsWatcher(*m_ioService, realPathToFolder.string().c_str(),
bind(&Dispatcher::Did_LocalFile_AddOrModify, m_dispatcher.get(), _1),
bind(&Dispatcher::Did_LocalFile_Delete, m_dispatcher.get(), _1)));
try {
m_ioServiceManager = new IoServiceManager(*m_ioService);
m_NetworkThread = std::thread(&IoServiceManager::run, m_ioServiceManager);
}
catch (const std::exception& e) {
_LOG_ERROR("Start IO service or Face failed");
openWarningMessageBox("", "WARNING: Cannot allocate thread for face and io_service!",
QString("Starting chronoshare failed"
"Exception caused: %1")
.arg(e.what()));
// stop filewatching ASAP
m_watcher.reset();
m_dispatcher.reset();
return;
}
if (m_httpServer != 0) {
// no need to restart webserver if it already exists
return;
}
QFileInfo indexHtmlInfo(":/html/index.html");
if (indexHtmlInfo.exists()) {
try {
m_httpServer = new http::server::server(HTTP_SERVER_ADDRESS, HTTP_SERVER_PORT, DOC_ROOT);
m_httpServerThread = std::thread(&http::server::server::run, m_httpServer);
}
catch (const std::exception& e) {
_LOG_ERROR("Start http server failed");
m_httpServer = 0; // just to make sure
openWarningMessageBox("WARNING", "WARNING: Cannot start http server!",
QString("Starting http server failed. You will "
"not be able to check history from web "
"brower. Exception caused: %1")
.arg(e.what()));
}
}
else {
_LOG_ERROR("Http server doc root dir does not exist!");
}
}
ChronoShareGui::~ChronoShareGui()
{
// stop filewatching ASAP
m_watcher.reset();
// stop dispatcher ASAP, but after watcher to prevent triggering callbacks on the deleted object
m_dispatcher.reset();
m_ioServiceWork.reset();
m_ioServiceManager->handle_stop();
m_NetworkThread.join();
delete m_ioServiceManager;
if (m_httpServer != 0) {
m_httpServer->handle_stop();
m_httpServerThread.join();
delete m_httpServer;
}
// cleanup
delete m_trayIcon;
delete m_trayIconMenu;
delete m_openFolder;
delete m_viewSettings;
delete m_changeFolder;
delete m_quitProgram;
delete labelUsername;
delete labelSharedFolder;
delete editUsername;
delete editSharedFolder;
delete button;
delete label;
delete mainLayout;
}
void
ChronoShareGui::openMessageBox(QString title, QString text)
{
QMessageBox messageBox(this);
messageBox.setWindowTitle(title);
messageBox.setText(text);
messageBox.setIconPixmap(QPixmap(ICON_BIG_FILE));
messageBox.exec();
}
void
ChronoShareGui::openMessageBox(QString title, QString text, QString infotext)
{
QMessageBox messageBox(this);
messageBox.setWindowTitle(title);
messageBox.setText(text);
messageBox.setInformativeText(infotext);
messageBox.setIconPixmap(QPixmap(ICON_BIG_FILE));
messageBox.exec();
}
void
ChronoShareGui::openWarningMessageBox(QString title, QString text, QString infotext)
{
QMessageBox messageBox(this);
messageBox.setWindowTitle(title);
messageBox.setText(text);
messageBox.setInformativeText(infotext);
messageBox.setIcon(QMessageBox::Warning);
messageBox.setStandardButtons(QMessageBox::Ok);
messageBox.exec();
}
void
ChronoShareGui::createActionsAndMenu()
{
_LOG_DEBUG("Create actions");
// create the "open folder" action
m_openFolder = new QAction(tr("&Open Folder"), this);
connect(m_openFolder, SIGNAL(triggered()), this, SLOT(openSharedFolder()));
m_openWeb = new QAction(tr("Open in &Browser"), this);
connect(m_openWeb, SIGNAL(triggered()), this, SLOT(openInWebBrowser()));
m_recentFilesMenu = new QMenu(tr("Recently Changed Files"), this);
for (int i = 0; i < 5; i++) {
m_fileActions[i] = new QAction(this);
m_fileActions[i]->setVisible(false);
connect(m_fileActions[i], SIGNAL(triggered()), this, SLOT(openFile()));
m_recentFilesMenu->addAction(m_fileActions[i]);
}
connect(m_recentFilesMenu, SIGNAL(aboutToShow()), this, SLOT(updateRecentFilesMenu()));
// create the "view settings" action
m_viewSettings = new QAction(tr("&View Settings"), this);
connect(m_viewSettings, SIGNAL(triggered()), this, SLOT(viewSettings()));
// create the "change folder" action
m_changeFolder = new QAction(tr("&Change Folder"), this);
connect(m_changeFolder, SIGNAL(triggered()), this, SLOT(openFileDialog()));
#ifdef AUTOUPDATE
m_checkForUpdates = new QAction("Check for updates", this);
connect(m_checkForUpdates, SIGNAL(triggered()), this, SLOT(checkForUpdates()));
#endif // AUTOUPDATE
// create the "quit program" action
m_quitProgram = new QAction(tr("&Quit"), this);
connect(m_quitProgram, SIGNAL(triggered()), qApp, SLOT(quit()));
}
void
ChronoShareGui::createTrayIcon()
{
// create a new icon menu
m_trayIconMenu = new QMenu(this);
// add actions to the menu
m_trayIconMenu->addAction(m_openFolder);
m_trayIconMenu->addAction(m_openWeb);
m_trayIconMenu->addMenu(m_recentFilesMenu);
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(m_viewSettings);
m_trayIconMenu->addAction(m_changeFolder);
#ifdef AUTOUPDATE
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(m_checkForUpdates);
#endif // AUTOUPDATE
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(m_quitProgram);
// create new tray icon
m_trayIcon = new QSystemTrayIcon(this);
// associate the menu with the tray icon
m_trayIcon->setContextMenu(m_trayIconMenu);
// handle left click of icon
connect(m_trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this,
SLOT(trayIconClicked(QSystemTrayIcon::ActivationReason)));
}
void
ChronoShareGui::setIcon()
{
// set the icon image
m_trayIcon->setIcon(QIcon(ICON_TRAY_FILE));
}
void
ChronoShareGui::openSharedFolder()
{
fs::path realPathToFolder(m_dirPath.toStdString());
realPathToFolder /= m_sharedFolderName.toStdString();
QString path = QDir::toNativeSeparators(realPathToFolder.string().c_str());
QDesktopServices::openUrl(QUrl("file:///" + path));
}
void
ChronoShareGui::openInWebBrowser()
{
QUrl url("http://localhost:9001/");
url.setFragment("folderHistory&"
"user=" +
QUrl::toPercentEncoding(m_username) + "&"
"folder=" +
QUrl::toPercentEncoding(m_sharedFolderName));
// i give up. there is double encoding and I have no idea how to fight it...
QDesktopServices::openUrl(url);
}
void
ChronoShareGui::openFile()
{
// figure out who sent the signal
QAction* pAction = qobject_cast<QAction*>(sender());
if (pAction->isEnabled()) {
// we stored full path of the file in this toolTip field
#ifdef Q_WS_MAC
// we do some hack so we could show the file in Finder highlighted
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + pAction->toolTip() + "/" + pAction->text() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached("osascript", args);
#else
// too bad qt couldn't do highlighting for Linux(or Mac)
QDesktopServices::openUrl(QUrl("file:///" + pAction->toolTip()));
#endif
}
}
void
ChronoShareGui::updateRecentFilesMenu()
{
for (int i = 0; i < 5; i++) {
m_fileActions[i]->setVisible(false);
}
m_dispatcher->LookupRecentFileActions(std::bind(&ChronoShareGui::checkFileAction, this, _1, _2, _3),
5);
}
void
ChronoShareGui::checkFileAction(const std::string& filename, int action, int index)
{
fs::path realPathToFolder(m_dirPath.toStdString());
realPathToFolder /= m_sharedFolderName.toStdString();
realPathToFolder /= filename;
QString fullPath = realPathToFolder.string().c_str();
QFileInfo fileInfo(fullPath);
if (fileInfo.exists()) {
// This is a hack, we just use some field to store the path
m_fileActions[index]->setToolTip(fileInfo.absolutePath());
m_fileActions[index]->setEnabled(true);
}
else {
// after half an hour frustrating test and search around,
// I think it's the problem of Qt.
// According to the Qt doc, the action cannot be clicked
// and the area would be grey, but it didn't happen
// User can still trigger the action, and not greyed
// added check in SLOT to see if the action is "enalbed"
// as a remedy
// Give up at least for now
m_fileActions[index]->setEnabled(false);
// UPDATE, file not fetched yet
if (action == 0) {
QFont font;
// supposed by change the font, didn't happen
font.setWeight(QFont::Light);
m_fileActions[index]->setFont(font);
m_fileActions[index]->setToolTip(tr("Fetching..."));
}
// DELETE
else {
QFont font;
// supposed by change the font, didn't happen
font.setStrikeOut(true);
m_fileActions[index]->setFont(font);
m_fileActions[index]->setToolTip(tr("Deleted..."));
}
}
m_fileActions[index]->setText(fileInfo.fileName());
m_fileActions[index]->setVisible(true);
}
void
ChronoShareGui::changeSettings()
{
QString oldUsername = m_username;
QString oldSharedFolderName = m_sharedFolderName;
if (!editUsername->text().isEmpty())
m_username = editUsername->text().trimmed();
else
editUsername->setText(m_username);
if (!editSharedFolder->text().isEmpty())
m_sharedFolderName = editSharedFolder->text().trimmed();
else
editSharedFolder->setText(m_sharedFolderName);
if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() ||
m_sharedFolderName == "") {
openMessageBox("Error", "Username and shared folder name cannot be empty");
}
else {
saveSettings();
this->hide();
if (m_username != oldUsername || oldSharedFolderName != m_sharedFolderName) {
startBackend(true); // restart dispatcher/fswatcher
}
}
}
void
ChronoShareGui::openFileDialog()
{
while (true) {
// prompt user for new directory
QString tempPath =
QFileDialog::getExistingDirectory(this, tr("Choose ChronoShare folder"), m_dirPath,
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (tempPath.isNull()) {
if (m_dirPath.isNull()) {
openMessageBox("Notice",
"ChronoShare will use [" + QDir::homePath() +
"/ChronoShare]. \n\nYou can change it later selecting \"Change Folder\" menu.");
m_dirPath = QDir::homePath() + "/ChronoShare";
editSharedFolderPath->setText(m_dirPath);
break;
}
else {
// user just cancelled, no need to do anything else
return;
}
}
QFileInfo qFileInfo(tempPath);
if (!qFileInfo.isDir()) {
openMessageBox("Error", "Please select an existing folder or create a new one.");
continue;
}
if (m_dirPath == tempPath) {
// user selected the same directory, no need to do anything
return;
}
m_dirPath = tempPath;
editSharedFolderPath->setText(m_dirPath);
break;
}
_LOG_DEBUG("Selected path: " << m_dirPath.toStdString());
// save settings
saveSettings();
startBackend(true); // restart dispatcher/fswatcher
}
void
ChronoShareGui::trayIconClicked(QSystemTrayIcon::ActivationReason reason)
{
// if double clicked, open shared folder
if (reason == QSystemTrayIcon::DoubleClick) {
openSharedFolder();
}
}
void
ChronoShareGui::viewSettings()
{
//simple for now
this->show();
this->raise();
this->activateWindow();
}
bool
ChronoShareGui::loadSettings()
{
bool successful = true;
// Load Settings
// QSettings settings(m_settingsFilePath, QSettings::NativeFormat);
QSettings settings;
_LOG_DEBUG("load settings");
if (settings.contains("username")) {
m_username = settings.value("username", "admin").toString();
}
else {
successful = false;
}
editUsername->setText(m_username);
if (settings.contains("sharedfoldername")) {
m_sharedFolderName = settings.value("sharedfoldername", "shared").toString();
}
else {
successful = false;
}
editSharedFolder->setText(m_sharedFolderName);
if (settings.contains("dirPath")) {
m_dirPath = settings.value("dirPath", QDir::homePath()).toString();
}
else {
successful = false;
}
editSharedFolderPath->setText(m_dirPath);
_LOG_DEBUG("Found configured path: " << (successful ? m_dirPath.toStdString() : std::string("no")));
return successful;
}
void
ChronoShareGui::saveSettings()
{
// Save Settings
// QSettings settings(m_settingsFilePath, QSettings::NativeFormat);
QSettings settings;
settings.setValue("dirPath", m_dirPath);
settings.setValue("username", m_username);
settings.setValue("sharedfoldername", m_sharedFolderName);
}
void
ChronoShareGui::closeEvent(QCloseEvent* event)
{
_LOG_DEBUG("Close Event");
if (m_username.isNull() || m_username == "" || m_sharedFolderName.isNull() ||
m_sharedFolderName == "") {
openMessageBox("ChronoShare is not active",
"Username and/or shared folder name are not set.\n\n To activate ChronoShare, "
"open Settings menu and configure your username and shared folder name");
}
this->hide();
event->ignore(); // don't let the event propagate to the base class
}
#ifdef AUTOUPDATE
void
ChronoShareGui::checkForUpdates()
{
m_sparkle.checkForUpdates();
}
#endif // AUTOUPDATE
} // namespace chronoshare
} // namespace ndn
#include "chronosharegui.moc"