Manitou-Mail logo title

Source file: src/tagsdialog.cpp

/* Copyright (C) 2004-2010 Daniel Verite

   This file is part of Manitou-Mail (see http://www.manitou-mail.org)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 2 as
   published by the Free Software Foundation.

   This program 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 a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/

#include "tagsdialog.h"

#include <QLayout>
#include <QPushButton>
#include <QMessageBox>
#include <QDropEvent>
#include <QCloseEvent>
#include <QTimer>

#include "main.h"
#include "db.h"
#include "tags.h"
#include "errors.h"
#include "message_port.h"
#include "icons.h"

//
// tags_dialog
//
tags_dialog::tags_dialog(QWidget* parent) : QDialog(parent)
{
  m_new_tag_default_name = tr("(New Tag)");
  m_new_id=-1;
  //  setWFlags(getWFlags() | Qt::WDestructiveClose);
  setWindowTitle(tr("Message Tags"));
  setWindowIcon(UI_ICON(FT_ICON16_TAGS));
  resize(400, 600);
  QVBoxLayout* top_layout = new QVBoxLayout(this);
  top_layout->setMargin(5);
  top_layout->setSpacing(5);

  m_qlist=new tags_treeview(this);
  m_qlist->set_tag_default_name(m_new_tag_default_name);
  m_qlist->setRootIsDecorated(true);

  top_layout->addWidget(m_qlist);

  m_qlist->setHeaderLabel(tr("Tag name"));
  connect(m_qlist, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
	  this, SLOT(sel_changed()));

  QHBoxLayout* hb = new QHBoxLayout();
  top_layout->addLayout(hb);
  hb->setSpacing(5);
  m_btn_new = new QPushButton(tr("New"));
  m_btn_edit = new QPushButton(tr("Edit"));
  m_btn_delete = new QPushButton(tr("Delete"));
  hb->addWidget(m_btn_new);
  hb->addWidget(m_btn_edit);
  hb->addWidget(m_btn_delete);
  connect(m_btn_new, SIGNAL(clicked()), this, SLOT(action_new()));
  connect(m_btn_edit, SIGNAL(clicked()), this, SLOT(action_edit()));
  connect(m_btn_delete, SIGNAL(clicked()), this, SLOT(action_delete()));

  QHBoxLayout* hbl = new QHBoxLayout();
  top_layout->addLayout(hbl);

  hbl->addStretch(1);
  QPushButton* bclose=new QPushButton(tr("OK"), this);
  hbl->addWidget(bclose);
  connect(bclose, SIGNAL(clicked()), this, SLOT(ok()));
  hbl->setStretchFactor(bclose, 3);
  hbl->addStretch(1);
  QPushButton* bcancel=new QPushButton(tr("Cancel"), this);
  connect(bcancel, SIGNAL(clicked()), this, SLOT(reject()));
  hbl->addWidget(bcancel);
  hbl->setStretchFactor(bcancel, 3);
  hbl->addStretch(1);

  m_root_tag.setName(tr("(Root)"));
  m_root_item = new tag_item(m_qlist, m_root_tag);
  m_root_item->setFlags(m_root_item->flags() & ~(Qt::ItemIsEditable|Qt::ItemIsDragEnabled));
  m_root_item->setExpanded(true);

  m_list.fetch();
  int cnt=m_list.size();
  tags_definition_list::iterator iter;
  std::map<int,tag_item*> map_done;
  std::map<int,tag_item*>::iterator ip;
  int prev_cnt=cnt+1;

  while (cnt!=0 && cnt<prev_cnt) {
    prev_cnt=cnt;
    for (iter=m_list.begin(); iter!=m_list.end(); ++iter) {
      ip = map_done.find(iter->getId());
      if (ip==map_done.end()) {
	if (!iter->parent_id()) {
	  map_done[iter->getId()] = new tag_item(m_root_item, *iter);
	  cnt--;
	}
	else {
	  ip = map_done.find(iter->parent_id());
	  if (ip!=map_done.end()) {
	    // if the parent exists, then create the child
	    map_done[iter->getId()] = new tag_item(ip->second, *iter);
	    ip->second->setExpanded(true);
	    cnt--;
	  }
	}
      }
    }
  }
  if (cnt>0) {
    DBG_PRINTF(2, "cnt=%d, prev_cnt=%d", cnt, prev_cnt);
    m_qlist->clear();
    throw ui_error(tr("Inconsistant hierarchy in 'tags' table"));
  }

  m_qlist->sortItems(0, Qt::AscendingOrder);
  sel_changed();

  connect(m_qlist, SIGNAL(itemChanged(QTreeWidgetItem*, int)),
	  this, SLOT(item_changed(QTreeWidgetItem*, int)));

  message_port::connect_sender(this, SIGNAL(tags_restructured()), SLOT(tags_updated()));

}

tags_dialog::~tags_dialog()
{
}


void
tags_dialog::closeEvent(QCloseEvent* event)
{
  bool need_save = false;

  QTreeWidgetItemIterator it(m_qlist);
  for ( ; !need_save && *it; ++it) {
    tag_item* item = dynamic_cast<tag_item*>(*it);
    need_save = item->is_dirty();
  }

  if (need_save) {
    QString msg = tr("There are unsaved changes.\nSave now?");
    int rep = QMessageBox::question(this, tr("Confirmation"), msg, QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel);

    if (rep==QMessageBox::Cancel) {
      event->ignore();
      return;
    }
    if (rep==QMessageBox::Save) {
      event->ignore();
      ok();
    }
    else
      event->accept();
  }
}

void
tags_dialog::item_changed(QTreeWidgetItem* item, int col)
{
  Q_UNUSED(col);
  bool cancel=false;
  tag_item* q = static_cast<tag_item*>(item);
  message_tag& t = q->tag();
  if (q->text(0).trimmed().isEmpty()) {
    // rule: no empty tag name
    QMessageBox::critical(this, tr("Error"), tr("A tag name cannot be empty or contain only spaces"));
    cancel=true;
  }
  else {
    // rule: no name duplicate at the same level of hierarchy
    QString comp_name = q->text(0).trimmed().toLower();
    QTreeWidgetItem* parent = item->parent();
    if (parent) {
      int index = parent->indexOfChild(item);
      for (int i=0; i<parent->childCount() && !cancel; i++) {
	if (index!=i && parent->child(i)->text(0).trimmed().toLower() == comp_name) {
	  QMessageBox::critical(this, tr("Error"),
				tr("This name is already used for another tag of the same branch"));
	  cancel=true;
	  
	}
      }
    }
  }
	
  if (cancel) {
    //    q->setText(0, t.name());
    /* Keep editing. FIXME: if the user cancels the edition after that point,
       the value that we just rejected will be the current value.
       We need a second pass at save time to filter again against these values. */
    m_qlist->last_failed = item;
    QTimer::singleShot(0, m_qlist, SLOT(reedit()));
  }
  else {
    t.set_name(q->text(0));
    q->set_dirty(true);
  }
}

void
tags_dialog::action_new()
{
  tag_item* nitem;
  tag_item* item = dynamic_cast<tag_item*>(m_qlist->currentItem());
  if (!item)
    item = m_root_item;

  message_tag mt(m_new_id--, m_new_tag_default_name);

  m_qlist->blockSignals(true); 	// prevents itemChanged signals
  nitem = new tag_item(item, mt);
  m_qlist->blockSignals(false);

  if (item->parent()) {
    nitem->tag().set_parent_id(item->tag().id());
  }
  m_qlist->setCurrentItem(nitem);
  m_qlist->scrollToItem(nitem);
  m_qlist->editItem(nitem);

  
}

void
tags_dialog::action_edit()
{
  QTreeWidgetItem* item = m_qlist->currentItem();
  if (item)
    m_qlist->editItem(item);
}



// Record the ids of all sub-tags and the parent
void
tags_dialog::collect_childs(tag_item* parent, std::set<int>& set)
{
  for (int i=0; i<parent->childCount(); ++i) {
    collect_childs((tag_item*)parent->child(i), set);
  }
  set.insert(parent->tag().id());
}

void
tags_dialog::action_delete()
{
  tag_item* t = dynamic_cast<tag_item*>(m_qlist->currentItem());
  if (!t) return;

  // remove tag
  if (t->tag().id()==0) {
    QMessageBox::warning(this, tr("Error"), tr("The root element cannot be deleted"));
    return;
  }

  QMessageBox box(this);
  box.setTextFormat(Qt::RichText);
  box.setIcon(QMessageBox::Warning);

  QString msg;
  QString fullname = t->fullname();
  if (t->childCount()==0) {
    msg = tr("Please confirm the removal of the tag:\n<center><b>%1</b></center>").arg(fullname);
  }
  else {
    msg = tr("Please confirm the removal of the tag:<br><center><b>%1</b></center><br> and all its sub-tags.").arg(fullname);
    box.setDetailedText(tr("This will include %1 sub-tag(s)").arg(t->childCount()));
  }
  box.setText(msg);
  box.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
  box.setDefaultButton(QMessageBox::Cancel);
  
  int rep = box.exec();
  if (rep==QMessageBox::Cancel)
    return;

   collect_childs(t, m_delete);
   // delete the UI object
   delete t;
}

void
tags_dialog::sel_changed()
{
  tag_item* q = (tag_item*) m_qlist->currentItem();
  // New child and Delete buttons are active when an item is selected
  m_btn_edit->setEnabled(q!=NULL);
  m_btn_delete->setEnabled(q!=NULL);
}


void
tags_dialog::ok()
{
  db_cnx db;
  bool update=false;

  try {
    db.begin_transaction();

    // Definitive removal
    std::set<int>::iterator itd;
    for (itd=m_delete.begin(); itd!=m_delete.end(); ++itd) {
      message_tag mt(*itd, QString::null);
      if (mt.remove()) {
	update=true;
      }
    }

    /* translation table of negative tag_id (=item in memory) to the
       corresponding positive id in database once the item has been
       inserted */
    std::map<int,int> trans_id;

    // New tags and renamed tags
    QTreeWidgetItemIterator it(m_qlist);
    for ( ; *it; ++it) {
      tag_item* item = dynamic_cast<tag_item*>(*it);
      if (item->is_dirty()) {
	update=true;
	message_tag& mq=item->tag();
	int prev_id=mq.id();
	if (mq.parent_id()<0) {
	  // if the parent has a negative id, find its positive counterpart
	  // the parent should be inserted in db already because we're
	  // scanning the tree from top to bottom
	  std::map<int,int>::iterator i2 = trans_id.find(mq.parent_id());
	  if (i2!=trans_id.end()) {
	    mq.set_parent_id(i2->second);
	  }
	}
	if (mq.store()) {
	  item->set_dirty(false);
	  if (prev_id != mq.id()) {
	    trans_id[prev_id] = mq.id();
	  }
	}
      }
    }

    // Commit and close the window
    db.commit_transaction();
    close();

    if (update) {
      tags_repository::reset();
      tags_repository::fetch();
      DBG_PRINTF(3, "emit tags_restructured");
      emit tags_restructured();
    }

    accept();			// QDialog::accept()
  }
  catch(db_excpt& p) {
    db.rollback_transaction();
    DBEXCPT(p);
  }
}

//
// tag_item
//
tag_item::tag_item(QTreeWidget* q, const message_tag& mq): QTreeWidgetItem(q)


{
  init(mq);
}

tag_item::tag_item(QTreeWidgetItem* q, const message_tag& mq): QTreeWidgetItem(q)
{
  init(mq);
}

tag_item::~tag_item()
{
}

void
tag_item::init(const message_tag& mq)
{
  setFlags(flags()|Qt::ItemIsEditable|Qt::ItemIsDragEnabled|Qt::ItemIsDropEnabled);
  setText(0, mq.getName());
  m_tag=mq;
  set_dirty(false);
}

QString
tag_item::fullname() const
{
  QString s=text(0);
  QTreeWidgetItem* p = parent();
  while (p && p->parent()) {		// skip the root node, which has an id=0 and no name
    s.prepend(QChar(0x2799)); //"->");
    s.prepend(p->text(0));
    p = p->parent();
  }
  return s;
}

//
// tags_treeview
//
tags_treeview::tags_treeview(QWidget* parent) : QTreeWidget(parent)
{
  setAcceptDrops(true);
  setDragEnabled(true);
  setDropIndicatorShown(true);
  setDragDropMode(QAbstractItemView::InternalMove);
  setSelectionMode(QAbstractItemView::SingleSelection);
}


tags_treeview::~tags_treeview()
{
}

Qt::DropActions
tags_treeview::supportedDropActions() const
{
  return Qt::MoveAction;/* | Qt::CopyAction;*/
}

void
tags_treeview::dropEvent(QDropEvent* event)
{
  DBG_PRINTF(5, "dropEvent");
  QList<QModelIndex> idxs = selectedIndexes();
  QTreeWidgetItem* dragged_item;
  tag_item* dragged_tag;
  if (idxs.size()==1) {
    dragged_item = itemFromIndex(idxs.at(0));
    if (dragged_item) {
      dragged_tag = dynamic_cast<tag_item*>(dragged_item);
    }
  }
  else
    return; // shouldn't happen since the treewidget is in single selection mode
    
  QModelIndex index = indexAt(event->pos());
  if (!index.isValid()) {
    event->setDropAction(Qt::IgnoreAction);
    return;
  }
  QTreeWidgetItem* item = itemFromIndex(index);
  tag_item* dest_item = dynamic_cast<tag_item*>(item);
  if (dest_item) {
    DBG_PRINTF(5, "tag dest=%s", dest_item->tagconst().name().toLocal8Bit().constData());
    // disallow moving a tag below its parent (it's a no-op)
    if (dest_item == dragged_item->parent()) {
      event->setDropAction(Qt::IgnoreAction);
      return;
    }
    if (dragged_item->parent() != NULL) { // should always be true
      // move = remove and insert
      int index = dragged_item->parent()->indexOfChild(dragged_item);
      tag_item* rem_item = dynamic_cast<tag_item*>(dragged_item->parent()->takeChild(index));
      dest_item->addChild(rem_item);
      dest_item->sortChildren(0, Qt::AscendingOrder);
      dragged_tag->tag().set_parent_id(dest_item->tag().id());
      dragged_tag->set_dirty(true);
      setCurrentItem(dragged_tag);
    }
  }
}

/* There should be only one new item that hasn't yet be renamed by the
   user from the default name. This function searches for this item
   and returns it */
tag_item*
tags_treeview::find_new_item()
{
  QTreeWidgetItemIterator it(this);
  QTreeWidgetItem* p;
  while ((p=*it)!=NULL) {
    tag_item* ti = static_cast<tag_item*>(p);
    // find an item without a tag_id but which is _not_ the root item
    if (ti->tag().id()<=0 && ti->parent() && ti->tag().name()==m_tag_default_name)
      return ti;
    ++it;
  }
  return NULL;
}


/* Slot. Force the user to re-edit an entry if our validation failed.
   We call it as a slot because doing it from the code called by the
   itemChanged signal doesn't work (outputs an "edit: editing failed"
   error message) */

void
tags_treeview::reedit()
{
  setCurrentItem(last_failed);
  editItem(last_failed);
}

HTML source code generated by GNU Source-Highlight plus some custom post-processing

List of all available source files