diff --git a/CMakeLists.txt b/CMakeLists.txt index 3853ce12..0d63b530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,6 +230,7 @@ SET(moc_headers src/ui/event-details-dialog.h src/ui/event-details-tree.h src/ui/set-repo-password-dialog.h + src/ui/sync-errors-dialog.h src/filebrowser/file-browser-manager.h src/filebrowser/file-browser-dialog.h src/filebrowser/file-browser-requests.h @@ -368,6 +369,7 @@ SET(seafile_client_sources src/rpc/rpc-client.cpp src/rpc/local-repo.cpp src/rpc/clone-task.cpp + src/rpc/sync-error.cpp src/ui/main-window.cpp src/ui/init-seafile-dialog.cpp src/ui/login-dialog.cpp @@ -410,6 +412,7 @@ SET(seafile_client_sources src/ui/event-details-dialog.cpp src/ui/event-details-tree.cpp src/ui/set-repo-password-dialog.cpp + src/ui/sync-errors-dialog.cpp src/filebrowser/file-browser-manager.cpp src/filebrowser/file-browser-dialog.cpp src/filebrowser/file-browser-requests.cpp diff --git a/qt-mac.css b/qt-mac.css index 0932fcbc..e34a2e2d 100644 --- a/qt-mac.css +++ b/qt-mac.css @@ -52,3 +52,8 @@ FileBrowserDialog QToolBar QToolButton#backwardButton { FileBrowserDialog QToolBar QToolButton#forwardButton { margin-left: 0px; } + +SyncErrorsDialog QWidget#mainWidget { + border : 0; + border-radius: 0px; +} diff --git a/qt-win.css b/qt-win.css index 5e13df96..164064b5 100644 --- a/qt-win.css +++ b/qt-win.css @@ -13,3 +13,7 @@ RepoTreeView { FileTableView QHeaderView::section:first { padding-left: 36px; } + +SyncErrorsTableView::item { + padding-left: 2px; +} diff --git a/qt.css b/qt.css index 5fe8f5b7..6ba507ab 100644 --- a/qt.css +++ b/qt.css @@ -618,3 +618,74 @@ SharedItemsTableView QHeaderView::section { padding-top: 10px; font-family: Arial; } + +SyncErrorsDialog { + margin: 0; + min-width: 700px; + min-height: 300px; +} + +SyncErrorsDialog QSizeGrip { + margin-right: 2px; +} + +SyncErrorsDialog QWidget#mainWidget { + border: 1px solid #333; + border-radius: 5px; + background-color: #f2f2f2; +} + +SyncErrorsDialog QWidget#mHeader { + min-height: 25px; + max-height: 35px; + margin-left: 10px; + margin-right: 10px; + padding-top: 2px; + padding-bottom: 5px; + /* border: 1px solid red; */ +} + +SyncErrorsDialog QWidget#mHeader QLabel { + padding-left: 60px; + margin-left: 4px; +} + +SyncErrorsDialog QWidget#mHeader QPushButton { + margin-left: 4px; +} + + +SyncErrorsTableView { + border: 0; + margin: 0; + padding-left: 14px; + padding-right: 14px; + border-bottom: 1px solid #e0e0e0; + background-color: white; + qproperty-focusPolicy: NoFocus; +} + +SyncErrorsTableView QHeaderView::section { + border: 0; + padding: 8px; + padding-left: 5px; + outline: 0px; + color: #b3b3b3; + background-color: white; + font-size: 12px; + padding-top: 20px; +} + +SyncErrorsTableView::item { + padding-top: 5px; + padding-left: 0px; + border-top: 1px solid #e0e0e0; +} + +SyncErrorsTableView::item :last { + padding-right: 13px; +} + +SyncErrorsTableView::item:selected { + background-color: #F9E0C7; +} diff --git a/seafile-applet.rc.in b/seafile-applet.rc.in index 054e1263..93cd4779 100644 --- a/seafile-applet.rc.in +++ b/seafile-applet.rc.in @@ -23,7 +23,7 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Seafile Ltd.\0" - VALUE "FileDescription", "The seafile client executable\0" + VALUE "FileDescription", "Seafile Client" VALUE "FileVersion", RC_VERSION_STR VALUE "InternalName", "seafile-applet\0" VALUE "OriginalFilename", "seafile-applet.exe\0" diff --git a/src/rpc/rpc-client.cpp b/src/rpc/rpc-client.cpp index 01c31ff2..4d3c958d 100644 --- a/src/rpc/rpc-client.cpp +++ b/src/rpc/rpc-client.cpp @@ -20,6 +20,7 @@ extern "C" { #include "utils/utils.h" #include "local-repo.h" #include "clone-task.h" +#include "sync-error.h" #include "api/commit-details.h" #include "rpc-client.h" @@ -1035,3 +1036,24 @@ bool SeafileRpcClient::getCommitDiff(const QString& repo_id, g_list_free (objlist); return true; } + +bool SeafileRpcClient::getSyncErrors(std::vector *errors, int offset, int limit) +{ + GError *error = NULL; + GList *objlist = searpc_client_call__objlist( + seafile_rpc_client_, + "seafile_get_file_sync_errors", + SEAFILE_TYPE_FILE_SYNC_ERROR, + &error, 2, "int", offset, "int", limit); + + + for (GList *ptr = objlist; ptr; ptr = ptr->next) { + SyncError error = SyncError::fromGObject((GObject *)ptr->data); + errors->push_back(error); + } + + g_list_foreach (objlist, (GFunc)g_object_unref, NULL); + g_list_free (objlist); + + return true; +} diff --git a/src/rpc/rpc-client.h b/src/rpc/rpc-client.h index 15aa6939..c1ef4852 100644 --- a/src/rpc/rpc-client.h +++ b/src/rpc/rpc-client.h @@ -17,6 +17,7 @@ struct _CcnetClient; class LocalRepo; class CloneTask; +class SyncError; class CommitDetails; class SeafileRpcClient : public QObject { @@ -129,6 +130,8 @@ public: const QString& previous_commit_id, CommitDetails *details); + bool getSyncErrors(std::vector *errors, int offset=0, int limit=10); + private: Q_DISABLE_COPY(SeafileRpcClient) diff --git a/src/rpc/sync-error.cpp b/src/rpc/sync-error.cpp new file mode 100644 index 00000000..7e916a41 --- /dev/null +++ b/src/rpc/sync-error.cpp @@ -0,0 +1,82 @@ +#include +#include +#include + +#include "utils/utils.h" +#include "sync-error.h" + +SyncError SyncError::fromGObject(GObject *obj) +{ + SyncError error; + + char *repo_id = NULL; + char *repo_name = NULL; + char *path = NULL; + int error_id = 0; + qint64 timestamp = 0; + + g_object_get (obj, + "repo_id", &repo_id, + "repo_name", &repo_name, + "path", &path, + "err_id", &error_id, + "timestamp", ×tamp, + NULL); + + error.repo_id = repo_id; + error.repo_name = QString::fromUtf8(repo_name); + error.path = QString::fromUtf8(path); + + error.error_id = error_id; + error.timestamp = timestamp; + + g_free (repo_id); + g_free (repo_name); + g_free (path); + + error.translateErrorStr(); + + return error; +} + +// Copied from seafile/daemon/repo-mgr.h +#define SYNC_ERROR_ID_FILE_LOCKED_BY_APP 0 +#define SYNC_ERROR_ID_FOLDER_LOCKED_BY_APP 1 +#define SYNC_ERROR_ID_FILE_LOCKED 2 +#define SYNC_ERROR_ID_INVALID_PATH 3 +#define SYNC_ERROR_ID_INDEX_ERROR 4 +#define SYNC_ERROR_ID_PATH_END_SPACE_PERIOD 5 +#define SYNC_ERROR_ID_PATH_INVALID_CHARACTER 6 + +void SyncError::translateErrorStr() +{ + readable_time_stamp = translateCommitTime(timestamp); + + switch (error_id) { + case SYNC_ERROR_ID_FILE_LOCKED_BY_APP: + error_str = QObject::tr("File is locked by another application"); + break; + case SYNC_ERROR_ID_FOLDER_LOCKED_BY_APP: + error_str = QObject::tr("Folder is locked by another application"); + break; + case SYNC_ERROR_ID_FILE_LOCKED: + error_str = QObject::tr("File is locked by another user"); + break; + case SYNC_ERROR_ID_INVALID_PATH: + error_str = QObject::tr("Path is invalid"); + break; + case SYNC_ERROR_ID_INDEX_ERROR: + error_str = QObject::tr("Error when indexing"); + break; + case SYNC_ERROR_ID_PATH_END_SPACE_PERIOD: + error_str = QObject::tr("Path ends with space or period character"); + break; + case SYNC_ERROR_ID_PATH_INVALID_CHARACTER: + error_str = QObject::tr("Path contains invalid characters like '|' or ':'"); + break; + default: + // unreachable + qWarning("unknown sync error id %d", error_id); + error_str = ""; + } +} diff --git a/src/rpc/sync-error.h b/src/rpc/sync-error.h new file mode 100644 index 00000000..2e75e97f --- /dev/null +++ b/src/rpc/sync-error.h @@ -0,0 +1,38 @@ +#ifndef SEAFILE_CLIENT_RPC_SYNC_ERROR_H +#define SEAFILE_CLIENT_RPC_SYNC_ERROR_H + +#include +#include + +struct _GObject; + +class SyncError { +public: + QString repo_id; + QString repo_name; + QString path; + qint64 timestamp; + int error_id; + + // Generated fields. + QString readable_time_stamp; + QString error_str; + + static SyncError fromGObject(_GObject *obj); + + void translateErrorStr(); + + bool operator==(const SyncError& rhs) const { + return repo_id == rhs.repo_id + && repo_name == rhs.repo_name + && path == rhs.path + && error_id == rhs.error_id + && timestamp == rhs.timestamp; + } + + bool operator!=(const SyncError& rhs) const { + return !(*this == rhs); + } +}; + +#endif // SEAFILE_CLIENT_RPC_SYNC_ERROR_H diff --git a/src/ui/repo-tree-view.cpp b/src/ui/repo-tree-view.cpp index 56964192..2de0bfda 100644 --- a/src/ui/repo-tree-view.cpp +++ b/src/ui/repo-tree-view.cpp @@ -437,8 +437,13 @@ void RepoTreeView::createActions() connect(share_repo_to_group_action_, SIGNAL(triggered()), this, SLOT(shareRepoToGroup())); - // In German translation there is a "seafile" string, so need to use tr("..").arg(..) here - open_in_filebrowser_action_ = new QAction(tr("&Open cloud file browser").arg(getBrand()), this); + QString open_action_text = tr("&Open cloud file browser"); + if (open_action_text.contains("%")) { + // In German translation there is a "seafile" string, so need to use tr("..").arg(..) here + open_action_text = open_action_text.arg(getBrand()); + } + + open_in_filebrowser_action_ = new QAction(open_action_text, this); open_in_filebrowser_action_->setIcon(QIcon(":/images/cloud-gray.png")); open_in_filebrowser_action_->setStatusTip(tr("open this library in embedded Cloud File Browser")); open_in_filebrowser_action_->setIconVisibleInMenu(true); diff --git a/src/ui/sync-errors-dialog.cpp b/src/ui/sync-errors-dialog.cpp new file mode 100644 index 00000000..928fcfa2 --- /dev/null +++ b/src/ui/sync-errors-dialog.cpp @@ -0,0 +1,437 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "QtAwesome.h" +#include "utils/utils.h" +#include "seafile-applet.h" +#include "rpc/rpc-client.h" +#include "rpc/sync-error.h" +#include "rpc/local-repo.h" +#include "sync-errors-dialog.h" + +namespace { + +const int kUpdateErrorsIntervalMSecs = 3000; + +const int kDefaultColumnWidth = 120; +const int kDefaultColumnHeight = 40; + +const int kRepoNameColumnWidth = 100; +const int kPathColumnWidth = 150; +const int kErrorColumnWidth = 200; +const int kTimestampColumnWidth = 80; +const int kExtraPadding = 80; + +const int kDefaultColumnSum = kRepoNameColumnWidth + kPathColumnWidth + kErrorColumnWidth + kTimestampColumnWidth + kExtraPadding; + +enum { + INDEX_EMPTY_VIEW = 0, + INDEX_TABE_VIEW +}; + +enum { + COLUMN_REPO_NAME = 0, + COLUMN_PATH, + COLUMN_ERROR_STR, + COLUMN_TIMESTAMP, + MAX_COLUMN, +}; + + +} // namespace + +// TODO: There are lots of common logic used in FileBrowserDialog and +// SyncErrorsDialog. We should refactor out a base dialog class. +SyncErrorsDialog::SyncErrorsDialog(QWidget *parent) + : QDialog(parent) +{ + // setupUi(this); + + setWindowTitle(tr("File Sync Errors")); + setWindowIcon(QIcon(":/images/seafile.png")); + setWindowFlags((windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::Dialog) +#if !defined(Q_OS_MAC) + | Qt::FramelessWindowHint +#endif + | Qt::Window); + + resizer_ = new QSizeGrip(this); + resizer_->resize(resizer_->sizeHint()); + setAttribute(Qt::WA_TranslucentBackground, true); + + createTitleBar(); + createEmptyView(); + + table_ = new SyncErrorsTableView; + model_ = new SyncErrorsTableModel(this); + table_->setModel(model_); + + QWidget* widget = new QWidget; + widget->setObjectName("mainWidget"); + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + setLayout(layout); + layout->addWidget(widget); + + QVBoxLayout *vlayout = new QVBoxLayout; + vlayout->setContentsMargins(1, 0, 1, 0); + vlayout->setSpacing(0); + widget->setLayout(vlayout); + + stack_ = new QStackedWidget; + stack_->insertWidget(INDEX_EMPTY_VIEW, empty_view_); + stack_->insertWidget(INDEX_TABE_VIEW, table_); + stack_->setContentsMargins(0, 0, 0, 0); + + vlayout->addWidget(header_); + vlayout->addWidget(stack_); + +#ifdef Q_OS_MAC + header_->setVisible(false); +#endif + + onModelReset(); + connect(model_, SIGNAL(modelReset()), this, SLOT(onModelReset())); +} + +void SyncErrorsDialog::createTitleBar() +{ + header_ = new QWidget; + header_->setObjectName("mHeader"); + QHBoxLayout *layout = new QHBoxLayout; + layout->setContentsMargins(1, 1, 1, 1); + layout->setSpacing(0); + header_->setLayout(layout); + + QSpacerItem *spacer1 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + layout->addSpacerItem(spacer1); + + brand_label_ = new QLabel(windowTitle()); + brand_label_->setObjectName("mBrand"); + layout->addWidget(brand_label_); + + QSpacerItem *spacer2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + layout->addSpacerItem(spacer2); + + minimize_button_ = new QPushButton; + minimize_button_->setObjectName("mMinimizeBtn"); + minimize_button_->setToolTip(tr("Minimize")); + minimize_button_->setIcon(awesome->icon(icon_minus, QColor("#808081"))); + layout->addWidget(minimize_button_); + connect(minimize_button_, SIGNAL(clicked()), this, SLOT(showMinimized())); + + close_button_ = new QPushButton; + close_button_->setObjectName("mCloseBtn"); + close_button_->setToolTip(tr("Close")); + close_button_->setIcon(awesome->icon(icon_remove, QColor("#808081"))); + layout->addWidget(close_button_); + connect(close_button_, SIGNAL(clicked()), this, SLOT(close())); + + header_->installEventFilter(this); +} + +bool SyncErrorsDialog::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == header_) { + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *ev = (QMouseEvent *)event; + QRect frame_rect = frameGeometry(); + old_pos_ = ev->globalPos() - frame_rect.topLeft(); + return true; + } else if (event->type() == QEvent::MouseMove) { + QMouseEvent *ev = (QMouseEvent *)event; + move(ev->globalPos() - old_pos_); + return true; + } + } + return QDialog::eventFilter(obj, event); +} + +void SyncErrorsDialog::closeEvent(QCloseEvent *event) +{ + event->ignore(); + this->hide(); +} + +void SyncErrorsDialog::resizeEvent(QResizeEvent *event) +{ + resizer_->move(rect().bottomRight() - resizer_->rect().bottomRight()); + resizer_->raise(); +} + +void SyncErrorsDialog::updateErrors() +{ + model_->updateErrors(); +} + +void SyncErrorsDialog::onModelReset() +{ + if (model_->rowCount() == 0) { + stack_->setCurrentIndex(INDEX_EMPTY_VIEW); + } else { + stack_->setCurrentIndex(INDEX_TABE_VIEW); + } +} + + +void SyncErrorsDialog::createEmptyView() +{ + empty_view_ = new QWidget(this); + + QVBoxLayout *layout = new QVBoxLayout; + empty_view_->setLayout(layout); + + QLabel *label = new QLabel; + label->setText(tr("No sync errors.")); + label->setAlignment(Qt::AlignCenter); + + layout->addWidget(label); +} + +SyncErrorsTableView::SyncErrorsTableView(QWidget *parent) + : QTableView(parent) +{ + verticalHeader()->hide(); + verticalHeader()->setDefaultSectionSize(36); + horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + horizontalHeader()->setStretchLastSection(true); + horizontalHeader()->setCascadingSectionResizes(true); + horizontalHeader()->setHighlightSections(false); + horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + + setGridStyle(Qt::NoPen); + setShowGrid(false); + setContentsMargins(0, 0, 0, 0); + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); + setMouseTracking(true); + + createContextMenu(); + + connect(this, SIGNAL(doubleClicked(const QModelIndex&)), + this, SLOT(onItemDoubleClicked(const QModelIndex&))); +} + +void SyncErrorsTableView::contextMenuEvent(QContextMenuEvent *event) +{ + QPoint pos = event->pos(); + int row = rowAt(pos.y()); + qDebug("row = %d\n", row); + if (row == -1) { + return; + } + + SyncErrorsTableModel *model = (SyncErrorsTableModel *)this->model(); + + SyncError error = model->errorAt(row); + + prepareContextMenu(error); + pos = viewport()->mapToGlobal(pos); + context_menu_->exec(pos); +} + +void SyncErrorsTableView::prepareContextMenu(const SyncError& error) +{ +} + +void SyncErrorsTableView::createContextMenu() +{ + context_menu_ = new QMenu(this); +} + +void SyncErrorsTableView::resizeEvent(QResizeEvent *event) +{ + QTableView::resizeEvent(event); + SyncErrorsTableModel *m = (SyncErrorsTableModel *)(model()); + m->onResize(event->size()); +} + +void SyncErrorsTableView::onItemDoubleClicked(const QModelIndex& index) +{ + SyncErrorsTableModel *model = (SyncErrorsTableModel *)this->model(); + SyncError error = model->errorAt(index.row()); + + // printf("error repo id is %s\n", error.repo_id.toUtf8().data()); + if (!error.repo_id.isEmpty()) { + LocalRepo repo; + seafApplet->rpcClient()->getLocalRepo(error.repo_id, &repo); + if (repo.isValid()) { + QDesktopServices::openUrl(QUrl::fromLocalFile(repo.worktree)); + } + } +} + +SyncErrorsTableModel::SyncErrorsTableModel(QObject *parent) + : QAbstractTableModel(parent), + repo_name_column_width_(kRepoNameColumnWidth), + path_column_width_(kPathColumnWidth), + error_column_width_(kErrorColumnWidth) +{ + update_timer_ = new QTimer(this); + connect(update_timer_, SIGNAL(timeout()), this, SLOT(updateErrors())); + update_timer_->start(kUpdateErrorsIntervalMSecs); + + updateErrors(); +} + +void SyncErrorsTableModel::updateErrors() +{ + std::vector errors; + int ret = seafApplet->rpcClient()->getSyncErrors(&errors, 0, 50); + if (ret < 0) { + qDebug("failed to get sync errors"); + return; + } + + // SyncError fake_error; + // fake_error.repo_id = "xxx"; + // fake_error.repo_name = "NotSoGood"; + // fake_error.path = "/tmp/NotSoGood/BadFile"; + // fake_error.error_id = 5; + // fake_error.timestamp = 1483056000; + // fake_error.translateErrorStr(); + // errors.push_back(fake_error); + + if (errors_ == errors) { + return; + } + + if (errors_.size() != errors.size()) { + beginResetModel(); + errors_ = errors; + endResetModel(); + return; + } + + for (int i = 0, n = errors.size(); i < n; i++) { + if (errors_[i] == errors[i]) { + continue; + } + + errors_[i] = errors[i]; + QModelIndex start = QModelIndex().child(i, 0); + QModelIndex stop = QModelIndex().child(i, MAX_COLUMN - 1); + emit dataChanged(start, stop); + } +} + +int SyncErrorsTableModel::rowCount(const QModelIndex& parent) const +{ + return errors_.size(); +} + +int SyncErrorsTableModel::columnCount(const QModelIndex& parent) const +{ + return MAX_COLUMN; +} + +void SyncErrorsTableModel::onResize(const QSize &size) +{ + int extra_width = size.width() - kDefaultColumnSum; + int extra_width_per_column = extra_width / 3; + + repo_name_column_width_ = kRepoNameColumnWidth + extra_width_per_column; + path_column_width_ = kPathColumnWidth + extra_width_per_column; + error_column_width_ = kErrorColumnWidth + extra_width_per_column; + + // name_column_width_ should be always larger than kPathColumnWidth + if (errors_.empty()) + return; + + // printf ("path_column_width_ = %d\n", path_column_width_); + emit dataChanged( + index(0, COLUMN_ERROR_STR), + index(errors_.size() - 1 , COLUMN_ERROR_STR)); +} + +QVariant SyncErrorsTableModel::data(const QModelIndex & index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + int column = index.column(); + + if (role == Qt::TextAlignmentRole) + return Qt::AlignLeft + Qt::AlignVCenter; + + if (role == Qt::ToolTipRole) + return tr("Double click to open the library"); + + if (role == Qt::SizeHintRole) { + int h = kDefaultColumnHeight; + int w = kDefaultColumnWidth; + switch (column) { + case COLUMN_REPO_NAME: + w = repo_name_column_width_; + break; + case COLUMN_PATH: + w = path_column_width_; + break; + case COLUMN_ERROR_STR: + w = error_column_width_; + break; + case COLUMN_TIMESTAMP: + w = kTimestampColumnWidth; + break; + default: + break; + } + return QSize(w, h); + } + + if (role != Qt::DisplayRole) { + return QVariant(); + } + + const SyncError &error = errors_[index.row()]; + + if (column == COLUMN_REPO_NAME) { + return error.repo_name; + } else if (column == COLUMN_PATH) { + return QDir::toNativeSeparators(error.path); + } else if (column == COLUMN_ERROR_STR) { + return error.error_str; + } else if (column == COLUMN_TIMESTAMP) { + return error.readable_time_stamp; + } + + return QVariant(); +} + +QVariant SyncErrorsTableModel::headerData(int section, + Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (role == Qt::TextAlignmentRole) + return Qt::AlignLeft + Qt::AlignVCenter; + + if (role != Qt::DisplayRole) + return QVariant(); + + if (section == COLUMN_REPO_NAME) { + return tr("Library"); + } else if (section == COLUMN_PATH) { + return tr("Path"); + } else if (section == COLUMN_ERROR_STR) { + return tr("Error"); + } else if (section == COLUMN_TIMESTAMP) { + return tr("Time"); + } + + + return QVariant(); +} diff --git a/src/ui/sync-errors-dialog.h b/src/ui/sync-errors-dialog.h new file mode 100644 index 00000000..2135c22d --- /dev/null +++ b/src/ui/sync-errors-dialog.h @@ -0,0 +1,109 @@ +#ifndef SEAFILE_CLIENT_SYNC_ERRORS_DIALOG_H +#define SEAFILE_CLIENT_SYNC_ERRORS_DIALOG_H + +#include + +#include +#include +#include +#include + +#include "rpc/sync-error.h" + +class QTimer; +class QStackedWidget; +class QResizeEvent; +class QSizeGrip; +class QLabel; +class QEvent; + +class SyncError; +class SyncErrorsTableView; +class SyncErrorsTableModel; + +class SyncErrorsDialog : public QDialog +{ + Q_OBJECT + +public: + SyncErrorsDialog(QWidget *parent=0); + void updateErrors(); + + void resizeEvent(QResizeEvent *event); + void closeEvent(QCloseEvent *event); + +private slots: + void onModelReset(); + +private: + void createTitleBar(); + void createEmptyView(); + + bool eventFilter(QObject *obj, QEvent *event); + + // title bar (windows and osx only) + QWidget *header_; + QLabel *brand_label_; + QPushButton *minimize_button_; + QPushButton *close_button_; + QPoint old_pos_; + + QSizeGrip *resizer_; + + QStackedWidget *stack_; + SyncErrorsTableView *table_; + SyncErrorsTableModel *model_; + QWidget *empty_view_; +}; + +class SyncErrorsTableView : public QTableView +{ + Q_OBJECT + +public: + SyncErrorsTableView(QWidget *parent=0); + + void contextMenuEvent(QContextMenuEvent *event); + void resizeEvent(QResizeEvent *event); + +private slots: + void onItemDoubleClicked(const QModelIndex& index); + +private: + void createContextMenu(); + void prepareContextMenu(const SyncError& error); + +private: + QMenu *context_menu_; +}; + + +class SyncErrorsTableModel : public QAbstractTableModel +{ + Q_OBJECT +public: + SyncErrorsTableModel(QObject *parent=0); + + int rowCount(const QModelIndex& parent=QModelIndex()) const; + int columnCount(const QModelIndex& parent=QModelIndex()) const; + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + + SyncError errorAt(size_t i) const { return (i >= errors_.size()) ? SyncError() : errors_[i]; } + + void onResize(const QSize& size); + +public slots: + void updateErrors(); + +private: + + std::vector errors_; + QTimer *update_timer_; + int repo_name_column_width_; + int path_column_width_; + int error_column_width_; +}; + +#endif // SEAFILE_CLIENT_SYNC_ERRORS_DIALOG_H diff --git a/src/ui/tray-icon.cpp b/src/ui/tray-icon.cpp index 5f220f84..377bca1d 100644 --- a/src/ui/tray-icon.cpp +++ b/src/ui/tray-icon.cpp @@ -31,6 +31,7 @@ extern "C" { #include "seahub-notifications-monitor.h" #include "server-status-service.h" #include "api/commit-details.h" +#include "sync-errors-dialog.h" #include "tray-icon.h" #if defined(Q_OS_MAC) @@ -112,7 +113,8 @@ SeafileTrayIcon::SeafileTrayIcon(QObject *parent) nth_trayicon_(0), rotate_counter_(0), state_(STATE_DAEMON_UP), - next_message_msec_(0) + next_message_msec_(0), + sync_errors_dialog_(nullptr) { setState(STATE_DAEMON_DOWN); rotate_timer_ = new QTimer(this); @@ -185,6 +187,10 @@ void SeafileTrayIcon::createActions() open_log_directory_action_->setStatusTip(tr("open %1 log folder").arg(getBrand())); connect(open_log_directory_action_, SIGNAL(triggered()), this, SLOT(openLogDirectory())); + show_sync_errors_action_ = new QAction(tr("Show file sync errors"), this); + show_sync_errors_action_->setStatusTip(tr("Show file sync errors")); + connect(show_sync_errors_action_, SIGNAL(triggered()), this, SLOT(showSyncErrorsDialog())); + about_action_ = new QAction(tr("&About"), this); about_action_->setStatusTip(tr("Show the application's About box")); connect(about_action_, SIGNAL(triggered()), this, SLOT(about())); @@ -206,6 +212,7 @@ void SeafileTrayIcon::createContextMenu() context_menu_->addAction(open_seafile_folder_action_); context_menu_->addAction(settings_action_); context_menu_->addAction(open_log_directory_action_); + context_menu_->addAction(show_sync_errors_action_); // context_menu_->addMenu(help_menu_); context_menu_->addSeparator(); context_menu_->addAction(about_action_); @@ -245,6 +252,7 @@ void SeafileTrayIcon::createGlobalMenuBar() global_menu_->addAction(open_seafile_folder_action_); global_menu_->addAction(settings_action_); global_menu_->addAction(open_log_directory_action_); + global_menu_->addAction(show_sync_errors_action_); global_menu_->addSeparator(); global_menu_->addAction(enable_auto_sync_action_); global_menu_->addAction(disable_auto_sync_action_); @@ -653,3 +661,17 @@ void SeafileTrayIcon::checkTrayIconMessageQueue() commit_id_ = msg.commit_id; next_message_msec_ = now + kMessageDisplayTimeMSecs; } + + +void SeafileTrayIcon::showSyncErrorsDialog() +{ + // CloneTasksDialog dialog(this); + if (sync_errors_dialog_ == nullptr) { + sync_errors_dialog_ = new SyncErrorsDialog; + } + + sync_errors_dialog_->updateErrors(); + sync_errors_dialog_->show(); + sync_errors_dialog_->raise(); + sync_errors_dialog_->activateWindow(); +} diff --git a/src/ui/tray-icon.h b/src/ui/tray-icon.h index c3ecf1c0..d9047970 100644 --- a/src/ui/tray-icon.h +++ b/src/ui/tray-icon.h @@ -12,6 +12,8 @@ class QMenuBar; class TrayNotificationManager; #endif +class SyncErrorsDialog; + class SeafileTrayIcon : public QSystemTrayIcon { Q_OBJECT @@ -68,6 +70,8 @@ private slots: // only used on windows void onMessageClicked(); + void showSyncErrorsDialog(); + private: Q_DISABLE_COPY(SeafileTrayIcon) @@ -92,6 +96,7 @@ private: QAction *settings_action_; QAction *open_seafile_folder_action_; QAction *open_log_directory_action_; + QAction *show_sync_errors_action_; QAction *view_unread_seahub_notifications_action_; QAction *about_action_; @@ -128,6 +133,8 @@ private: // displayed at least several seconds. QQueue pending_messages_; qint64 next_message_msec_; + + SyncErrorsDialog *sync_errors_dialog_; }; #endif // SEAFILE_CLIENT_TRAY_ICON_H diff --git a/src/utils/process-win.cpp b/src/utils/process-win.cpp index eac08e6d..7b14b2ba 100644 --- a/src/utils/process-win.cpp +++ b/src/utils/process-win.cpp @@ -117,7 +117,7 @@ int count_process (const char *process_name_in) HANDLE hProcess; DWORD length; int count = 0; - int i, j; + DWORD i; if (strstr(process_name_in, ".exe")) { snprintf (name, sizeof(name), "%s", process_name_in);