You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tellico/src/document.cpp

688 lines
21 KiB

/***************************************************************************
copyright : (C) 2001-2006 by Robby Stephenson
email : robby@periapsis.org
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of version 2 of the GNU General Public License as *
* published by the Free Software Foundation; *
* *
***************************************************************************/
#include "document.h"
#include "mainwindow.h" // needed for calling fileSave()
#include "collectionfactory.h"
#include "translators/tellicoimporter.h"
#include "translators/tellicosaximporter.h"
#include "translators/tellicozipexporter.h"
#include "translators/tellicoxmlexporter.h"
#include "collection.h"
#include "filehandler.h"
#include "controller.h"
#include "borrower.h"
#include "tellico_kernel.h"
#include "latin1literal.h"
#include "tellico_debug.h"
#include "imagefactory.h"
#include "image.h"
#include "stringset.h"
#include "progressmanager.h"
#include "core/tellico_config.h"
#include <tdemessagebox.h>
#include <tdelocale.h>
#include <tdeglobal.h>
#include <tdeapplication.h>
#include <tqregexp.h>
#include <tqtimer.h>
// use a vector so we can use a sort functor
#include <vector>
#include <algorithm>
using Tellico::Data::Document;
Document* Document::s_self = 0;
Document::Document() : TQObject(), m_coll(0), m_isModified(false),
m_loadAllImages(false), m_validFile(false), m_importer(0), m_cancelImageWriting(false),
m_fileFormat(Import::TellicoImporter::Unknown) {
m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile;
newDocument(Collection::Book);
}
Document::~Document() {
delete m_importer;
m_importer = 0;
}
Tellico::Data::CollPtr Document::collection() const {
return m_coll;
}
void Document::setURL(const KURL& url_) {
m_url = url_;
if(m_url.fileName() != i18n("Untitled")) {
ImageFactory::setLocalDirectory(m_url);
}
}
void Document::slotSetModified(bool m_/*=true*/) {
m_isModified = m_;
emit signalModified(m_isModified);
}
void Document::slotDocumentRestored() {
slotSetModified(false);
}
bool Document::newDocument(int type_) {
// kdDebug() << "Document::newDocument()" << endl;
delete m_importer;
m_importer = 0;
deleteContents();
m_coll = CollectionFactory::collection(static_cast<Collection::Type>(type_), true);
m_coll->setTrackGroups(true);
Kernel::self()->resetHistory();
Controller::self()->slotCollectionAdded(m_coll);
slotSetModified(false);
KURL url;
url.setFileName(i18n("Untitled"));
setURL(url);
m_validFile = false;
m_fileFormat = Import::TellicoImporter::Unknown;
return true;
}
bool Document::openDocument(const KURL& url_) {
myLog() << "Document::openDocument() - " << url_.prettyURL() << endl;
m_loadAllImages = false;
// delayed image loading only works for local files
if(!url_.isLocalFile()) {
m_loadAllImages = true;
}
delete m_importer;
#ifdef SAX_SUPPORT
myLog() << "Document::openDocument() - using SAX loader" << endl;
m_importer = new Import::TellicoSaxImporter(url_, m_loadAllImages);
#else
m_importer = new Import::TellicoImporter(url_, m_loadAllImages);
#endif
CollPtr coll = m_importer->collection();
// delayed image loading only works for zip files
// format is only known AFTER collection() is called
m_fileFormat = m_importer->format();
m_allImagesOnDisk = !m_importer->hasImages();
if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) {
m_loadAllImages = true;
}
if(!coll) {
// myDebug() << "Document::openDocument() - returning false" << endl;
Kernel::self()->sorry(m_importer->statusMessage());
m_validFile = false;
return false;
}
deleteContents();
m_coll = coll;
m_coll->setTrackGroups(true);
setURL(url_);
m_validFile = true;
Kernel::self()->resetHistory();
Controller::self()->slotCollectionAdded(m_coll);
// m_importer might have been deleted?
slotSetModified(m_importer && m_importer->modifiedOriginal());
// if(pruneImages()) {
// slotSetModified(true);
// }
if(m_importer->hasImages()) {
m_cancelImageWriting = false;
TQTimer::singleShot(500, this, TQ_SLOT(slotLoadAllImages()));
} else {
emit signalCollectionImagesLoaded(m_coll);
m_importer->deleteLater();
m_importer = 0;
}
return true;
}
bool Document::saveModified() {
bool completed = true;
if(m_isModified) {
MainWindow* app = static_cast<MainWindow*>(Kernel::self()->widget());
TQString str = i18n("The current file has been modified.\n"
"Do you want to save it?");
int want_save = KMessageBox::warningYesNoCancel(Kernel::self()->widget(), str, i18n("Unsaved Changes"),
KStdGuiItem::save(), KStdGuiItem::discard());
switch(want_save) {
case KMessageBox::Yes:
completed = app->fileSave();
break;
case KMessageBox::No:
slotSetModified(false);
completed = true;
break;
case KMessageBox::Cancel:
default:
completed = false;
break;
}
}
return completed;
}
bool Document::saveDocument(const KURL& url_) {
if(!FileHandler::queryExists(url_)) {
return false;
}
// DEBUG_BLOCK;
// in case we're still loading images, give that a chance to cancel
m_cancelImageWriting = true;
kapp->processEvents();
ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false);
ProgressItem::Done done(this);
// will always save as zip file, no matter if has images or not
int imageLocation = Config::imageLocation();
bool includeImages = imageLocation == Config::ImagesInFile;
int totalSteps;
// write all images to disk cache if needed
// have to do this before executing exporter in case
// the user changed the imageInFile setting from Yes to No, in which
// case saving will over write the old file that has the images in it!
if(includeImages) {
totalSteps = 10;
item.setTotalSteps(10);
// since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress
} else {
totalSteps = 100;
item.setTotalSteps(100);
m_cancelImageWriting = false;
writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_);
}
Export::Exporter* exporter;
if(m_fileFormat == Import::TellicoImporter::XML) {
exporter = new Export::TellicoXMLExporter();
static_cast<Export::TellicoXMLExporter*>(exporter)->setIncludeImages(includeImages);
} else {
exporter = new Export::TellicoZipExporter();
static_cast<Export::TellicoZipExporter*>(exporter)->setIncludeImages(includeImages);
}
item.setProgress(int(0.8*totalSteps));
exporter->setEntries(m_coll->entries());
exporter->setURL(url_);
// since we already asked about overwriting the file, force the save
long opt = exporter->options() | Export::ExportForce | Export::ExportProgress;
// only write the image sizes if they're known already
opt &= ~Export::ExportImageSize;
exporter->setOptions(opt);
bool success = exporter->exec();
item.setProgress(int(0.9*totalSteps));
if(success) {
Kernel::self()->resetHistory();
setURL(url_);
// if successful, doc is no longer modified
slotSetModified(false);
} else {
myDebug() << "Document::saveDocument() - not successful saving to " << url_.prettyURL() << endl;
}
delete exporter;
return success;
}
bool Document::closeDocument() {
delete m_importer;
m_importer = 0;
deleteContents();
return true;
}
void Document::deleteContents() {
if(m_coll) {
Controller::self()->slotCollectionDeleted(m_coll);
}
// don't delete the m_importer here, bad things will happen
// since the collection holds a pointer to each entry and each entry
// hold a pointer to the collection, and they're both sharedptrs,
// neither will ever get deleted, unless the entries are removed from the collection
if(m_coll) {
m_coll->clear();
}
m_coll = 0; // old collection gets deleted as a TDESharedPtr
m_cancelImageWriting = true;
}
void Document::appendCollection(CollPtr coll_) {
if(!coll_) {
return;
}
m_coll->blockSignals(true);
Data::FieldVec fields = coll_->fields();
for(FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) {
m_coll->mergeField(field);
}
EntryVec entries = coll_->entries();
for(EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) {
Data::EntryPtr newEntry = new Data::Entry(*entry);
newEntry->setCollection(m_coll);
}
m_coll->addEntries(entries);
// TODO: merge filters and loans
m_coll->blockSignals(false);
}
Tellico::Data::MergePair Document::mergeCollection(CollPtr coll_) {
MergePair pair;
if(!coll_) {
return pair;
}
m_coll->blockSignals(true);
Data::FieldVec fields = coll_->fields();
for(FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) {
m_coll->mergeField(field);
}
EntryVec currEntries = m_coll->entries();
EntryVec newEntries = coll_->entries();
for(EntryVec::Iterator newIt = newEntries.begin(); newIt != newEntries.end(); ++newIt) {
int bestMatch = 0;
Data::EntryPtr matchEntry;
for(EntryVec::Iterator currIt = currEntries.begin(); currIt != currEntries.end(); ++currIt) {
int match = m_coll->sameEntry(&*currIt, &*newIt);
if(match >= Collection::ENTRY_PERFECT_MATCH) {
matchEntry = currIt;
break;
} else if(match >= Collection::ENTRY_GOOD_MATCH && match > bestMatch) {
bestMatch = match;
matchEntry = currIt;
// don't break, keep looking for better one
}
}
if(matchEntry) {
m_coll->mergeEntry(matchEntry, &*newIt, false /*overwrite*/);
} else {
Data::EntryPtr e = new Data::Entry(*newIt);
e->setCollection(m_coll);
// keep track of which entries got added
pair.first.append(e);
}
}
m_coll->addEntries(pair.first);
// TODO: merge filters and loans
m_coll->blockSignals(false);
return pair;
}
void Document::replaceCollection(CollPtr coll_) {
if(!coll_) {
return;
}
// kdDebug() << "Document::replaceCollection()" << endl;
KURL url;
url.setFileName(i18n("Untitled"));
setURL(url);
m_validFile = false;
// the collection gets cleared by the CollectionCommand that called this function
// no need to do it here
m_coll = coll_;
m_coll->setTrackGroups(true);
m_cancelImageWriting = true;
// CollectionCommand takes care of calling Controller signals
}
void Document::unAppendCollection(CollPtr coll_, FieldVec origFields_) {
if(!coll_) {
return;
}
m_coll->blockSignals(true);
StringSet origFieldNames;
for(FieldVec::Iterator field = origFields_.begin(); field != origFields_.end(); ++field) {
m_coll->modifyField(field);
origFieldNames.add(field->name());
}
EntryVec entries = coll_->entries();
for(EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) {
// probably don't need to do this, but on the safe side...
entry->setCollection(coll_);
}
m_coll->removeEntries(entries);
// since Collection::removeField() iterates over all entries to reset the value of the field
// don't removeField() until after removeEntry() is done
FieldVec currFields = m_coll->fields();
for(FieldVec::Iterator field = currFields.begin(); field != currFields.end(); ++field) {
if(!origFieldNames.has(field->name())) {
m_coll->removeField(field);
}
}
m_coll->blockSignals(false);
}
void Document::unMergeCollection(CollPtr coll_, FieldVec origFields_, MergePair entryPair_) {
if(!coll_) {
return;
}
m_coll->blockSignals(true);
TQStringList origFieldNames;
for(FieldVec::Iterator field = origFields_.begin(); field != origFields_.end(); ++field) {
m_coll->modifyField(field);
origFieldNames << field->name();
}
// first item in pair are the entries added by the operation, remove them
EntryVec entries = entryPair_.first;
m_coll->removeEntries(entries);
// second item in pair are the entries which got modified by the original merge command
const TQString track = TQString::fromLatin1("track");
PairVector trackChanges = entryPair_.second;
// need to go through them in reverse since one entry may have been modified multiple times
// first item in the pair is the entry pointer
// second item is the old value of the track field
for(int i = trackChanges.count()-1; i >= 0; --i) {
trackChanges[i].first->setField(track, trackChanges[i].second);
}
// since Collection::removeField() iterates over all entries to reset the value of the field
// don't removeField() until after removeEntry() is done
FieldVec currFields = m_coll->fields();
for(FieldVec::Iterator field = currFields.begin(); field != currFields.end(); ++field) {
if(origFieldNames.findIndex(field->name()) == -1) {
m_coll->removeField(field);
}
}
m_coll->blockSignals(false);
}
bool Document::isEmpty() const {
//an empty doc may contain a collection, but no entries
return (!m_coll || m_coll->entries().isEmpty());
}
bool Document::loadImage(const TQString& id_) {
// myLog() << "Document::loadImage() - id = " << id_ << endl;
if(!m_coll) {
return false;
}
bool b = !m_loadAllImages && m_validFile && m_importer && m_importer->loadImage(id_);
if(b) {
m_allImagesOnDisk = false;
}
return b;
}
bool Document::loadAllImagesNow() const {
// myLog() << "Document::loadAllImagesNow()" << endl;
if(!m_coll || !m_validFile) {
return false;
}
if(m_loadAllImages) {
myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!" << endl;
return false;
}
return Import::TellicoImporter::loadAllImages(m_url);
}
Tellico::Data::EntryVec Document::filteredEntries(Filter::Ptr filter_) const {
Data::EntryVec matches;
Data::EntryVec entries = m_coll->entries();
for(EntryVec::Iterator it = entries.begin(); it != entries.end(); ++it) {
if(filter_->matches(it.data())) {
matches.append(it);
}
}
return matches;
}
void Document::checkOutEntry(Data::EntryPtr entry_) {
if(!entry_) {
return;
}
const TQString loaned = TQString::fromLatin1("loaned");
if(!m_coll->hasField(loaned)) {
FieldPtr f = new Field(loaned, i18n("Loaned"), Field::Bool);
f->setFlags(Field::AllowGrouped);
f->setCategory(i18n("Personal"));
m_coll->addField(f);
}
entry_->setField(loaned, TQString::fromLatin1("true"));
EntryVec vec;
vec.append(entry_);
m_coll->updateDicts(vec);
}
void Document::checkInEntry(Data::EntryPtr entry_) {
if(!entry_) {
return;
}
const TQString loaned = TQString::fromLatin1("loaned");
if(!m_coll->hasField(loaned)) {
return;
}
entry_->setField(loaned, TQString());
m_coll->updateDicts(EntryVec(entry_));
}
void Document::renameCollection(const TQString& newTitle_) {
m_coll->setTitle(newTitle_);
}
// this only gets called when a zip file with images is opened
// by loading every image, it gets pulled out of the zip file and
// copied to disk. then the zip file can be closed and not retained in memory
void Document::slotLoadAllImages() {
TQString id;
StringSet images;
Data::EntryVec entries = m_coll->entries();
Data::FieldVec imageFields = m_coll->imageFields();
for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) {
for(Data::FieldVec::Iterator field = imageFields.begin(); field != imageFields.end() && !m_cancelImageWriting; ++field) {
id = entry->field(field);
if(id.isEmpty() || images.has(id)) {
continue;
}
// this is the early loading, so just by calling imageById()
// the image gets sucked from the zip file and written to disk
//by ImageFactory::imageById()
if(ImageFactory::imageById(id).isNull()) {
myDebug() << "Document::slotLoadAllImages() - entry title: " << entry->title() << endl;
}
images.add(id);
}
if(m_cancelImageWriting) {
break;
}
// stay responsive, do this in the background
kapp->processEvents();
}
if(m_cancelImageWriting) {
myLog() << "Document::slotLoadAllImages() - cancel image writing" << endl;
} else {
emit signalCollectionImagesLoaded(m_coll);
}
m_cancelImageWriting = false;
}
void Document::writeAllImages(int cacheDir_, const KURL& localDir_) {
// images get 80 steps in saveDocument()
const uint stepSize = 1 + TQMAX(1, m_coll->entryCount()/80); // add 1 since it could round off
uint j = 1;
TQString oldLocalDir = ImageFactory::localDir();
ImageFactory::setLocalDirectory(localDir_);
ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_);
TQString id;
StringSet images;
EntryVec entries = m_coll->entries();
FieldVec imageFields = m_coll->imageFields();
for(EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) {
for(FieldVec::Iterator field = imageFields.begin(); field != imageFields.end() && !m_cancelImageWriting; ++field) {
id = entry->field(field);
if(id.isEmpty() || images.has(id)) {
continue;
}
images.add(id);
if(ImageFactory::imageInfo(id).linkOnly) {
continue;
}
if(!ImageFactory::writeCachedImage(id, cacheDir)) {
myDebug() << "Document::writeAllImages() - did not write image for entry title: " << entry->title() << endl;
}
}
if(j%stepSize == 0) {
ProgressManager::self()->setProgress(this, j/stepSize);
kapp->processEvents();
}
++j;
if(m_cancelImageWriting) {
break;
}
}
if(m_cancelImageWriting) {
myDebug() << "Document::writeAllImages() - cancel image writing" << endl;
}
m_cancelImageWriting = false;
ImageFactory::setLocalDirectory(oldLocalDir);
}
bool Document::pruneImages() {
bool found = false;
TQString id;
StringSet images;
Data::EntryVec entries = m_coll->entries();
Data::FieldVec imageFields = m_coll->imageFields();
for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) {
for(Data::FieldVec::Iterator field = imageFields.begin(); field != imageFields.end(); ++field) {
id = entry->field(field);
if(id.isEmpty() || images.has(id)) {
continue;
}
const Data::Image& img = ImageFactory::imageById(id);
if(img.isNull()) {
entry->setField(field, TQString());
found = true;
myDebug() << "Document::pruneImages() - removing null image for " << entry->title() << ": " << id << endl;
} else {
images.add(id);
}
}
}
return found;
}
int Document::imageCount() const {
if(!m_coll) {
return 0;
}
StringSet images;
FieldVec fields = m_coll->imageFields();
EntryVec entries = m_coll->entries();
for(FieldVecIt f = fields.begin(); f != fields.end(); ++f) {
for(EntryVecIt e = entries.begin(); e != entries.end(); ++e) {
images.add(e->field(f->name()));
}
}
return images.count();
}
Tellico::Data::EntryVec Document::sortEntries(EntryVec entries_) const {
std::vector<EntryPtr> vec;
for(EntryVecIt e = entries_.begin(); e != entries_.end(); ++e) {
vec.push_back(e);
}
TQStringList titles = Controller::self()->sortTitles();
// have to go in reverse for sorting
for(int i = titles.count()-1; i >= 0; --i) {
if(titles[i].isEmpty()) {
continue;
}
TQString field = m_coll->fieldNameByTitle(titles[i]);
std::sort(vec.begin(), vec.end(), EntryCmp(field));
}
Data::EntryVec sorted;
for(std::vector<EntryPtr>::iterator it = vec.begin(); it != vec.end(); ++it) {
sorted.append(*it);
}
return sorted;
}
void Document::removeImagesNotInCollection(EntryVec entries_, EntryVec entriesToKeep_) {
// first get list of all images in collection
StringSet images;
FieldVec fields = m_coll->imageFields();
EntryVec allEntries = m_coll->entries();
for(FieldVecIt f = fields.begin(); f != fields.end(); ++f) {
for(EntryVecIt e = allEntries.begin(); e != allEntries.end(); ++e) {
images.add(e->field(f->name()));
}
for(EntryVecIt e = entriesToKeep_.begin(); e != entriesToKeep_.end(); ++e) {
images.add(e->field(f->name()));
}
}
// now for all images not in the cache, we can clear them
StringSet imagesToCheck = ImageFactory::imagesNotInCache();
// if entries_ is not empty, that means we want to limit the images removed
// to those that are referenced in those entries
StringSet imagesToRemove;
for(FieldVecIt f = fields.begin(); f != fields.end(); ++f) {
for(EntryVecIt e = entries_.begin(); e != entries_.end(); ++e) {
TQString id = e->field(f->name());
if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) {
imagesToRemove.add(id);
}
}
}
const TQStringList realImagesToRemove = imagesToRemove.toList();
for(TQStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) {
ImageFactory::removeImage(*it, false); // doesn't delete, just remove link
}
}
#include "document.moc"