Codebase list mozc / 262156b src / gui / config_dialog / generic_table_editor.cc
262156b

Tree @262156b (Download .tar.gz)

generic_table_editor.cc @262156braw · history · blame

// Copyright 2010-2020, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "gui/config_dialog/generic_table_editor.h"

#include <QtCore/QFile>
#include <QtGui/QtGui>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>

#include <algorithm>  // for unique
#include <cctype>
#include <sstream>
#include <string>
#include <vector>

#include "base/file_stream.h"
#include "base/logging.h"
#include "base/util.h"
#include "protocol/commands.pb.h"

namespace mozc {
namespace gui {
namespace {
const size_t kMaxEntrySize = 10000;

int GetTableHeight(QTableWidget *widget) {
  // Dragon Hack:
  // Here we use "龍" to calc font size, as it looks almsot square
  const char kHexBaseChar[] = "龍";
  const QRect rect =
      QFontMetrics(widget->font()).boundingRect(QObject::trUtf8(kHexBaseChar));
#ifdef OS_WIN
  return static_cast<int>(rect.height() * 1.3);
#else
  return static_cast<int>(rect.height() * 1.4);
#endif  // OS_WIN
}
}  // namespace

GenericTableEditorDialog::GenericTableEditorDialog(QWidget *parent,
                                                   size_t column_size)
    : QDialog(parent), edit_menu_(new QMenu(this)), column_size_(column_size) {
  setupUi(this);
  editorTableWidget->setAlternatingRowColors(true);
  setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint |
                 Qt::Tool);
  editorTableWidget->setColumnCount(column_size_);

  CHECK_GT(column_size_, 0);

  // Mac style
#ifdef __APPLE__
  editorTableWidget->setShowGrid(false);
  layout()->setContentsMargins(0, 0, 0, 4);
  gridLayout->setHorizontalSpacing(12);
  gridLayout->setVerticalSpacing(12);
#endif  // __APPLE__

  editButton->setText(tr("Edit"));
  editButton->setMenu(edit_menu_);

  QObject::connect(edit_menu_, SIGNAL(triggered(QAction *)), this,
                   SLOT(OnEditMenuAction(QAction *)));

  QObject::connect(editorButtonBox, SIGNAL(clicked(QAbstractButton *)), this,
                   SLOT(Clicked(QAbstractButton *)));

  editorTableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
  connect(editorTableWidget, SIGNAL(customContextMenuRequested(const QPoint &)),
          this, SLOT(OnContextMenuRequested(const QPoint &)));

  editorTableWidget->horizontalHeader()->setStretchLastSection(true);
  editorTableWidget->horizontalHeader()->setSortIndicatorShown(true);
  editorTableWidget->horizontalHeader()->setHighlightSections(false);
  // Do not use QAbstractItemView::AllEditTriggers so that user can easily
  // select multiple items. See b/6488800.
  editorTableWidget->setEditTriggers(QAbstractItemView::AnyKeyPressed |
                                     QAbstractItemView::DoubleClicked |
                                     QAbstractItemView::SelectedClicked);
  editorTableWidget->setSortingEnabled(true);

  editorTableWidget->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
  editorTableWidget->verticalHeader()->setDefaultSectionSize(
      GetTableHeight(editorTableWidget));

  UpdateMenuStatus();
}

GenericTableEditorDialog::~GenericTableEditorDialog() {}

QTableWidget *GenericTableEditorDialog::mutable_table_widget() {
  return editorTableWidget;
}

const string &GenericTableEditorDialog::table() const { return table_; }

string *GenericTableEditorDialog::mutable_table() { return &table_; }

QMenu *GenericTableEditorDialog::mutable_edit_menu() { return edit_menu_; }

bool GenericTableEditorDialog::LoadFromString(const string &str) {
  std::istringstream istr(str);
  return LoadFromStream(&istr);
}

void GenericTableEditorDialog::DeleteSelectedItems() {
  std::vector<int> rows;
  QList<QTableWidgetItem *> selected = editorTableWidget->selectedItems();

  // remember the last column as user chooses the
  // last rows from top to bottom in general.
  const int column = selected.isEmpty() ? 0 : selected.back()->column();

  for (int i = 0; i < selected.size(); ++i) {
    rows.push_back(selected[i]->row());
  }

  std::vector<int>::iterator last = unique(rows.begin(), rows.end());
  rows.erase(last, rows.end());

  if (rows.empty()) {
    QMessageBox::warning(this, windowTitle(), tr("No entry is selected"));
    return;
  }

  // Choose next or prev item.
  QTableWidgetItem *item = editorTableWidget->item(rows.back() + 1, column);
  if (item == nullptr) {
    item = editorTableWidget->item(rows.back() - 1, column);
  }

  // select item
  if (item != nullptr) {
    editorTableWidget->setCurrentItem(item);
  }

  // remove from the buttom
  for (int i = rows.size() - 1; i >= 0; --i) {
    editorTableWidget->removeRow(rows[i]);
  }

  UpdateMenuStatus();
}

void GenericTableEditorDialog::InsertEmptyItem(int row) {
  editorTableWidget->verticalHeader()->hide();

  // It is important to disable auto-sorting before we programmatically edit
  // multiple items. Otherwise, one single edit of cell such as
  //   editorTableWidget->setItem(row, col, data);
  // will cause auto-sorting and the target row will be moved to different
  // place.
  const bool sorting_enabled = editorTableWidget->isSortingEnabled();
  if (sorting_enabled) {
    editorTableWidget->setSortingEnabled(false);
  }

  editorTableWidget->insertRow(row);
  for (size_t i = 0; i < column_size_; ++i) {
    editorTableWidget->setItem(row, i, new QTableWidgetItem(""));
  }
  QTableWidgetItem *item = editorTableWidget->item(row, 0);
  if (item != nullptr) {
    editorTableWidget->setCurrentItem(item);
    editorTableWidget->scrollToItem(item, QAbstractItemView::PositionAtCenter);
    editorTableWidget->editItem(item);
  }

  // Restore auto-sorting setting if necessary.
  if (sorting_enabled) {
    // From the usability perspective, auto-sorting should be disabled until
    // a user explicitly enables it again by clicking the table header.
    // To achieve it, set -1 to the |logicalIndex| in setSortIndicator.
    editorTableWidget->horizontalHeader()->setSortIndicator(-1,
                                                            Qt::AscendingOrder);
    editorTableWidget->setSortingEnabled(true);
  }

  UpdateMenuStatus();
}

void GenericTableEditorDialog::InsertItem() {
  QTableWidgetItem *current = editorTableWidget->currentItem();
  if (current == nullptr) {
    QMessageBox::warning(this, windowTitle(), tr("No entry is selected"));
    return;
  }
  InsertEmptyItem(current->row() + 1);
}

void GenericTableEditorDialog::AddNewItem() {
  if (editorTableWidget->rowCount() >= max_entry_size()) {
    QMessageBox::warning(
        this, windowTitle(),
        tr("You can't have more than %1 entries").arg(max_entry_size()));
    return;
  }

  InsertEmptyItem(editorTableWidget->rowCount());
}

void GenericTableEditorDialog::Import() {
  const QString filename = QFileDialog::getOpenFileName(
      this, tr("import from file"), QDir::homePath());
  if (filename.isEmpty()) {
    return;
  }

  QFile file(filename);
  if (!file.exists()) {
    QMessageBox::warning(this, windowTitle(), tr("File not found"));
    return;
  }

  const qint64 kMaxSize = 100 * 1024;
  if (file.size() >= kMaxSize) {
    QMessageBox::warning(this, windowTitle(),
                         tr("The specified file is too large (>=100K byte)"));
    return;
  }

  InputFileStream ifs(filename.toStdString().c_str());
  if (!LoadFromStream(&ifs)) {
    QMessageBox::warning(this, windowTitle(), tr("Import failed"));
    return;
  }
}

void GenericTableEditorDialog::Export() {
  if (!Update()) {
    return;
  }

  const QString filename =
      QFileDialog::getSaveFileName(this, tr("export to file"),
                                   QDir::homePath() + QDir::separator() +
                                       QString(GetDefaultFilename().c_str()));
  if (filename.isEmpty()) {
    return;
  }

  OutputFileStream ofs(filename.toStdString().c_str());
  if (!ofs) {
    QMessageBox::warning(this, windowTitle(), tr("Export failed"));
    return;
  }

  ofs << table();
}

void GenericTableEditorDialog::Clicked(QAbstractButton *button) {
  // Workaround for http://b/242686
  // By changing the foucs, incomplete entries in QTableView are
  // submitted to the model.
  editButton->setFocus(Qt::MouseFocusReason);

  switch (editorButtonBox->buttonRole(button)) {
    /// number of role might be increased in the future.
    case QDialogButtonBox::AcceptRole:
      if (Update()) {
        QDialog::accept();
      }
      break;
    default:
      QDialog::reject();
      break;
  }
}

void GenericTableEditorDialog::OnContextMenuRequested(const QPoint &pos) {
  QTableWidgetItem *item = editorTableWidget->itemAt(pos);
  if (item == nullptr) {
    return;
  }

  QMenu *menu = new QMenu(this);
  QAction *edit_action = nullptr;
  const QList<QTableWidgetItem *> selected_items =
      editorTableWidget->selectedItems();
  if (selected_items.count() == 1) {
    edit_action = menu->addAction(tr("Edit entry"));
  }
  QAction *rename_action = menu->addAction(tr("New entry"));
  QAction *delete_action = menu->addAction(tr("Remove entry"));
  QAction *selected_action = menu->exec(QCursor::pos());

  if (edit_action != nullptr && selected_action == edit_action) {
    editorTableWidget->editItem(selected_items[0]);
  } else if (selected_action == rename_action) {
    AddNewItem();
  } else if (selected_action == delete_action) {
    DeleteSelectedItems();
  }
}

void GenericTableEditorDialog::UpdateOKButton(bool status) {
  QPushButton *button = editorButtonBox->button(QDialogButtonBox::Ok);
  if (button != nullptr) {
    button->setEnabled(status);
  }
}

size_t GenericTableEditorDialog::max_entry_size() const {
  return kMaxEntrySize;
}

bool GenericTableEditorDialog::LoadFromStream(std::istream *is) { return true; }

bool GenericTableEditorDialog::Update() { return true; }

void GenericTableEditorDialog::UpdateMenuStatus() {}

void GenericTableEditorDialog::OnEditMenuAction(QAction *action) {}
}  // namespace gui
}  // namespace mozc