diff options
Diffstat (limited to 'src')
485 files changed, 88997 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..8ffd5e4 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,140 @@ +####### kdevelop will overwrite this part!!! (begin)########## +bin_PROGRAMS = tellico + +tellico_SOURCES = borrower.cpp borrowerdialog.cpp \ + borroweritem.cpp calendarhandler.cpp collection.cpp collectionfactory.cpp \ + collectionfieldsdialog.cpp configdialog.cpp controller.cpp detailedentryitem.cpp \ + detailedlistview.cpp document.cpp entry.cpp entryeditdialog.cpp entrygroupitem.cpp \ + entryiconfactory.cpp entryiconview.cpp entryitem.cpp entrymerger.cpp entryupdater.cpp \ + entryview.cpp exportdialog.cpp fetchdialog.cpp fetcherconfigdialog.cpp field.cpp \ + fieldcompletion.cpp filehandler.cpp filter.cpp filterdialog.cpp filteritem.cpp \ + filterview.cpp groupiterator.cpp groupview.cpp image.cpp imagefactory.cpp \ + importdialog.cpp isbnvalidator.cpp iso5426converter.cpp iso6937converter.cpp \ + listviewcomparison.cpp loandialog.cpp loanitem.cpp loanview.cpp main.cpp mainwindow.cpp \ + progressmanager.cpp reportdialog.cpp statusbar.cpp tellico_kernel.cpp tellico_strings.cpp \ + tellico_utils.cpp upcvalidator.cpp viewstack.cpp xmphandler.cpp lccnvalidator.cpp + +if !USE_LIBBTPARSE + LDADD_LIBBTPARSE = ./translators/btparse/libbtparse.a +endif + +if ENABLE_WEBCAM + WEBCAMDIR = barcode + LDADD_LIBBARCODE = ./barcode/libbarcode.a +endif +SUBDIRS = core gui collections translators fetch tests commands \ + cite newstuff rtf2html $(WEBCAMDIR) + +tellico_LDADD = ./core/libcore.a ./cite/libcite.a ./fetch/libfetch.a \ + ./collections/libcollections.a ./translators/libtranslators.a \ + ./newstuff/libnewstuff.a ./translators/pilotdb/libpilotdb.a \ + ./translators/pilotdb/libflatfile/liblibflatfile.a ./gui/libgui.a \ + ./translators/pilotdb/libpalm/liblibpalm.a ./rtf2html/librtf2html.a \ + ./commands/libcommands.a -lexslt $(LIB_KFILE) $(LIB_KHTML) $(LIB_KDEUI) $(LIB_KDECORE) $(LIB_QT) \ + $(LIBSOCKET) $(LIBXSLT_LIBS) $(TAGLIB_LIBS) $(KCDDB_LIBS) $(YAZ_LIBS) $(LIB_KIO) \ + $(LIB_KABC) $(KCAL_LIBS) $(LDADD_LIBBTPARSE) $(LIB_KNEWSTUFF) $(EXEMPI_LIBS) \ + $(POPPLER_LIBS) $(LDADD_LIBBARCODE) + +EXTRA_DIST = tellicoui.rc tellicorc \ +borrowerdialog.cpp entryiconview.h\ +borrowerdialog.h entryitem.cpp importdialog.cpp \ +borrower.h entryitem.h importdialog.h \ +borroweritem.cpp entryview.cpp isbnvalidator.cpp \ +borroweritem.h entryview.h isbnvalidator.h \ +loanview.cpp exportdialog.cpp \ +loanview.h exportdialog.h \ +collection.cpp fetchdialog.cpp \ +collectionfactory.cpp fetchdialog.h \ +collectionfactory.h fieldcompletion.cpp latin1literal.h \ +collectionfieldsdialog.h collectionfieldsdialog.cpp fieldcompletion.h \ +collection.h field.h main.cpp \ +configdialog.cpp mainwindow.cpp \ +configdialog.h mainwindow.h \ +controller.cpp filehandler.cpp \ +controller.h filehandler.h \ +filter.cpp filterdialog.cpp \ +filterdialog.h reportdialog.cpp \ +filter.h reportdialog.h \ +filteritem.cpp \ +filteritem.h \ +detailedlistview.h detailedlistview.cpp \ +document.cpp tellico_kernel.cpp \ +document.h tellico_kernel.h \ +entry.cpp groupview.cpp tellico_strings.cpp \ +entryeditdialog.cpp groupview.h tellico_strings.h \ +entryeditdialog.h image.cpp tellico_utils.cpp \ +entrygroupitem.cpp imagefactory.cpp tellico_utils.h \ +entrygroupitem.h imagefactory.h viewstack.cpp \ +entry.h image.h viewstack.h \ +entryiconview.cpp loandialog.h \ +loandialog.cpp ptrvector.h borrower.cpp datavectors.h \ +calendarhandler.h calendarhandler.cpp \ +loanitem.h loanitem.cpp groupiterator.h groupiterator.cpp \ +stringset.h observer.h filterview.h filterview.cpp \ +entryiconfactory.h entryiconfactory.cpp \ +tellico_debug.h \ +entryupdater.h entryupdater.cpp \ +detailedentryitem.h detailedentryitem.cpp \ +statusbar.h statusbar.cpp \ +progressmanager.h progressmanager.cpp \ +upcvalidator.h upcvalidator.cpp \ +fetcherconfigdialog.h fetcherconfigdialog.cpp \ +iso5426converter.h iso5426converter.cpp \ +iso6937converter.h iso6937converter.cpp \ +tellico_map.h \ +listviewcomparison.h listviewcomparison.cpp \ +entrymerger.h entrymerger.cpp \ +xmphandler.h xmphandler.cpp \ +lccnvalidator.h lccnvalidator.cpp + +####### kdevelop will overwrite this part!!! (end)############ +# These paths are KDE specific. Use them: +# kde_appsdir Where your application's menu entry (.desktop) should go to. +# kde_icondir Where your icon should go to - better use KDE_ICON. +# kde_sounddir Where your sounds should go to. +# kde_htmldir Where your docs should go to. (contains lang subdirs) +# kde_datadir Where you install application data. (Use a subdir) +# kde_locale Where translation files should go to. (contains lang subdirs) +# kde_cgidir Where cgi-bin executables should go to. +# kde_confdir Where config files should go to (system-wide ones with default values). +# kde_mimedir Where mimetypes .desktop files should go to. +# kde_servicesdir Where services .desktop files should go to. +# kde_servicetypesdir Where servicetypes .desktop files should go to. +# kde_toolbardir Where general toolbar icons should go to (deprecated, use KDE_ICON). +# kde_wallpaperdir Where general wallpapers should go to. +# kde_templatesdir Where templates for the "New" menu (Konqueror/KDesktop) should go to. +# kde_bindir Where executables should go to. Use bin_PROGRAMS or bin_SCRIPTS. +# kde_libdir Where shared libraries should go to. Use lib_LTLIBRARIES. +# kde_moduledir Where modules (e.g. parts) should go to. Use kde_module_LTLIBRARIES. +# kde_styledir Where Qt/KDE widget styles should go to (new in KDE 3). +# kde_designerdir Where Qt Designer plugins should go to (new in KDE 3). + +# set the include path for X, qt and KDE +AM_CPPFLAGS = $(all_includes) \ + $(LIBXML_CFLAGS) \ + $(LIBXSLT_CFLAGS) \ + $(EXEMPI_CFLAGS) + +METASOURCES = AUTO + +KDE_OPTIONS = noautodist + +CLEANFILES = *~ *.loT + +# the library search path. +tellico_LDFLAGS = $(all_libraries) $(KDE_RPATH) + +uidir = $(kde_datadir)/tellico +ui_DATA = tellicoui.rc + +rcdir = $(kde_confdir) +rc_DATA = tellicorc + +messages: rc.cpp + @ touch $(podir)/tellico.pot; + LIST=`find . -name \*.h -o -name \*.hh -o -name \*.H -o -name \*.hxx -o -name \*.hpp -o -name \*.cpp -o -name \*.cc -o -name \*.cxx -o -name \*.ecpp -o -name \*.C`; \ + if test -n "$$LIST"; then \ + $(XGETTEXT) --join-existing --add-comments="TRANSLATORS:" $$LIST -o $(podir)/tellico.pot; \ + fi + +#include ../admin/Doxyfile.am diff --git a/src/barcode/Makefile.am b/src/barcode/Makefile.am new file mode 100644 index 0000000..df8f8e2 --- /dev/null +++ b/src/barcode/Makefile.am @@ -0,0 +1,14 @@ +AM_CPPFLAGS = $(all_includes) + +noinst_LIBRARIES = libbarcode.a +libbarcode_a_SOURCES = barcode.cpp barcode_v4l.cpp + +libbarcode_a_METASOURCES = AUTO + +KDE_OPTIONS = noautodist + +EXTRA_DIST = \ +barcode.h barcode.cpp \ +barcode_v4l.h barcode_v4l.cpp + +CLEANFILES = *~ *.loT diff --git a/src/barcode/barcode.cpp b/src/barcode/barcode.cpp new file mode 100644 index 0000000..2427f4e --- /dev/null +++ b/src/barcode/barcode.cpp @@ -0,0 +1,877 @@ +/*************************************************************************** + copyright : (C) 2007 by Sebastian Held + email : sebastian.held@gmx.de + ***************************************************************************/ + +/*************************************************************************** + * * + * ### based on BaToo: http://people.inf.ethz.ch/adelmanr/batoo/ ### * + * * + * 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; * + * * + ***************************************************************************/ + +// includes code from http://v4l2spec.bytesex.org/spec/a16323.htm + +#include <qimage.h> +#include <qmutex.h> +#include "barcode.h" + +#include <stdlib.h> + +using barcodeRecognition::barcodeRecognitionThread; +using barcodeRecognition::Barcode_EAN13; +using barcodeRecognition::Decoder_EAN13; +using barcodeRecognition::MatchMakerResult; + +barcodeRecognitionThread::barcodeRecognitionThread() +{ + m_stop = false; + m_barcode_v4l = new barcode_v4l(); +} +barcodeRecognitionThread::~barcodeRecognitionThread() +{ + delete m_barcode_v4l; +} + +bool barcodeRecognitionThread::isWebcamAvailable() +{ + return m_barcode_v4l->isOpen(); +} + +void barcodeRecognitionThread::run() +{ + bool stop; + m_stop_mutex.lock(); + stop = m_stop; + m_stop_mutex.unlock(); + + if (!isWebcamAvailable()) + stop = true; + + Barcode_EAN13 old; + while (!stop) { + QImage img; + m_barcode_img_mutex.lock(); + if (m_barcode_img.isNull()) + img = m_barcode_v4l->grab_one2(); + else { + img = m_barcode_img; + m_barcode_img = QImage(); + } + m_barcode_img_mutex.unlock(); + +// //DEBUG +// img.load( "/home/sebastian/black.png" ); +// m_stop = true; +// //DEBUG + + if (!img.isNull()) { + QImage preview = img.scale( 320, 240, QImage::ScaleMin ); + emit gotImage( preview ); + Barcode_EAN13 barcode = recognize( img ); + if (barcode.isValid() && (old != barcode)) { + emit recognized( barcode.toString() ); + old = barcode; + } + } + msleep( 10 ); // reduce load + + m_stop_mutex.lock(); + stop = m_stop; + m_stop_mutex.unlock(); + } +} + +void barcodeRecognitionThread::stop() +{ + // attention! This function is called from GUI context + m_stop_mutex.lock(); + m_stop = true; + m_stop_mutex.unlock(); +} + + +void barcodeRecognitionThread::recognizeBarcode( QImage img ) +{ + // attention! This function is called from GUI context + m_barcode_img_mutex.lock(); + m_barcode_img = img; + m_barcode_img_mutex.unlock(); +} + +Barcode_EAN13 barcodeRecognitionThread::recognize( QImage img ) +{ + // PARAMETERS: + int amount_scanlines = 30; + int w = img.width(); + int h = img.height(); + + // the array which will contain the result: + QValueVector< QValueVector<int> > numbers( amount_scanlines, QValueVector<int>(13,-1) ); // no init in java source!!!!!!!!! + + // generate and initialize the array that will contain all detected + // digits at a specific code position: + int possible_numbers[10][13][2]; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 13; j++) { + possible_numbers[i][j][0] = -1; + possible_numbers[i][j][1] = 0; + } + } + + int successfull_lines = 0; + + // try to detect the barcode along scanlines: + for (int i = 0; i < amount_scanlines; i++) { + int x1 = 0; + int y = (h / amount_scanlines) * i; + int x2 = w - 1; + + // try to recognize a barcode along that path: + Barcode_EAN13 ean13_code = recognizeCode( img, x1, x2, y ); + numbers[i] = ean13_code.getNumbers(); + + if (ean13_code.isValid()) { + successfull_lines++; + // add the recognized digits to the array of possible numbers: + addNumberToPossibleNumbers( numbers[i], possible_numbers, true ); + } else { + numbers[i] = ean13_code.getNumbers(); + // add the recognized digits to the array of possible numbers: + addNumberToPossibleNumbers(numbers[i], possible_numbers, false); + } + +#ifdef BarcodeDecoder_DEBUG + // show the information that has been recognized along the scanline: + qDebug( "Scanline %i result: %s\n", i, ean13_code.toString().latin1() ); +#endif + } + + // sort the detected digits at each code position, in accordance to the + // amount of their detection: + sortDigits(possible_numbers); + +#ifdef BarcodeDecoder_DEBUG + fprintf( stderr, "detected digits:\n" ); + printArray( possible_numbers, 0 ); + fprintf( stderr, "# of their occurence:\n" ); + printArray( possible_numbers, 1 ); +#endif + + // get the most likely barcode: + Barcode_EAN13 code = extractBarcode(possible_numbers); + + return code; +} + +void barcodeRecognitionThread::printArray( int array[10][13][2], int level ) +{ + for (int i = 0; i < 10; i++) { + QCString temp; + temp.setNum( i ); + fprintf( stderr, temp + " : " ); + for (int j = 0; j < 13; j++) { + if (array[i][j][level] == -1) + fprintf( stderr, "x " ); + else { + QCString temp; + temp.setNum( array[i][j][level] ); + fprintf( stderr, temp + " " ); + } + } + fprintf( stderr, "\n" ); + } +} + +barcodeRecognition::Barcode_EAN13 barcodeRecognitionThread::recognizeCode( QImage img, int x1, int x2, int y ) +{ + QValueVector<QRgb> raw_path(x2-x1+1); + for (int x=x1; x<=x2; x++) + raw_path[x-x1] = img.pixel(x,y); + // convert the given path into a string of black and white pixels: + QValueVector<int> string = transformPathToBW( raw_path ); + + // convert the string of black&white pixels into a list, containing + // information about the black and white fields + // first indes = field nr. + // second index: 0 = color of the field + // 1 = field length + QValueVector< QValueVector<int> > fields = extractFieldInformation( string ); + + // try to recognize a EAN13 code: + Barcode_EAN13 barcode = Decoder_EAN13::recognize( fields ); + + return barcode; +} + +void barcodeRecognitionThread::addNumberToPossibleNumbers( QValueVector<int> number, int possible_numbers[10][13][2], bool correct_code ) +{ + int i; + bool digit_contained; + for (int j = 0; j < 13; j++) { + if (number[j] >= 0) { + i = 0; + digit_contained = false; + while ((i < 10) && (possible_numbers[i][j][0] >= 0)) { + if (possible_numbers[i][j][0] == number[j]) { + digit_contained = true; + if (correct_code) + possible_numbers[i][j][1] = possible_numbers[i][j][1] + 100; + else + possible_numbers[i][j][1]++; + break; + } + i++; + } + if ((i < 10) && (!digit_contained)) { + // add new digit: + possible_numbers[i][j][0] = number[j]; + if (correct_code) + possible_numbers[i][j][1] = possible_numbers[i][j][1] + 100; + else + possible_numbers[i][j][1]++; + } + } + } +} + +void barcodeRecognitionThread::sortDigits( int possible_numbers[10][13][2] ) +{ + int i; + int temp_value; + int temp_occurence; + bool changes; + + for (int j = 0; j < 13; j++) { + i = 1; + changes = false; + while (true) { + if ((possible_numbers[i - 1][j][0] >= 0) && (possible_numbers[i][j][0] >= 0)) { + if (possible_numbers[i - 1][j][1] < possible_numbers[i][j][1]) { + temp_value = possible_numbers[i - 1][j][0]; + temp_occurence = possible_numbers[i - 1][j][1]; + possible_numbers[i - 1][j][0] = possible_numbers[i][j][0]; + possible_numbers[i - 1][j][1] = possible_numbers[i][j][1]; + possible_numbers[i][j][0] = temp_value; + possible_numbers[i][j][1] = temp_occurence; + + changes = true; + } + } + + if ((possible_numbers[i][j][0] < 0) || (i >= 9)) { + if (!changes) + break; + else { + i = 1; + changes = false; + } + } else + i++; + } + } +} + +Barcode_EAN13 barcodeRecognitionThread::extractBarcode( int possible_numbers[10][13][2] ) +{ + // create and initialize the temporary variables: + QValueVector<int> temp_code(13); + for (int i = 0; i < 13; i++) + temp_code[i] = possible_numbers[0][i][0]; + +#ifdef Barcode_DEBUG + fprintf( stderr, "barcodeRecognitionThread::extractBarcode(): " ); + for (int i=0; i<13; i++) + fprintf( stderr, "%i", temp_code[i] ); + fprintf( stderr, "\n" ); +#endif + + return Barcode_EAN13(temp_code); +} + +Barcode_EAN13 barcodeRecognitionThread::detectValidBarcode ( int possible_numbers[10][13][2], int max_amount_of_considered_codes ) +{ + // create and initialize the temporary variables: + QValueVector<int> temp_code(13); + for ( int i = 0; i < 13; i++ ) + temp_code[i] = possible_numbers[0][i][0]; + + int alternative_amount = 0; + + QValueVector<int> counter( 13 ); // no init in java source!!! + int counter_nr = 11; + + // check if there is at least one complete code present: + for ( int i = 0; i < 13; i++ ) { + // exit and return the "most likely" code parts: + if ( temp_code[i] < 0 ) + return Barcode_EAN13( temp_code ); + } + + // if there is at least one complete node, try to detect a valid barcode: + while ( alternative_amount < max_amount_of_considered_codes ) { + // fill the temporary code array with one possible version: + for ( int i = 0; i < 13; i++ ) + temp_code[i] = possible_numbers[counter[i]][i][0]; + + alternative_amount++; + + // check if this version represents a valid code: + if (isValid( temp_code )) + return Barcode_EAN13( temp_code ); + + // increment the counters: + if ( ( counter[counter_nr] < 9 ) && ( possible_numbers[counter[counter_nr] + 1][counter_nr][0] >= 0 ) ) { + // increment the actual counter. + counter[counter_nr]++; + } else { + // check if we have reached the end and no valid barcode has been found: + if ( counter_nr == 1 ) { + // exit and return the "most likely" code parts: + for ( int i = 0; i < 13; i++ ) + temp_code[i] = possible_numbers[0][i][0]; + return Barcode_EAN13( temp_code ); + } else { + // reset the actual counter and increment the next one(s): + counter[counter_nr] = 0; + + while ( true ) { + if ( counter_nr > 2 ) + counter_nr--; + else { + for ( int i = 0; i < 13; i++ ) + temp_code[i] = possible_numbers[0][i][0]; + return Barcode_EAN13( temp_code ); + } + if ( counter[counter_nr] < 9 ) { + counter[counter_nr]++; + if ( possible_numbers[counter[counter_nr]][counter_nr][0] < 0 ) + counter[counter_nr] = 0; + else + break; + } else + counter[counter_nr] = 0; + } + counter_nr = 12; + } + } + } + + for ( int i = 0; i < 13; i++ ) + temp_code[i] = possible_numbers[0][i][0]; + return Barcode_EAN13( temp_code ); +} + +bool barcodeRecognitionThread::isValid( int numbers[13] ) +{ + QValueVector<int> temp(13); + for (int i=0; i<13; i++) + temp[i] = numbers[i]; + return isValid( temp ); +} +bool barcodeRecognitionThread::isValid( QValueVector<int> numbers ) +{ + Q_ASSERT( numbers.count() == 13 ); + // calculate the checksum of the barcode: + int sum1 = numbers[0] + numbers[2] + numbers[4] + numbers[6] + numbers[8] + numbers[10]; + int sum2 = 3 * (numbers[1] + numbers[3] + numbers[5] + numbers[7] + numbers[9] + numbers[11]); + int checksum_value = sum1 + sum2; + int checksum_digit = 10 - (checksum_value % 10); + if (checksum_digit == 10) + checksum_digit = 0; + +#ifdef Barcode_DEBUG + fprintf( stderr, "barcodeRecognitionThread::isValid(): " ); + for (int i=0; i<13; i++) + fprintf( stderr, "%i", numbers[i] ); + fprintf( stderr, "\n" ); +#endif + + return (numbers[12] == checksum_digit); +} + +QValueVector<int> barcodeRecognitionThread::transformPathToBW( QValueVector<QRgb> line ) +{ + int w = line.count(); + QValueVector<int> bw_line(w,0); + bw_line[0] = 255; + + // create greyscale values: + QValueVector<int> grey_line(w,0); + int average_illumination = 0; + for (int x = 0; x < w; x++) { + grey_line[x] = (qRed(line.at(x)) + qGreen(line.at(x)) + qBlue(line.at(x))) / 3; + average_illumination = average_illumination + grey_line[x]; + } + average_illumination = average_illumination / w; + + // perform the binarization: + int range = w / 20; + + // temp values: + int moving_sum; + int moving_average; + int v1_index = -range + 1; + int v2_index = range; + int v1 = grey_line[0]; + int v2 = grey_line[range]; + int current_value; + int comparison_value; + + // initialize the moving sum: + moving_sum = grey_line[0] * range; + for (int i = 0; i < range; i++) + moving_sum = moving_sum + grey_line[i]; + + // apply the adaptive thresholding algorithm: + for (int i = 1; i < w - 1; i++) { + if (v1_index > 0) v1 = grey_line[v1_index]; + if (v2_index < w) v2 = grey_line[v2_index]; + else v2 = grey_line[w - 1]; + moving_sum = moving_sum - v1 + v2; + moving_average = moving_sum / (range << 1); + v1_index++; + v2_index++; + + current_value = (grey_line[i - 1] + grey_line[i]) >> 1; + + // decide if the current pixel should be black or white: + comparison_value = (3 * moving_average + average_illumination) >> 2; + if ((current_value < comparison_value - 3)) bw_line[i] = 0; + else bw_line[i] = 255; + } + + // filter the values: (remove too small fields) + + if (w >= 640) { + for (int x = 1; x < w - 1; x++) { + if ((bw_line[x] != bw_line[x - 1]) && (bw_line[x] != bw_line[x + 1])) bw_line[x] = bw_line[x - 1]; + } + } + + QValueVector<int> ret(w,0); + for (int i=0; i<w; i++) + ret[i] = bw_line[i]; + +#ifdef Barcode_DEBUG + fprintf( stderr, "barcodeRecognitionThread::transformPathToBW(): " ); + for (int i=0; i<ret.count(); i++) + if (bw_line[i] == 0) + fprintf( stderr, "0" ); + else + fprintf( stderr, "#" ); + fprintf( stderr, "\n" ); +#endif + + return ret; +} + +QValueVector< QValueVector<int> > barcodeRecognitionThread::extractFieldInformation( QValueVector<int> string ) +{ + QValueVector< QValueVector<int> > temp_fields( string.count(), QValueVector<int>(2,0) ); + + if (string.count() == 0) + return QValueVector< QValueVector<int> >(); + + int field_counter = 0; + int last_value = string.at(0); + int last_fields = 1; + for (uint i = 1; i < string.size(); i++) { + if ((string.at(i) == last_value) && (i < string.size() - 1)) { + last_fields++; + } else { + // create new field entry: + temp_fields[field_counter][0] = last_value; + temp_fields[field_counter][1] = last_fields; + + last_value = string.at(i); + last_fields = 0; + field_counter++; + } + } + + temp_fields.resize( field_counter ); + +#ifdef Barcode_DEBUG + fprintf( stderr, "barcodeRecognitionThread::extractFieldInformation(): " ); + for (int i=0; i<temp_fields.count(); i++) + fprintf( stderr, "%i,%i ", temp_fields.at(i).at(0), temp_fields.at(i).at(1) ); + fprintf( stderr, "\n" ); +#endif + + return temp_fields; +} + +//ok +Barcode_EAN13::Barcode_EAN13() +{ + m_numbers.resize(13,-1); + m_null = true; +} + +//ok +Barcode_EAN13::Barcode_EAN13( QValueVector<int> code ) +{ + setCode( code ); +} + +//ok +void Barcode_EAN13::setCode( QValueVector<int> code ) +{ + if (code.count() != 13) { + m_numbers.clear(); + m_numbers.resize(13,-1); + m_null = true; + return; + } + m_numbers = code; + m_null = false; +} + +//ok +bool Barcode_EAN13::isValid() const +{ + if (m_null) + return false; + + for (int i = 0; i < 13; i++) + if ((m_numbers[i] < 0) || (m_numbers[i] > 9)) + return false; + + // calculate the checksum of the barcode: + int sum1 = m_numbers[0] + m_numbers[2] + m_numbers[4] + m_numbers[6] + m_numbers[8] + m_numbers[10]; + int sum2 = 3 * (m_numbers[1] + m_numbers[3] + m_numbers[5] + m_numbers[7] + m_numbers[9] + m_numbers[11]); + int checksum_value = sum1 + sum2; + int checksum_digit = 10 - (checksum_value % 10); + if (checksum_digit == 10) + checksum_digit = 0; + + return (m_numbers[12] == checksum_digit); +} + +//ok +QValueVector<int> Barcode_EAN13::getNumbers() const +{ + return m_numbers; +} + +//ok +QString Barcode_EAN13::toString() const +{ + QString s; + for (int i = 0; i < 13; i++) + if ((m_numbers[i] >= 0) && (m_numbers[i] <= 9)) + s += QString::number(m_numbers[i]); + else + s += '?'; + return s; +} + +//ok +bool Barcode_EAN13::operator!= ( const Barcode_EAN13 &code ) +{ + if (m_null != code.m_null) + return true; + if (!m_null) + for (int i=0; i<13; i++) + if (m_numbers[i] != code.m_numbers[i]) + return true; + return false; +} + +//ok +Barcode_EAN13 Decoder_EAN13::recognize( QValueVector< QValueVector<int> > fields ) +{ + // try to extract the encoded information from the field series: + QValueVector<int> numbers = decode( fields, 0, fields.count() ); + Barcode_EAN13 barcode( numbers ); + + // return the results: + return barcode; +} + +QValueVector<int> Decoder_EAN13::decode( QValueVector< QValueVector<int> > fields, int start_i, int end_i ) +{ + // determine the length of the path in pixels + int length = 0; + for (uint i = 0; i < fields.size(); i++) + length += fields.at(i).at(1); + + // set the parameters accordingly: + int max_start_sentry_bar_differences; + int max_unit_length; + int min_unit_length; + + if (length <= 800) { + max_start_sentry_bar_differences = 6; + max_unit_length = 10; + min_unit_length = 1; + } else { + max_start_sentry_bar_differences = 30; + max_unit_length = 50; + min_unit_length = 1; + } + + // consistency checks: + if (fields.count() <= 0) + return QValueVector<int>(); + if (start_i > end_i - 3) + return QValueVector<int>(); + if (end_i - start_i < 30) + return QValueVector<int>(); // (just a rough value) + + // relevant indexes: + int start_sentinel_i; + int end_sentinel_i; + int left_numbers_i; + int middle_guard_i; + int right_numbers_i; + + // relevant parameters: + float unit_length; + + // results: + QValueVector<int> numbers( 13, -1 ); // the java source does no initialization + + // determine the relevant positions: + + // Try to detect the start sentinel (a small black-white-black serie): + start_sentinel_i = -1; + for (int i = start_i; i < end_i - 56; i++) { + if (fields[i][0] == 0) { + if ((fields[i][1] >= min_unit_length) && (fields[i][1] <= max_unit_length)) { + if ((abs(fields[i][1] - fields[i + 1][1]) <= max_start_sentry_bar_differences) + && (abs(fields[i][1] - fields[i + 2][1]) <= max_start_sentry_bar_differences) && (fields[i + 3][1] < fields[i][1] << 3)) { + start_sentinel_i = i; + break; + } + } + } + } + +#ifdef Decoder_EAN13_DEBUG + fprintf( stderr, "start_sentinal_index: %i\n", start_sentinel_i ); +#endif + + if (start_sentinel_i < 0) + return QValueVector<int>(); + + // calculate the other positions: + left_numbers_i = start_sentinel_i + 3; + middle_guard_i = left_numbers_i + 6 * 4; + right_numbers_i = middle_guard_i + 5; + end_sentinel_i = right_numbers_i + 6 * 4; + + if (end_sentinel_i + 3 > end_i) + return QValueVector<int>(); + + // calculate the average (pixel) length of a bar that is one unit wide: + // (a complete barcode consists out of 95 length units) + int temp_length = 0; + int field_amount = (end_sentinel_i - start_sentinel_i + 3); + for (int i = start_sentinel_i; i < start_sentinel_i + field_amount; i++) + temp_length += fields[i][1]; + unit_length = (float) ((float) temp_length / 95.0f); + +#ifdef Decoder_EAN13_DEBUG + fprintf( stderr, "unit_width: %f\n", unit_length ); +#endif + + QValueVector< QValueVector<int> > current_number_field( 4, QValueVector<int>(2,0) ); + + if (left_numbers_i + 1 > end_i) + return QValueVector<int>(); + + + // test the side from which we are reading the barcode: + for (int j = 0; j < 4; j++) { + current_number_field[j][0] = fields[left_numbers_i + j][0]; + current_number_field[j][1] = fields[left_numbers_i + j][1]; + } + MatchMakerResult matchMakerResult = recognizeNumber( current_number_field, BOTH_TABLES ); + + + if (matchMakerResult.isEven()) { + // we are reading the barcode from the back side: + + // use the already obtained information: + numbers[12] = matchMakerResult.getDigit(); + + // try to recognize the "right" numbers: + int counter = 11; + for (int i = left_numbers_i + 4; i < left_numbers_i + 24; i = i + 4) { + for (int j = 0; j < 4; j++) { + current_number_field[j][0] = fields[i + j][0]; + current_number_field[j][1] = fields[i + j][1]; + } + matchMakerResult = recognizeNumber(current_number_field, EVEN_TABLE); + numbers[counter] = matchMakerResult.getDigit(); + counter--; + } + + bool parity_pattern[6]; // true = even, false = odd + + //(counter has now the value 6) + + // try to recognize the "left" numbers: + for (int i = right_numbers_i; i < right_numbers_i + 24; i = i + 4) { + for (int j = 0; j < 4; j++) { + current_number_field[j][0] = fields[i + j][0]; + current_number_field[j][1] = fields[i + j][1]; + } + matchMakerResult = recognizeNumber(current_number_field, BOTH_TABLES); + numbers[counter] = matchMakerResult.getDigit(); + parity_pattern[counter-1] = !matchMakerResult.isEven(); + counter--; + } + + // try to determine the system code: + matchMakerResult = recognizeSystemCode(parity_pattern); + numbers[0] = matchMakerResult.getDigit(); + } else { + // we are reading the barcode from the "correct" side: + + bool parity_pattern[6]; // true = even, false = odd + + // use the already obtained information: + numbers[1] = matchMakerResult.getDigit(); + parity_pattern[0] = matchMakerResult.isEven(); + + // try to recognize the left numbers: + int counter = 2; + for (int i = left_numbers_i + 4; i < left_numbers_i + 24; i = i + 4) { + for (int j = 0; j < 4; j++) { + current_number_field[j][0] = fields[i + j][0]; + current_number_field[j][1] = fields[i + j][1]; + } + matchMakerResult = recognizeNumber(current_number_field, BOTH_TABLES); + numbers[counter] = matchMakerResult.getDigit(); + parity_pattern[counter-1] = matchMakerResult.isEven(); + counter++; + } + + // try to determine the system code: + matchMakerResult = recognizeSystemCode(parity_pattern); + numbers[0] = matchMakerResult.getDigit(); + + // try to recognize the right numbers: + counter = 0; + for (int i = right_numbers_i; i < right_numbers_i + 24; i = i + 4) { + for (int j = 0; j < 4; j++) { + current_number_field[j][0] = fields[i + j][0]; + current_number_field[j][1] = fields[i + j][1]; + } + matchMakerResult = recognizeNumber(current_number_field, ODD_TABLE); + numbers[counter + 7] = matchMakerResult.getDigit(); + counter++; + } + } + + return numbers; +} + +MatchMakerResult Decoder_EAN13::recognizeNumber( QValueVector< QValueVector<int> > fields, int code_table_to_use) +{ + // convert the pixel lenghts of the four black&white fields into + // normed values that have together a length of 70; + int pixel_sum = fields[0][1] + fields[1][1] + fields[2][1] + fields[3][1]; + int b[4]; + for (int i = 0; i < 4; i++) { + b[i] = round((((float) fields[i][1]) / ((float) pixel_sum)) * 70); + } + +#ifdef Decoder_EAN13_DEBUG + fprintf( stderr, "Recognize Number (code table to use: %i):\n", code_table_to_use ); + fprintf( stderr, "lengths: %i %i %i %i\n", fields[0][1], fields[1][1], fields[2][1], fields[3][1] ); + fprintf( stderr, "normed lengths: %i %i %i %i\n", b[0], b[1], b[2], b[3] ); +#endif + + // try to detect the digit that is encoded by the set of four normed bar lenghts: + int max_difference_for_acceptance = 60; + int temp; + + int even_min_difference = 100000; + int even_min_difference_index = 0; + int odd_min_difference = 100000; + int odd_min_difference_index = 0; + + if ((code_table_to_use == BOTH_TABLES)||(code_table_to_use == EVEN_TABLE)) { + QValueVector<int> even_differences(10,0); + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 4; j++) { + // calculate the differences in the even group: + temp = b[j] - code_even[i][j]; + if (temp < 0) + even_differences[i] = even_differences[i] + ((-temp) << 1); + else + even_differences[i] = even_differences[i] + (temp << 1); + } + if (even_differences[i] < even_min_difference) { + even_min_difference = even_differences[i]; + even_min_difference_index = i; + } + } + } + + if ((code_table_to_use == BOTH_TABLES) || (code_table_to_use == ODD_TABLE)) { + QValueVector<int> odd_differences(10,0); + + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 4; j++) { + // calculate the differences in the odd group: + temp = b[j] - code_odd[i][j]; + if (temp < 0) + odd_differences[i] = odd_differences[i] + ((-temp) << 1); + else + odd_differences[i] = odd_differences[i] + (temp << 1); + } + if (odd_differences[i] < odd_min_difference) { + odd_min_difference = odd_differences[i]; + odd_min_difference_index = i; + } + } + } + + // select the digit and parity with the lowest difference to the found pattern: + if (even_min_difference <= odd_min_difference) { + if (even_min_difference < max_difference_for_acceptance) + return MatchMakerResult( true, even_min_difference_index ); + } else { + if (odd_min_difference < max_difference_for_acceptance) + return MatchMakerResult( false, odd_min_difference_index ); + } + + return MatchMakerResult( false, -1 ); +} + +MatchMakerResult Decoder_EAN13::recognizeSystemCode( bool parity_pattern[6] ) +{ + // search for a fitting parity pattern: + bool fits = false; + for (int i = 0; i < 10; i++) { + fits = true; + for (int j = 0; j < 6; j++) { + if (parity_pattern_list[i][j] != parity_pattern[j]) { + fits = false; + break; + } + } + if (fits) + return MatchMakerResult( false, i ); + } + + return MatchMakerResult( false, -1 ); +} + +//ok +MatchMakerResult::MatchMakerResult( bool even, int digit ) +{ + m_even = even; + m_digit = digit; +} + +#include "barcode.moc" diff --git a/src/barcode/barcode.h b/src/barcode/barcode.h new file mode 100644 index 0000000..b8d71bc --- /dev/null +++ b/src/barcode/barcode.h @@ -0,0 +1,135 @@ +/*************************************************************************** + copyright : (C) 2007 by Sebastian Held + email : sebastian.held@gmx.de + ***************************************************************************/ + +/*************************************************************************** + * * + * ### based on BaToo: http://people.inf.ethz.ch/adelmanr/batoo/ ### * + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef BARCODE_H +#define BARCODE_H + +#include <qthread.h> +#include <qimage.h> +#include <qvaluevector.h> +#include <qobject.h> +#include <qmutex.h> +#include <math.h> + +#include "barcode_v4l.h" + +//#define BarcodeDecoder_DEBUG +//#define Decoder_EAN13_DEBUG +//#define Barcode_DEBUG + +namespace barcodeRecognition { + static int code_odd[][4] = { { 30, 20, 10, 10 }, + { 20, 20, 20, 10 }, + { 20, 10, 20, 20 }, + { 10, 40, 10, 10 }, + { 10, 10, 30, 20 }, + { 10, 20, 30, 10 }, + { 10, 10, 10, 40 }, + { 10, 30, 10, 20 }, + { 10, 20, 10, 30 }, + { 30, 10, 10, 20 } }; + + static int code_even[][4] = { { 10, 10, 20, 30 }, + { 10, 20, 20, 20 }, + { 20, 20, 10, 20 }, + { 10, 10, 40, 10 }, + { 20, 30, 10, 10 }, + { 10, 30, 20, 10 }, + { 40, 10, 10, 10 }, + { 20, 10, 30, 10 }, + { 30, 10, 20, 10 }, + { 20, 10, 10, 30 } }; + + static bool parity_pattern_list[][6] = { { false, false, false, false, false, false }, + { false, false, true, false, true, true }, + { false, false, true, true, false, true }, + { false, false, true, true, true, false }, + { false, true, false, false, true, true }, + { false, true, true, false, false, true }, + { false, true, true, true, false, false }, + { false, true, false, true, false, true }, + { false, true, false, true, true, false }, + { false, true, true, false, true, false } }; + + class Barcode_EAN13 { + public: + Barcode_EAN13(); + Barcode_EAN13( QValueVector<int> code ); + bool isNull() const { return m_null; } + bool isValid() const; + QValueVector<int> getNumbers() const; + void setCode( QValueVector<int> code ); + QString toString() const; + bool operator!= ( const Barcode_EAN13 &code ); + protected: + QValueVector<int> m_numbers; + bool m_null; + }; + + class MatchMakerResult { + public: + MatchMakerResult( bool even, int digit ); + bool isEven() const {return m_even;} + int getDigit() const {return m_digit;} + protected: + int m_digit; + bool m_even; + }; + + class Decoder_EAN13 { + public: + enum { BOTH_TABLES = 0, EVEN_TABLE = 1, ODD_TABLE = 2 }; + static Barcode_EAN13 recognize( QValueVector< QValueVector<int> > fields ); + static QValueVector<int> decode( QValueVector< QValueVector<int> > fields, int start_i, int end_i ); + static MatchMakerResult recognizeNumber( QValueVector< QValueVector<int> > fields, int code_table_to_use ); + static MatchMakerResult recognizeSystemCode( bool parity_pattern[6] ); + }; + + /** \brief this thread handles barcode recognition using webcams + * @author Sebastian Held <sebastian.held@gmx.de> + */ + class barcodeRecognitionThread : public QObject, public QThread { + Q_OBJECT + public: + barcodeRecognitionThread(); + ~barcodeRecognitionThread(); + virtual void run(); + void stop(); + void recognizeBarcode( QImage img ); + bool isWebcamAvailable(); + signals: + void recognized( QString barcode ); + void gotImage( QImage &img ); + protected: + volatile bool m_stop; + QImage m_barcode_img; + QMutex m_stop_mutex, m_barcode_img_mutex; + barcode_v4l *m_barcode_v4l; + + Barcode_EAN13 recognize( QImage img ); + Barcode_EAN13 recognizeCode( QImage img, int x1, int x2, int y ); + void addNumberToPossibleNumbers( QValueVector<int> number, int possible_numbers[10][13][2], bool correct_code ); + void sortDigits( int possible_numbers[10][13][2] ); + Barcode_EAN13 extractBarcode( int possible_numbers[10][13][2] ); + QValueVector<int> transformPathToBW( QValueVector<QRgb> line); + QValueVector< QValueVector<int> > extractFieldInformation( QValueVector<int> string ); + Barcode_EAN13 detectValidBarcode ( int possible_numbers[10][13][2], int max_amount_of_considered_codes ); + bool isValid( int numbers[13] ); + bool isValid( QValueVector<int> numbers ); + void printArray( int array[10][13][2], int level ); + }; +} + +#endif diff --git a/src/barcode/barcode_v4l.cpp b/src/barcode/barcode_v4l.cpp new file mode 100644 index 0000000..f644fe1 --- /dev/null +++ b/src/barcode/barcode_v4l.cpp @@ -0,0 +1,538 @@ +/*************************************************************************** + copyright : (C) 2007 by Sebastian Held + email : sebastian.held@gmx.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +// uses code from xawtv: webcam.c (c) 1998-2002 Gerd Knorr + +//#include <stdio.h> +//#include <stdlib.h> +#include <fcntl.h> /* low-level i/o */ +//#include <unistd.h> +#include <errno.h> +//#include <malloc.h> +//#include <sys/stat.h> +//#include <sys/types.h> +//#include <sys/time.h> +//#include <sys/mman.h> +#include <sys/ioctl.h> +//#include <asm/types.h> /* for videodev2.h */ + +#include "barcode_v4l.h" +#include "../tellico_debug.h" + +using barcodeRecognition::ng_vid_driver; +using barcodeRecognition::ng_vid_driver_v4l; +using barcodeRecognition::barcode_v4l; +using barcodeRecognition::video_converter; + +QPtrList<barcodeRecognition::video_converter> barcodeRecognition::ng_vid_driver::m_converter; +const char *barcodeRecognition::device_cap[] = { "capture", "tuner", "teletext", "overlay", "chromakey", "clipping", "frameram", "scales", "monochrome", 0 }; +const unsigned int barcodeRecognition::ng_vfmt_to_depth[] = { + 0, /* unused */ + 8, /* RGB8 */ + 8, /* GRAY8 */ + 16, /* RGB15 LE */ + 16, /* RGB16 LE */ + 16, /* RGB15 BE */ + 16, /* RGB16 BE */ + 24, /* BGR24 */ + 32, /* BGR32 */ + 24, /* RGB24 */ + 32, /* RGB32 */ + 16, /* LUT2 */ + 32, /* LUT4 */ + 16, /* YUYV */ + 16, /* YUV422P */ + 12, /* YUV420P */ + 0, /* MJPEG */ + 0, /* JPEG */ + 16, /* UYVY */ +}; + +const char* barcodeRecognition::ng_vfmt_to_desc[] = { + "none", + "8 bit PseudoColor (dithering)", + "8 bit StaticGray", + "15 bit TrueColor (LE)", + "16 bit TrueColor (LE)", + "15 bit TrueColor (BE)", + "16 bit TrueColor (BE)", + "24 bit TrueColor (LE: bgr)", + "32 bit TrueColor (LE: bgr-)", + "24 bit TrueColor (BE: rgb)", + "32 bit TrueColor (BE: -rgb)", + "16 bit TrueColor (lut)", + "32 bit TrueColor (lut)", + "16 bit YUV 4:2:2 (packed, YUYV)", + "16 bit YUV 4:2:2 (planar)", + "12 bit YUV 4:2:0 (planar)", + "MJPEG (AVI)", + "JPEG (JFIF)", + "16 bit YUV 4:2:2 (packed, UYVY)", +}; + +unsigned int barcodeRecognition::ng_yuv_gray[256]; +unsigned int barcodeRecognition::ng_yuv_red[256]; +unsigned int barcodeRecognition::ng_yuv_blue[256]; +unsigned int barcodeRecognition::ng_yuv_g1[256]; +unsigned int barcodeRecognition::ng_yuv_g2[256]; +unsigned int barcodeRecognition::ng_clip[256 + 2 * CLIP]; + +void barcodeRecognition::ng_color_yuv2rgb_init() +{ + int i; +# define RED_NULL 128 +# define BLUE_NULL 128 +# define LUN_MUL 256 +# define RED_MUL 512 +# define BLUE_MUL 512 +#define GREEN1_MUL (-RED_MUL/2) +#define GREEN2_MUL (-BLUE_MUL/6) +#define RED_ADD (-RED_NULL * RED_MUL) +#define BLUE_ADD (-BLUE_NULL * BLUE_MUL) +#define GREEN1_ADD (-RED_ADD/2) +#define GREEN2_ADD (-BLUE_ADD/6) + + /* init Lookup tables */ + for (i = 0; i < 256; i++) { + barcodeRecognition::ng_yuv_gray[i] = i * LUN_MUL >> 8; + barcodeRecognition::ng_yuv_red[i] = (RED_ADD + i * RED_MUL) >> 8; + barcodeRecognition::ng_yuv_blue[i] = (BLUE_ADD + i * BLUE_MUL) >> 8; + barcodeRecognition::ng_yuv_g1[i] = (GREEN1_ADD + i * GREEN1_MUL) >> 8; + barcodeRecognition::ng_yuv_g2[i] = (GREEN2_ADD + i * GREEN2_MUL) >> 8; + } + for (i = 0; i < CLIP; i++) + barcodeRecognition::ng_clip[i] = 0; + for (; i < CLIP + 256; i++) + barcodeRecognition::ng_clip[i] = i - CLIP; + for (; i < 2 * CLIP + 256; i++) + barcodeRecognition::ng_clip[i] = 255; +} + + + + + + + + + + + +barcode_v4l::barcode_v4l() +{ + m_devname = "/dev/video0"; + m_grab_width = 640; + m_grab_height = 480; + m_drv = 0; + m_conv = 0; + + barcodeRecognition::ng_color_yuv2rgb_init(); + //ng_vid_driver::register_video_converter( new barcodeRecognition::yuv422p_to_rgb24() ); + //ng_vid_driver::register_video_converter( new barcodeRecognition::yuv422_to_rgb24() ); + ng_vid_driver::register_video_converter( new barcodeRecognition::yuv420p_to_rgb24() ); + + grab_init(); +} + +barcode_v4l::~barcode_v4l() +{ + if (m_drv) { + m_drv->close(); + delete m_drv; + } +} + +bool barcode_v4l::isOpen() +{ + return m_drv; +} + +QImage barcode_v4l::grab_one2() +{ + if (!m_drv) { + myDebug() << "no driver/device available" << endl; + return QImage(); + } + + QByteArray *cap; + if (!(cap = m_drv->getimage2())) { + myDebug() << "capturing image failed" << endl; + return QImage(); + } + + QByteArray *buf; + if (m_conv) { + buf = new QByteArray( 3*m_fmt.width*m_fmt.height ); + m_conv->frame( buf, cap, m_fmt_drv ); + } else { + buf = new QByteArray(*cap); + } + delete cap; + + int depth = 32; + QByteArray *buf2 = new QByteArray( depth/8 *m_fmt.width*m_fmt.height ); + for (uint i=0; i<m_fmt.width*m_fmt.height; i++) { + (*buf2)[i*4+0] = buf->at(i*3+2); + (*buf2)[i*4+1] = buf->at(i*3+1); + (*buf2)[i*4+2] = buf->at(i*3+0); + (*buf2)[i*4+3] = 0; + } + delete buf; + QImage *temp = new QImage( (uchar*)buf2->data(), m_fmt.width, m_fmt.height, depth, 0, 0, QImage::LittleEndian ); + QImage temp2 = temp->copy(); + delete temp; + delete buf2; + + return temp2; +} + + + + + + + +bool barcode_v4l::grab_init() +{ + m_drv = ng_vid_driver::createDriver( m_devname ); + if (!m_drv) { + myDebug() << "no grabber device available" << endl; + return false; + } + if (!(m_drv->capabilities() & CAN_CAPTURE)) { + myDebug() << "device doesn't support capture" << endl; + m_drv->close(); + delete m_drv; + m_drv = 0; + return false; + } + + /* try native */ + m_fmt.fmtid = VIDEO_RGB24; + m_fmt.width = m_grab_width; + m_fmt.height = m_grab_height; + if (m_drv->setformat( &m_fmt )) { + m_fmt_drv = m_fmt; + return true; + } + + /* check all available conversion functions */ + m_fmt.bytesperline = m_fmt.width * ng_vfmt_to_depth[m_fmt.fmtid] / 8; + for (int i = 0; i<VIDEO_FMT_COUNT; i++) { + video_converter *conv = ng_vid_driver::find_video_converter( VIDEO_RGB24, i ); + if (!conv) + continue; + + m_fmt_drv = m_fmt; + m_fmt_drv.fmtid = conv->fmtid_in(); + m_fmt_drv.bytesperline = 0; + if (m_drv->setformat( &m_fmt_drv )) { + m_fmt.width = m_fmt_drv.width; + m_fmt.height = m_fmt_drv.height; + m_conv = conv; + //m_conv->init( &m_fmt ); + return true; + } + } + + myDebug() << "can't get rgb24 data" << endl; + m_drv->close(); + delete m_drv; + m_drv = 0; + return false; +} + + +ng_vid_driver* ng_vid_driver::createDriver( QString device ) +{ + /* check all grabber drivers */ + ng_vid_driver *drv = new ng_vid_driver_v4l(); + if (!drv->open2( device )) { + myDebug() << "no v4l device found" << endl; + delete drv; + drv = 0; + } + + return drv; +} + +int ng_vid_driver::xioctl( int fd, int cmd, void *arg ) +{ + int rc; + + rc = ioctl(fd,cmd,arg); + if (rc == 0) + return 0; + //print_ioctl(stderr,ioctls_v4l1,PREFIX,cmd,arg); + qDebug( ": %s\n",(rc == 0) ? "ok" : strerror(errno) ); + return rc; +} + + + +void ng_vid_driver::register_video_converter( video_converter *conv ) +{ + if (!conv) + return; + + m_converter.append( conv ); +} + +video_converter *ng_vid_driver::find_video_converter( int out, int in ) +{ + video_converter *conv; + for (conv = m_converter.first(); conv; conv = m_converter.next() ) + if ((conv->fmtid_in() == in) && (conv->fmtid_out() == out)) + return conv; + return 0; +} + + + + + + + + + + + + + + + + + + +ng_vid_driver_v4l::ng_vid_driver_v4l() +{ + m_name = "v4l"; + m_drv = 0; + m_fd = -1; + + m_use_read = false; + + for (int i=0; i<VIDEO_FMT_COUNT; i++) + format2palette[i] = 0; + format2palette[VIDEO_RGB08] = VIDEO_PALETTE_HI240; + format2palette[VIDEO_GRAY] = VIDEO_PALETTE_GREY; + format2palette[VIDEO_RGB15_LE] = VIDEO_PALETTE_RGB555; + format2palette[VIDEO_RGB16_LE] = VIDEO_PALETTE_RGB565; + format2palette[VIDEO_BGR24] = VIDEO_PALETTE_RGB24; + format2palette[VIDEO_BGR32] = VIDEO_PALETTE_RGB32; + format2palette[VIDEO_YUYV] = VIDEO_PALETTE_YUV422; + format2palette[VIDEO_UYVY] = VIDEO_PALETTE_UYVY; + format2palette[VIDEO_YUV422P] = VIDEO_PALETTE_YUV422P; + format2palette[VIDEO_YUV420P] = VIDEO_PALETTE_YUV420P; +} + +bool ng_vid_driver_v4l::open2( QString device ) +{ + /* open device */ + if ((m_fd = ::open( device.latin1(), O_RDWR )) == -1) { + qDebug( "v4l: open %s: %s\n", device.latin1(), strerror(errno) ); + return false; + } + if (ioctl( m_fd, VIDIOCGCAP, &m_capability ) == -1) { + ::close( m_fd ); + return false; + } + +#ifdef Barcode_DEBUG + qDebug( "v4l: open: %s (%s)\n", device.latin1(), m_capability.name ); +#endif + + fcntl( m_fd, F_SETFD, FD_CLOEXEC ); // close on exit + +#ifdef Barcode_DEBUG + myDebug() << " capabilities: " << endl; + for (int i = 0; device_cap[i] != NULL; i++) + if (m_capability.type & (1 << i)) + qDebug( " %s", device_cap[i] ); + qDebug( " size : %dx%d => %dx%d", m_capability.minwidth, m_capability.minheight, m_capability.maxwidth, m_capability.maxheight ); +#endif + +#ifdef Barcode_DEBUG + fprintf(stderr," v4l: using read() for capture\n"); +#endif + m_use_read = true; + + return true; +} + +void ng_vid_driver_v4l::close() +{ +#ifdef Barcode_DEBUG + fprintf(stderr, "v4l: close\n"); +#endif + + if (m_fd != -1) + ::close(m_fd); + m_fd = -1; + return; +} + +int ng_vid_driver_v4l::capabilities() +{ + int ret = 0; + + if (m_capability.type & VID_TYPE_OVERLAY) + ret |= CAN_OVERLAY; + if (m_capability.type & VID_TYPE_CAPTURE) + ret |= CAN_CAPTURE; + if (m_capability.type & VID_TYPE_TUNER) + ret |= CAN_TUNE; + if (m_capability.type & VID_TYPE_CHROMAKEY) + ret |= NEEDS_CHROMAKEY; + return ret; +} + +bool ng_vid_driver_v4l::setformat( ng_video_fmt *fmt ) +{ + bool rc = false; + +#ifdef Barcode_DEBUG + fprintf(stderr,"v4l: setformat\n"); +#endif + if (m_use_read) { + rc = read_setformat( fmt ); + } else { + Q_ASSERT( false ); + } + + m_fmt = *fmt; + + return rc; +} + + + + + +bool ng_vid_driver_v4l::read_setformat( ng_video_fmt *fmt ) +{ + xioctl( m_fd, VIDIOCGCAP, &m_capability ); + if (fmt->width > static_cast<uint>(m_capability.maxwidth)) + fmt->width = m_capability.maxwidth; + if (fmt->height > static_cast<uint>(m_capability.maxheight)) + fmt->height = m_capability.maxheight; + fmt->bytesperline = fmt->width * ng_vfmt_to_depth[fmt->fmtid] / 8; + + xioctl( m_fd, VIDIOCGPICT, &m_pict ); + m_pict.depth = ng_vfmt_to_depth[fmt->fmtid]; + m_pict.palette = GETELEM(format2palette,fmt->fmtid,0); + m_pict.brightness = 20000; + if (-1 == xioctl( m_fd, VIDIOCSPICT, &m_pict )) + return false; + + xioctl( m_fd, VIDIOCGWIN, &m_win); + m_win.width = m_fmt.width; + m_win.height = m_fmt.height; + m_win.clipcount = 0; + if (-1 == xioctl( m_fd, VIDIOCSWIN, &m_win )) + return false; + + return true; +} + +QByteArray* ng_vid_driver_v4l::getimage2() +{ +#ifdef Barcode_DEBUG + myDebug() << "v4l: getimage2" << endl; +#endif + + return read_getframe2(); +} + + +QByteArray* ng_vid_driver_v4l::read_getframe2() +{ + QByteArray* buf; + int size; + + size = m_fmt.bytesperline * m_fmt.height; + buf = new QByteArray( size ); + if (size != read( m_fd, buf->data(), size )) { + delete( buf ); + return 0; + } + return buf; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#define GRAY(val) barcodeRecognition::ng_yuv_gray[val] +#define RED(gray,red) ng_clip[ CLIP + gray + barcodeRecognition::ng_yuv_red[red] ] +#define GREEN(gray,red,blue) ng_clip[ CLIP + gray + barcodeRecognition::ng_yuv_g1[red] + \ + barcodeRecognition::ng_yuv_g2[blue] ] +#define BLUE(gray,blue) ng_clip[ CLIP + gray + barcodeRecognition::ng_yuv_blue[blue] ] + +void barcodeRecognition::yuv420p_to_rgb24::frame( QByteArray *out, const QByteArray *in, const ng_video_fmt fmt ) +{ + unsigned char *y, *u, *v, *d; + unsigned char *us,*vs; + unsigned char *dp; + unsigned int i,j; + int gray; + + dp = (unsigned char*)out->data(); + y = (unsigned char*)in->data(); + u = y + fmt.width * fmt.height; + v = u + fmt.width * fmt.height / 4; + + for (i = 0; i < fmt.height; i++) { + d = dp; + us = u; vs = v; + for (j = 0; j < fmt.width; j+= 2) { + gray = GRAY(*y); + *(d++) = RED(gray,*v); + *(d++) = GREEN(gray,*v,*u); + *(d++) = BLUE(gray,*u); + y++; + gray = GRAY(*y); + *(d++) = RED(gray,*v); + *(d++) = GREEN(gray,*v,*u); + *(d++) = BLUE(gray,*u); + y++; u++; v++; + } + if (0 == (i % 2)) { + u = us; v = vs; + } + dp += 3*fmt.width; + } +} diff --git a/src/barcode/barcode_v4l.h b/src/barcode/barcode_v4l.h new file mode 100644 index 0000000..f0cde90 --- /dev/null +++ b/src/barcode/barcode_v4l.h @@ -0,0 +1,184 @@ +/*************************************************************************** + copyright : (C) 2007 by Sebastian Held + email : sebastian.held@gmx.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef BARCODE_V4L_H +#define BARCODE_V4L_H + +#define GETELEM(array,index,default) \ + (index < sizeof(array)/sizeof(array[0]) ? array[index] : default) + +/* + * Taken from xawtv-3.5.9 source 8/15/01 by George Staikos <staikos@kde.org> + */ +#ifdef __STRICT_ANSI__ +#undef __STRICT_ANSI__ +#define FOO__STRICT_ANSI__ +#endif +#include <asm/types.h> +#ifdef FOO__STRICT_ANSI__ +#define __STRICT_ANSI__ 1 +#undef FOO__STRICT_ANSI__ +#endif + +//#include <linux/videodev2.h> +#include <linux/videodev.h> + +#include <qstring.h> +#include <qimage.h> + +namespace barcodeRecognition { + +struct ng_video_fmt { + unsigned int fmtid; /* VIDEO_* */ + unsigned int width; + unsigned int height; + unsigned int bytesperline; /* zero for compressed formats */ +}; +enum { CAN_OVERLAY=1, CAN_CAPTURE=2, CAN_TUNE=4, NEEDS_CHROMAKEY=8 }; +enum { VIDEO_NONE=0, VIDEO_RGB08, VIDEO_GRAY, VIDEO_RGB15_LE, VIDEO_RGB16_LE, + VIDEO_RGB15_BE, VIDEO_RGB16_BE, VIDEO_BGR24, VIDEO_BGR32, VIDEO_RGB24, + VIDEO_RGB32, VIDEO_LUT2, VIDEO_LUT4, VIDEO_YUYV, VIDEO_YUV422P, + VIDEO_YUV420P, VIDEO_MJPEG, VIDEO_JPEG, VIDEO_UYVY, VIDEO_FMT_COUNT }; +extern const char *device_cap[]; +extern const unsigned int ng_vfmt_to_depth[]; +extern const char* ng_vfmt_to_desc[]; + +/* lookup tables */ +#define CLIP 320 +extern unsigned int ng_yuv_gray[256]; +extern unsigned int ng_yuv_red[256]; +extern unsigned int ng_yuv_blue[256]; +extern unsigned int ng_yuv_g1[256]; +extern unsigned int ng_yuv_g2[256]; +extern unsigned int ng_clip[256 + 2 * CLIP]; +void ng_color_yuv2rgb_init(); + + + +class video_converter +{ +public: + video_converter() {;} + virtual ~video_converter() {;} + virtual void init( ng_video_fmt* ) {;} + virtual void exit() {;} + virtual void frame( QByteArray*, const QByteArray*, ng_video_fmt ) {;} + int fmtid_in() {return m_fmtid_in;} + int fmtid_out() {return m_fmtid_out;} +protected: + int m_fmtid_in, m_fmtid_out; +}; + +class yuv422p_to_rgb24 : public video_converter +{ +public: + yuv422p_to_rgb24() {m_fmtid_in=VIDEO_YUV422P; m_fmtid_out=VIDEO_RGB24;} + virtual void frame( QByteArray *, const QByteArray *, ng_video_fmt ) {;} +}; +class yuv422_to_rgb24 : public video_converter +{ +public: + yuv422_to_rgb24() {m_fmtid_in=VIDEO_YUYV; m_fmtid_out=VIDEO_RGB24;} + virtual void frame( QByteArray *, const QByteArray *, ng_video_fmt ) {;} +}; +class yuv420p_to_rgb24 : public video_converter +{ +public: + yuv420p_to_rgb24() {m_fmtid_in=VIDEO_YUV420P; m_fmtid_out=VIDEO_RGB24;} + virtual void frame( QByteArray *, const QByteArray *, ng_video_fmt ); +}; + +class ng_vid_driver +{ +public: + static ng_vid_driver *createDriver( QString device ); + static int xioctl( int fd, int cmd, void *arg ); + + /* open/close */ + virtual bool open2( QString device ) = 0; + virtual void close() = 0; + + /* attributes */ + virtual QString get_devname() {return m_name;} + virtual int capabilities() = 0; + + /* capture */ + virtual bool setformat( ng_video_fmt *fmt ) = 0; + virtual QByteArray* getimage2() = 0; /* single image */ + + // video converter + static void register_video_converter( video_converter *conv ); + static video_converter *find_video_converter( int out, int in ); + +protected: + QString m_name; + static QPtrList<video_converter> m_converter; +}; + + +class barcode_v4l +{ +public: + barcode_v4l(); + ~barcode_v4l(); + QImage grab_one2(); + bool isOpen(); + +protected: + bool grab_init(); + + QString m_devname; + int m_grab_width, m_grab_height; + ng_vid_driver *m_drv; + ng_video_fmt m_fmt, m_fmt_drv; + video_converter *m_conv; + +}; + + +class ng_vid_driver_v4l : public ng_vid_driver +{ +public: + ng_vid_driver_v4l(); + + /* open/close */ + //virtual bool open( QString device ); + virtual bool open2( QString device ); + virtual void close(); + + /* attributes */ + virtual int capabilities(); + + /* capture */ + virtual bool setformat( ng_video_fmt *fmt ); + //virtual ng_video_buf* getimage(); /* single image */ + virtual QByteArray* getimage2(); /* single image */ + +protected: + bool read_setformat( ng_video_fmt *fmt ); + QByteArray* read_getframe2(); + void *m_drv; + int m_fd; + video_capability m_capability; + video_picture m_pict; + video_window m_win; + + /* capture */ + bool m_use_read; + ng_video_fmt m_fmt; + + unsigned short format2palette[VIDEO_FMT_COUNT]; +}; + +} // namespace +#endif diff --git a/src/borrower.cpp b/src/borrower.cpp new file mode 100644 index 0000000..2e3fa0b --- /dev/null +++ b/src/borrower.cpp @@ -0,0 +1,75 @@ +/*************************************************************************** + copyright : (C) 2005-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 "borrower.h" +#include "entry.h" +#include "tellico_utils.h" + +using Tellico::Data::Loan; +using Tellico::Data::Borrower; + +Loan::Loan(Data::EntryPtr entry, const QDate& loanDate, const QDate& dueDate, const QString& note) + : KShared(), m_uid(Tellico::uid()), m_borrower(0), m_entry(entry), m_loanDate(loanDate), m_dueDate(dueDate), + m_note(note), m_inCalendar(false) { +} + +Loan::Loan(const Loan& other) : KShared(other), m_uid(Tellico::uid()), m_borrower(other.m_borrower), + m_entry(other.m_entry), m_loanDate(other.m_loanDate), m_dueDate(other.m_dueDate), + m_note(other.m_note), m_inCalendar(false) { +} + +Tellico::Data::BorrowerPtr Loan::borrower() const { + return m_borrower; +} + +Tellico::Data::EntryPtr Loan::entry() const { + return m_entry; +} + +Borrower::Borrower(const QString& name_, const QString& uid_) + : KShared(), m_name(name_), m_uid(uid_) { +} + +Borrower::Borrower(const Borrower& b) + : KShared(b), m_name(b.m_name), m_uid(b.m_uid), m_loans(b.m_loans) { +} + +Borrower& Borrower::operator=(const Borrower& other_) { + if(this == &other_) return *this; + + static_cast<KShared&>(*this) = static_cast<const KShared&>(other_); + m_name = other_.m_name; + m_uid = other_.m_uid; + m_loans = other_.m_loans; + return *this; +} + +Tellico::Data::LoanPtr Borrower::loan(Data::ConstEntryPtr entry_) { + for(LoanVec::Iterator it = m_loans.begin(); it != m_loans.end(); ++it) { + if(it->entry() == entry_) { + return it; + } + } + return 0; +} + +void Borrower::addLoan(Data::LoanPtr loan_) { + if(loan_) { + m_loans.append(loan_); + loan_->setBorrower(this); + } +} + +bool Borrower::removeLoan(Data::LoanPtr loan_) { + return m_loans.remove(loan_); +} diff --git a/src/borrower.h b/src/borrower.h new file mode 100644 index 0000000..049badf --- /dev/null +++ b/src/borrower.h @@ -0,0 +1,95 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_BORROWER_H +#define TELLICO_BORROWER_H + +#include "datavectors.h" + +#include <ksharedptr.h> + +#include <qdatetime.h> + +namespace Tellico { + namespace Data { + +class Loan : public KShared { + +public: + Loan(Data::EntryPtr entry, const QDate& loanDate, const QDate& dueDate, const QString& note); + Loan(const Loan& other); + + Data::BorrowerPtr borrower() const; + void setBorrower(Data::BorrowerPtr b) { m_borrower = b; } + + const QString& uid() const { return m_uid; } + void setUID(const QString& uid) { m_uid = uid; } + + Data::EntryPtr entry() const; + + const QDate& loanDate() const { return m_loanDate; } + + const QDate& dueDate() const { return m_dueDate; } + void setDueDate(const QDate& date) { m_dueDate = date; } + + const QString& note() const { return m_note; } + void setNote(const QString& text) { m_note = text; } + + bool inCalendar() const { return m_inCalendar; } + void setInCalendar(bool inCalendar) { m_inCalendar = inCalendar; } + +private: + Loan& operator=(const Loan&); + + QString m_uid; + Data::BorrowerPtr m_borrower; + Data::EntryPtr m_entry; + QDate m_loanDate; + QDate m_dueDate; + QString m_note; + bool m_inCalendar; +}; + +typedef KSharedPtr<Loan> LoanPtr; +typedef Vector<Loan> LoanVec; +typedef LoanVec::Iterator LoanVecIt; + +/** + * @author Robby Stephenson + */ +class Borrower : public KShared { + +public: + Borrower(const QString& name, const QString& uid); + Borrower(const Borrower& other); + Borrower& operator=(const Borrower& other); + + const QString& uid() const { return m_uid; } + const QString& name() const { return m_name; } + const LoanVec& loans() const { return m_loans; } + bool isEmpty() const { return m_loans.isEmpty(); } + int count() const { return m_loans.count(); } + + Data::LoanPtr loan(Data::ConstEntryPtr entry); + void addLoan(Data::LoanPtr loan); + bool removeLoan(Data::LoanPtr loan); + +private: + QString m_name; + QString m_uid; // uid used by KABC + LoanVec m_loans; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/borrowerdialog.cpp b/src/borrowerdialog.cpp new file mode 100644 index 0000000..4346b6b --- /dev/null +++ b/src/borrowerdialog.cpp @@ -0,0 +1,136 @@ +/*************************************************************************** + copyright : (C) 2005-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 "borrowerdialog.h" +#include "document.h" +#include "collection.h" + +#include <klocale.h> +#include <klineedit.h> +#include <kabc/addressee.h> +#include <kabc/stdaddressbook.h> +#include <kiconloader.h> + +#include <qlayout.h> + +using Tellico::BorrowerDialog; + +BorrowerDialog::Item::Item(KListView* parent_, const KABC::Addressee& add_) + : KListViewItem(parent_), m_uid(add_.uid()) { + setText(0, add_.realName()); + setPixmap(0, SmallIcon(QString::fromLatin1("kaddressbook"))); +} + +BorrowerDialog::Item::Item(KListView* parent_, const Data::Borrower& bor_) + : KListViewItem(parent_), m_uid(bor_.uid()) { + setText(0, bor_.name()); + setPixmap(0, SmallIcon(QString::fromLatin1("tellico"))); +} + +// default button is going to be used as a print button, so it's separated +BorrowerDialog::BorrowerDialog(QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, true, i18n("Select Borrower"), Ok|Cancel) { + QWidget* mainWidget = new QWidget(this, "BorrowerDialog mainWidget"); + setMainWidget(mainWidget); + QVBoxLayout* topLayout = new QVBoxLayout(mainWidget, 0, KDialog::spacingHint()); + + m_listView = new KListView(mainWidget); + topLayout->addWidget(m_listView); + m_listView->addColumn(i18n("Name")); + m_listView->setFullWidth(true); + connect(m_listView, SIGNAL(doubleClicked(QListViewItem*)), SLOT(slotOk())); + connect(m_listView, SIGNAL(selectionChanged(QListViewItem*)), SLOT(updateEdit(QListViewItem*))); + + m_lineEdit = new KLineEdit(mainWidget); + topLayout->addWidget(m_lineEdit); + connect(m_lineEdit->completionObject(), SIGNAL(match(const QString&)), + SLOT(selectItem(const QString&))); + m_lineEdit->setFocus(); + m_lineEdit->completionObject()->setIgnoreCase(true); + + KABC::AddressBook* abook = KABC::StdAddressBook::self(true); + connect(abook, SIGNAL(addressBookChanged(AddressBook*)), + SLOT(slotLoadAddressBook())); + connect(abook, SIGNAL(loadingFinished(Resource*)), + SLOT(slotLoadAddressBook())); + slotLoadAddressBook(); + + setMinimumWidth(400); +} + +void BorrowerDialog::slotLoadAddressBook() { + m_listView->clear(); + m_itemDict.clear(); + m_lineEdit->completionObject()->clear(); + + const KABC::AddressBook* const abook = KABC::StdAddressBook::self(true); + for(KABC::AddressBook::ConstIterator it = abook->begin(), end = abook->end(); + it != end; ++it) { + // skip people with no name + if((*it).realName().isEmpty()) { + continue; + } + Item* item = new Item(m_listView, *it); + m_itemDict.insert((*it).realName(), item); + m_lineEdit->completionObject()->addItem((*it).realName()); + } + + // add current borrowers, too + const Data::BorrowerVec& borrowers = Data::Document::self()->collection()->borrowers(); + for(Data::BorrowerVec::ConstIterator it = borrowers.constBegin(); it != borrowers.constEnd(); ++it) { + if(m_itemDict[it->name()]) { + continue; // if an item already exists with this name + } + Item* item = new Item(m_listView, *it); + m_itemDict.insert(it->name(), item); + m_lineEdit->completionObject()->addItem(it->name()); + } + m_listView->setSorting(0, true); + m_listView->sort(); +} + +void BorrowerDialog::selectItem(const QString& str_) { + if(str_.isEmpty()) { + return; + } + + QListViewItem* item = m_itemDict.find(str_); + if(item) { + m_listView->blockSignals(true); + m_listView->setSelected(item, true); + m_listView->ensureItemVisible(item); + m_listView->blockSignals(false); + } +} + +void BorrowerDialog::updateEdit(QListViewItem* item_) { + m_lineEdit->setText(item_->text(0)); + m_lineEdit->setSelection(0, item_->text(0).length()); + m_uid = static_cast<Item*>(item_)->uid(); +} + +Tellico::Data::BorrowerPtr BorrowerDialog::borrower() { + return new Data::Borrower(m_lineEdit->text(), m_uid); +} + +// static +Tellico::Data::BorrowerPtr BorrowerDialog::getBorrower(QWidget* parent_) { + BorrowerDialog dlg(parent_); + + if(dlg.exec() == QDialog::Accepted) { + return dlg.borrower(); + } + return 0; +} + +#include "borrowerdialog.moc" diff --git a/src/borrowerdialog.h b/src/borrowerdialog.h new file mode 100644 index 0000000..2ad2684 --- /dev/null +++ b/src/borrowerdialog.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef BORROWERDIALOG_H +#define BORROWERDIALOG_H + +class KLineEdit; + +#include "borrower.h" + +#include <kdialogbase.h> + +#include <klistview.h> +#include <qdict.h> + +namespace KABC { + class Addressee; +} + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class BorrowerDialog : public KDialogBase { +Q_OBJECT + +public: + static Data::BorrowerPtr getBorrower(QWidget* parent); + +private slots: + void selectItem(const QString& name); + void updateEdit(QListViewItem* item); + void slotLoadAddressBook(); + +private: + /** + * The constructor sets up the dialog. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + BorrowerDialog(QWidget* parent, const char* name=0); + Data::BorrowerPtr borrower(); + + QString m_uid; + KListView* m_listView; + KLineEdit* m_lineEdit; + QDict<KListViewItem> m_itemDict; + +class Item : public KListViewItem { +public: + Item(KListView* parent, const KABC::Addressee& addressee); + Item(KListView* parent, const Data::Borrower& borrower); + const QString& uid() const { return m_uid; } + +private: + QString m_uid; +}; + +}; + +} // end namespace +#endif diff --git a/src/borroweritem.cpp b/src/borroweritem.cpp new file mode 100644 index 0000000..0a9ee51 --- /dev/null +++ b/src/borroweritem.cpp @@ -0,0 +1,40 @@ +/*************************************************************************** + copyright : (C) 2005-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 "borroweritem.h" +#include "borrower.h" +#include "entry.h" + +#include "kiconloader.h" + +using Tellico::BorrowerItem; + +BorrowerItem::BorrowerItem(GUI::ListView* parent_, Data::BorrowerPtr borrower_) + : GUI::CountedItem(parent_), m_borrower(borrower_) { + setText(0, borrower_->name()); + setPixmap(0, SmallIcon(QString::fromLatin1("kaddressbook"))); +} + +int BorrowerItem::count() const { + return m_borrower ? m_borrower->count() : GUI::CountedItem::count(); +} + +Tellico::Data::EntryVec BorrowerItem::entries() const { + Data::EntryVec entries; + for(Data::LoanVec::ConstIterator loan = m_borrower->loans().begin(); loan != m_borrower->loans().end(); ++loan) { + if(!entries.contains(loan->entry())) { + entries.append(loan->entry()); + } + } + return entries; +} diff --git a/src/borroweritem.h b/src/borroweritem.h new file mode 100644 index 0000000..ad0b7d6 --- /dev/null +++ b/src/borroweritem.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_BORROWERITEM_H +#define TELLICO_BORROWERITEM_H + +#include "gui/counteditem.h" +#include "datavectors.h" + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class BorrowerItem : public GUI::CountedItem { +public: + BorrowerItem(GUI::ListView* parent, Data::BorrowerPtr filter); + + virtual bool isBorrowerItem() const { return true; } + Data::BorrowerPtr borrower() { return m_borrower; } + + virtual int count() const; + virtual Data::EntryVec entries() const; + +private: + Data::BorrowerPtr m_borrower; +}; + +} + +#endif diff --git a/src/calendarhandler.cpp b/src/calendarhandler.cpp new file mode 100644 index 0000000..c8953fd --- /dev/null +++ b/src/calendarhandler.cpp @@ -0,0 +1,251 @@ +/*************************************************************************** + copyright : (C) 2005-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 "calendarhandler.h" +#include "entry.h" +#include "tellico_kernel.h" +#include "tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> + +#ifdef USE_KCAL +#include <libkcal/calendarresources.h> +#include <libkcal/todo.h> +#include <libkcal/resourcelocal.h> +// #include <libkcal/resourceremote.h> // this file is moving around, API differences +#endif + +// needed for ::readlink +#include <unistd.h> +#include <limits.h> + +// most of this code came from konsolekalendar in kdepim + +using Tellico::CalendarHandler; + +void CalendarHandler::addLoans(Data::LoanVec loans_) { +#ifdef USE_KCAL + addLoans(loans_, 0); +#else + Q_UNUSED(loans_); +#endif +} + +#ifdef USE_KCAL +void CalendarHandler::addLoans(Data::LoanVec loans_, KCal::CalendarResources* resources_) { + if(loans_.isEmpty()) { + return; + } + + KCal::CalendarResources* calendarResources; + if(resources_) { + calendarResources = resources_; + } else { + calendarResources = new KCal::CalendarResources(timezone()); + calendarResources->readConfig(); + calendarResources->load(); + if(!checkCalendar(calendarResources)) { + return; + } + } + + for(Data::LoanVec::Iterator loan = loans_.begin(); loan != loans_.end(); ++loan) { + // only add loans with a due date + if(loan->dueDate().isNull()) { + continue; + } + + KCal::Todo* todo = new KCal::Todo(); + populateTodo(todo, loan); + + calendarResources->addTodo(todo); + loan->setInCalendar(true); + } + calendarResources->save(); + // don't close if a pointer was passed + if(!resources_) { + calendarResources->close(); + calendarResources->deleteLater(); + } +} +#endif + +void CalendarHandler::modifyLoans(Data::LoanVec loans_) { +#ifndef USE_KCAL + Q_UNUSED(loans_); + return; +#else + if(loans_.isEmpty()) { + return; + } + + KCal::CalendarResources calendarResources(timezone()); + calendarResources.readConfig(); + if(!checkCalendar(&calendarResources)) { + return; + } + calendarResources.load(); + + for(Data::LoanVec::Iterator loan = loans_.begin(); loan != loans_.end(); ++loan) { + KCal::Todo* todo = calendarResources.todo(loan->uid()); + if(!todo) { +// myDebug() << "couldn't find existing todo, adding a new todo" << endl; + Data::LoanVec newLoans; + newLoans.append(loan); + addLoans(newLoans, &calendarResources); // add loan + continue; + } + if(loan->dueDate().isNull()) { + myDebug() << "removing todo" << endl; + calendarResources.deleteIncidence(todo); + continue; + } + + populateTodo(todo, loan); + todo->updated(); + + loan->setInCalendar(true); + } + calendarResources.save(); + calendarResources.close(); +#endif +} + +void CalendarHandler::removeLoans(Data::LoanVec loans_) { +#ifndef USE_KCAL + Q_UNUSED(loans_); + return; +#else + if(loans_.isEmpty()) { + return; + } + + KCal::CalendarResources calendarResources(timezone()); + calendarResources.readConfig(); + if(!checkCalendar(&calendarResources)) { + return; + } + calendarResources.load(); + + for(Data::LoanVec::Iterator loan = loans_.begin(); loan != loans_.end(); ++loan) { + KCal::Todo* todo = calendarResources.todo(loan->uid()); + if(todo) { + // maybe this is too much, we could just set the todo as done + calendarResources.deleteIncidence(todo); + } + } + calendarResources.save(); + calendarResources.close(); +#endif +} + +#ifdef USE_KCAL +bool CalendarHandler::checkCalendar(KCal::CalendarResources* resources) { + KCal::CalendarResourceManager* manager = resources->resourceManager(); + if(manager->isEmpty()) { + kdWarning() << "Tellico::CalendarHandler::checkCalendar() - adding default calendar" << endl; + KConfig config(QString::fromLatin1("korganizerrc")); + config.setGroup("General"); + QString fileName = config.readPathEntry("Active Calendar"); + + QString resourceName; + KCal::ResourceCalendar* defaultResource = 0; + if(fileName.isEmpty()) { + fileName = locateLocal("appdata", QString::fromLatin1("std.ics")); + resourceName = i18n("Default Calendar"); + defaultResource = new KCal::ResourceLocal(fileName); + } else { + KURL url = KURL::fromPathOrURL(fileName); + if(url.isLocalFile()) { + defaultResource = new KCal::ResourceLocal(url.path()); + } else { +// defaultResource = new KCal::ResourceRemote(url); + Kernel::self()->sorry(i18n("At the moment, Tellico only supports local calendar resources. " + "The active calendar is remotely located, so your loans will not " + "be added.")); + return false; + } + resourceName = i18n("Active Calendar"); + } + + defaultResource->setResourceName(resourceName); + + manager->add(defaultResource); + manager->setStandardResource(defaultResource); + } + return true; +} + +void CalendarHandler::populateTodo(KCal::Todo* todo_, Data::LoanPtr loan_) { + if(!todo_ || !loan_) { + return; + } + + todo_->setUid(loan_->uid()); + + todo_->setDtStart(loan_->loanDate()); + todo_->setHasStartDate(true); + todo_->setDtDue(loan_->dueDate()); + todo_->setHasDueDate(true); + QString person = loan_->borrower()->name(); + QString summary = i18n("Tellico: %1 is due to return \"%2\"").arg(person).arg(loan_->entry()->title()); + todo_->setSummary(summary); + QString note = loan_->note(); + if(note.isEmpty()) { + note = summary; + } + todo_->setDescription(note); + todo_->setSecrecy(KCal::Incidence::SecrecyPrivate); // private by default + + todo_->clearAttendees(); + // not adding email of borrower for now, but request RSVP? + KCal::Attendee* attendee = new KCal::Attendee(loan_->borrower()->name(), + QString::null, false /*rsvp*/, + KCal::Attendee::NeedsAction, + KCal::Attendee::ReqParticipant, + loan_->borrower()->uid()); + todo_->addAttendee(attendee); + + todo_->clearAlarms(); + KCal::Alarm* alarm = todo_->newAlarm(); + alarm->setDisplayAlarm(summary); + alarm->setEnabled(true); +} + +// taken from kpimprefs.cpp +QString CalendarHandler::timezone() { + QString zone; + + KConfig korgcfg(locate(QString::fromLatin1("config"), QString::fromLatin1("korganizerrc"))); + korgcfg.setGroup("Time & Date"); + QString tz(korgcfg.readEntry("TimeZoneId")); + if(!tz.isEmpty()) { + zone = tz; + } else { + char zonefilebuf[PATH_MAX]; + + int len = ::readlink("/etc/localtime", zonefilebuf, PATH_MAX); + if(len > 0 && len < PATH_MAX) { + zone = QString::fromLocal8Bit(zonefilebuf, len); + zone = zone.mid(zone.find(QString::fromLatin1("zoneinfo/")) + 9); + } else { + tzset(); + zone = tzname[0]; + } + } + return zone; +} + +#endif diff --git a/src/calendarhandler.h b/src/calendarhandler.h new file mode 100644 index 0000000..5e01b62 --- /dev/null +++ b/src/calendarhandler.h @@ -0,0 +1,57 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CALENDARHANDLER_H +#define TELLICO_CALENDARHANDLER_H + +#include <config.h> +#include "borrower.h" + +#include <kdeversion.h> + +// libkcal is not binary compatible between versions +// for now, just support KDE 3.4 and higher +#if defined(HAVE_KCAL) && KDE_IS_VERSION(3,3,90) +#define USE_KCAL +#endif + +namespace KCal { + class CalendarResources; + class Todo; +} + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class CalendarHandler { +public: + static void addLoans(Data::LoanVec loans); + static void modifyLoans(Data::LoanVec loans); + static void removeLoans(Data::LoanVec loans); + +private: + static QString timezone(); + +#ifdef USE_KCAL + // helper function + static void addLoans(Data::LoanVec loans, KCal::CalendarResources* resources); + static bool checkCalendar(KCal::CalendarResources* resources); + static void populateTodo(KCal::Todo* todo, Data::LoanPtr loan); +#endif +}; + +} // end namespace + +#endif diff --git a/src/cite/Makefile.am b/src/cite/Makefile.am new file mode 100644 index 0000000..74e7c81 --- /dev/null +++ b/src/cite/Makefile.am @@ -0,0 +1,20 @@ +AM_CPPFLAGS = $(all_includes) +METASOURCES = AUTO +KDE_OPTIONS = noautodist +CLEANFILES = *~ *.loT +noinst_LIBRARIES = libcite.a +libcite_a_SOURCES = lyxpipe.cpp actionmanager.cpp clipboard.cpp openoffice.cpp + +EXTRA_DIST = \ +actionmanager.h actionmanager.cpp \ +lyxpipe.h lyxpipe.cpp \ +actionmanager.h actionmanager.cpp \ +openoffice.h openoffice.cpp \ +clipboard.h clipboard.cpp \ +handler.h + +OOO_SUBDIR = ooo + +if WITH_OOO +SUBDIRS = $(OOO_SUBDIR) +endif diff --git a/src/cite/actionmanager.cpp b/src/cite/actionmanager.cpp new file mode 100644 index 0000000..905f81a --- /dev/null +++ b/src/cite/actionmanager.cpp @@ -0,0 +1,85 @@ +/*************************************************************************** + copyright : (C) 2005-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 "actionmanager.h" +#include "lyxpipe.h" +#include "clipboard.h" +#include "openoffice.h" +#include "../entry.h" +#include "../tellico_debug.h" + +using Tellico::Cite::ActionManager; + +ActionManager::ActionManager* ActionManager::self() { + static ActionManager self; + return &self; +} + +ActionManager::ActionManager() : m_action(0) { +} + +ActionManager::~ActionManager() { + delete m_action; +} + +bool ActionManager::connect(CiteAction action_) { + if(m_action && m_action->type() == action_) { + return m_action->connect(); + } else if(m_action) { + delete m_action; + m_action = 0; + } + + switch(action_) { + case Cite::CiteClipboard: + m_action = new Clipboard(); + break; + + case Cite::CiteLyxpipe: + m_action = new Lyxpipe(); + break; + + case Cite::CiteOpenOffice: + m_action = new OpenOffice(); + break; + } + return m_action ? m_action->connect() : false; +} + +bool ActionManager::cite(CiteAction action_, Data::EntryVec entries_) { + if(entries_.isEmpty()) { + myDebug() << "ActionManager::cite() - no entries to cite" << endl; + return false; + } + if(m_action && m_action->type() != action_) { + delete m_action; + m_action = 0; + } + if(!m_action && !connect(action_)) { + myDebug() << "ActionManager::cite() - unable to connect" << endl; + return false; + } + if(!m_action) { + myDebug() << "ActionManager::cite() - no action found" << endl; + return false; + } + + return m_action->cite(entries_); +} + +bool ActionManager::isEnabled(CiteAction action_) { + if(action_ == CiteOpenOffice) { + return OpenOffice::hasLibrary(); + } + return true; +} diff --git a/src/cite/actionmanager.h b/src/cite/actionmanager.h new file mode 100644 index 0000000..fd6ffd1 --- /dev/null +++ b/src/cite/actionmanager.h @@ -0,0 +1,64 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_ACTIONMANAGER_H +#define TELLICO_CITE_ACTIONMANAGER_H + +#include "../datavectors.h" +#include "handler.h" + +namespace Tellico { + namespace Cite { + +enum CiteAction { + CiteClipboard, + CiteLyxpipe, + CiteOpenOffice +}; + +/** + * @author Robby Stephenson + */ +class Action { +public: + Action() {} + virtual ~Action() {} + + virtual CiteAction type() const = 0; + virtual bool connect() { return true; } + virtual bool cite(Data::EntryVec entries) = 0; + virtual State state() const { return Success; } +}; + +/** + * @author Robby Stephenson + */ +class ActionManager { +public: + static ActionManager* self(); + ~ActionManager(); + + bool cite(CiteAction action, Data::EntryVec entries); + static bool isEnabled(CiteAction action); + +private: + ActionManager(); + bool connect(CiteAction action); + + Action* m_action; +}; + + } +} + +#endif diff --git a/src/cite/clipboard.cpp b/src/cite/clipboard.cpp new file mode 100644 index 0000000..9b98b89 --- /dev/null +++ b/src/cite/clipboard.cpp @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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 "clipboard.h" +#include "../collection.h" +#include "../translators/bibtexhandler.h" + +#include <klocale.h> + +#include <qapplication.h> +#include <qclipboard.h> + +using Tellico::Cite::Clipboard; + +Clipboard::Clipboard() : Action() { +} + +bool Clipboard::cite(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return false; + } + + Data::CollPtr coll = entries_.front()->collection(); + if(!coll || coll->type() != Data::Collection::Bibtex) { + return false; + } + + QString s = QString::fromLatin1("\\cite{"); + for(Data::EntryVecIt it = entries_.begin(); it != entries_.end(); ++it) { + s += BibtexHandler::bibtexKey(it.data()); + if(!it.nextEnd()) { + s += QString::fromLatin1(", "); + } + } + s += '}'; + + QClipboard *cb = QApplication::clipboard(); + cb->setText(s, QClipboard::Clipboard); + return true; +} + diff --git a/src/cite/clipboard.h b/src/cite/clipboard.h new file mode 100644 index 0000000..153f02f --- /dev/null +++ b/src/cite/clipboard.h @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_CLIPBOARD_H +#define TELLICO_CITE_CLIPBOARD_H + +#include "actionmanager.h" + +namespace Tellico { + namespace Cite { + +/** + * @author Robby Stephenson +*/ +class Clipboard : public Action { +public: + Clipboard(); + + virtual CiteAction type() const { return CiteClipboard; } + virtual bool cite(Data::EntryVec entries); +}; + + } +} + +#endif diff --git a/src/cite/handler.h b/src/cite/handler.h new file mode 100644 index 0000000..d03a3e7 --- /dev/null +++ b/src/cite/handler.h @@ -0,0 +1,61 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_HANDLER_H +#define TELLICO_CITE_HANDLER_H + +#include <map> +#include <string> + +namespace Tellico { + namespace Cite { + +enum State { + NoConnection, + NoDocument, + NoCitation, + Success +}; + +typedef std::map<std::string, std::string> Map; + +/** + * @author Robby Stephenson + */ +class Handler { +public: + Handler() {} + virtual ~Handler() {} + + virtual bool connect() = 0; + virtual bool cite(Map& s) = 0; + virtual State state() const = 0; + + // really specific to ooo + void setHost(const char* host) { m_host = host; } + const std::string& host() const { return m_host; } + void setPort(int port) { m_port = port; } + int port() const { return m_port; } + void setPipe(const char* pipe) { m_pipe = pipe; } + const std::string& pipe() const { return m_pipe; } + +private: + std::string m_host; + int m_port; + std::string m_pipe; +}; + + } +} + +#endif diff --git a/src/cite/lyxpipe.cpp b/src/cite/lyxpipe.cpp new file mode 100644 index 0000000..6075324 --- /dev/null +++ b/src/cite/lyxpipe.cpp @@ -0,0 +1,92 @@ +/*************************************************************************** + copyright : (C) 2005-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 "lyxpipe.h" +#include "../collection.h" +#include "../translators/bibtexhandler.h" +#include "../tellico_kernel.h" +#include "../core/tellico_config.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +#include <qfile.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> + +using Tellico::Cite::Lyxpipe; + +Lyxpipe::Lyxpipe() : Action() { +} + +bool Lyxpipe::cite(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return false; + } + + Data::CollPtr coll = entries_.front()->collection(); + if(!coll || coll->type() != Data::Collection::Bibtex) { + myDebug() << "Lyxpipe::cite() - collection must be a bibliography!" << endl; + return false; + } + + QString lyxpipe = Config::lyxpipe(); + lyxpipe += QString::fromLatin1(".in"); +// myDebug() << "Lyxpipe::cite() - " << lyxpipe << endl; + + QString errorStr = i18n("<qt>Tellico is unable to write to the server pipe at <b>%1</b>.</qt>").arg(lyxpipe); + + QFile file(lyxpipe); + if(!file.exists()) { + Kernel::self()->sorry(errorStr); + return false; + } + + int pipeFd = ::open(QFile::encodeName(lyxpipe), O_WRONLY); + if(!file.open(IO_WriteOnly, pipeFd)) { + Kernel::self()->sorry(errorStr); + ::close(pipeFd); + return false; + } + + QString output; + QTextStream ts(&file); + for(Data::EntryVecIt it = entries_.begin(); it != entries_.end(); ++it) { + QString s = BibtexHandler::bibtexKey(it.data()); + if(s.isEmpty()) { + continue; + } + output += s; + if(!it.nextEnd() && !output.isEmpty()) { + // pybliographer uses comma-space, and pyblink expects the space there + output += QString::fromLatin1(", "); + } + } + if(output.isEmpty()) { + myDebug() << "Lyxpipe::cite() - no available bibtex keys!" << endl; + return false; + } + +// ts << "LYXSRV:tellico:hello\n"; + ts << "LYXCMD:tellico:citation-insert:"; + ts << output.local8Bit(); + ts << "\n"; +// ts << "LYXSRV:tellico:bye\n"; + file.flush(); + file.close(); + ::close(pipeFd); + return true; +} diff --git a/src/cite/lyxpipe.h b/src/cite/lyxpipe.h new file mode 100644 index 0000000..da738ae --- /dev/null +++ b/src/cite/lyxpipe.h @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson + email : $EMAIL + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_LYXPIPE_H +#define TELLICO_CITE_LYXPIPE_H + +#include "actionmanager.h" + +namespace Tellico { + namespace Cite { + +/** + * @author Robby Stephenson + */ +class Lyxpipe : public Action { +public: + Lyxpipe(); + + virtual CiteAction type() const { return CiteClipboard; } + virtual bool cite(Data::EntryVec entries); +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/cite/ooo/Makefile.am b/src/cite/ooo/Makefile.am new file mode 100644 index 0000000..ff11fca --- /dev/null +++ b/src/cite/ooo/Makefile.am @@ -0,0 +1,88 @@ +METASOURCES = AUTO + +KDE_CXXFLAGS = $(USE_EXCEPTIONS) +KDE_OPTIONS = noautodist + +COMID=gcc3 +CPPULIB=-luno_cppu +CPPUHELPERLIB=-luno_cppuhelper$(COMID) +SALLIB=-luno_sal +SALHELPERLIB=-luno_salhelper$(COMID) +# REGLIB=-lreg + +# Where the UNO includes will be generated +INCDIR = $(srcdir)/.include +UNODIR = $(INCDIR)/uno + +# OpenOffice.org additional includes and libraries +# might have to be adjusted for other architectures +OFFICE_includes = -I$(INCDIR) -I$(UNODIR) -DUNX -DGCC -DLINUX -DCPPU_ENV=$(COMID) -DOSL_DEBUG_LEVEL=0 +OFFICE_libraries = $(CPPULIB) $(CPPUHELPERLIB) $(SALLIB) $(SALHELPERLIB) + +AM_CPPFLAGS = $(all_includes) $(OFFICE_SDK_includes) $(OFFICE_includes) + +kde_module_LTLIBRARIES = tellico_ooo.la + +tellico_ooo_la_SOURCES = ooohandler.cpp interface.cpp +tellico_ooo_la_LDFLAGS = -module $(KDE_PLUGIN) $(KDE_RPATH) \ + $(all_libraries) $(OFFICE_libraries) +tellico_ooo_la_LIBADD = $(OFFICE_libadd) + +EXTRA_DIST = ooohandler.h ooohandler.cpp \ + interface.h interface.cpp + +CLEANFILES = *~ *.loT + +# Clean target for the generated stuff +clean-local: + rm -rf $(UNODIR) $(INCDIR) $(CLEANFILES) + +UNOTYPES := \ + com.sun.star.uno.XComponentContext \ + com.sun.star.lang.XMultiServiceFactory \ + com.sun.star.lang.XSingleComponentFactory \ + com.sun.star.lang.XComponent \ + com.sun.star.lang.XServiceInfo \ + com.sun.star.bridge.XUnoUrlResolver \ + com.sun.star.frame.XDesktop \ + com.sun.star.frame.XComponentLoader \ + com.sun.star.text.ControlCharacter \ + com.sun.star.text.XDocumentIndexesSupplier \ + com.sun.star.text.XDocumentIndex \ + com.sun.star.text.XTextDocument \ + com.sun.star.text.XTextField \ + com.sun.star.text.XTextViewCursor \ + com.sun.star.text.XTextViewCursorSupplier \ + com.sun.star.text.BibliographyDataType \ + com.sun.star.container.XIndexAccess \ + com.sun.star.container.XHierarchicalNameAccess \ + com.sun.star.registry.XSimpleRegistry \ + com.sun.star.beans.XPropertySet \ + com.sun.star.sdbc.XRow \ + com.sun.star.sdbc.XRowSet \ + com.sun.star.sdbc.XResultSetMetaDataSupplier \ + com.sun.star.sdbc.XResultSetUpdate \ + com.sun.star.sdbc.XRowUpdate \ + com.sun.star.sdbc.SQLException \ + com.sun.star.sdb.CommandType \ + com.sun.star.document.XEventListener \ + com.sun.star.document.XEventBroadcaster \ + com.sun.star.uno.XWeak \ + com.sun.star.uno.XAggregation \ + com.sun.star.lang.XTypeProvider + + +UNOHPPFILES = $(foreach t,$(UNOTYPES),$(UNODIR)/$(subst .,/,$(t)).hpp) + +interface.o: $(UNOHPPFILES) $(INCDIR) + +$(INCDIR): + mkdir -p $(INCDIR) + +$(UNODIR): + mkdir -p $(UNODIR) + +$(UNOHPPFILES): $(UNODIR) + $(CPPUMAKER) -Gc -BUCR -O$(UNODIR) $(foreach t,$(UNOTYPES),-T$(t)) \ + $(OFFICE_registry) + diff --git a/src/cite/ooo/interface.cpp b/src/cite/ooo/interface.cpp new file mode 100644 index 0000000..383ed1e --- /dev/null +++ b/src/cite/ooo/interface.cpp @@ -0,0 +1,430 @@ +/*************************************************************************** + copyright : (C) 2005-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 "interface.h" + +#include <cppuhelper/bootstrap.hxx> +#include <cppuhelper/implbase1.hxx> +#include <com/sun/star/bridge/XUnoUrlResolver.hpp> +#include <com/sun/star/lang/XServiceInfo.hpp> +#include <com/sun/star/frame/XComponentLoader.hpp> +#include <com/sun/star/frame/XDesktop.hpp> +#include <com/sun/star/container/XIndexAccess.hpp> +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/text/ControlCharacter.hpp> +#include <com/sun/star/text/XDocumentIndexesSupplier.hpp> +#include <com/sun/star/text/XDocumentIndex.hpp> +#include <com/sun/star/text/XTextField.hpp> +#include <com/sun/star/text/XTextViewCursorSupplier.hpp> +#include <com/sun/star/text/BibliographyDataType.hpp> +#include <com/sun/star/sdbc/XRowSet.hpp> +#include <com/sun/star/sdbc/XResultSetMetaDataSupplier.hpp> +#include <com/sun/star/sdbc/XResultSetUpdate.hpp> +#include <com/sun/star/sdbc/XRow.hpp> +#include <com/sun/star/sdbc/XRowUpdate.hpp> +#include <com/sun/star/sdbc/SQLException.hpp> +#include <com/sun/star/sdb/CommandType.hpp> +#include <com/sun/star/document/XEventListener.hpp> +#include <com/sun/star/document/XEventBroadcaster.hpp> + +#include <iostream> + +#define DEBUG(s) std::cout << s << std::endl +#define OUSTR(s) ::rtl::OUString(RTL_CONSTASCII_USTRINGPARAM(s)) +#define OU2O(s) OUStringToOString(s, RTL_TEXTENCODING_ASCII_US) +#define O2OU(s) OStringToOUString(s.c_str(), RTL_TEXTENCODING_UTF8) + +using Tellico::Cite::OOOHandler; + +using namespace com::sun::star; +using namespace com::sun::star::uno; +using namespace rtl; +using namespace cppu; + +namespace Tellico { + namespace Cite { + typedef cppu::WeakImplHelper1<document::XEventListener> EventListenerHelper; + } +} + +class OOOHandler::Interface::EventListener : public cppu::WeakImplHelper1<document::XEventListener> { +public: + EventListener(OOOHandler::Interface* i) : EventListenerHelper(), m_interface(i) {} + virtual void SAL_CALL disposing(const lang::EventObject&) throw(RuntimeException) { + DEBUG("Document is being disposed"); + m_interface->disconnect(); + } + virtual void SAL_CALL notifyEvent(const document::EventObject&) throw(RuntimeException) { +// std::cout << "Event: " << rtl::OUStringToOString(aEvent.EventName,RTL_TEXTENCODING_ISO_8859_1).getStr() << std::endl; + } +private: + OOOHandler::Interface* m_interface; +}; + +OOOHandler::Interface::Interface() : m_listener(0) { +} + +OOOHandler::Interface::~Interface() { + delete m_listener; + m_listener = 0; +} + +bool OOOHandler::Interface::isConnected() const { + return m_gsmgr.is(); +} + +bool OOOHandler::Interface::connect(const std::string& host_, int port_, const std::string& pipe_) { + if(isConnected()) { + return true; + } + + // create the initial component context + Reference<uno::XComponentContext> context; + try { + context = defaultBootstrap_InitialComponentContext(); + } catch(Exception& e) { + OString o = OUStringToOString(e.Message, RTL_TEXTENCODING_ASCII_US); + std::cout << "Unable to get initial component context: " << o << std::endl; + return false; + } catch(...) { + DEBUG("Unable to get initial component context."); + return false; + } + + // retrieve the servicemanager from the context + Reference<lang::XMultiComponentFactory> rServiceManager; + try { + rServiceManager = context->getServiceManager(); + } catch(...) { + DEBUG("Unable to get initial service manager."); + return false; + } + + // instantiate a sample service with the servicemanager. + OUString s = OUString::createFromAscii("com.sun.star.bridge.UnoUrlResolver"); + Reference<uno::XInterface> rInstance; + try { + rInstance = rServiceManager->createInstanceWithContext(s, context); + } catch(...) { + DEBUG("Unable to get initial instance."); + return false; + } + + // Query for the XUnoUrlResolver interface + Reference<bridge::XUnoUrlResolver> rResolver(rInstance, UNO_QUERY); + if(!rResolver.is()) { + DEBUG("Error: Couldn't instantiate com.sun.star.bridge.UnoUrlResolver service"); + return false; + } + + // "uno:socket,host=%s,port=%s;urp;StarOffice.ComponentContext"%(host,port) + // "uno:pipe,name=%s;urp;StarOffice.ComponentContext"%pipe + if(pipe_.empty()) { + s = OUSTR("socket,host=") + O2OU(host_) + OUSTR(",port=") + OUString::valueOf((sal_Int32)port_); + } else { + s = OUSTR("pipe,name=") + O2OU(pipe_); + } + std::cout << "Connection string: " << OU2O(s) << std::endl; + s = OUSTR("uno:") + s + OUSTR(";urp;StarOffice.ServiceManager"); + + try { + rInstance = rResolver->resolve(s); + if(!rInstance.is()) { + DEBUG("StarOffice.ServiceManager is not exported from remote counterpart"); + return false; + } + + m_gsmgr = Reference<lang::XMultiServiceFactory>(rInstance, UNO_QUERY); + if(m_gsmgr.is()) { + DEBUG("Connected successfully to office"); + } else { + DEBUG("XMultiServiceFactory interface is not exported"); + } + } catch(Exception& e) { + std::cout << "Error: " << OU2O(e.Message) << std::endl; + } catch(...) { + DEBUG("Unable to resolve connection."); + return false; + } + return m_gsmgr.is(); +} + +bool OOOHandler::Interface::disconnect() { + m_gsmgr = 0; + m_dsmgr = 0; + m_doc = 0; + m_bib = 0; + m_cursor = 0; + return true; +} + +bool OOOHandler::Interface::createDocument() { + if(!m_gsmgr.is()) { + return false; + } + + if(m_doc.is()) { + return true; + } + + // get the desktop service using createInstance, returns an XInterface type + Reference<uno::XInterface> xInstance = m_gsmgr->createInstance(OUString::createFromAscii("com.sun.star.frame.Desktop")); + Reference<frame::XDesktop> desktop(xInstance, UNO_QUERY); + + Reference<lang::XComponent> writer = desktop->getCurrentComponent(); + Reference<lang::XServiceInfo> info(writer, UNO_QUERY); + if(info.is() && info->getImplementationName() == OUString::createFromAscii("SwXTextDocument")) { + DEBUG("Document already open"); + } else { + DEBUG("Opening a new document"); + //query for the XComponentLoader interface + Reference<frame::XComponentLoader> rComponentLoader(desktop, UNO_QUERY); + if(!rComponentLoader.is()){ + DEBUG("XComponentloader failed to instantiate"); + return 0; + } + + //get an instance of the OOowriter document + writer = rComponentLoader->loadComponentFromURL(OUSTR("private:factory/swriter"), + OUSTR("_default"), + 0, + Sequence<beans::PropertyValue>()); + } + + //Manage many events with EventListener + Reference<document::XEventBroadcaster> eventBroadcast(writer, UNO_QUERY); + m_listener = new EventListener(this); + Reference<document::XEventListener> xEventListener = static_cast<document::XEventListener*>(m_listener); + eventBroadcast->addEventListener(xEventListener); + + Reference<frame::XController> controller = Reference<frame::XModel>(writer, UNO_QUERY)->getCurrentController(); + m_cursor = Reference<text::XTextViewCursorSupplier>(controller, UNO_QUERY)->getViewCursor(); + m_doc = Reference<text::XTextDocument>(writer, UNO_QUERY); + if(m_doc.is()) { + m_dsmgr = Reference<lang::XMultiServiceFactory>(m_doc, UNO_QUERY); + } + return m_doc.is(); +} + +bool OOOHandler::Interface::updateBibliography() { + if(!m_bib.is()) { + createBibliography(); + if(!m_bib.is()) { + DEBUG("ERROR: could not create or find bibliography index"); + return false; + } + } + + m_bib->update(); + return true; +} + +void OOOHandler::Interface::createBibliography() { + Reference<container::XIndexAccess> indexes(Reference<text::XDocumentIndexesSupplier>(m_doc, UNO_QUERY)->getDocumentIndexes(), UNO_QUERY); + for(int i = 0; i < indexes->getCount(); ++i) { + Reference<lang::XServiceInfo> info(indexes->getByIndex(i), UNO_QUERY); + if(info->supportsService(OUSTR("com.sun.star.text.Bibliography"))) { + DEBUG("Found existing bibliography..."); + m_bib = Reference<text::XDocumentIndex>(indexes->getByIndex(i), UNO_QUERY); + break; + } + } + + if(!m_bib.is()) { + DEBUG("Creating new bibliography..."); + Reference<text::XText> text = m_doc->getText(); + Reference<text::XTextRange> textRange(text->createTextCursor(), UNO_QUERY); + Reference<text::XTextCursor> cursor(textRange, UNO_QUERY); + cursor->gotoEnd(false); + text->insertControlCharacter(textRange, text::ControlCharacter::PARAGRAPH_BREAK, false); + m_bib = Reference<text::XDocumentIndex>(m_dsmgr->createInstance(OUSTR("com.sun.star.text.Bibliography")), UNO_QUERY); + Reference<text::XTextContent> textContent(m_bib, UNO_QUERY); + text->insertTextContent(textRange, textContent, false); + } +} + +bool OOOHandler::Interface::insertCitations(Cite::Map& fields) { + Reference<text::XTextField> entry(m_dsmgr->createInstance(OUString::createFromAscii("com.sun.star.text.TextField.Bibliography")), UNO_QUERY); + if(!entry.is()) { + DEBUG("Interface::insertCitation - can't create TextField"); + return false; + } + Sequence<beans::PropertyValue> values(fields.size()); + int i = 0; + for(Cite::Map::iterator it = fields.begin(); it != fields.end(); ++it, ++i) { + values[i] = propValue(it->first, it->second); + std::cout << "Setting " << OU2O(values[i].Name) << " = " << it->second << std::endl; + } + Reference<beans::XPropertySet>(entry, UNO_QUERY)->setPropertyValue(OUSTR("Fields"), Any(values)); + + Reference<text::XText> text = m_doc->getText(); + Reference<text::XTextCursor> cursor = text->createTextCursorByRange(Reference<text::XTextRange>(m_cursor, UNO_QUERY)); + Reference<text::XTextRange> textRange(cursor, UNO_QUERY); + Reference<text::XTextContent> textContent(entry, UNO_QUERY); + text->insertTextContent(textRange, textContent, false); + return true; +} + +beans::PropertyValue OOOHandler::Interface::propValue(const std::string& field, const std::string& value) { + return beans::PropertyValue(O2OU(field), 0, fieldValue(field, value), beans::PropertyState_DIRECT_VALUE); +} + +uno::Any OOOHandler::Interface::fieldValue(const std::string& field, const std::string& value) { + if(field == "BibiliographicType" || field == "BibliographicType") { // in case the typo gets fixed + return typeValue(value); + } + return Any(O2OU(value)); +} + +uno::Any OOOHandler::Interface::typeValue(const std::string& value) { + if(value == "article") { + return Any(text::BibliographyDataType::ARTICLE); + } else if(value == "book") { + return Any(text::BibliographyDataType::BOOK); + } else if(value == "booklet") { + return Any(text::BibliographyDataType::BOOKLET); + } else if(value == "conference") { + return Any(text::BibliographyDataType::CONFERENCE); + } else if(value == "inbook") { + return Any(text::BibliographyDataType::INBOOK); + } else if(value == "incollection") { + return Any(text::BibliographyDataType::INCOLLECTION); + } else if(value == "inproceedings") { + return Any(text::BibliographyDataType::INPROCEEDINGS); + } else if(value == "journal") { + return Any(text::BibliographyDataType::JOURNAL); + } else if(value == "manual") { + return Any(text::BibliographyDataType::MANUAL); + } else if(value == "mastersthesis") { + return Any(text::BibliographyDataType::MASTERSTHESIS); + } else if(value == "misc") { + return Any(text::BibliographyDataType::MISC); + } else if(value == "phdthesis") { + return Any(text::BibliographyDataType::PHDTHESIS); + } else if(value == "proceedings") { + return Any(text::BibliographyDataType::PROCEEDINGS); + } else if(value == "techreport") { + return Any(text::BibliographyDataType::TECHREPORT); + } else if(value == "unpublished") { + return Any(text::BibliographyDataType::UNPUBLISHED); + } else { + // periodical ? + return Any(text::BibliographyDataType::BOOK); + } +} + +bool OOOHandler::Interface::insertRecords(Cite::Map& fields) { + Reference<uno::XInterface> interface; + try { + interface = m_gsmgr->createInstance(OUString::createFromAscii("com.sun.star.sdb.RowSet")); + } catch(Exception& e) { + std::cout << "Error: " << OU2O(e.Message) << std::endl; + } + if(!interface.is()) { + DEBUG("Could not create rowset interface"); + return false; + } + + Reference<sdbc::XRowSet> rowSet(interface, UNO_QUERY); + if(!rowSet.is()) { + DEBUG("Could not create rowset interface"); + return false; + } + + Reference<beans::XPropertySet> props(rowSet, UNO_QUERY); + props->setPropertyValue(OUSTR("DataSourceName"), Any(OUSTR("Bibliography"))); + props->setPropertyValue(OUSTR("CommandType"), Any(sdb::CommandType::COMMAND)); + OUString s = OUSTR("SELECT COUNT(*) FROM \"biblio\" WHERE identifier='") + O2OU(fields["Identifier"]) + OUSTR("'"); + props->setPropertyValue(OUSTR("Command"), Any(s)); + + try { + rowSet->execute(); + } catch(sdbc::SQLException& e) { + DEBUG(OU2O(s)); + DEBUG(OUSTR("SQL error - ") + e.SQLState); + return false; + } catch(Exception& e) { + DEBUG(OU2O(s)); + DEBUG(OUSTR("General error - ") + e.Message); + return false; + } + + Reference<sdbc::XRow> row(rowSet, UNO_QUERY); + int count = 0; + if(rowSet->next()) { + count = row->getString(1).toInt32(); + } + + if(count > 0) { + DEBUG("Found existing bibliographic entries, updating..."); + } else { + DEBUG("Inserting new bibliographic entry..."); + } + + s = OUSTR("SELECT * FROM \"biblio\""); + if(count > 0) { + s += OUSTR(" WHERE identifier='") + O2OU(fields["Identifier"]) + OUSTR("'"); + } + props->setPropertyValue(OUSTR("Command"), Any(s)); + + try { + rowSet->execute(); + } catch(sdbc::SQLException& e) { + DEBUG(OU2O(s)); + DEBUG(OUSTR("SQL error(2) - ") + e.SQLState); + return false; + } catch(Exception& e) { + DEBUG(OU2O(s)); + DEBUG(OUSTR("General error(2) - ") + e.Message); + return false; + } + + Reference<sdbc::XResultSet> resultSet(rowSet, UNO_QUERY); + Reference<sdbc::XResultSetMetaDataSupplier> mdSupplier(resultSet, UNO_QUERY); + Reference<sdbc::XResultSetMetaData> metaData = mdSupplier->getMetaData(); + + Reference<sdbc::XRowUpdate> rowUpdate(rowSet, UNO_QUERY); + Reference<sdbc::XResultSetUpdate> update(rowSet, UNO_QUERY); + if(count > 0) { + resultSet->last(); + } else { + update->moveToInsertRow(); + } + + const long colCount = metaData->getColumnCount(); + // column numbers start with 1 + for(long i = 1; i <= colCount; ++i) { + std::string s = OU2O(metaData->getColumnName(i)).getStr(); + std::string value = fields[s]; + if(!value.empty()) { + std::cout << "column " << i << ": " << OU2O(metaData->getColumnName(i)) << "..." << std::endl; + std::cout << s << " = " << value << std::endl; + try { + rowUpdate->updateString(i, O2OU(value)); + } catch(sdbc::SQLException& e) { + DEBUG(OUSTR("SQL error(3) - ") + e.SQLState); + } catch(Exception& e) { + DEBUG(OUSTR("General error(3) - ") + e.Message); + } + } + } + if(count > 0) { + update->updateRow(); + } else { + update->insertRow(); + } + + Reference<lang::XComponent>(rowSet, UNO_QUERY)->dispose(); + return true; +} diff --git a/src/cite/ooo/interface.h b/src/cite/ooo/interface.h new file mode 100644 index 0000000..c1a0f33 --- /dev/null +++ b/src/cite/ooo/interface.h @@ -0,0 +1,62 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_OOOHANDLER_INTERFACE_H +#define TELLICO_CITE_OOOHANDLER_INTERFACE_H + +#include "ooohandler.h" + +#include <com/sun/star/lang/XMultiServiceFactory.hpp> +#include <com/sun/star/text/XTextDocument.hpp> +#include <com/sun/star/text/XDocumentIndex.hpp> +#include <com/sun/star/text/XTextViewCursor.hpp> + +namespace Tellico { + namespace Cite { + +class OOOHandler::Interface { + friend class OOOHandler; + + Interface(); + ~Interface(); + + bool isConnected() const; + bool connect(const std::string& host, int port, const std::string& pipe); + bool disconnect(); + bool createDocument(); + bool updateBibliography(); + bool insertCitations(Cite::Map& fields); + bool insertRecords(Cite::Map& fields); + +private: + void createBibliography(); + com::sun::star::beans::PropertyValue propValue(const std::string& field, const std::string& value); + com::sun::star::uno::Any fieldValue(const std::string& field, const std::string& value); + com::sun::star::uno::Any typeValue(const std::string& value); + + // global service manager + com::sun::star::uno::Reference<com::sun::star::lang::XMultiServiceFactory> m_gsmgr; + // document service manager + com::sun::star::uno::Reference<com::sun::star::lang::XMultiServiceFactory> m_dsmgr; + com::sun::star::uno::Reference<com::sun::star::text::XTextDocument> m_doc; + com::sun::star::uno::Reference<com::sun::star::text::XDocumentIndex> m_bib; + com::sun::star::uno::Reference<com::sun::star::text::XTextViewCursor> m_cursor; + + class EventListener; + EventListener* m_listener; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/cite/ooo/ooohandler.cpp b/src/cite/ooo/ooohandler.cpp new file mode 100644 index 0000000..1741896 --- /dev/null +++ b/src/cite/ooo/ooohandler.cpp @@ -0,0 +1,159 @@ +/*************************************************************************** + copyright : (C) 2005-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 "ooohandler.h" +#include "interface.h" + +#include <iostream> + +extern "C" { + Tellico::Cite::Handler* handler() { + return new Tellico::Cite::OOOHandler(); + } +} + +using Tellico::Cite::OOOHandler; +Tellico::Cite::Map OOOHandler::s_fieldsMap; + +/* +QString OOOHandler::OUString2Q(const rtl::OUString& str) { + const uint len = str.getLength(); + QChar* uni = new QChar[len + 1]; + + const sal_Unicode* ouPtr = str.getStr(); + const sal_Unicode* ouEnd = ouPtr + len; + QChar* qPtr = uni; + + while (ouPtr != ouEnd) { + *(qPtr++) = *(ouPtr++); + } + + *qPtr = 0; + + QString ret(uni, len); + delete[] uni; + + return ret; +} + +rtl::OUString OOOHandler::QString2OU(const QString& str) { + const uint len = str.length(); + sal_Unicode* uni = new sal_Unicode[len + 1]; + + const QChar* qPtr = str.unicode(); + const QChar* qEnd = qPtr + len; + sal_Unicode* uPtr = uni; + + while (qPtr != qEnd) { + *(uPtr++) = (*(qPtr++)).unicode(); + } + + *uPtr = 0; + + rtl::OUString ret(uni, len); + delete[] uni; + + return ret; +} +*/ + +void OOOHandler::buildFieldsMap() { +// s_fieldsMap["entry-type"] = "BibliographicType"; + s_fieldsMap["entry-type"] = "BibiliographicType"; // typo in OpenOffice + s_fieldsMap["key"] = "Identifier"; + s_fieldsMap["title"] = "Title"; + s_fieldsMap["author"] = "Author"; + s_fieldsMap["booktitle"] = "Booktitle"; + s_fieldsMap["address"] = "Address"; + s_fieldsMap["chapter"] = "Chapter"; + s_fieldsMap["edition"] = "Edition"; + s_fieldsMap["editor"] = "Editor"; + s_fieldsMap["organization"] = "Organizations"; // OOO has an 's' + s_fieldsMap["publisher"] = "Publisher"; + s_fieldsMap["pages"] = "Pages"; + s_fieldsMap["howpublished"] = "Howpublished"; + s_fieldsMap["institution"] = "Institution"; + s_fieldsMap["journal"] = "Journal"; + s_fieldsMap["month"] = "Month"; + s_fieldsMap["number"] = "Number"; + s_fieldsMap["note"] = "Note"; + s_fieldsMap["annote"] = "Annote"; + s_fieldsMap["series"] = "Series"; + s_fieldsMap["volume"] = "Volume"; + s_fieldsMap["year"] = "Year"; + s_fieldsMap["url"] = "URL"; + s_fieldsMap["isbn"] = "ISBN"; +} + +OOOHandler::OOOHandler() : Handler(), m_interface(0), m_state(NoConnection) { +} + +Tellico::Cite::State OOOHandler::state() const { + // possibly the write got closed underneath us + if(m_state != NoConnection && m_interface && !m_interface->isConnected()) { + m_state = NoConnection; + } + return m_state; +} + +bool OOOHandler::connect() { + if(!m_interface) { + m_interface = new Interface(); + } + + if(!m_interface->connect(host(), port(), pipe())) { + return false; + } + + if(!m_interface->createDocument()) { + m_state = NoDocument; + return false; + } + + m_state = NoCitation; + return true; +} + +bool OOOHandler::cite(Map& fields) { + if(!m_interface && !connect()) { + return false; + } + Cite::Map newFields = convertFields(fields); + // the ooo interface can insert records in the database, but the citations in the + // document ARE NOT linked to them, meaning they aren't updated by changing the database + // the user has to manually edit each entry + bool success = m_interface->insertCitations(newFields) && m_interface->updateBibliography(); +// bool success = m_interface->insertRecords(newFields); + if(success) { + m_state = Success; +// m_interface->disconnect(); +// m_state = NoConnection; + } + return success; +} + +Tellico::Cite::Map OOOHandler::convertFields(Cite::Map& fields) { + if(s_fieldsMap.empty()) { + buildFieldsMap(); + } + + Cite::Map values; + for(Cite::Map::iterator it = s_fieldsMap.begin(); it != s_fieldsMap.end(); ++it) { + std::string value = fields[it->first]; + if(!value.empty()) { + values[it->second] = value; + } + } + + return values; +} diff --git a/src/cite/ooo/ooohandler.h b/src/cite/ooo/ooohandler.h new file mode 100644 index 0000000..fd7f308 --- /dev/null +++ b/src/cite/ooo/ooohandler.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_OOOHANDLER_H +#define TELLICO_CITE_OOOHANDLER_H + +#include "../handler.h" + +namespace rtl { + class OUString; +} + +namespace Tellico { + namespace Cite { + +/** + * @author Robby Stephenson + */ +class OOOHandler : public Handler { +public: + OOOHandler(); + + virtual State state() const; + virtual bool connect(); + virtual bool cite(Cite::Map& fields); + +private: +// static QString OUString2Q(const rtl::OUString& str); +// static rtl::OUString QString2OU(const QString& str); + static Cite::Map s_fieldsMap; + static void buildFieldsMap(); + + Cite::Map convertFields(Cite::Map& values); + + class Interface; + Interface* m_interface; + // mutable since I want to change it inside state() + mutable State m_state; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/cite/openoffice.cpp b/src/cite/openoffice.cpp new file mode 100644 index 0000000..ced8b13 --- /dev/null +++ b/src/cite/openoffice.cpp @@ -0,0 +1,267 @@ +/*************************************************************************** + copyright : (C) 2005-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 "openoffice.h" +#include "handler.h" +#include "../collections/bibtexcollection.h" +#include "../entry.h" +#include "../tellico_utils.h" +#include "../latin1literal.h" +#include "../statusbar.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" + +#include <klibloader.h> +#include <kdialogbase.h> +#include <knuminput.h> +#include <kapplication.h> +#include <kconfig.h> +#include <klineedit.h> +#include <kiconloader.h> +#include <klocale.h> + +#include <qiconset.h> +#include <qlayout.h> +#include <qradiobutton.h> +#include <qbuttongroup.h> +#include <qfile.h> +#include <qvgroupbox.h> + +using Tellico::Cite::OpenOffice; + +class OpenOffice::Private { + friend class OpenOffice; + + Private() : handler(0), port(-1) { + KLibrary* library = Tellico::openLibrary(QString::fromLatin1("tellico_ooo")); + if(library) { + void* func = library->symbol("handler"); + if(func) { + handler = ((Handler* (*)())func)(); + } + } + } + + Handler* handler; + // empty pipe string indicates tcp should be used + QString host; + int port; + QString pipe; +}; + +OpenOffice::OpenOffice() : Action(), d(new Private()) { +} + +OpenOffice::~OpenOffice() { + delete d; +} + +Tellico::Cite::State OpenOffice::state() const { + if(!d->handler) { + return NoConnection; + } + return d->handler->state(); +} + +bool OpenOffice::connect() { + if(!d->handler) { + myDebug() << "OpenOffice::connect() - unable to open OpenOffice.org plugin" << endl; + return false; + } + + StatusBar::self()->setStatus(i18n("Connecting to OpenOffice.org...")); + + if(d->port == -1) { + KConfigGroup config(kapp->config(), "OpenOffice.org"); + d->host = config.readEntry("Host", QString::fromLatin1("localhost")); + d->port = config.readNumEntry("Port", 2083); + d->pipe = config.readPathEntry("Pipe"); + // the ooohandler will depend on pipe.isEmpty() to indicate the port should be used + d->handler->setHost(d->host); + d->handler->setPort(d->port); + if(!d->pipe.isEmpty()) { + d->handler->setPipe(QFile::encodeName(d->pipe)); + } + } + + bool success = d->handler->connect(); + bool needInput = !success; + while(needInput) { + switch(state()) { + case NoConnection: + myDebug() << "OpenOffice::connect() - NoConnection" << endl; + // try to reconnect + if(connectionDialog()) { + success = d->handler->connect(); + needInput = !success; + } else { + needInput = false; + } + break; + case NoDocument: + myDebug() << "OpenOffice::connect() - NoDocument" << endl; + break; + default: + myDebug() << "OpenOffice::connect() - weird state" << endl; + break; + } + } + StatusBar::self()->clearStatus(); + return success; +} + +bool OpenOffice::cite(Data::EntryVec entries_) { + if(!connect()) { + myDebug() << "OpenOffice::cite() - can't connect to OpenOffice" << endl; + return false; + } + if(entries_.isEmpty()) { + myDebug() << "OpenOffice::cite() - no entries" << endl; + return false; + } + + Data::CollPtr coll = entries_.front()->collection(); + if(!coll || coll->type() != Data::Collection::Bibtex) { + myDebug() << "OpenOffice::cite() - not a bibtex collection" << endl; + return false; + } + + const QString bibtex = QString::fromLatin1("bibtex"); + Data::FieldVec vec = coll->fields(); + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + Cite::Map values; + for(Data::FieldVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + QString bibtexField = it->property(bibtex); + if(!bibtexField.isEmpty()) { + QString s = entry->field(it->name()); + if(!s.isEmpty()) { + values[bibtexField] = s.utf8(); // always utf8 + } + } else if(it->name() == Latin1Literal("isbn")) { + // OOO includes ISBN + QString s = entry->field(it->name()); + if(!s.isEmpty()) { + values[it->name()] = s.utf8(); + } + } + } + d->handler->cite(values); + } + return true; +} + +bool OpenOffice::connectionDialog() { + KDialogBase dlg(Kernel::self()->widget(), "ooo connection dialog", + true, i18n("OpenOffice.org Connection"), + KDialogBase::Ok|KDialogBase::Cancel|KDialogBase::Help); + + dlg.setHelp(QString::fromLatin1("openoffice-org")); + + QWidget* widget = new QWidget(&dlg); + QBoxLayout* topLayout = new QVBoxLayout(widget, KDialog::spacingHint()); + dlg.setMainWidget(widget); + // is there a better way to do a multi-line label than to insert newlines in the text? + QBoxLayout* blay = new QHBoxLayout(topLayout); + QLabel* l = new QLabel(widget); + l->setPixmap(DesktopIcon(QString::fromLatin1("ooo_writer"), 64)); + blay->addWidget(l); + l = new QLabel(widget); + l->setText(i18n("Tellico was unable to connect to OpenOffice.org. " + "Please verify the connection settings below, and " + "that OpenOffice.org Writer is currently running.")); + l->setTextFormat(Qt::RichText); + blay->addWidget(l); + blay->setStretchFactor(l, 10); + + QVGroupBox* gbox = new QVGroupBox(i18n("OpenOffice.org Connection"), widget); + topLayout->addWidget(gbox); + + QWidget* w2 = new QWidget(gbox); + QGridLayout* gl = new QGridLayout(w2, 2, 3, 0 /*margin*/, KDialog::spacingHint()); + QRadioButton* radioPipe = new QRadioButton(i18n("Pipe"), w2); + gl->addWidget(radioPipe, 0, 0); + QRadioButton* radioTCP = new QRadioButton(i18n("TCP/IP"), w2); + gl->addWidget(radioTCP, 1, 0); + QButtonGroup* btnGroup = new QButtonGroup(); + btnGroup->setRadioButtonExclusive(true); + btnGroup->insert(radioPipe, 0); + btnGroup->insert(radioTCP, 1); + + KLineEdit* pipeEdit = new KLineEdit(w2); + pipeEdit->setText(d->pipe); + gl->addMultiCellWidget(pipeEdit, 0, 0, 1, 2); + pipeEdit->connect(radioPipe, SIGNAL(toggled(bool)), SLOT(setEnabled(bool))); + + KLineEdit* hostEdit = new KLineEdit(w2); + hostEdit->setText(d->host); + gl->addWidget(hostEdit, 1, 1); + hostEdit->connect(radioTCP, SIGNAL(toggled(bool)), SLOT(setEnabled(bool))); + KIntSpinBox* portSpin = new KIntSpinBox(w2); + portSpin->setMaxValue(99999); + portSpin->setValue(d->port); + gl->addWidget(portSpin, 1, 2); + portSpin->connect(radioTCP, SIGNAL(toggled(bool)), SLOT(setEnabled(bool))); + + if(d->pipe.isEmpty()) { + radioTCP->setChecked(true); + hostEdit->setEnabled(true); + portSpin->setEnabled(true); + pipeEdit->setEnabled(false); + } else { + radioPipe->setChecked(true); + hostEdit->setEnabled(false); + portSpin->setEnabled(false); + pipeEdit->setEnabled(true); + } + topLayout->addStretch(10); + + if(dlg.exec() != QDialog::Accepted) { + return false; + } + + int p = d->port; + QString h = d->host; + QString s; + if(radioTCP->isChecked()) { + h = hostEdit->text(); + if(h.isEmpty()) { + h = QString::fromLatin1("localhost"); + } + p = portSpin->value(); + } else { + s = pipeEdit->text(); + } + d->host = h; + d->port = p; + d->pipe = s; + + if(!d->host.isEmpty()) { + d->handler->setHost(d->host); + } + d->handler->setPort(d->port); + if(!d->pipe.isEmpty()) { + d->handler->setPipe(QFile::encodeName(d->pipe)); + } + + KConfigGroup config(kapp->config(), "OpenOffice.org"); + config.writeEntry("Host", d->host); + config.writeEntry("Port", d->port); + config.writePathEntry("Pipe", d->pipe); + return true; +} + +bool OpenOffice::hasLibrary() { + QString path = KLibLoader::findLibrary("tellico_ooo"); + if(!path.isEmpty()) myDebug() << "OpenOffice::hasLibrary() - Found OOo plugin: " << path << endl; + return !path.isEmpty(); +} diff --git a/src/cite/openoffice.h b/src/cite/openoffice.h new file mode 100644 index 0000000..a5204ac --- /dev/null +++ b/src/cite/openoffice.h @@ -0,0 +1,47 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_CITE_OPENOFFICE_H +#define TELLICO_CITE_OPENOFFICE_H + +#include "actionmanager.h" + +namespace Tellico { + namespace Cite { + +/** + * @author Robby Stephenson + */ +class OpenOffice : public Action { +public: + OpenOffice(); + ~OpenOffice(); + + virtual CiteAction type() const { return CiteOpenOffice; } + + virtual bool connect(); + virtual bool cite(Data::EntryVec entries); + virtual State state() const; + + static bool hasLibrary(); + +private: + bool connectionDialog(); + class Private; + Private* d; +}; + + } +} + +#endif diff --git a/src/collection.cpp b/src/collection.cpp new file mode 100644 index 0000000..c6f77ca --- /dev/null +++ b/src/collection.cpp @@ -0,0 +1,915 @@ +/*************************************************************************** + 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 "collection.h" +#include "field.h" +#include "entry.h" +#include "tellico_debug.h" +#include "latin1literal.h" +#include "tellico_utils.h" +#include "controller.h" +#include "collectionfactory.h" +#include "stringset.h" +#include "tellico_kernel.h" + +#include <klocale.h> + +#include <qregexp.h> +#include <qvaluestack.h> + +using Tellico::Data::Collection; + +const char* Collection::s_emptyGroupTitle = I18N_NOOP("(Empty)"); +const QString Collection::s_peopleGroupName = QString::fromLatin1("_people"); + +Collection::Collection(const QString& title_) + : QObject(), KShared(), m_nextEntryId(0), m_title(title_), m_entryIdDict(997) + , m_trackGroups(false) { + m_entryGroupDicts.setAutoDelete(true); + + m_id = getID(); +} + +Collection::~Collection() { +} + +QString Collection::typeName() const { + return CollectionFactory::typeName(type()); +} + +bool Collection::addFields(FieldVec list_) { + bool success = true; + for(FieldVec::Iterator it = list_.begin(); it != list_.end(); ++it) { + success &= addField(it); + } + return success; +} + +bool Collection::addField(FieldPtr field_) { + if(!field_) { + return false; + } + + // this essentially checks for duplicates + if(hasField(field_->name())) { + myDebug() << "Collection::addField() - replacing " << field_->name() << endl; + removeField(fieldByName(field_->name()), true); + } + + // it's not sufficient to merely check the new field + if(dependentFieldHasRecursion(field_)) { + field_->setDescription(QString()); + } + + m_fields.append(field_); + if(field_->formatFlag() == Field::FormatName) { + m_peopleFields.append(field_); // list of people attributes + if(m_peopleFields.count() > 1) { + // the second time that a person field is added, add a "pseudo-group" for people + if(m_entryGroupDicts.find(s_peopleGroupName) == 0) { + EntryGroupDict* d = new EntryGroupDict(); + d->setAutoDelete(true); + m_entryGroupDicts.insert(s_peopleGroupName, d); + m_entryGroups.prepend(s_peopleGroupName); + } + } + } + m_fieldNameDict.insert(field_->name(), field_); + m_fieldTitleDict.insert(field_->title(), field_); + m_fieldNames << field_->name(); + m_fieldTitles << field_->title(); + if(field_->type() == Field::Image) { + m_imageFields.append(field_); + } + + if(!field_->category().isEmpty() && m_fieldCategories.findIndex(field_->category()) == -1) { + m_fieldCategories << field_->category(); + } + + if(field_->flags() & Field::AllowGrouped) { + // m_entryGroupsDicts autoDeletes each QDict when the Collection d'tor is called + EntryGroupDict* dict = new EntryGroupDict(); + dict->setAutoDelete(true); + m_entryGroupDicts.insert(field_->name(), dict); + // cache the possible groups of entries + m_entryGroups << field_->name(); + } + + if(m_defaultGroupField.isEmpty() && field_->flags() & Field::AllowGrouped) { + m_defaultGroupField = field_->name(); + } + + // refresh all dependent fields, in case one references this new one + for(FieldVec::Iterator it = m_fields.begin(); it != m_fields.end(); ++it) { + if(it->type() == Field::Dependent) { + emit signalRefreshField(it); + } + } + + return true; +} + +bool Collection::mergeField(FieldPtr newField_) { + if(!newField_) { + return false; + } + + FieldPtr currField = fieldByName(newField_->name()); + if(!currField) { + // does not exist in current collection, add it + Data::FieldPtr f = new Field(*newField_); + bool success = addField(f); + Controller::self()->addedField(this, f); + return success; + } + + if(newField_->type() == Field::Table2) { + newField_->setType(Data::Field::Table); + newField_->setProperty(QString::fromLatin1("columns"), QChar('2')); + } + + // the original field type is kept + if(currField->type() != newField_->type()) { + myDebug() << "Collection::mergeField() - skipping, field type mismatch for " << currField->title() << endl; + return false; + } + + // if field is a Choice, then make sure all values are there + if(currField->type() == Field::Choice && currField->allowed() != newField_->allowed()) { + QStringList allowed = currField->allowed(); + const QStringList& newAllowed = newField_->allowed(); + for(QStringList::ConstIterator it = newAllowed.begin(); it != newAllowed.end(); ++it) { + if(allowed.findIndex(*it) == -1) { + allowed.append(*it); + } + } + currField->setAllowed(allowed); + } + + // don't change original format flags + // don't change original category + // add new description if current is empty + if(currField->description().isEmpty()) { + currField->setDescription(newField_->description()); + if(dependentFieldHasRecursion(currField)) { + currField->setDescription(QString()); + } + } + + // if new field has additional extended properties, add those + for(StringMap::ConstIterator it = newField_->propertyList().begin(); it != newField_->propertyList().end(); ++it) { + const QString propName = it.key(); + const QString currValue = currField->property(propName); + if(currValue.isEmpty()) { + currField->setProperty(propName, it.data()); + } else if (it.data() != currValue) { + if(currField->type() == Field::URL && propName == Latin1Literal("relative")) { + kdWarning() << "Collection::mergeField() - relative URL property does not match for " << currField->name() << endl; + } else if((currField->type() == Field::Table && propName == Latin1Literal("columns")) + || (currField->type() == Field::Rating && propName == Latin1Literal("maximum"))) { + bool ok; + uint currNum = Tellico::toUInt(currValue, &ok); + uint newNum = Tellico::toUInt(it.data(), &ok); + if(newNum > currNum) { // bigger values + currField->setProperty(propName, QString::number(newNum)); + } + } else if(currField->type() == Field::Rating && propName == Latin1Literal("minimum")) { + bool ok; + uint currNum = Tellico::toUInt(currValue, &ok); + uint newNum = Tellico::toUInt(it.data(), &ok); + if(newNum < currNum) { // smaller values + currField->setProperty(propName, QString::number(newNum)); + } + } + } + } + + // combine flags + currField->setFlags(currField->flags() | newField_->flags()); + return true; +} + +// be really careful with these field pointers, try not to call too many other functions +// which may depend on the field list +bool Collection::modifyField(FieldPtr newField_) { + if(!newField_) { + return false; + } +// myDebug() << "Collection::modifyField() - " << newField_->name() << endl; + +// the field name never changes + const QString fieldName = newField_->name(); + FieldPtr oldField = fieldByName(fieldName); + if(!oldField) { + myDebug() << "Collection::modifyField() - no field named " << fieldName << endl; + return false; + } + + // update name dict + m_fieldNameDict.replace(fieldName, newField_); + + // update titles + const QString oldTitle = oldField->title(); + const QString newTitle = newField_->title(); + if(oldTitle == newTitle) { + m_fieldTitleDict.replace(newTitle, newField_); + } else { + m_fieldTitleDict.remove(oldTitle); + m_fieldTitles.remove(oldTitle); + m_fieldTitleDict.insert(newTitle, newField_); + m_fieldTitles.append(newTitle); + } + + // now replace the field pointer in the list + FieldVec::Iterator it = m_fields.find(oldField); + if(it != m_fields.end()) { + m_fields.insert(it, newField_); + m_fields.remove(oldField); + } else { + myDebug() << "Collection::modifyField() - no index found!" << endl; + return false; + } + + // update category list. + if(oldField->category() != newField_->category()) { + m_fieldCategories.clear(); + for(FieldVec::Iterator it = m_fields.begin(); it != m_fields.end(); ++it) { + // add category if it's not in the list yet + if(!it->category().isEmpty() && !m_fieldCategories.contains(it->category())) { + m_fieldCategories += it->category(); + } + } + } + + if(dependentFieldHasRecursion(newField_)) { + newField_->setDescription(QString()); + } + + // keep track of if the entry groups will need to be reset + bool resetGroups = false; + + // if format is different, go ahead and invalidate all formatted entry values + if(oldField->formatFlag() != newField_->formatFlag()) { + // invalidate cached format strings of all entry attributes of this name + for(EntryVecIt it = m_entries.begin(); it != m_entries.end(); ++it) { + it->invalidateFormattedFieldValue(fieldName); + } + resetGroups = true; + } + + // check to see if the people "pseudo-group" needs to be updated + // only if only one of the two is a name + bool wasPeople = oldField->formatFlag() == Field::FormatName; + bool isPeople = newField_->formatFlag() == Field::FormatName; + if(wasPeople) { + m_peopleFields.remove(oldField); + if(!isPeople) { + resetGroups = true; + } + } + if(isPeople) { + // if there's more than one people field and no people dict exists yet, add it + if(m_peopleFields.count() > 1 && m_entryGroupDicts.find(s_peopleGroupName) == 0) { + EntryGroupDict* d = new EntryGroupDict(); + d->setAutoDelete(true); + m_entryGroupDicts.insert(s_peopleGroupName, d); + // put it at the top of the list + m_entryGroups.prepend(s_peopleGroupName); + } + m_peopleFields.append(newField_); + if(!wasPeople) { + resetGroups = true; + } + } + + bool wasGrouped = oldField->flags() & Field::AllowGrouped; + bool isGrouped = newField_->flags() & Field::AllowGrouped; + if(wasGrouped) { + if(!isGrouped) { + // in order to keep list in the same order, don't remove unless new field is not groupable + m_entryGroups.remove(fieldName); + m_entryGroupDicts.remove(fieldName); + myDebug() << "Collection::modifyField() - no longer grouped: " << fieldName << endl; + resetGroups = true; + } else { + // don't do this, it wipes out the old groups! +// m_entryGroupDicts.replace(fieldName, new EntryGroupDict()); + } + } else if(isGrouped) { + EntryGroupDict* d = new EntryGroupDict(); + d->setAutoDelete(true); + m_entryGroupDicts.insert(fieldName, d); + if(!wasGrouped) { + // cache the possible groups of entries + m_entryGroups << fieldName; + } + resetGroups = true; + } + + if(oldField->type() == Field::Image) { + m_imageFields.remove(oldField); + } + if(newField_->type() == Field::Image) { + m_imageFields.append(newField_); + } + + if(resetGroups) { + myLog() << "Collection::modifyField() - invalidating groups" << endl; + invalidateGroups(); + } + + // now to update all entries if the field is a dependent and the description changed + if(newField_->type() == Field::Dependent && oldField->description() != newField_->description()) { + emit signalRefreshField(newField_); + } + + return true; +} + +bool Collection::removeField(const QString& name_, bool force_) { + return removeField(fieldByName(name_), force_); +} + +// force allows me to force the deleting of the title field if I need to +bool Collection::removeField(FieldPtr field_, bool force_/*=false*/) { + if(!field_ || !m_fields.contains(field_)) { + if(field_) { + myDebug() << "Collection::removeField - false: " << field_->name() << endl; + } + return false; + } +// myDebug() << "Collection::removeField() - name = " << field_->name() << endl; + + // can't delete the title field + if((field_->flags() & Field::NoDelete) && !force_) { + return false; + } + + bool success = true; + if(field_->formatFlag() == Field::FormatName) { + success &= m_peopleFields.remove(field_); + } + + if(field_->type() == Field::Image) { + success &= m_imageFields.remove(field_); + } + success &= m_fieldNameDict.remove(field_->name()); + success &= m_fieldTitleDict.remove(field_->title()); + success &= m_fieldNames.remove(field_->name()); + success &= m_fieldTitles.remove(field_->title()); + + if(fieldsByCategory(field_->category()).count() == 1) { + success &= m_fieldCategories.remove(field_->category()); + } + + for(EntryVecIt it = m_entries.begin(); it != m_entries.end(); ++it) { + // setting the fields to an empty string removes the value from the entry's list + it->setField(field_, QString::null); + } + + if(field_->flags() & Field::AllowGrouped) { + success &= m_entryGroupDicts.remove(field_->name()); + success &= m_entryGroups.remove(field_->name()); + if(field_->name() == m_defaultGroupField) { + setDefaultGroupField(m_entryGroups[0]); + } + } + + success &= m_fields.remove(field_); + + // refresh all dependent fields, rather lazy, but there's + // likely to be weird effects when checking dependent fields + // while removing one, so refresh all of them + for(FieldVec::Iterator it = m_fields.begin(); it != m_fields.end(); ++it) { + if(it->type() == Field::Dependent) { + emit signalRefreshField(it); + } + } + + return success; +} + +void Collection::reorderFields(const FieldVec& list_) { +// assume the lists have the same pointers! + m_fields = list_; + + // also reset category list, since the order may have changed + m_fieldCategories.clear(); + for(FieldVec::Iterator it = m_fields.begin(); it != m_fields.end(); ++it) { + if(!it->category().isEmpty() && !m_fieldCategories.contains(it->category())) { + m_fieldCategories << it->category(); + } + } +} + +void Collection::addEntries(EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + + for(EntryVec::Iterator entry = entries_.begin(); entry != entries_.end(); ++entry) { + bool foster = false; + if(this != entry->collection()) { + entry->setCollection(this); + foster = true; + } + + m_entries.append(entry); +// myDebug() << "Collection::addEntries() - added entry (" << entry->title() << ")" << endl; + + if(entry->id() >= m_nextEntryId) { + m_nextEntryId = entry->id() + 1; + } else if(entry->id() == -1) { + entry->setId(m_nextEntryId); + ++m_nextEntryId; + } else if(m_entryIdDict.find(entry->id())) { + if(!foster) { + myDebug() << "Collection::addEntries() - the collection already has an entry with id = " << entry->id() << endl; + } + entry->setId(m_nextEntryId); + ++m_nextEntryId; + } + m_entryIdDict.insert(entry->id(), entry.data()); + } + if(m_trackGroups) { + populateCurrentDicts(entries_); + } +} + +void Collection::removeEntriesFromDicts(EntryVec entries_) { + PtrVector<EntryGroup> modifiedGroups; + for(EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + // need a copy of the vector since it gets changed + PtrVector<EntryGroup> groups = entry->groups(); + for(PtrVector<EntryGroup>::Iterator group = groups.begin(); group != groups.end(); ++group) { + if(entry->removeFromGroup(group.ptr()) && !modifiedGroups.contains(group.ptr())) { + modifiedGroups.push_back(group.ptr()); + } + if(group->isEmpty() && !m_groupsToDelete.contains(group.ptr())) { + m_groupsToDelete.push_back(group.ptr()); + } + } + } + emit signalGroupsModified(this, modifiedGroups); +} + +// this function gets called whenever an entry is modified. Its purpose is to keep the +// groupDicts current. It first removes the entry from every group to which it belongs, +// then it repopulates the dicts with the entry's fields +void Collection::updateDicts(EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + + removeEntriesFromDicts(entries_); + populateCurrentDicts(entries_); + cleanGroups(); +} + +bool Collection::removeEntries(EntryVec vec_) { + if(vec_.isEmpty()) { + return false; + } + +// myDebug() << "Collection::deleteEntry() - deleted entry - " << entry_->title() << endl; + removeEntriesFromDicts(vec_); + bool success = true; + for(EntryVecIt entry = vec_.begin(); entry != vec_.end(); ++entry) { + m_entryIdDict.remove(entry->id()); + success &= m_entries.remove(entry); + } + cleanGroups(); + return success; +} + +Tellico::Data::FieldVec Collection::fieldsByCategory(const QString& cat_) { +#ifndef NDEBUG + if(m_fieldCategories.findIndex(cat_) == -1) { + myDebug() << "Collection::fieldsByCategory() - '" << cat_ << "' is not in category list" << endl; + } +#endif + if(cat_.isEmpty()) { + myDebug() << "Collection::fieldsByCategory() - empty category!" << endl; + return FieldVec(); + } + + FieldVec list; + for(FieldVec::Iterator it = m_fields.begin(); it != m_fields.end(); ++it) { + if(it->category() == cat_) { + list.append(it); + } + } + return list; +} + +const QString& Collection::fieldNameByTitle(const QString& title_) const { + if(title_.isEmpty()) { + return QString::null; + } + FieldPtr f = fieldByTitle(title_); + if(!f) { // might happen in MainWindow::saveCollectionOptions + return QString::null; + } + return f->name(); +} + +const QString& Collection::fieldTitleByName(const QString& name_) const { + if(name_.isEmpty()) { + return QString::null; + } + FieldPtr f = fieldByName(name_); + if(!f) { + kdWarning() << "Collection::fieldTitleByName() - no field named " << name_ << endl; + return QString::null; + } + return f->title(); +} + +QStringList Collection::valuesByFieldName(const QString& name_) const { + if(name_.isEmpty()) { + return QStringList(); + } + bool multiple = (fieldByName(name_)->flags() & Field::AllowMultiple); + + StringSet values; + for(EntryVec::ConstIterator it = m_entries.begin(); it != m_entries.end(); ++it) { + if(multiple) { + values.add(it->fields(name_, false)); + } else { + values.add(it->field(name_)); + } + } // end entry loop + + return values.toList(); +} + +Tellico::Data::FieldPtr Collection::fieldByName(const QString& name_) const { + return m_fieldNameDict.isEmpty() ? 0 : name_.isEmpty() ? 0 : m_fieldNameDict.find(name_); +} + +Tellico::Data::FieldPtr Collection::fieldByTitle(const QString& title_) const { + return m_fieldTitleDict.isEmpty() ? 0 : title_.isEmpty() ? 0 : m_fieldTitleDict.find(title_); +} + +bool Collection::hasField(const QString& name_) const { + return fieldByName(name_) != 0; +} + +bool Collection::isAllowed(const QString& key_, const QString& value_) const { + // empty string is always allowed + if(value_.isEmpty()) { + return true; + } + + // find the field with a name of 'key_' + FieldPtr field = fieldByName(key_); + + // if the type is not multiple choice or if value_ is allowed, return true + if(field && (field->type() != Field::Choice || field->allowed().findIndex(value_) > -1)) { + return true; + } + + return false; +} + +Tellico::Data::EntryGroupDict* Collection::entryGroupDictByName(const QString& name_) { +// myDebug() << "Collection::entryGroupDictByName() - " << name_ << endl; + if(name_.isEmpty()) { + return 0; + } + EntryGroupDict* dict = m_entryGroupDicts.isEmpty() ? 0 : m_entryGroupDicts.find(name_); + if(dict && dict->isEmpty()) { + GUI::CursorSaver cs; + const bool b = signalsBlocked(); + // block signals so all the group created/modified signals don't fire + blockSignals(true); + populateDict(dict, name_, m_entries); + blockSignals(b); + } + return dict; +} + +void Collection::populateDict(EntryGroupDict* dict_, const QString& fieldName_, EntryVec entries_) { +// myDebug() << "Collection::populateDict() - " << fieldName_ << endl; + bool isBool = hasField(fieldName_) && fieldByName(fieldName_)->type() == Field::Bool; + + PtrVector<EntryGroup> modifiedGroups; + for(EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + QStringList groups = entryGroupNamesByField(entry, fieldName_); + for(QStringList::ConstIterator groupIt = groups.begin(); groupIt != groups.end(); ++groupIt) { + // find the group for this group name + // bool fields used the field title + QString groupTitle = *groupIt; + if(isBool && groupTitle != i18n(s_emptyGroupTitle)) { + groupTitle = fieldTitleByName(fieldName_); + } + EntryGroup* group = dict_->find(groupTitle); + // if the group doesn't exist, create it + if(!group) { + group = new EntryGroup(groupTitle, fieldName_); + dict_->insert(groupTitle, group); + } else if(group->isEmpty()) { + // if it's empty, then it was added to the vector of groups to delete + // remove it from that vector now that we're adding to it + m_groupsToDelete.remove(group); + } + if(entry->addToGroup(group)) { + modifiedGroups.push_back(group); + } + } // end group loop + } // end entry loop + emit signalGroupsModified(this, modifiedGroups); +} + +void Collection::populateCurrentDicts(EntryVec entries_) { + if(m_entryGroupDicts.isEmpty()) { + return; + } + + // special case when adding an entry to a new empty collection + // there are no existing non-empty groups + bool allEmpty = true; + + // iterate over all the possible groupDicts + // for each dict, get the value of that field for the entry + // if multiple values are allowed, split the value and then insert the + // entry pointer into the dict for each value + QDictIterator<EntryGroupDict> dictIt(m_entryGroupDicts); + for( ; dictIt.current(); ++dictIt) { + // only populate if it's not empty, since they are + // populated on demand + if(!dictIt.current()->isEmpty()) { + populateDict(dictIt.current(), dictIt.currentKey(), entries_); + allEmpty = false; + } + } + + if(allEmpty) { + // need to populate the current group dict + const QString group = Controller::self()->groupBy(); + EntryGroupDict* dict = m_entryGroupDicts[group]; + if(dict) { + populateDict(dict, group, entries_); + } + } +} + +// return a string list for all the groups that the entry belongs to +// for a given field. Normally, this would just be splitting the entry's value +// for the field, but if the field name is the people pseudo-group, then it gets +// a bit more complicated +QStringList Collection::entryGroupNamesByField(EntryPtr entry_, const QString& fieldName_) { + if(fieldName_ != s_peopleGroupName) { + return entry_->groupNamesByFieldName(fieldName_); + } + + StringSet values; + for(FieldVec::Iterator it = m_peopleFields.begin(); it != m_peopleFields.end(); ++it) { + values.add(entry_->groupNamesByFieldName(it->name())); + } + values.remove(i18n(s_emptyGroupTitle)); + return values.toList(); +} + +void Collection::invalidateGroups() { + QDictIterator<EntryGroupDict> dictIt(m_entryGroupDicts); + for( ; dictIt.current(); ++dictIt) { + dictIt.current()->clear(); + } + + // populateDicts() will make signals that the group view is connected to, block those + blockSignals(true); + for(EntryVecIt it = m_entries.begin(); it != m_entries.end(); ++it) { + it->invalidateFormattedFieldValue(); + it->clearGroups(); + } + blockSignals(false); +} + +Tellico::Data::EntryPtr Collection::entryById(long id_) { + return m_entryIdDict[id_]; +} + +void Collection::addBorrower(Data::BorrowerPtr borrower_) { + if(!borrower_) { + return; + } + m_borrowers.append(borrower_); +} + +void Collection::addFilter(FilterPtr filter_) { + if(!filter_) { + return; + } + + m_filters.append(filter_); +} + +bool Collection::removeFilter(FilterPtr filter_) { + if(!filter_) { + return false; + } + + // TODO: test for success + m_filters.remove(filter_); + return true; +} + +void Collection::clear() { + // 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 collection removes + // all held pointers, specifically to entries + m_fields.clear(); + m_peopleFields.clear(); + m_imageFields.clear(); + m_fieldNameDict.clear(); + m_fieldTitleDict.clear(); + + m_entries.clear(); + m_entryIdDict.clear(); + m_entryGroupDicts.clear(); + m_groupsToDelete.clear(); + m_filters.clear(); + m_borrowers.clear(); +} + +void Collection::cleanGroups() { + for(PtrVector<EntryGroup>::Iterator it = m_groupsToDelete.begin(); it != m_groupsToDelete.end(); ++it) { + EntryGroupDict* dict = entryGroupDictByName(it->fieldName()); + if(!dict) { + continue; + } + dict->remove(it->groupName()); + } + m_groupsToDelete.clear(); +} + +bool Collection::dependentFieldHasRecursion(FieldPtr field_) { + if(!field_ || field_->type() != Field::Dependent) { + return false; + } + + StringSet fieldNamesFound; + fieldNamesFound.add(field_->name()); + QValueStack<FieldPtr> fieldsToCheck; + fieldsToCheck.push(field_); + while(!fieldsToCheck.isEmpty()) { + FieldPtr f = fieldsToCheck.pop(); + const QStringList depFields = f->dependsOn(); + for(QStringList::ConstIterator it = depFields.begin(); it != depFields.end(); ++it) { + if(fieldNamesFound.has(*it)) { + // we have recursion + return true; + } + fieldNamesFound.add(*it); + FieldPtr f = fieldByName(*it); + if(!f) { + f = fieldByTitle(*it); + } + if(f) { + fieldsToCheck.push(f); + } + } + } + return false; +} + +int Collection::sameEntry(Data::EntryPtr entry1_, Data::EntryPtr entry2_) const { + if(!entry1_ || !entry2_) { + return 0; + } + // used to just return 0, but we really want a default generic implementation + // that specific collections can override. + + // start with twice the title score + // and since the minimum is > 10, then need more than just a perfect title match + int res = 2*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("title"), this); + // then add score for each field + FieldVec fields = entry1_->collection()->fields(); + for(Data::FieldVecIt it = fields.begin(); it != fields.end(); ++it) { + res += Entry::compareValues(entry1_, entry2_, it->name(), this); + } + return res; +} + +// static +// merges values from e2 into e1 +bool Collection::mergeEntry(EntryPtr e1, EntryPtr e2, bool overwrite_, bool askUser_) { + if(!e1 || !e2) { + myDebug() << "Collection::mergeEntry() - bad entry pointer" << endl; + return false; + } + bool ret = true; + FieldVec fields = e1->collection()->fields(); + for(FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) { + if(e2->field(field).isEmpty()) { + continue; + } +// myLog() << "Collection::mergeEntry() - reading field: " << field->name() << endl; + if(overwrite_ || e1->field(field).isEmpty()) { +// myLog() << e1->title() << ": updating field(" << field->name() << ") to " << e2->field(field->name()) << endl; + e1->setField(field, e2->field(field)); + ret = true; + } else if(e1->field(field) == e2->field(field)) { + continue; + } else if(field->type() == Field::Para) { + // for paragraph fields, concatenate the values, if they're not equal + e1->setField(field, e1->field(field) + QString::fromLatin1("<br/><br/>") + e2->field(field)); + ret = true; + } else if(field->type() == Field::Table) { + // if field F is a table-type field (album tracks, files, etc.), merge rows (keep their position) + // if e1's F val in [row i, column j] empty, replace with e2's val at same position + // if different (non-empty) vals at same position, CONFLICT! + const QString sep = QString::fromLatin1("::"); + QStringList vals1 = e1->fields(field, false); + QStringList vals2 = e2->fields(field, false); + while(vals1.count() < vals2.count()) { + vals1 += QString(); + } + for(uint i = 0; i < vals2.count(); ++i) { + if(vals2[i].isEmpty()) { + continue; + } + if(vals1[i].isEmpty()) { + vals1[i] = vals2[i]; + ret = true; + } else { + QStringList parts1 = QStringList::split(sep, vals1[i], true); + QStringList parts2 = QStringList::split(sep, vals2[i], true); + bool changedPart = false; + while(parts1.count() < parts2.count()) { + parts1 += QString(); + } + for(uint j = 0; j < parts2.count(); ++j) { + if(parts2[j].isEmpty()) { + continue; + } + if(parts1[j].isEmpty()) { + parts1[j] = parts2[j]; + changedPart = true; + } else if(askUser_ && parts1[j] != parts2[j]) { + int ret = Kernel::self()->askAndMerge(e1, e2, field, parts1[j], parts2[j]); + if(ret == 0) { + return false; // we got cancelled + } + if(ret == 1) { + parts1[j] = parts2[j]; + changedPart = true; + } + } + } + if(changedPart) { + vals1[i] = parts1.join(sep); + ret = true; + } + } + } + if(ret) { + e1->setField(field, vals1.join(QString::fromLatin1("; "))); + } + } else if(field->flags() & Data::Field::AllowMultiple) { + // if field F allows multiple values and not a Table (see above case), + // e1's F values = (e1's F values) U (e2's F values) (union) + // replace e1's field with union of e1's and e2's values for this field + QStringList items1 = e1->fields(field, false); + QStringList items2 = e2->fields(field, false); + for(QStringList::ConstIterator it = items2.begin(); it != items2.end(); ++it) { + // possible to have one value formatted and the other one not... + if(!items1.contains(*it) && !items1.contains(Field::format(*it, field->formatFlag()))) { + items1.append(*it); + } + } +// not sure if I think it should be sorted or not +// items1.sort(); + e1->setField(field, items1.join(QString::fromLatin1("; "))); + ret = true; + } else if(askUser_ && e1->field(field) != e2->field(field)) { + int ret = Kernel::self()->askAndMerge(e1, e2, field); + if(ret == 0) { + return false; // we got cancelled + } + if(ret == 1) { + e1->setField(field, e2->field(field)); + } + } + } + return ret; +} + +long Collection::getID() { + static long id = 0; + return ++id; +} + +#include "collection.moc" diff --git a/src/collection.h b/src/collection.h new file mode 100644 index 0000000..b7dae1f --- /dev/null +++ b/src/collection.h @@ -0,0 +1,394 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef COLLECTION_H +#define COLLECTION_H + +#include "field.h" +#include "entry.h" +#include "filter.h" +#include "borrower.h" +#include "datavectors.h" + +#include <ksharedptr.h> + +#include <qstringlist.h> +#include <qptrlist.h> +#include <qstring.h> +#include <qdict.h> +#include <qintdict.h> +#include <qobject.h> + +namespace Tellico { + namespace Data { + class EntryGroup; + typedef QDict<EntryGroup> EntryGroupDict; + +/** + * The Collection class is the primary data object, holding a + * list of fields and entries. + * + * A collection holds entries of a single type, whether it be books, CDs, or whatever. + * It has a list of attributes which apply for the whole collection. A unique id value + * identifies each collection object. + * + * @see Entry + * @see Field + * + * @author Robby Stephenson + */ +class Collection : public QObject, public KShared { +Q_OBJECT + +public: + enum Type { + Base = 1, + Book = 2, + Video = 3, + Album = 4, + Bibtex = 5, + ComicBook = 6, + Wine = 7, + Coin = 8, + Stamp = 9, + Card = 10, + Game = 11, + File = 12, + BoardGame = 13 + // if you want to add custom collection types, use a number sure to be unique like 101 + }; + + /** + * The constructor is only used to create custom collections. It adds a title field, + * in the "General" group. The iconName is set to be the typeName; + * + * @param title The title of the collection itself + * @param entryTitle The title of the entries, which can be translated + */ + explicit Collection(const QString& title); + /** + */ + virtual ~Collection(); + + /** + * Returns the type of the collection. + * + * @return The type + */ + virtual Type type() const { return Base; } + /** + * Returns the id of the collection. + * + * @return The id + */ + long id() const { return m_id; } + /** + * Returns the name of the collection. + * + * @return The name + */ + const QString& title() const { return m_title; } + /** + * Sets the title of the collection. + * + * @param title The new collection title + */ + void setTitle(const QString& title) { m_title = title; } + /** + * Returns the name of the entries in the collection, e.g. "book". + * Not translated. + * + * @return The type name + */ + QString typeName() const; + /** + * Returns a reference to the list of all the entries in the collection. + * + * @return The list of entries + */ + const EntryVec& entries() const { return m_entries; } + /** + * Returns a reference to the list of the collection attributes. + * + * @return The list of fields + */ + const FieldVec& fields() const { return m_fields; } + EntryPtr entryById(long id); + /** + * Returns a reference to the list of the collection's people fields. + * + * @return The list of fields + */ + const FieldVec& peopleFields() const { return m_peopleFields; } + /** + * Returns a reference to the list of the collection's image fields. + * + * @return The list of fields + */ + const FieldVec& imageFields() const { return m_imageFields; } + /** + * Returns a reference to the list of field groups. This value is cached rather + * than generated with each call, so the method should be fairly fast. + * + * @return The list of group names + */ + const QStringList& fieldCategories() const { return m_fieldCategories; } + /** + * Returns the name of the field used to group the entries by default. + * + * @return The field name + */ + const QString& defaultGroupField() const { return m_defaultGroupField; } + /** + * Sets the name of the default field used to group the entries. + * + * @param name The name of the field + */ + void setDefaultGroupField(const QString& name) { m_defaultGroupField = name; } + /** + * Returns the number of entries in the collection. + * + * @return The number of entries + */ + size_t entryCount() const { return m_entries.count(); } + /** + * Adds a entry to the collection. The collection takes ownership of the entry object. + * + * @param entry A pointer to the entry + */ + void addEntries(EntryVec entries); + /** + * Updates the dicts that include the entry. + * + * @param entry A pointer to the entry + */ + void updateDicts(EntryVec entries); + /** + * Deletes a entry from the collection. + * + * @param entry The pointer to the entry + * @return A boolean indicating if the entry was in the collection and was deleted + */ + bool removeEntries(EntryVec entries); + /** + * Adds a whole list of attributes. It's gotta be virtual since it calls + * @ref addField, which is virtual. + * + * @param list List of attributes to add + * @return A boolean indicating if the field was added or not + */ + virtual bool addFields(FieldVec list); + /** + * Adds an field to the collection, unless an field with that name + * already exists. The collection takes ownership of the field object. + * + * @param field A pointer to the field + * @return A boolean indicating if the field was added or not + */ + virtual bool addField(FieldPtr field); + virtual bool mergeField(FieldPtr field); + virtual bool modifyField(FieldPtr field); + virtual bool removeField(FieldPtr field, bool force=false); + virtual bool removeField(const QString& name, bool force=false); + void reorderFields(const FieldVec& list); + + // the reason this is not static is so I can call it from a collection pointer + // it also gets virtualized for different collection types + // the return values should be compared against the GOOD and PERFECT + // static match constants in this class + virtual int sameEntry(Data::EntryPtr, Data::EntryPtr) const; + + /** + * Determines whether or not a certain value is allowed for an field. + * + * @param key The name of the field + * @param value The desired value + * @return A boolean indicating if the value is an allowed value for that field + */ + bool isAllowed(const QString& key, const QString& value) const; + /** + * Returns a list of all the field names. + * + * @return The list of names + */ + const QStringList& fieldNames() const { return m_fieldNames; } + /** + * Returns a list of all the field titles. + * + * @return The list of titles + */ + const QStringList& fieldTitles() const { return m_fieldTitles; } + /** + * Returns the title of an field, given its name. + * + * @param name The field name + * @return The field title + */ + const QString& fieldTitleByName(const QString& name) const; + /** + * Returns the name of an field, given its title. + * + * @param title The field title + * @return The field name + */ + const QString& fieldNameByTitle(const QString& title) const; + /** + * Returns a list of the values of a given field for every entry + * in the collection. The values in the list are not repeated. Attribute + * values which contain ";" are split into separate values. Since this method + * iterates over all the entries, for large collections, it is expensive. + * + * @param name The name of the field + * @return The list of values + */ + QStringList valuesByFieldName(const QString& name) const; + /** + * Returns a list of all the fields in a given category. + * + * @param category The name of the category + * @return The field list + */ + FieldVec fieldsByCategory(const QString& category); + /** + * Returns a pointer to an field given its name. If none is found, a NULL pointer + * is returned. + * + * @param name The field name + * @return The field pointer + */ + FieldPtr fieldByName(const QString& name) const; + /** + * Returns a pointer to an field given its title. If none is found, a NULL pointer + * is returned. This lookup is slower than by name. + * + * @param title The field title + * @return The field pointer + */ + FieldPtr fieldByTitle(const QString& title) const; + /** + * Returns @p true if the collection contains a field named @ref name; + */ + bool hasField(const QString& name) const; + /** + * Returns a list of all the possible entry groups. This value is cached rather + * than generated with each call, so the method should be fairly fast. + * + * @return The list of groups + */ + const QStringList& entryGroups() const { return m_entryGroups; } + /** + * Returns a pointer to a dict of all the entries grouped by + * a certain field + * + * @param name The name of the field by which the entries are grouped + * @return The list of group names + */ + EntryGroupDict* entryGroupDictByName(const QString& name); + /** + * Invalidates all group names in the collection. + */ + void invalidateGroups(); + /** + * Returns true if the collection contains at least one Image field. + * + * @return Returns true if the collection contains at least one Image field; + */ + bool hasImages() const { return !m_imageFields.isEmpty(); } + + void setTrackGroups(bool b) { m_trackGroups = b; } + + void addBorrower(Data::BorrowerPtr borrower); + const BorrowerVec& borrowers() const { return m_borrowers; } + /** + * Clears all vectors which contain shared ptrs + */ + void clear(); + + void addFilter(FilterPtr filter); + bool removeFilter(FilterPtr filter); + const FilterVec& filters() const { return m_filters; } + + static bool mergeEntry(EntryPtr entry1, EntryPtr entry2, bool overwrite, bool askUser=false); + /** + * The string used for empty values. This forces consistency. + */ + static const char* s_emptyGroupTitle; + /** + * The string used for the people pseudo-group. This forces consistency. + */ + static const QString s_peopleGroupName; + + // these are the values that should be compared against + // the result from sameEntry() + static const int ENTRY_GOOD_MATCH = 10; + static const int ENTRY_PERFECT_MATCH = 20; + +signals: + void signalGroupsModified(Tellico::Data::CollPtr coll, PtrVector<Tellico::Data::EntryGroup> groups); + void signalRefreshField(Tellico::Data::FieldPtr field); + +private: + QStringList entryGroupNamesByField(EntryPtr entry, const QString& fieldName); + void removeEntriesFromDicts(EntryVec entries); + void populateDict(EntryGroupDict* dict, const QString& fieldName, EntryVec entries); + void populateCurrentDicts(EntryVec entries); + void cleanGroups(); + bool dependentFieldHasRecursion(FieldPtr field); + + /* + * Gets the preferred ID of the collection. Currently, it just gets incremented as + * new collections are created. + */ + static long getID(); + + /** + * The copy constructor is private, to ensure that it's never used. + */ + Collection(const Collection& coll); + /** + * The assignment operator is private, to ensure that it's never used. + */ + Collection operator=(const Collection& coll); + + long m_id; + long m_nextEntryId; + QString m_title; + QString m_typeName; + QString m_iconName; + QString m_defaultGroupField; + + FieldVec m_fields; + FieldVec m_peopleFields; // keep separate list of people fields + FieldVec m_imageFields; // keep track of image fields + QDict<Field> m_fieldNameDict; + QDict<Field> m_fieldTitleDict; + QStringList m_fieldCategories; + QStringList m_fieldNames; + QStringList m_fieldTitles; + + EntryVec m_entries; + QIntDict<Entry> m_entryIdDict; + + QDict<EntryGroupDict> m_entryGroupDicts; + QStringList m_entryGroups; + PtrVector<EntryGroup> m_groupsToDelete; + + FilterVec m_filters; + BorrowerVec m_borrowers; + + bool m_trackGroups : 1; +}; + + } // end namespace +} //end namespace +#endif diff --git a/src/collectionfactory.cpp b/src/collectionfactory.cpp new file mode 100644 index 0000000..25f9d6f --- /dev/null +++ b/src/collectionfactory.cpp @@ -0,0 +1,210 @@ +/*************************************************************************** + copyright : (C) 2003-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 "collectionfactory.h" +#include "collections/bookcollection.h" +#include "collections/bibtexcollection.h" +#include "collections/musiccollection.h" +#include "collections/videocollection.h" +#include "collections/comicbookcollection.h" +#include "collections/coincollection.h" +#include "collections/stampcollection.h" +#include "collections/cardcollection.h" +#include "collections/winecollection.h" +#include "collections/gamecollection.h" +#include "collections/filecatalog.h" +#include "collections/boardgamecollection.h" +#include "field.h" +#include "tellico_debug.h" + +#include <klocale.h> + +using Tellico::CollectionFactory; + +// static +Tellico::Data::CollPtr CollectionFactory::collection(int type_, bool addFields_) { + switch(type_) { + case Data::Collection::Book: + return new Data::BookCollection(addFields_); + + case Data::Collection::Video: + return new Data::VideoCollection(addFields_); + + case Data::Collection::Album: + return new Data::MusicCollection(addFields_); + + case Data::Collection::Bibtex: + return new Data::BibtexCollection(addFields_); + + case Data::Collection::Coin: + return new Data::CoinCollection(addFields_); + + case Data::Collection::Card: + return new Data::CardCollection(addFields_); + + case Data::Collection::Stamp: + return new Data::StampCollection(addFields_); + + case Data::Collection::Wine: + return new Data::WineCollection(addFields_); + + case Data::Collection::ComicBook: + return new Data::ComicBookCollection(addFields_); + + case Data::Collection::Game: + return new Data::GameCollection(addFields_); + + case Data::Collection::File: + return new Data::FileCatalog(addFields_); + + case Data::Collection::BoardGame: + return new Data::BoardGameCollection(addFields_); + + case Data::Collection::Base: + break; + + default: + kdWarning() << "CollectionFactory::collection() - collection type not implemented: " << type_ << endl; + // fall through + } + + Data::CollPtr c = new Data::Collection(i18n("My Collection")); + Data::FieldPtr f = new Data::Field(QString::fromLatin1("title"), i18n("Title")); + f->setCategory(i18n("General")); + f->setFlags(Data::Field::NoDelete); + f->setFormatFlag(Data::Field::FormatTitle); + c->addField(f); + return c; +} + +// static +Tellico::Data::CollPtr CollectionFactory::collection(const QString& typeName_, bool addFields_) { + if(typeName_ == typeName(Data::Collection::Book)) { + return new Data::BookCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Album)) { + return new Data::MusicCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Video)) { + return new Data::VideoCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Bibtex)) { + return new Data::BibtexCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Bibtex)) { + return new Data::ComicBookCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::ComicBook)) { + return new Data::CardCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Coin)) { + return new Data::CoinCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Stamp)) { + return new Data::StampCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Wine)) { + return new Data::WineCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::Game)) { + return new Data::GameCollection(addFields_); + } else if(typeName_ == typeName(Data::Collection::File)) { + return new Data::FileCatalog(addFields_); + } else if(typeName_ == typeName(Data::Collection::BoardGame)) { + return new Data::BoardGameCollection(addFields_); + } else { + kdWarning() << "CollectionFactory::collection() - collection type not implemented: " << typeName_ << endl; + return 0; + } +} + +Tellico::CollectionNameMap CollectionFactory::nameMap() { + CollectionNameMap map; + map[Data::Collection::Book] = i18n("Book Collection"); + map[Data::Collection::Bibtex] = i18n("Bibliography"); + map[Data::Collection::ComicBook] = i18n("Comic Book Collection"); + map[Data::Collection::Video] = i18n("Video Collection"); + map[Data::Collection::Album] = i18n("Music Collection"); + map[Data::Collection::Coin] = i18n("Coin Collection"); + map[Data::Collection::Stamp] = i18n("Stamp Collection"); + map[Data::Collection::Wine] = i18n("Wine Collection"); + map[Data::Collection::Card] = i18n("Card Collection"); + map[Data::Collection::Game] = i18n("Game Collection"); + map[Data::Collection::File] = i18n("File Catalog"); + map[Data::Collection::BoardGame] = i18n("Board Game Collection"); + map[Data::Collection::Base] = i18n("Custom Collection"); + return map; +} + +QString CollectionFactory::typeName(int type_) { + switch(type_) { + case Data::Collection::Book: + return QString::fromLatin1("book"); + break; + + case Data::Collection::Video: + return QString::fromLatin1("video"); + break; + + case Data::Collection::Album: + return QString::fromLatin1("album"); + break; + + case Data::Collection::Bibtex: + return QString::fromLatin1("bibtex"); + break; + + case Data::Collection::ComicBook: + return QString::fromLatin1("comic"); + break; + + case Data::Collection::Wine: + return QString::fromLatin1("wine"); + break; + + case Data::Collection::Coin: + return QString::fromLatin1("coin"); + break; + + case Data::Collection::Stamp: + return QString::fromLatin1("stamp"); + break; + + case Data::Collection::Card: + return QString::fromLatin1("card"); + break; + + case Data::Collection::Game: + return QString::fromLatin1("game"); + break; + + case Data::Collection::File: + return QString::fromLatin1("file"); + break; + + case Data::Collection::BoardGame: + return QString::fromLatin1("boardgame"); + break; + + case Data::Collection::Base: + return QString::fromLatin1("entry"); + break; + + default: + kdWarning() << "CollectionFactory::collection() - collection type not implemented: " << type_ << endl; + return QString::fromLatin1("entry"); + break; + } +} + +bool CollectionFactory::isDefaultField(int type_, const QString& name_) { + Data::CollPtr coll = collection(type_, true); + Data::FieldVec fields = coll->fields(); + for(Data::FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) { + if(field->name() == name_) { + return true; + } + } + return false; +} diff --git a/src/collectionfactory.h b/src/collectionfactory.h new file mode 100644 index 0000000..72d42b1 --- /dev/null +++ b/src/collectionfactory.h @@ -0,0 +1,40 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef COLLECTIONFACTORY_H +#define COLLECTIONFACTORY_H + +#include "datavectors.h" + +#include <qmap.h> + +namespace Tellico { + +typedef QMap<int, QString> CollectionNameMap; + +/** + * A factory class for dealing with the different types of collections. + * + * @author Robby Stephenson + */ +class CollectionFactory { +public: + static Data::CollPtr collection(int type, bool addFields); + static Data::CollPtr collection(const QString& typeName, bool addFields); + static CollectionNameMap nameMap(); + static QString typeName(int type); + static bool isDefaultField(int type, const QString& name); +}; + +} // end namespace +#endif diff --git a/src/collectionfieldsdialog.cpp b/src/collectionfieldsdialog.cpp new file mode 100644 index 0000000..cc9aede --- /dev/null +++ b/src/collectionfieldsdialog.cpp @@ -0,0 +1,1036 @@ +/*************************************************************************** + copyright : (C) 2003-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 "collectionfieldsdialog.h" +#include "collection.h" +#include "field.h" +#include "collectionfactory.h" +#include "gui/stringmapdialog.h" +#include "tellico_kernel.h" +#include "translators/tellico_xml.h" +#include "tellico_utils.h" +#include "tellico_debug.h" + +#include <klocale.h> +#include <kcombobox.h> +#include <klineedit.h> +#include <kiconloader.h> +#include <kmessagebox.h> +#include <kpushbutton.h> +#include <kaccelmanager.h> + +#include <qgroupbox.h> +#include <qlayout.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qlabel.h> +#include <qradiobutton.h> +#include <qbuttongroup.h> +#include <qcheckbox.h> +#include <qregexp.h> +#include <qwhatsthis.h> +#include <qtimer.h> + +using Tellico::FieldListBox; +using Tellico::CollectionFieldsDialog; + +FieldListBox::FieldListBox(QListBox* listbox_, Data::FieldPtr field_) + : GUI::ListBoxText(listbox_, field_->title()), m_field(field_) { +} + +FieldListBox::FieldListBox(QListBox* listbox_, Data::FieldPtr field_, QListBoxItem* after_) + : GUI::ListBoxText(listbox_, field_->title(), after_), m_field(field_) { +} + +CollectionFieldsDialog::CollectionFieldsDialog(Data::CollPtr coll_, QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, false, i18n("Collection Fields"), Help|Default|Ok|Apply|Cancel, Ok, false), + m_coll(coll_), + m_defaultCollection(0), + m_currentField(0), + m_modified(false), + m_updatingValues(false), + m_reordered(false), + m_oldIndex(-1) { + QWidget* page = new QWidget(this); + setMainWidget(page); + QHBoxLayout* topLayout = new QHBoxLayout(page, 0, KDialog::spacingHint()); + + QGroupBox* fieldsGroup = new QGroupBox(1, Qt::Horizontal, i18n("Current Fields"), page); + topLayout->addWidget(fieldsGroup, 1); + m_fieldsBox = new QListBox(fieldsGroup); + m_fieldsBox->setMinimumWidth(150); + + Data::FieldVec fields = m_coll->fields(); + for(Data::FieldVec::Iterator it = fields.begin(); it != fields.end(); ++it) { + // ignore ReadOnly + if(it->type() != Data::Field::ReadOnly) { + (void) new FieldListBox(m_fieldsBox, it); + } + } + connect(m_fieldsBox, SIGNAL(highlighted(int)), SLOT(slotHighlightedChanged(int))); + + QHBox* hb1 = new QHBox(fieldsGroup); + hb1->setSpacing(KDialog::spacingHint()); + m_btnNew = new KPushButton(i18n("New Field", "&New"), hb1); + m_btnNew->setIconSet(BarIcon(QString::fromLatin1("filenew"), KIcon::SizeSmall)); + QWhatsThis::add(m_btnNew, i18n("Add a new field to the collection")); + m_btnDelete = new KPushButton(i18n("Delete Field", "&Delete"), hb1); + m_btnDelete->setIconSet(BarIconSet(QString::fromLatin1("editdelete"), KIcon::SizeSmall)); + QWhatsThis::add(m_btnDelete, i18n("Remove a field from the collection")); + + connect(m_btnNew, SIGNAL(clicked()), SLOT(slotNew()) ); + connect(m_btnDelete, SIGNAL(clicked()), SLOT(slotDelete())); + + QHBox* hb2 = new QHBox(fieldsGroup); + hb2->setSpacing(KDialog::spacingHint()); + m_btnUp = new KPushButton(hb2); + m_btnUp->setPixmap(BarIcon(QString::fromLatin1("up"), KIcon::SizeSmall)); + QWhatsThis::add(m_btnUp, i18n("Move this field up in the list. The list order is important " + "for the layout of the entry editor.")); + m_btnDown = new KPushButton(hb2); + m_btnDown->setPixmap(BarIcon(QString::fromLatin1("down"), KIcon::SizeSmall)); + QWhatsThis::add(m_btnDown, i18n("Move this field down in the list. The list order is important " + "for the layout of the entry editor.")); + + connect(m_btnUp, SIGNAL(clicked()), SLOT(slotMoveUp()) ); + connect(m_btnDown, SIGNAL(clicked()), SLOT(slotMoveDown())); + + QVBox* vbox = new QVBox(page); + vbox->setSpacing(KDialog::spacingHint()); + topLayout->addWidget(vbox, 2); + + QGroupBox* propGroup = new QGroupBox(1, Qt::Horizontal, i18n("Field Properties"), vbox); + + QWidget* grid = new QWidget(propGroup); + // (parent, nrows, ncols, margin, spacing) + QGridLayout* layout = new QGridLayout(grid, 4, 4, 0, KDialog::spacingHint()); + + int row = -1; + QLabel* label = new QLabel(i18n("&Title:"), grid); + layout->addWidget(label, ++row, 0); + m_titleEdit = new KLineEdit(grid); + layout->addWidget(m_titleEdit, row, 1); + label->setBuddy(m_titleEdit); + QString whats = i18n("The title of the field"); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_titleEdit, whats); + connect(m_titleEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + label = new QLabel(i18n("T&ype:"), grid); + layout->addWidget(label, row, 2); + m_typeCombo = new KComboBox(grid); + layout->addWidget(m_typeCombo, row, 3); + label->setBuddy(m_typeCombo); + whats = QString::fromLatin1("<qt>"); + whats += i18n("The type of the field determines what values may be used. "); + whats += i18n("<i>Simple Text</i> is used for most fields. "); + whats += i18n("<i>Paragraph</i> is for large text blocks. "); + whats += i18n("<i>Choice</i> limits the field to certain values. "); + whats += i18n("<i>Checkbox</i> is for a simple yes/no value. "); + whats += i18n("<i>Number</i> indicates that the field contains a numerical value. "); + whats += i18n("<i>URL</i> is for fields which refer to URLs, including references to other files. "); + whats += i18n("A <i>Table</i> may hold one or more columns of values. "); + whats += i18n("An <i>Image</i> field holds a picture. "); + whats += i18n("A <i>Date</i> field can be used for values with a day, month, and year. "); + whats += i18n("A <i>Rating</i> field uses stars to show a rating number. "); + whats += i18n("A <i>Dependent</i> field depends on the values of other " + "fields, and is formatted according to the field description. "); + whats += i18n("A <i>Read Only</i> is for internal values, possibly useful for import and export. "); + whats += QString::fromLatin1("</qt>"); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_typeCombo, whats); + // the typeTitles match the fieldMap().values() but in a better order + m_typeCombo->insertStringList(Data::Field::typeTitles()); + connect(m_typeCombo, SIGNAL(activated(int)), SLOT(slotModified())); + connect(m_typeCombo, SIGNAL(activated(const QString&)), SLOT(slotTypeChanged(const QString&))); + + label = new QLabel(i18n("Cate&gory:"), grid); + layout->addWidget(label, ++row, 0); + m_catCombo = new KComboBox(true, grid); + layout->addWidget(m_catCombo, row, 1); + label->setBuddy(m_catCombo); + whats = i18n("The field category determines where the field is placed in the editor."); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_catCombo, whats); + + // I don't want to include the categories for singleCategory fields + QStringList cats; + const QStringList allCats = m_coll->fieldCategories(); + for(QStringList::ConstIterator it = allCats.begin(); it != allCats.end(); ++it) { + Data::FieldVec fields = m_coll->fieldsByCategory(*it); + if(!fields.isEmpty() && !fields.begin()->isSingleCategory()) { + cats.append(*it); + } + } + m_catCombo->insertStringList(cats); + m_catCombo->setDuplicatesEnabled(false); + connect(m_catCombo, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + label = new QLabel(i18n("Descr&iption:"), grid); + layout->addWidget(label, ++row, 0); + m_descEdit = new KLineEdit(grid); + m_descEdit->setMinimumWidth(150); + layout->addMultiCellWidget(m_descEdit, row, row, 1, 3); + label->setBuddy(m_descEdit); + /* TRANSLATORS: Do not translate %{year} and %{title}. */ + whats = i18n("The description is a useful reminder of what information is contained in the " + "field. For <i>Dependent</i> fields, the description is a format string such as " + "\"%{year} %{title}\" where the named fields get substituted in the string."); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_descEdit, whats); + connect(m_descEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + label = new QLabel(i18n("&Default value:"), grid); + layout->addWidget(label, ++row, 0); + m_defaultEdit = new KLineEdit(grid); + layout->addMultiCellWidget(m_defaultEdit, row, row, 1, 3); + label->setBuddy(m_defaultEdit); + whats = i18n("<qt>A default value can be set for new entries.</qt>"); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_defaultEdit, whats); + connect(m_defaultEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + label = new QLabel(i18n("A&llowed values:"), grid); + layout->addWidget(label, ++row, 0); + m_allowEdit = new KLineEdit(grid); + layout->addMultiCellWidget(m_allowEdit, row, row, 1, 3); + label->setBuddy(m_allowEdit); + whats = i18n("<qt>For <i>Choice</i>-type fields, these are the only values allowed. They are " + "placed in a combo box. The possible values have to be separated by a semi-colon, " + "for example: \"dog; cat; mouse\"</qt>"); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_allowEdit, whats); + connect(m_allowEdit, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + label = new QLabel(i18n("Extended &properties:"), grid); + layout->addWidget(label, ++row, 0); + m_btnExtended = new KPushButton(i18n("&Set..."), grid); + m_btnExtended->setIconSet(BarIcon(QString::fromLatin1("bookmark"), KIcon::SizeSmall)); + layout->addWidget(m_btnExtended, row, 1); + label->setBuddy(m_btnExtended); + whats = i18n("Extended field properties are used to specify things such as the corresponding bibtex field."); + QWhatsThis::add(label, whats); + QWhatsThis::add(m_btnExtended, whats); + connect(m_btnExtended, SIGNAL(clicked()), SLOT(slotShowExtendedProperties())); + + QButtonGroup* bg = new QButtonGroup(1, Qt::Horizontal, i18n("Format Options"), vbox); + m_formatNone = new QRadioButton(i18n("No formatting"), bg); + QWhatsThis::add(m_formatNone, i18n("This option prevents the field from ever being " + "automatically formatted or capitalized.")); + m_formatPlain = new QRadioButton(i18n("Allow auto-capitalization only"), bg); + QWhatsThis::add(m_formatPlain, i18n("This option allows the field to be capitalized, but " + "not specially formatted.")); + m_formatTitle = new QRadioButton(i18n("Format as a title"), bg); + QWhatsThis::add(m_formatTitle, i18n("This option capitalizes and formats the field as a " + "title, but only if those options are globally set.")); + m_formatName = new QRadioButton(i18n("Format as a name"), bg); + QWhatsThis::add(m_formatName, i18n("This option capitalizes and formats the field as a " + "name, but only if those options are globally set.")); + connect(bg, SIGNAL(clicked(int)), SLOT(slotModified())); + + QGroupBox* optionsGroup = new QGroupBox(1, Qt::Horizontal, i18n("Field Options"), vbox); + m_complete = new QCheckBox(i18n("Enable auto-completion"), optionsGroup); + QWhatsThis::add(m_complete, i18n("If checked, KDE auto-completion will be enabled in the " + "text edit box for this field.")); + m_multiple = new QCheckBox(i18n("Allow multiple values"), optionsGroup); + QWhatsThis::add(m_multiple, i18n("If checked, Tellico will parse the values in the field " + "for multiple values, separated by a semi-colon.")); + m_grouped = new QCheckBox(i18n("Allow grouping"), optionsGroup); + QWhatsThis::add(m_grouped, i18n("If checked, this field may be used to group the entries in " + "the group view.")); + connect(m_complete, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_multiple, SIGNAL(clicked()), SLOT(slotModified())); + connect(m_grouped, SIGNAL(clicked()), SLOT(slotModified())); + + // need to stretch at bottom + vbox->setStretchFactor(new QWidget(vbox), 1); + KAcceleratorManager::manage(vbox); + + // keep a default collection + m_defaultCollection = CollectionFactory::collection(m_coll->type(), true); + + QWhatsThis::add(actionButton(KDialogBase::Default), + i18n("Revert the selected field's properties to the default values.")); + + enableButtonOK(false); + enableButtonApply(false); + + setHelp(QString::fromLatin1("fields-dialog")); + + // initially the m_typeCombo is populated with all types, but as soon as something is + // selected in the fields box, the combo box is cleared and filled with the allowable + // new types. The problem is that when more types are added, the size of the combo box + // doesn't change. So when everything is laid out, the combo box needs to have all the + // items there. + QTimer::singleShot(0, this, SLOT(slotSelectInitial())); +} + +CollectionFieldsDialog::~CollectionFieldsDialog() { +} + +void CollectionFieldsDialog::slotSelectInitial() { + m_fieldsBox->setSelected(0, true); +} + +void CollectionFieldsDialog::slotOk() { + updateField(); + if(!checkValues()) { + return; + } + + applyChanges(); + accept(); +} + +void CollectionFieldsDialog::slotApply() { + updateField(); + if(!checkValues()) { + return; + } + + applyChanges(); +} + +void CollectionFieldsDialog::applyChanges() { +// start a command group, "Modify" is a generic term here since the commands could be add, modify, or delete + Kernel::self()->beginCommandGroup(i18n("Modify Fields")); + + Data::FieldPtr field; + for(Data::FieldVec::Iterator it = m_copiedFields.begin(); it != m_copiedFields.end(); ++it) { + field = it; + // check for Choice fields with removed values to warn user + if(field->type() == Data::Field::Choice || field->type() == Data::Field::Rating) { + QStringList oldValues = m_coll->fieldByName(field->name())->allowed(); + QStringList newValues = field->allowed(); + for(QStringList::ConstIterator vIt = oldValues.begin(); vIt != oldValues.end(); ++vIt) { + if(newValues.contains(*vIt)) { + continue; + } + int ret = KMessageBox::warningContinueCancel(this, + i18n("<qt>Removing allowed values from the <i>%1</i> field which " + "currently exist in the collection may cause data corruption. " + "Do you want to keep your modified values or cancel and revert " + "to the current ones?</qt>").arg(field->title()), + QString::null, + i18n("Keep modified values")); + if(ret != KMessageBox::Continue) { + if(field->type() == Data::Field::Choice) { + field->setAllowed(oldValues); + } else { // rating field + Data::FieldPtr oldField = m_coll->fieldByName(field->name()); + field->setProperty(QString::fromLatin1("minimum"), oldField->property(QString::fromLatin1("minimum"))); + field->setProperty(QString::fromLatin1("maximum"), oldField->property(QString::fromLatin1("maximum"))); + } + } + break; + } + } + Kernel::self()->modifyField(field); + } + + for(Data::FieldVec::Iterator it = m_newFields.begin(); it != m_newFields.end(); ++it) { + Kernel::self()->addField(it); + } + + // set all text not to be colored, and get new list + Data::FieldVec fields; + for(QListBoxItem* item = m_fieldsBox->firstItem(); item; item = item->next()) { + static_cast<FieldListBox*>(item)->setColored(false); + if(m_reordered) { + Data::FieldPtr field = static_cast<FieldListBox*>(item)->field(); + if(field) { + fields.append(field); + } + } + } + + // if reordering fields, need to add ReadOnly fields since they were not shown + if(m_reordered) { + Data::FieldVec allFields = m_coll->fields(); + for(Data::FieldVec::Iterator it = allFields.begin(); it != allFields.end(); ++it) { + if(it->type() == Data::Field::ReadOnly) { + fields.append(it); + } + } + } + + if(fields.count() > 0) { + Kernel::self()->reorderFields(fields); + } + + // commit command group + Kernel::self()->endCommandGroup(); + + // now clear copied fields + m_copiedFields.clear(); + // clear new ones, too + m_newFields.clear(); + + m_currentField = static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->field(); + + // the field type might have changed, so need to update the type combo list with possible values + if(m_currentField) { + // set the updating flag since the values are changing and slots are firing + // but we don't care about UI indications of changes + bool wasUpdating = m_updatingValues; + m_updatingValues = true; + QString currType = m_typeCombo->currentText(); + m_typeCombo->clear(); + m_typeCombo->insertStringList(newTypesAllowed(m_currentField->type())); + m_typeCombo->setCurrentItem(currType); + // description might have been changed for dependent fields + m_descEdit->setText(m_currentField->description()); + m_updatingValues = wasUpdating; + } + enableButtonApply(false); +} + +void CollectionFieldsDialog::slotNew() { + // first update the current one with all the values from the edit widgets + updateField(); + + // next check old values + if(!checkValues()) { + return; + } + + QString name = QString::fromLatin1("custom") + QString::number(m_newFields.count()+1); + int count = m_newFields.count() + 1; + QString title = i18n("New Field") + QString::fromLatin1(" %1").arg(count); + while(m_fieldsBox->findItem(title)) { + ++count; + title = i18n("New Field") + QString::fromLatin1(" %1").arg(count); + } + + Data::FieldPtr field = new Data::Field(name, title); + m_newFields.append(field); +// myDebug() << "CollectionFieldsDialog::slotNew() - adding new field " << title << endl; + + m_currentField = field; + + FieldListBox* box = new FieldListBox(m_fieldsBox, field); + m_fieldsBox->setSelected(box, true); + box->setColored(true); + m_fieldsBox->ensureCurrentVisible(); + slotModified(); + m_titleEdit->setFocus(); + m_titleEdit->selectAll(); +} + +void CollectionFieldsDialog::slotDelete() { + if(!m_currentField) { + return; + } + + if(m_newFields.contains(m_currentField)) { + // remove field from vector before deleting item containing field + m_newFields.remove(m_currentField); + m_fieldsBox->removeItem(m_fieldsBox->currentItem()); + m_fieldsBox->setSelected(m_fieldsBox->currentItem(), true); + m_fieldsBox->ensureCurrentVisible(); + m_currentField = static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->field(); // KShared gets auto-deleted + return; + } + + bool success = Kernel::self()->removeField(m_currentField); + if(success) { + emit signalCollectionModified(); + m_fieldsBox->removeItem(m_fieldsBox->currentItem()); + m_fieldsBox->setSelected(m_fieldsBox->currentItem(), true); + m_fieldsBox->ensureCurrentVisible(); + m_currentField = static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->field(); + enableButtonOK(true); + } +} + +void CollectionFieldsDialog::slotTypeChanged(const QString& type_) { + Data::Field::Type type = Data::Field::Undef; + const Data::Field::FieldMap fieldMap = Data::Field::typeMap(); + for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { + if(it.data() == type_) { + type = it.key(); + break; + } + } + if(type == Data::Field::Undef) { + kdWarning() << "CollectionFieldsDialog::slotTypeChanged() - type name not recognized: " << type_ << endl; + type = Data::Field::Line; + } + + // only choice types gets allowed values + m_allowEdit->setEnabled(type == Data::Field::Choice); + + // paragraphs, tables, and images are their own category + bool isCategory = (type == Data::Field::Para || type == Data::Field::Table || + type == Data::Field::Table2 || type == Data::Field::Image); + m_catCombo->setEnabled(!isCategory); + + // formatting is only applicable when the type is simple text or a table + bool isText = (type == Data::Field::Line || type == Data::Field::Table || + type == Data::Field::Table2); + // formatNone is the default + m_formatPlain->setEnabled(isText); + m_formatName->setEnabled(isText); + m_formatTitle->setEnabled(isText); + + // multiple is only applicable for simple text and number + isText = (type == Data::Field::Line || type == Data::Field::Number); + m_multiple->setEnabled(isText); + + // completion is only applicable for simple text, number, and URL + isText = (isText || type == Data::Field::URL); + m_complete->setEnabled(isText); + + // grouping is not possible with paragraphs or images + m_grouped->setEnabled(type != Data::Field::Para && type != Data::Field::Image); +} + +void CollectionFieldsDialog::slotHighlightedChanged(int index_) { +// myDebug() << "CollectionFieldsDialog::slotHighlightedChanged() - " << index_ << endl; + + // use this instead of blocking signals everywhere + m_updatingValues = true; + + // first update the current one with all the values from the edit widgets + updateField(); + + // next check old values + if(!checkValues()) { + m_fieldsBox->blockSignals(true); + m_fieldsBox->setSelected(m_oldIndex, true); + m_fieldsBox->blockSignals(false); + m_updatingValues = false; + return; + } + m_oldIndex = index_; + + m_btnUp->setEnabled(index_ > 0); + m_btnDown->setEnabled(index_ < static_cast<int>(m_fieldsBox->count())-1); + + FieldListBox* item = dynamic_cast<FieldListBox*>(m_fieldsBox->item(index_)); + if(!item) { + return; + } + + // need to get a pointer to the field with the new values to insert + Data::FieldPtr field = item->field(); + if(!field) { + myDebug() << "CollectionFieldsDialog::slotHighlightedChanged() - no field found!" << endl; + return; + } + + m_titleEdit->setText(field->title()); + + // type is limited to certain types, unless it's a new field + m_typeCombo->clear(); + if(m_newFields.contains(field)) { + m_typeCombo->insertStringList(newTypesAllowed(Data::Field::Undef)); + } else { + m_typeCombo->insertStringList(newTypesAllowed(field->type())); + } + // if the current name is not there, then this will change the list! + const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); + m_typeCombo->setCurrentText(fieldMap[field->type()]); + slotTypeChanged(fieldMap[field->type()]); // just setting the text doesn't emit the activated signal + + if(field->type() == Data::Field::Choice) { + m_allowEdit->setText(field->allowed().join(QString::fromLatin1("; "))); + } else { + m_allowEdit->clear(); + } + + m_catCombo->setCurrentText(field->category()); // have to do this here + m_descEdit->setText(field->description()); + m_defaultEdit->setText(field->defaultValue()); + + switch(field->formatFlag()) { + case Data::Field::FormatNone: + case Data::Field::FormatDate: // as yet unimplemented + m_formatNone->setChecked(true); + break; + + case Data::Field::FormatPlain: + m_formatPlain->setChecked(true); + break; + + case Data::Field::FormatTitle: + m_formatTitle->setChecked(true); + break; + + case Data::Field::FormatName: + m_formatName->setChecked(true); + break; + + default: + kdWarning() << "CollectionFieldsDialog::slotHighlightedChanged() - no format type!" << endl; + break; + } + + int flags = field->flags(); + m_complete->setChecked(flags & Data::Field::AllowCompletion); + m_multiple->setChecked(flags & Data::Field::AllowMultiple); + m_grouped->setChecked(flags & Data::Field::AllowGrouped); + + m_btnDelete->setEnabled(!(flags & Data::Field::NoDelete)); + + // default button is enabled only if default collection contains the field + if(m_defaultCollection) { + bool hasField = m_defaultCollection->hasField(field->name()); + actionButton(KDialogBase::Default)->setEnabled(hasField); + } + + m_currentField = field; + m_updatingValues = false; +} + +void CollectionFieldsDialog::updateField() { +// myDebug() << "CollectionFieldsDialog::updateField()" << endl; + Data::FieldPtr field = m_currentField; + if(!field || !m_modified) { + return; + } + + // only update name if it's one of the new ones + if(m_newFields.contains(field)) { + // name needs to be a valid XML element name + QString name = XML::elementName(m_titleEdit->text().lower()); + if(name.isEmpty()) { // might end up with empty string + name = QString::fromLatin1("custom") + QString::number(m_newFields.count()+1); + } + while(m_coll->hasField(name)) { // ensure name uniqueness + name += QString::fromLatin1("-new"); + } + field->setName(name); + } + + const QString title = m_titleEdit->text().simplifyWhiteSpace(); + updateTitle(title); + + const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); + for(Data::Field::FieldMap::ConstIterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { + if(it.data() == m_typeCombo->currentText()) { + field->setType(it.key()); + break; + } + } + + if(field->type() == Data::Field::Choice) { + const QRegExp rx(QString::fromLatin1("\\s*;\\s*")); + field->setAllowed(QStringList::split(rx, m_allowEdit->text())); + field->setProperty(QString::fromLatin1("minimum"), QString::null); + field->setProperty(QString::fromLatin1("maximum"), QString::null); + } else if(field->type() == Data::Field::Rating) { + QString v = field->property(QString::fromLatin1("minimum")); + if(v.isEmpty()) { + field->setProperty(QString::fromLatin1("minimum"), QString::number(1)); + } + v = field->property(QString::fromLatin1("maximum")); + if(v.isEmpty()) { + field->setProperty(QString::fromLatin1("maximum"), QString::number(5)); + } + } + + if(field->isSingleCategory()) { + field->setCategory(field->title()); + } else { + QString category = m_catCombo->currentText().simplifyWhiteSpace(); + field->setCategory(category); + m_catCombo->setCurrentItem(category, true); // if it doesn't exist, it's added + } + + field->setDescription(m_descEdit->text()); + field->setDefaultValue(m_defaultEdit->text()); + + if(m_formatTitle->isChecked()) { + field->setFormatFlag(Data::Field::FormatTitle); + } else if(m_formatName->isChecked()) { + field->setFormatFlag(Data::Field::FormatName); + } else if(m_formatPlain->isChecked()) { + field->setFormatFlag(Data::Field::FormatPlain); + } else { + field->setFormatFlag(Data::Field::FormatNone); + } + + int flags = 0; + if(m_complete->isChecked()) { + flags |= Data::Field::AllowCompletion; + } + if(m_grouped->isChecked()) { + flags |= Data::Field::AllowGrouped; + } + if(m_multiple->isChecked()) { + flags |= Data::Field::AllowMultiple; + } + field->setFlags(flags); + + m_modified = false; +} + +// The purpose here is to first set the modified flag. Then, if the field being edited is one +// that exists in the collection already, a deep copy needs to be made. +void CollectionFieldsDialog::slotModified() { +// myDebug() << "CollectionFieldsDialog::slotModified()" << endl; + // if I'm just updating the values, I don't care + if(m_updatingValues) { + return; + } + + m_modified = true; + + enableButtonOK(true); + enableButtonApply(true); + + if(!m_currentField) { + myDebug() << "CollectionFieldsDialog::slotModified() - no current field!" << endl; + m_currentField = static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->field(); + } + + // color the text + static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->setColored(true); + + // check if copy exists already + if(m_copiedFields.contains(m_currentField)) { + return; + } + + // or, check if is a new field, in which case no copy is needed + // check if copy exists already + if(m_newFields.contains(m_currentField)) { + return; + } + + m_currentField = new Data::Field(*m_currentField); + m_copiedFields.append(m_currentField); + static_cast<FieldListBox*>(m_fieldsBox->selectedItem())->setField(m_currentField); +} + +void CollectionFieldsDialog::updateTitle(const QString& title_) { +// myDebug() << "CollectionFieldsDialog::updateTitle()" << endl; + if(m_currentField && m_currentField->title() != title_) { + m_fieldsBox->blockSignals(true); + FieldListBox* oldItem = findItem(m_fieldsBox, m_currentField); + if(!oldItem) { + return; + } + oldItem->setText(title_); + // will always be colored since it's new + oldItem->setColored(true); + m_fieldsBox->triggerUpdate(true); + + m_currentField->setTitle(title_); + m_fieldsBox->blockSignals(false); + } +} + +void CollectionFieldsDialog::slotDefault() { + if(!m_currentField) { + return; + } + + Data::FieldPtr defaultField = m_defaultCollection->fieldByName(m_currentField->name()); + if(!defaultField) { + return; + } + + QString caption = i18n("Revert Field Properties"); + QString text = i18n("<qt><p>Do you really want to revert the properties for the <em>%1</em> " + "field back to their default values?</p></qt>").arg(m_currentField->title()); + QString dontAsk = QString::fromLatin1("RevertFieldProperties"); + int ret = KMessageBox::warningContinueCancel(this, text, caption, i18n("Revert"), dontAsk); + if(ret != KMessageBox::Continue) { + return; + } + + // now update all values with default + m_updatingValues = true; + m_titleEdit->setText(defaultField->title()); + + const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); + m_typeCombo->setCurrentText(fieldMap[defaultField->type()]); + slotTypeChanged(fieldMap[defaultField->type()]); // just setting the text doesn't emit the activated signal + + if(defaultField->type() == Data::Field::Choice) { + m_allowEdit->setText(defaultField->allowed().join(QString::fromLatin1("; "))); + } else { + m_allowEdit->clear(); + } + + m_catCombo->setCurrentText(defaultField->category()); // have to do this here + m_descEdit->setText(defaultField->description()); + m_defaultEdit->setText(defaultField->defaultValue()); + + switch(defaultField->formatFlag()) { + case Data::Field::FormatNone: + case Data::Field::FormatDate: + m_formatNone->setChecked(true); + break; + + case Data::Field::FormatPlain: + m_formatPlain->setChecked(true); + break; + + case Data::Field::FormatTitle: + m_formatTitle->setChecked(true); + break; + + case Data::Field::FormatName: + m_formatName->setChecked(true); + break; + + default: + break; + } + + int flags = defaultField->flags(); + m_complete->setChecked(flags & Data::Field::AllowCompletion); + m_multiple->setChecked(flags & Data::Field::AllowMultiple); + m_grouped->setChecked(flags & Data::Field::AllowGrouped); + + m_btnDelete->setEnabled(!(defaultField->flags() & Data::Field::NoDelete)); + +// m_titleEdit->setFocus(); +// m_titleEdit->selectAll(); + + m_updatingValues = false; + slotModified(); +} + +void CollectionFieldsDialog::slotMoveUp() { + QListBoxItem* item = m_fieldsBox->selectedItem(); + if(item) { + FieldListBox* prev = static_cast<FieldListBox*>(item->prev()); // could be 0 + if(prev) { + FieldListBox* newPrev = new FieldListBox(m_fieldsBox, prev->field(), item); + newPrev->setColored(prev->isColored()); + delete prev; + m_fieldsBox->ensureCurrentVisible(); + // since the current one doesn't get re-highlighted, need to highlighted doesn't get emitted + slotHighlightedChanged(m_fieldsBox->currentItem()); + } + } + m_reordered = true; + // don't call slotModified() since that creates a deep copy. + m_modified = true; + + enableButtonOK(true); + enableButtonApply(true); +} + +void CollectionFieldsDialog::slotMoveDown() { + FieldListBox* item = dynamic_cast<FieldListBox*>(m_fieldsBox->selectedItem()); + if(item) { + QListBoxItem* next = item->next(); // could be 0 + if(next) { + FieldListBox* newItem = new FieldListBox(m_fieldsBox, item->field(), next); + newItem->setColored(item->isColored()); + delete item; + m_fieldsBox->setSelected(newItem, true); + m_fieldsBox->ensureCurrentVisible(); + } + } + m_reordered = true; + // don't call slotModified() since that creates a deep copy. + m_modified = true; + + enableButtonOK(true); + enableButtonApply(true); +} + +Tellico::FieldListBox* CollectionFieldsDialog::findItem(const QListBox* box_, Data::FieldPtr field_) { +// myDebug() << "CollectionFieldsDialog::findItem()" << endl; + for(QListBoxItem* item = box_->firstItem(); item; item = item->next()) { + FieldListBox* textItem = static_cast<FieldListBox*>(item); + if(textItem->field() == field_) { + return textItem; + } + } + return 0; +} + +bool CollectionFieldsDialog::slotShowExtendedProperties() { + if(!m_currentField) { + return false; + } + + // the default value is included in properties, but it has a + // separate edit box + QString dv = m_currentField->defaultValue(); + StringMap props = m_currentField->propertyList(); + props.remove(QString::fromLatin1("default")); + + StringMapDialog dlg(props, this, "ExtendedPropertiesDialog", true); + dlg.setCaption(i18n("Extended Field Properties")); + dlg.setLabels(i18n("Property"), i18n("Value")); + if(dlg.exec() == QDialog::Accepted) { + props = dlg.stringMap(); + if(!dv.isEmpty()) { + props.insert(QString::fromLatin1("default"), dv); + } + m_currentField->setPropertyList(props); + slotModified(); + return true; + } + return false; +} + +bool CollectionFieldsDialog::checkValues() { + if(!m_currentField) { + return true; + } + + const QString title = m_currentField->title(); + // find total number of boxes with this title in case multiple new ones with same title were added + int titleCount = 0; + for(uint i = 0; i < m_fieldsBox->count(); ++i) { + if(m_fieldsBox->item(i)->text() == title) { + ++titleCount; + } + } + if((m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) || + titleCount > 1) { + // already have a field with this title + KMessageBox::sorry(this, i18n("A field with this title already exists. Please enter a different title.")); + m_titleEdit->selectAll(); + return false; + } + + const QString category = m_currentField->category(); + if(category.isEmpty()) { + KMessageBox::sorry(this, i18n("<qt>The category may not be empty. Please enter a category.</qt>")); + m_catCombo->lineEdit()->selectAll(); + return false; + } + + Data::FieldVec fields = m_coll->fieldsByCategory(category); + if(!fields.isEmpty() && fields.begin()->isSingleCategory() && fields.begin()->name() != m_currentField->name()) { + // can't have this category, cause it conflicts with a single-category field + KMessageBox::sorry(this, i18n("<qt>A field may not be in the same category as a <em>Paragraph</em>, " + "<em>Table</em> or <em>Image</em> field. Please enter a different category.</qt>")); + m_catCombo->lineEdit()->selectAll(); + return false; + } + + // the combobox is disabled for single-category fields + if(!m_catCombo->isEnabled() && m_coll->fieldByTitle(title) && m_coll->fieldNameByTitle(title) != m_currentField->name()) { + KMessageBox::sorry(this, i18n("A field's title may not be the same as an existing category. " + "Please enter a different title.")); + m_titleEdit->selectAll(); + return false; + } + + // check for rating values outside bounds + if(m_currentField->type() == Data::Field::Rating) { + bool ok; // ok to ignore this here + int low = Tellico::toUInt(m_currentField->property(QString::fromLatin1("minimum")), &ok); + int high = Tellico::toUInt(m_currentField->property(QString::fromLatin1("maximum")), &ok); + while(low < 1 || low > 9 || high < 1 || high > 10 || low >= high) { + KMessageBox::sorry(this, i18n("The range for a rating field must be between 1 and 10, " + "and the lower bound must be less than the higher bound. " + "Please enter different low and high properties.")); + if(slotShowExtendedProperties()) { + low = Tellico::toUInt(m_currentField->property(QString::fromLatin1("minimum")), &ok); + high = Tellico::toUInt(m_currentField->property(QString::fromLatin1("maximum")), &ok); + } else { + return false; + } + } + } else if(m_currentField->type() == Data::Field::Table) { + bool ok; // ok to ignore this here + int ncols = Tellico::toUInt(m_currentField->property(QString::fromLatin1("columns")), &ok); + // also enforced in GUI::TableFieldWidget + if(ncols > 10) { + KMessageBox::sorry(this, i18n("Tables are limited to a maximum of ten columns.")); + m_currentField->setProperty(QString::fromLatin1("columns"), QString::fromLatin1("10")); + } + } + + return true; +} + +// only certain type changes are allowed +QStringList CollectionFieldsDialog::newTypesAllowed(int type_ /*=0*/) { + // Undef means return all + if(type_ == Data::Field::Undef) { + return Data::Field::typeTitles(); + } + + const Data::Field::FieldMap& fieldMap = Data::Field::typeMap(); + + QStringList newTypes; + switch(type_) { + case Data::Field::Line: // might not work if converted to a number or URL, but ok + case Data::Field::Number: + case Data::Field::URL: + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Para]; + newTypes += fieldMap[Data::Field::Number]; + newTypes += fieldMap[Data::Field::URL]; + newTypes += fieldMap[Data::Field::Table]; + break; + + case Data::Field::Date: + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Date]; + break; + + case Data::Field::Bool: // doesn't really make sense, but can't hurt + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Para]; + newTypes += fieldMap[Data::Field::Bool]; + newTypes += fieldMap[Data::Field::Number]; + newTypes += fieldMap[Data::Field::URL]; + newTypes += fieldMap[Data::Field::Table]; + break; + + case Data::Field::Choice: + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Para]; + newTypes += fieldMap[Data::Field::Choice]; + newTypes += fieldMap[Data::Field::Number]; + newTypes += fieldMap[Data::Field::URL]; + newTypes += fieldMap[Data::Field::Table]; + newTypes += fieldMap[Data::Field::Rating]; + break; + + case Data::Field::Table: // not really a good idea since the "::" will be exposed, but allow it + case Data::Field::Table2: + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Number]; + newTypes += fieldMap[Data::Field::Table]; + break; + + case Data::Field::Para: + newTypes += fieldMap[Data::Field::Line]; + newTypes += fieldMap[Data::Field::Para]; + break; + + case Data::Field::Rating: + newTypes += fieldMap[Data::Field::Choice]; + newTypes += fieldMap[Data::Field::Rating]; + break; + + // these can never be changed + case Data::Field::Image: + case Data::Field::Dependent: + newTypes = fieldMap[static_cast<Data::Field::Type>(type_)]; + break; + + default: + myDebug() << "CollectionFieldsDialog::newTypesAllowed() - no match for " << type_ << endl; + newTypes = Data::Field::typeTitles(); + break; + } + return newTypes; +} + +#include "collectionfieldsdialog.moc" diff --git a/src/collectionfieldsdialog.h b/src/collectionfieldsdialog.h new file mode 100644 index 0000000..cad0492 --- /dev/null +++ b/src/collectionfieldsdialog.h @@ -0,0 +1,127 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef COLLECTIONFIELDSDIALOG_H +#define COLLECTIONFIELDSDIALOG_H + +class KComboBox; +class KLineEdit; +class KPushButton; + +class QRadioButton; +class QCheckBox; +class QPainter; + +#include "datavectors.h" +#include "gui/listboxtext.h" + +#include <kdialogbase.h> + +#include <qmap.h> + +namespace Tellico { + namespace Data { + class Collection; + } + +class FieldListBox : public GUI::ListBoxText { +public: + FieldListBox(QListBox* listbox, Data::FieldPtr field); + FieldListBox(QListBox* listbox, Data::FieldPtr field, QListBoxItem* after); + + Data::FieldPtr field() const { return m_field; } + void setField(Data::FieldPtr field) { m_field = field; } + +private: + Data::FieldPtr m_field; +}; + +/** + * @author Robby Stephenson + */ +class CollectionFieldsDialog : public KDialogBase { +Q_OBJECT + +public: + /** + * The constructor sets up the dialog. + * + * @param coll A pointer to the collection parent of all the attributes + * @param parent A pointer to the parent widget + * @param name The widget name + */ + CollectionFieldsDialog(Data::CollPtr coll, QWidget* parent, const char* name=0); + ~CollectionFieldsDialog(); + +signals: + void signalCollectionModified(); + +protected slots: + virtual void slotOk(); + virtual void slotApply(); + virtual void slotDefault(); + +private slots: + void slotNew(); + void slotDelete(); + void slotMoveUp(); + void slotMoveDown(); + void slotTypeChanged(const QString& type); + void slotHighlightedChanged(int index); + void slotModified(); + bool slotShowExtendedProperties(); + void slotSelectInitial(); + +private: + void applyChanges(); + void updateField(); + void updateTitle(const QString& title); + bool checkValues(); + FieldListBox* findItem(const QListBox* box, Data::FieldPtr field); + QStringList newTypesAllowed(int type); + + Data::CollPtr m_coll; + Data::CollPtr m_defaultCollection; + Data::FieldVec m_copiedFields; + Data::FieldVec m_newFields; + Data::FieldPtr m_currentField; + bool m_modified; + bool m_updatingValues; + bool m_reordered; + int m_oldIndex; + + QListBox* m_fieldsBox; + KPushButton* m_btnNew; + KPushButton* m_btnDelete; + KPushButton* m_btnUp; + KPushButton* m_btnDown; + + KLineEdit* m_titleEdit; + KComboBox* m_typeCombo; + KLineEdit* m_allowEdit; + KLineEdit* m_defaultEdit; + KComboBox* m_catCombo; + KLineEdit* m_descEdit; + KPushButton* m_btnExtended; + + QRadioButton* m_formatNone; + QRadioButton* m_formatPlain; + QRadioButton* m_formatTitle; + QRadioButton* m_formatName; + QCheckBox* m_complete; + QCheckBox* m_multiple; + QCheckBox* m_grouped; +}; + +} // end namespace +#endif diff --git a/src/collections/Makefile.am b/src/collections/Makefile.am new file mode 100644 index 0000000..21543d0 --- /dev/null +++ b/src/collections/Makefile.am @@ -0,0 +1,32 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = libcollections.a + +AM_CPPFLAGS = $(all_includes) + +libcollections_a_METASOURCES = AUTO + +libcollections_a_SOURCES = winecollection.cpp stampcollection.cpp \ + comicbookcollection.cpp cardcollection.cpp coincollection.cpp \ + bibtexcollection.cpp musiccollection.cpp videocollection.cpp \ + bookcollection.cpp gamecollection.cpp filecatalog.cpp \ + boardgamecollection.cpp + +####### kdevelop will overwrite this part!!! (end)############ + +KDE_OPTIONS = noautodist + +CLEANFILES = *~ *.loT + +EXTRA_DIST = \ +bookcollection.cpp bookcollection.h \ +videocollection.cpp videocollection.h \ +musiccollection.cpp musiccollection.h \ +bibtexcollection.cpp bibtexcollection.h \ +coincollection.cpp coincollection.h \ +cardcollection.cpp cardcollection.h \ +comicbookcollection.cpp comicbookcollection.h \ +stampcollection.cpp stampcollection.h \ +winecollection.cpp winecollection.h \ +gamecollection.h gamecollection.cpp \ +filecatalog.h filecatalog.cpp \ +boardgamecollection.h boardgamecollection.cpp diff --git a/src/collections/bibtexcollection.cpp b/src/collections/bibtexcollection.cpp new file mode 100644 index 0000000..f069cc9 --- /dev/null +++ b/src/collections/bibtexcollection.cpp @@ -0,0 +1,400 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bibtexcollection.h" +#include "../latin1literal.h" +#include "../document.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Data::BibtexCollection; + +namespace { + static const char* bibtex_general = I18N_NOOP("General"); + static const char* bibtex_publishing = I18N_NOOP("Publishing"); + static const char* bibtex_misc = I18N_NOOP("Miscellaneous"); +} + +BibtexCollection::BibtexCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("Bibliography") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("author")); + + // Bibtex has some default macros for the months + addMacro(QString::fromLatin1("jan"), QString::null); + addMacro(QString::fromLatin1("feb"), QString::null); + addMacro(QString::fromLatin1("mar"), QString::null); + addMacro(QString::fromLatin1("apr"), QString::null); + addMacro(QString::fromLatin1("may"), QString::null); + addMacro(QString::fromLatin1("jun"), QString::null); + addMacro(QString::fromLatin1("jul"), QString::null); + addMacro(QString::fromLatin1("aug"), QString::null); + addMacro(QString::fromLatin1("sep"), QString::null); + addMacro(QString::fromLatin1("oct"), QString::null); + addMacro(QString::fromLatin1("nov"), QString::null); + addMacro(QString::fromLatin1("dec"), QString::null); +} + +Tellico::Data::FieldVec BibtexCollection::defaultFields() { + FieldVec list; + FieldPtr field; + +/******************* General ****************************/ + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("title")); + field->setCategory(i18n("General")); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + QStringList types; + types << QString::fromLatin1("article") << QString::fromLatin1("book") + << QString::fromLatin1("booklet") << QString::fromLatin1("inbook") + << QString::fromLatin1("incollection") << QString::fromLatin1("inproceedings") + << QString::fromLatin1("manual") << QString::fromLatin1("mastersthesis") + << QString::fromLatin1("misc") << QString::fromLatin1("phdthesis") + << QString::fromLatin1("proceedings") << QString::fromLatin1("techreport") + << QString::fromLatin1("unpublished") << QString::fromLatin1("periodical") + << QString::fromLatin1("conference"); + field = new Field(QString::fromLatin1("entry-type"), i18n("Entry Type"), types); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("entry-type")); + field->setCategory(i18n(bibtex_general)); + field->setFlags(Field::AllowGrouped | Field::NoDelete); + field->setDescription(i18n("These entry types are specific to bibtex. See the bibtex documentation.")); + list.append(field); + + field = new Field(QString::fromLatin1("author"), i18n("Author")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("author")); + field->setCategory(i18n(bibtex_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("bibtex-key"), i18n("Bibtex Key")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("key")); + field->setCategory(i18n("General")); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("booktitle"), i18n("Book Title")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("booktitle")); + field->setCategory(i18n(bibtex_general)); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("editor"), i18n("Editor")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("editor")); + field->setCategory(i18n(bibtex_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("organization"), i18n("Organization")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("organization")); + field->setCategory(i18n(bibtex_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + +// field = new Field(QString::fromLatin1("institution"), i18n("Institution")); +// field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("institution")); +// field->setCategory(i18n(bibtex_general)); +// field->setFlags(Field::AllowDelete); +// field->setFormatFlag(Field::FormatTitle); +// list.append(field); + +/******************* Publishing ****************************/ + field = new Field(QString::fromLatin1("publisher"), i18n("Publisher")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("publisher")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("address"), i18n("Address")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("address")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("edition"), i18n("Edition")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("edition")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowCompletion); + list.append(field); + + // don't make it a nuumber, it could have latex processing commands in it + field = new Field(QString::fromLatin1("pages"), i18n("Pages")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("pages")); + field->setCategory(i18n(bibtex_publishing)); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Year"), Field::Number); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("year")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("isbn"), i18n("ISBN#")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("isbn")); + field->setCategory(i18n(bibtex_publishing)); + field->setDescription(i18n("International Standard Book Number")); + list.append(field); + + field = new Field(QString::fromLatin1("journal"), i18n("Journal")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("journal")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("doi"), i18n("DOI")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("doi")); + field->setCategory(i18n(bibtex_publishing)); + field->setDescription(i18n("Digital Object Identifier")); + list.append(field); + + // could make this a string list, but since bibtex import could have funky values + // keep it an editbox + field = new Field(QString::fromLatin1("month"), i18n("Month")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("month")); + field->setCategory(i18n(bibtex_publishing)); + field->setFlags(Field::AllowCompletion); + list.append(field); + + field = new Field(QString::fromLatin1("number"), i18n("Number"), Field::Number); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("number")); + field->setCategory(i18n(bibtex_publishing)); + list.append(field); + + field = new Field(QString::fromLatin1("howpublished"), i18n("How Published")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("howpublished")); + field->setCategory(i18n(bibtex_publishing)); + list.append(field); + +// field = new Field(QString::fromLatin1("school"), i18n("School")); +// field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("school")); +// field->setCategory(i18n(bibtex_publishing)); +// field->setFlags(Field::AllowCompletion | Field::AllowGrouped); +// list.append(field); + +/******************* Classification ****************************/ + field = new Field(QString::fromLatin1("chapter"), i18n("Chapter"), Field::Number); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("chapter")); + field->setCategory(i18n(bibtex_misc)); + list.append(field); + + field = new Field(QString::fromLatin1("series"), i18n("Series")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("series")); + field->setCategory(i18n(bibtex_misc)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("volume"), i18n("Volume"), Field::Number); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("volume")); + field->setCategory(i18n(bibtex_misc)); + list.append(field); + + field = new Field(QString::fromLatin1("crossref"), i18n("Cross-Reference")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("crossref")); + field->setCategory(i18n(bibtex_misc)); + list.append(field); + +// field = new Field(QString::fromLatin1("annote"), i18n("Annotation")); +// field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("annote")); +// field->setCategory(i18n(bibtex_misc)); +// list.append(field); + + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("keywords")); + field->setCategory(i18n(bibtex_misc)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("url"), i18n("URL"), Field::URL); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("url")); + field->setCategory(i18n(bibtex_misc)); + list.append(field); + + field = new Field(QString::fromLatin1("abstract"), i18n("Abstract"), Data::Field::Para); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("abstract")); + list.append(field); + + field = new Field(QString::fromLatin1("note"), i18n("Notes"), Field::Para); + field->setProperty(QString::fromLatin1("bibtex"), QString::fromLatin1("note")); + list.append(field); + + return list; +} + +bool BibtexCollection::addField(FieldPtr field_) { + if(!field_) { + return false; + } +// myDebug() << "BibtexCollection::addField()" << endl; + bool success = Collection::addField(field_); + if(success) { + QString bibtex = field_->property(QString::fromLatin1("bibtex")); + if(!bibtex.isEmpty()) { + m_bibtexFieldDict.insert(bibtex, field_); + } + } + return success; +} + +bool BibtexCollection::modifyField(FieldPtr newField_) { + if(!newField_) { + return false; + } +// myDebug() << "BibtexCollection::modifyField()" << endl; + bool success = Collection::modifyField(newField_); + FieldPtr oldField = fieldByName(newField_->name()); + QString oldBibtex = oldField->property(QString::fromLatin1("bibtex")); + QString newBibtex = newField_->property(QString::fromLatin1("bibtex")); + if(!oldBibtex.isEmpty()) { + success &= m_bibtexFieldDict.remove(oldBibtex); + } + if(!newBibtex.isEmpty()) { + oldField->setProperty(QString::fromLatin1("bibtex"), newBibtex); + m_bibtexFieldDict.insert(newBibtex, oldField); + } + return success; +} + +bool BibtexCollection::deleteField(FieldPtr field_, bool force_) { + if(!field_) { + return false; + } +// myDebug() << "BibtexCollection::deleteField()" << endl; + bool success = true; + QString bibtex = field_->property(QString::fromLatin1("bibtex")); + if(!bibtex.isEmpty()) { + success &= m_bibtexFieldDict.remove(bibtex); + } + return success && Collection::removeField(field_, force_); +} + +Tellico::Data::FieldPtr BibtexCollection::fieldByBibtexName(const QString& bibtex_) const { + return m_bibtexFieldDict.isEmpty() ? 0 : m_bibtexFieldDict.find(bibtex_); +} + +// same as BookCollection::sameEntry() +int BibtexCollection::sameEntry(Data::EntryPtr entry1_, Data::EntryPtr entry2_) const { + // equal isbn's or lccn's are easy, give it a weight of 100 + if(Entry::compareValues(entry1_, entry2_, QString::fromLatin1("isbn"), this) > 0 || + Entry::compareValues(entry1_, entry2_, QString::fromLatin1("lccn"), this) > 0 || + Entry::compareValues(entry1_, entry2_, QString::fromLatin1("doi"), this) > 0 || + Entry::compareValues(entry1_, entry2_, QString::fromLatin1("pmid"), this) > 0 || + Entry::compareValues(entry1_, entry2_, QString::fromLatin1("arxiv"), this) > 0) { + return 100; // good match + } + int res = 3*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("title"), this); +// if(res == 0) { +// myDebug() << "BookCollection::sameEntry() - different titles for " << entry1_->title() << " vs. " +// << entry2_->title() << endl; +// } + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("author"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("cr_year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("pub_year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("binding"), this); + return res; +} + +// static +Tellico::Data::CollPtr BibtexCollection::convertBookCollection(CollPtr coll_) { + const QString bibtex = QString::fromLatin1("bibtex"); + KSharedPtr<BibtexCollection> coll = new BibtexCollection(false, coll_->title()); + FieldVec fields = coll_->fields(); + for(FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + FieldPtr field = new Data::Field(*fIt); + + // if it already has a bibtex property, skip it + if(!field->property(bibtex).isEmpty()) { + coll->addField(field); + continue; + } + + // be sure to set bibtex property before adding it though + QString name = field->name(); + // this first group has bibtex field names the same as their own field name + if(name == Latin1Literal("title") + || name == Latin1Literal("author") + || name == Latin1Literal("editor") + || name == Latin1Literal("edition") + || name == Latin1Literal("publisher") + || name == Latin1Literal("isbn") + || name == Latin1Literal("lccn") + || name == Latin1Literal("url") + || name == Latin1Literal("language") + || name == Latin1Literal("pages") + || name == Latin1Literal("series")) { + field->setProperty(bibtex, name); + } else if(name == Latin1Literal("series_num")) { + field->setProperty(bibtex, QString::fromLatin1("number")); + } else if(name == Latin1Literal("pur_price")) { + field->setProperty(bibtex, QString::fromLatin1("price")); + } else if(name == Latin1Literal("cr_year")) { + field->setProperty(bibtex, QString::fromLatin1("year")); + } else if(name == Latin1Literal("bibtex-id")) { + field->setProperty(bibtex, QString::fromLatin1("key")); + } else if(name == Latin1Literal("keyword")) { + field->setProperty(bibtex, QString::fromLatin1("keywords")); + } else if(name == Latin1Literal("comments")) { + field->setProperty(bibtex, QString::fromLatin1("note")); + } + coll->addField(field); + } + + // also need to add required fields + FieldVec vec = defaultFields(); + for(FieldVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + if(!coll->hasField(it->name()) && (it->flags() & Field::NoDelete)) { + // but don't add a Bibtex Key if there's already a bibtex-id + if(it->property(bibtex) != Latin1Literal("key") + || !coll->hasField(QString::fromLatin1("bibtex-id"))) { + coll->addField(it); + } + } + } + + // set the entry-type to book + FieldPtr field = coll->fieldByBibtexName(QString::fromLatin1("entry-type")); + QString entryTypeName; + if(field) { + entryTypeName = field->name(); + } else { + kdWarning() << "BibtexCollection::convertBookCollection() - there must be an entry type field" << endl; + } + + EntryVec newEntries; + for(EntryVec::ConstIterator entryIt = coll_->entries().begin(); entryIt != coll_->entries().end(); ++entryIt) { + Data::EntryPtr entry = new Entry(*entryIt); + entry->setCollection(coll.data()); + if(!entryTypeName.isEmpty()) { + entry->setField(entryTypeName, QString::fromLatin1("book")); + } + newEntries.append(entry); + } + coll->addEntries(newEntries); + + // now need to make sure all images are transferred + Document::self()->loadAllImagesNow(); + + return coll.data(); +} + +#include "bibtexcollection.moc" diff --git a/src/collections/bibtexcollection.h b/src/collections/bibtexcollection.h new file mode 100644 index 0000000..628cc37 --- /dev/null +++ b/src/collections/bibtexcollection.h @@ -0,0 +1,70 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXCOLLECTION_H +#define BIBTEXCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection specifically for bibliographies, in Bibtex format. + * + * It has the following standard attributes: + * @li Title + * + * @author Robby Stephenson + */ +class BibtexCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields A boolean indicating whether the default attributes should be added + * @param title The title of the collection + */ + BibtexCollection(bool addFields, const QString& title = QString::null); + /** + */ + virtual ~BibtexCollection() {} + + virtual Type type() const { return Bibtex; } + virtual bool addField(FieldPtr field); + virtual bool modifyField(FieldPtr field); + virtual bool deleteField(FieldPtr field, bool force=false); + + FieldPtr fieldByBibtexName(const QString& name) const; + const QString& preamble() const { return m_preamble; } + void setPreamble(const QString& preamble) { m_preamble = preamble; } + const StringMap& macroList() const { return m_macros; } + void setMacroList(StringMap map) { m_macros = map; } + void addMacro(const QString& key, const QString& value) { m_macros.insert(key, value); } + + virtual int sameEntry(Data::EntryPtr entry1, Data::EntryPtr entry2) const; + + static FieldVec defaultFields(); + static CollPtr convertBookCollection(CollPtr coll); + +private: + QDict<Field> m_bibtexFieldDict; + QString m_preamble; + StringMap m_macros; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/boardgamecollection.cpp b/src/collections/boardgamecollection.cpp new file mode 100644 index 0000000..4899e0e --- /dev/null +++ b/src/collections/boardgamecollection.cpp @@ -0,0 +1,112 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson, Steve Beattie + email : robby@periapsis.org, sbeattie@suse.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "boardgamecollection.h" + +#include <klocale.h> + +namespace { + static const char* boardgame_general = I18N_NOOP("General"); + static const char* boardgame_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::BoardGameCollection; + +BoardGameCollection::BoardGameCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Board Games") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("genre")); +} + +Tellico::Data::FieldVec BoardGameCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("mechanism"), i18n("Mechanism")); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Release Year"), Field::Number); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("publisher"), i18n("Publisher")); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("designer"), i18n("Designer")); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("num-player"), i18n("Number of Players"), Field::Number); + field->setCategory(i18n(boardgame_general)); + field->setFlags(Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("description"), i18n("Description"), Field::Para); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Rating"), Field::Rating); + field->setCategory(i18n(boardgame_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(boardgame_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(boardgame_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(boardgame_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(boardgame_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(boardgame_personal)); + list.append(field); + + return list; +} + +#include "boardgamecollection.moc" diff --git a/src/collections/boardgamecollection.h b/src/collections/boardgamecollection.h new file mode 100644 index 0000000..642fc31 --- /dev/null +++ b/src/collections/boardgamecollection.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson, Steve Beattie + email : robby@periapsis.org, sbeattie@suse.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef BOARDGAMECOLLECTION_H +#define BOARDGAMECOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for board (not bored) games. + */ +class BoardGameCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + BoardGameCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return BoardGame; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/bookcollection.cpp b/src/collections/bookcollection.cpp new file mode 100644 index 0000000..0137156 --- /dev/null +++ b/src/collections/bookcollection.cpp @@ -0,0 +1,202 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bookcollection.h" + +#include <klocale.h> + +namespace { + static const char* book_general = I18N_NOOP("General"); + static const char* book_publishing = I18N_NOOP("Publishing"); + static const char* book_classification = I18N_NOOP("Classification"); + static const char* book_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::BookCollection; + +BookCollection::BookCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Books") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("author")); +} + +Tellico::Data::FieldVec BookCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setCategory(i18n("General")); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("subtitle"), i18n("Subtitle")); + field->setCategory(i18n(book_general)); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("author"), i18n("Author")); + field->setCategory(i18n(book_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("editor"), i18n("Editor")); + field->setCategory(i18n(book_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + QStringList binding; + binding << i18n("Hardback") << i18n("Paperback") << i18n("Trade Paperback") + << i18n("E-Book") << i18n("Magazine") << i18n("Journal"); + field = new Field(QString::fromLatin1("binding"), i18n("Binding"), binding); + field->setCategory(i18n(book_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(book_general)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(book_general)); + list.append(field); + + field = new Field(QString::fromLatin1("publisher"), i18n("Publisher")); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("edition"), i18n("Edition")); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowCompletion); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("cr_year"), i18n("Copyright Year"), Field::Number); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowGrouped | Field::AllowMultiple); + list.append(field); + + field = new Field(QString::fromLatin1("pub_year"), i18n("Publication Year"), Field::Number); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("isbn"), i18n("ISBN#")); + field->setCategory(i18n(book_publishing)); + field->setDescription(i18n("International Standard Book Number")); + list.append(field); + + field = new Field(QString::fromLatin1("lccn"), i18n("LCCN#")); + field->setCategory(i18n(book_publishing)); + field->setDescription(i18n("Library of Congress Control Number")); + list.append(field); + + field = new Field(QString::fromLatin1("pages"), i18n("Pages"), Field::Number); + field->setCategory(i18n(book_publishing)); + list.append(field); + + field = new Field(QString::fromLatin1("translator"), i18n("Translator")); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("language"), i18n("Language")); + field->setCategory(i18n(book_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped | Field::AllowMultiple); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(book_classification)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + // in document versions < 3, this was "keywords" and not "keyword" + // but the title didn't change, only the name + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setCategory(i18n(book_classification)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("series"), i18n("Series")); + field->setCategory(i18n(book_classification)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("series_num"), i18n("Series Number"), Field::Number); + field->setCategory(i18n(book_classification)); + list.append(field); + + QStringList cond; + cond << i18n("New") << i18n("Used"); + field = new Field(QString::fromLatin1("condition"), i18n("Condition"), cond); + field->setCategory(i18n(book_classification)); + list.append(field); + + field = new Field(QString::fromLatin1("signed"), i18n("Signed"), Field::Bool); + field->setCategory(i18n(book_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("read"), i18n("Read"), Field::Bool); + field->setCategory(i18n(book_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(book_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(book_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Rating"), Field::Rating); + field->setCategory(i18n(book_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Front Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(book_personal)); + list.append(field); + + return list; +} + +int BookCollection::sameEntry(Data::EntryPtr entry1_, Data::EntryPtr entry2_) const { + // equal isbn's or lccn's are easy, give it a weight of 100 + if(Entry::compareValues(entry1_, entry2_, QString::fromLatin1("isbn"), this) > 0 || + Entry::compareValues(entry1_, entry2_, QString::fromLatin1("lccn"), this) > 0) { + return 100; // good match + } + int res = 3*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("title"), this); +// if(res == 0) { +// myDebug() << "BookCollection::sameEntry() - different titles for " << entry1_->title() << " vs. " +// << entry2_->title() << endl; +// } + res += 2*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("author"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("cr_year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("pub_year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("binding"), this); + return res; +} + +#include "bookcollection.moc" diff --git a/src/collections/bookcollection.h b/src/collections/bookcollection.h new file mode 100644 index 0000000..0236dd6 --- /dev/null +++ b/src/collections/bookcollection.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BOOKCOLLECTION_H +#define BOOKCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for books. + * + * It has the following standard attributes: + * @li Title + * @li Subtitle + * @li Author + * @li Binding + * @li Purchase Date + * @li Purchase Price + * @li Publisher + * @li Edition + * @li Copyright Year + * @li Publication Year + * @li ISBN Number + * @li Library of Congress Catalog Number + * @li Pages + * @li Language + * @li Genre + * @li Keywords + * @li Series + * @li Series Number + * @li Condition + * @li Signed + * @li Read + * @li Gift + * @li Loaned + * @li Rating + * @li Comments + * + * @author Robby Stephenson + */ +class BookCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + BookCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Book; } + virtual int sameEntry(Data::EntryPtr entry1, Data::EntryPtr entry2) const; + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/cardcollection.cpp b/src/collections/cardcollection.cpp new file mode 100644 index 0000000..c29ad7f --- /dev/null +++ b/src/collections/cardcollection.cpp @@ -0,0 +1,121 @@ +/*************************************************************************** + copyright : (C) 2003-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 "cardcollection.h" + +#include <klocale.h> + +namespace { + static const char* card_general = I18N_NOOP("General"); + static const char* card_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::CardCollection; + +CardCollection::CardCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Cards") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("series")); +} + +Tellico::Data::FieldVec CardCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title"), Field::Dependent); + field->setCategory(i18n(card_general)); + field->setDescription(QString::fromLatin1("%{year} %{brand} %{player}")); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("player"), i18n("Player")); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("team"), i18n("Team")); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("brand"), i18n("Brand")); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + // might not be totally numeric! + field = new Field(QString::fromLatin1("number"), i18n("Card Number")); + field->setCategory(i18n(card_general)); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Year"), Field::Number); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("series"), i18n("Series")); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("type"), i18n("Card Type")); + field->setCategory(i18n(card_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(card_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(card_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("location"), i18n("Location")); + field->setCategory(i18n(card_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(card_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setCategory(i18n(card_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("quantity"), i18n("Quantity"), Field::Number); + field->setCategory(i18n(card_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("front"), i18n("Front Image"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("back"), i18n("Back Image"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + list.append(field); + + return list; +} + +#include "cardcollection.moc" diff --git a/src/collections/cardcollection.h b/src/collections/cardcollection.h new file mode 100644 index 0000000..61fd2b0 --- /dev/null +++ b/src/collections/cardcollection.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef CARDCOLLECTION_H +#define CARDCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for sports cards. + * + * It has the following standard attributes: + * @li Title + * + * @author Robby Stephenson + */ +class CardCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields A boolean indicating whether the default attributes should be added + * @param title The title of the collection + */ + CardCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Card; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/coincollection.cpp b/src/collections/coincollection.cpp new file mode 100644 index 0000000..1cd2ec0 --- /dev/null +++ b/src/collections/coincollection.cpp @@ -0,0 +1,135 @@ +/*************************************************************************** + copyright : (C) 2003-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 "coincollection.h" + +#include <klocale.h> + +namespace { + static const char* coin_general = I18N_NOOP("General"); + static const char* coin_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::CoinCollection; + +CoinCollection::CoinCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Coins") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("denomination")); +} + +Tellico::Data::FieldVec CoinCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title"), Field::Dependent); + field->setCategory(i18n(coin_general)); + // not i18n() + field->setDescription(QString::fromLatin1("%{year}%{mintmark} %{type} %{denomination}")); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("type"), i18n("Type")); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + /* TRANSLATORS: denomination refers to the monetary value. */ + field = new Field(QString::fromLatin1("denomination"), i18n("Denomination")); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Year"), Field::Number); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("mintmark"), i18n("Mint Mark")); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("country"), i18n("Country")); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("set"), i18n("Coin Set"), Field::Bool); + field->setCategory(i18n(coin_general)); + list.append(field); + + QStringList grade = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Coin grade levels - " + "Proof-65,Proof-60,Mint State-65,Mint State-60," + "Almost Uncirculated-55,Almost Uncirculated-50," + "Extremely Fine-40,Very Fine-30,Very Fine-20,Fine-12," + "Very Good-8,Good-4,Fair", + "Proof-65,Proof-60,Mint State-65,Mint State-60," + "Almost Uncirculated-55,Almost Uncirculated-50," + "Extremely Fine-40,Very Fine-30,Very Fine-20,Fine-12," + "Very Good-8,Good-4,Fair"), + false); + field = new Field(QString::fromLatin1("grade"), i18n("Grade"), grade); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + QStringList service = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Coin grading services - " + "PCGS,NGC,ANACS,ICG,ASA,PCI", + "PCGS,NGC,ANACS,ICG,ASA,PCI"), + false); + field = new Field(QString::fromLatin1("service"), i18n("Grading Service"), service); + field->setCategory(i18n(coin_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(coin_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(coin_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("location"), i18n("Location")); + field->setCategory(i18n(coin_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(coin_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("obverse"), i18n("Obverse"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("reverse"), i18n("Reverse"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(coin_personal)); + list.append(field); + + return list; +} + +#include "coincollection.moc" diff --git a/src/collections/coincollection.h b/src/collections/coincollection.h new file mode 100644 index 0000000..d22937d --- /dev/null +++ b/src/collections/coincollection.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef COINCOLLECTION_H +#define COINCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for coins. + * + * It has the following standard attributes: + * @li Title + * + * @author Robby Stephenson + */ +class CoinCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + CoinCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Coin; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/comicbookcollection.cpp b/src/collections/comicbookcollection.cpp new file mode 100644 index 0000000..30e1ef0 --- /dev/null +++ b/src/collections/comicbookcollection.cpp @@ -0,0 +1,157 @@ +/*************************************************************************** + copyright : (C) 2003-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 "comicbookcollection.h" + +#include <klocale.h> + +namespace { + static const char* comic_general = I18N_NOOP("General"); + static const char* comic_publishing = I18N_NOOP("Publishing"); + static const char* comic_classification = I18N_NOOP("Classification"); + static const char* comic_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::ComicBookCollection; + +ComicBookCollection::ComicBookCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Comic Books") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("series")); +} + +Tellico::Data::FieldVec ComicBookCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setCategory(i18n(comic_general)); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("subtitle"), i18n("Subtitle")); + field->setCategory(i18n(comic_general)); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("writer"), i18n("Writer")); + field->setCategory(i18n(comic_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("artist"), i18n("Comic Book Illustrator", "Artist")); + field->setCategory(i18n(comic_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("series"), i18n("Series")); + field->setCategory(i18n(comic_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("issue"), i18n("Issue"), Field::Number); + field->setCategory(i18n(comic_general)); + field->setFlags(Field::AllowMultiple); + list.append(field); + + field = new Field(QString::fromLatin1("publisher"), i18n("Publisher")); + field->setCategory(i18n(comic_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("edition"), i18n("Edition")); + field->setCategory(i18n(comic_publishing)); + field->setFlags(Field::AllowCompletion); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("pub_year"), i18n("Publication Year"), Field::Number); + field->setCategory(i18n(comic_publishing)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pages"), i18n("Pages"), Field::Number); + field->setCategory(i18n(comic_publishing)); + list.append(field); + + field = new Field(QString::fromLatin1("country"), i18n("Country")); + field->setCategory(i18n(comic_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped | Field::AllowMultiple); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("language"), i18n("Language")); + field->setCategory(i18n(comic_publishing)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped | Field::AllowMultiple); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(comic_classification)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setCategory(i18n(comic_classification)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + QStringList cond = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Comic book grade levels - " + "Mint,Near Mint,Very Fine,Fine,Very Good,Good,Fair,Poor", + "Mint,Near Mint,Very Fine,Fine,Very Good,Good,Fair,Poor"), + false); + field = new Field(QString::fromLatin1("condition"), i18n("Condition"), cond); + field->setCategory(i18n(comic_classification)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(comic_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(comic_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("signed"), i18n("Signed"), Field::Bool); + field->setCategory(i18n(comic_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(comic_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(comic_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Front Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(comic_personal)); + list.append(field); + + return list; +} + +#include "comicbookcollection.moc" diff --git a/src/collections/comicbookcollection.h b/src/collections/comicbookcollection.h new file mode 100644 index 0000000..36302c2 --- /dev/null +++ b/src/collections/comicbookcollection.h @@ -0,0 +1,48 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef COMICBOOKCOLLECTION_H +#define COMICBOOKCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for comic books. + * + * It has the following standard attributes: + * @li Title + * + * @author Robby Stephenson + */ +class ComicBookCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param title The title of the collection + */ + ComicBookCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return ComicBook; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/filecatalog.cpp b/src/collections/filecatalog.cpp new file mode 100644 index 0000000..8d375be --- /dev/null +++ b/src/collections/filecatalog.cpp @@ -0,0 +1,105 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson + email : robby@priapsis.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 "filecatalog.h" + +#include <klocale.h> + +namespace { + static const char* file_general = I18N_NOOP("General"); +} + +using Tellico::Data::FileCatalog; + +FileCatalog::FileCatalog(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Files") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("volume")); +} + +Tellico::Data::FieldVec FileCatalog::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Name")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("url"), i18n("URL"), Field::URL); + field->setCategory(i18n(file_general)); + list.append(field); + + field = new Field(QString::fromLatin1("description"), i18n("Description")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("volume"), i18n("Volume")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("folder"), i18n("Folder")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("mimetype"), i18n("Mimetype")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("size"), i18n("Size")); + field->setCategory(i18n(file_general)); + list.append(field); + + field = new Field(QString::fromLatin1("permissions"), i18n("Permissions")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("owner"), i18n("Owner")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("group"), i18n("Group")); + field->setCategory(i18n(file_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + // these dates are string fields, not dates, since the time is included + field = new Field(QString::fromLatin1("created"), i18n("Created")); + field->setCategory(i18n(file_general)); + list.append(field); + + field = new Field(QString::fromLatin1("modified"), i18n("Modified")); + field->setCategory(i18n(file_general)); + list.append(field); + + field = new Field(QString::fromLatin1("metainfo"), i18n("Meta Info"), Field::Table); + field->setProperty(QString::fromLatin1("columns"), QChar('2')); + field->setProperty(QString::fromLatin1("column1"), i18n("Property")); + field->setProperty(QString::fromLatin1("column2"), i18n("Value")); + list.append(field); + + field = new Field(QString::fromLatin1("icon"), i18n("Icon"), Field::Image); + list.append(field); + + return list; +} + +#include "filecatalog.moc" diff --git a/src/collections/filecatalog.h b/src/collections/filecatalog.h new file mode 100644 index 0000000..aa7a82c --- /dev/null +++ b/src/collections/filecatalog.h @@ -0,0 +1,38 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DATA_FILECATALOG_H +#define TELLICO_DATA_FILECATALOG_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * @author Robby Stephenson + */ +class FileCatalog : public Collection { +Q_OBJECT + +public: + FileCatalog(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return File; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/gamecollection.cpp b/src/collections/gamecollection.cpp new file mode 100644 index 0000000..b93176f --- /dev/null +++ b/src/collections/gamecollection.cpp @@ -0,0 +1,126 @@ +/*************************************************************************** + copyright : (C) 2005-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 "gamecollection.h" + +#include <klocale.h> + +namespace { + static const char* game_general = I18N_NOOP("General"); + static const char* game_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::GameCollection; + +GameCollection::GameCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Games") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("platform")); +} + +Tellico::Data::FieldVec GameCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setCategory(i18n(game_general)); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + QStringList platform; + platform << i18n("Xbox 360") << i18n("Xbox") + << i18n("PlayStation3") << i18n("PlayStation2") << i18n("PlayStation") << i18n("PlayStation Portable", "PSP") + << i18n("Nintendo Wii") << i18n("Nintendo DS") << i18n("GameCube") << i18n("Dreamcast") + << i18n("Game Boy Advance") << i18n("Game Boy Color") << i18n("Game Boy") + << i18n("Windows Platform", "Windows") << i18n("Mac OS") << i18n("Linux"); + field = new Field(QString::fromLatin1("platform"), i18n("Platform"), platform); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Release Year"), Field::Number); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("publisher"), i18n("Games - Publisher", "Publisher")); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("developer"), i18n("Developer")); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + QStringList cert = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Video game ratings - " + "Unrated, Adults Only, Mature, Teen, Everyone, Early Childhood, Pending", + "Unrated, Adults Only, Mature, Teen, Everyone, Early Childhood, Pending"), + false); + field = new Field(QString::fromLatin1("certification"), i18n("ESRB Rating"), cert); + field->setCategory(i18n(game_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("description"), i18n("Description"), Field::Para); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Personal Rating"), Field::Rating); + field->setCategory(i18n(game_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("completed"), i18n("Completed"), Field::Bool); + field->setCategory(i18n(game_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(game_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(game_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(game_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(game_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(game_personal)); + list.append(field); + + return list; +} + +#include "gamecollection.moc" diff --git a/src/collections/gamecollection.h b/src/collections/gamecollection.h new file mode 100644 index 0000000..7d6ce92 --- /dev/null +++ b/src/collections/gamecollection.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef GAMECOLLECTION_H +#define GAMECOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for games. + */ +class GameCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + GameCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Game; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/musiccollection.cpp b/src/collections/musiccollection.cpp new file mode 100644 index 0000000..d3c0445 --- /dev/null +++ b/src/collections/musiccollection.cpp @@ -0,0 +1,132 @@ +/*************************************************************************** + copyright : (C) 2003-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 "musiccollection.h" + +#include <klocale.h> + +namespace { + static const char* music_general = I18N_NOOP("General"); + static const char* music_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::MusicCollection; + +MusicCollection::MusicCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Music") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("artist")); +} + +Tellico::Data::FieldVec MusicCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Album")); + field->setCategory(i18n(music_general)); + field->setFlags(Field::NoDelete | Field::AllowCompletion); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + QStringList media; + media << i18n("Compact Disc") << i18n("DVD") << i18n("Cassette") << i18n("Vinyl"); + field = new Field(QString::fromLatin1("medium"), i18n("Medium"), media); + field->setCategory(i18n(music_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("artist"), i18n("Artist")); + field->setCategory(i18n(music_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped | Field::AllowMultiple); + field->setFormatFlag(Field::FormatTitle); // don't use FormatName + list.append(field); + + field = new Field(QString::fromLatin1("label"), i18n("Label")); + field->setCategory(i18n(music_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped | Field::AllowMultiple); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Year"), Field::Number); + field->setCategory(i18n(music_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(music_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("track"), i18n("Tracks"), Field::Table); + field->setFormatFlag(Field::FormatTitle); + field->setProperty(QString::fromLatin1("columns"), QChar('3')); + field->setProperty(QString::fromLatin1("column1"), i18n("Title")); + field->setProperty(QString::fromLatin1("column2"), i18n("Artist")); + field->setProperty(QString::fromLatin1("column3"), i18n("Length")); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Rating"), Field::Rating); + field->setCategory(i18n(music_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(music_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(music_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(music_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(music_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setCategory(i18n(music_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + field->setCategory(i18n(music_personal)); + list.append(field); + + return list; +} + +int MusicCollection::sameEntry(Data::EntryPtr entry1_, Data::EntryPtr entry2_) const { + // not enough for title to be equal, must also have another field + int res = 2*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("title"), this); +// if(res == 0) { +// myDebug() << "MusicCollection::sameEntry() - different titles for " << entry1_->title() << " vs. " +// << entry2_->title() << endl; +// } + res += 2*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("artist"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("label"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("medium"), this); + return res; +} + +#include "musiccollection.moc" diff --git a/src/collections/musiccollection.h b/src/collections/musiccollection.h new file mode 100644 index 0000000..ddada5b --- /dev/null +++ b/src/collections/musiccollection.h @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef MUSICCOLLECTION_H +#define MUSICCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for music, like CD's and cassettes. + * + * It has the following standard attributes: + * @li Title + * @li Artist + * @li Album + * @li Year + * @li Genre + * @li Comments + * + * @author Robby Stephenson + */ +class MusicCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + MusicCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Album; } + virtual int sameEntry(Data::EntryPtr, Data::EntryPtr) const; + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/stampcollection.cpp b/src/collections/stampcollection.cpp new file mode 100644 index 0000000..c26da9a --- /dev/null +++ b/src/collections/stampcollection.cpp @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2003-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 "stampcollection.h" + +#include <klocale.h> + +namespace { + static const char* stamp_general = I18N_NOOP("General"); + static const char* stamp_condition = I18N_NOOP("Condition"); + static const char* stamp_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::StampCollection; + +StampCollection::StampCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Stamps") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("denomination")); +} + +Tellico::Data::FieldVec StampCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title"), Field::Dependent); + field->setCategory(i18n(stamp_general)); + field->setDescription(QString::fromLatin1("%{year} %{description} %{denomination}")); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("description"), i18n("Description")); + field->setCategory(i18n(stamp_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + field = new Field(QString::fromLatin1("denomination"), i18n("Denomination")); + field->setCategory(i18n(stamp_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("country"), i18n("Country")); + field->setCategory(i18n(stamp_general)); + field->setFormatFlag(Field::FormatPlain); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Issue Year"), Field::Number); + field->setCategory(i18n(stamp_general)); + field->setFlags(Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("color"), i18n("Color")); + field->setCategory(i18n(stamp_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("scott"), i18n("Scott#")); + field->setCategory(i18n(stamp_general)); + list.append(field); + + QStringList grade = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Stamp grade levels - " + "Superb,Extremely Fine,Very Fine,Fine,Average,Poor", + "Superb,Extremely Fine,Very Fine,Fine,Average,Poor"), + false); + field = new Field(QString::fromLatin1("grade"), i18n("Grade"), grade); + field->setCategory(i18n(stamp_condition)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("cancelled"), i18n("Cancelled"), Field::Bool); + field->setCategory(i18n(stamp_condition)); + list.append(field); + + /* TRANSLATORS: See http://en.wikipedia.org/wiki/Stamp_hinge */ + field = new Field(QString::fromLatin1("hinged"), i18n("Hinged")); + field->setCategory(i18n(stamp_condition)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("centering"), i18n("Centering")); + field->setCategory(i18n(stamp_condition)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("gummed"), i18n("Gummed")); + field->setCategory(i18n(stamp_condition)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(stamp_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(stamp_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("location"), i18n("Location")); + field->setCategory(i18n(stamp_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(stamp_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("image"), i18n("Image"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + list.append(field); + + return list; +} + +#include "stampcollection.moc" diff --git a/src/collections/stampcollection.h b/src/collections/stampcollection.h new file mode 100644 index 0000000..8b81623 --- /dev/null +++ b/src/collections/stampcollection.h @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef STAMPCOLLECTION_H +#define STAMPCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A stamp collection. + * + * It has the following standard attributes: + * @li Title + * @li Description + * @li Denomination + * @li Country + * @li Year + * @li Color + * @li Scott + * @li Grade + * @li Cancelled + * @li Hinged + * @li Centering + * @li Gummed + * @li Pur_date + * @li Pur_price + * @li Location + * @li Gift + * @li Image + * @li Comments + * + * @author Robby Stephenson + */ +class StampCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields A boolean indicating whether the default attributes should be added + * @param title The title of the collection + */ + StampCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Stamp; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/videocollection.cpp b/src/collections/videocollection.cpp new file mode 100644 index 0000000..ad83b5f --- /dev/null +++ b/src/collections/videocollection.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + copyright : (C) 2003-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 "videocollection.h" + +#include <klocale.h> + +namespace { + static const char* video_general = I18N_NOOP("General"); + static const char* video_people = I18N_NOOP("Other People"); + static const char* video_features = I18N_NOOP("Features"); + static const char* video_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::VideoCollection; + +VideoCollection::VideoCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Videos") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("genre")); +} + +Tellico::Data::FieldVec VideoCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title")); + field->setCategory(i18n("General")); + field->setFlags(Field::NoDelete); + field->setFormatFlag(Field::FormatTitle); + list.append(field); + + QStringList media; + media << i18n("DVD") << i18n("VHS") << i18n("VCD") << i18n("DivX") << i18n("Blu-ray") << i18n("HD DVD"); + field = new Field(QString::fromLatin1("medium"), i18n("Medium"), media); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("year"), i18n("Production Year"), Field::Number); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + QStringList cert = QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + i18n("Movie ratings - " + "G (USA),PG (USA),PG-13 (USA),R (USA), U (USA)", + "G (USA),PG (USA),PG-13 (USA),R (USA), U (USA)"), + false); + field = new Field(QString::fromLatin1("certification"), i18n("Certification"), cert); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("genre"), i18n("Genre")); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + QStringList region; + region << i18n("Region 1") + << i18n("Region 2") + << i18n("Region 3") + << i18n("Region 4") + << i18n("Region 5") + << i18n("Region 6") + << i18n("Region 7") + << i18n("Region 8"); + field = new Field(QString::fromLatin1("region"), i18n("Region"), region); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("nationality"), i18n("Nationality")); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + QStringList format; + format << i18n("NTSC") << i18n("PAL") << i18n("SECAM"); + field = new Field(QString::fromLatin1("format"), i18n("Format"), format); + field->setCategory(i18n(video_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("cast"), i18n("Cast"), Field::Table); + field->setProperty(QString::fromLatin1("columns"), QChar('2')); + field->setProperty(QString::fromLatin1("column1"), i18n("Actor/Actress")); + field->setProperty(QString::fromLatin1("column2"), i18n("Role")); + field->setFormatFlag(Field::FormatName); + field->setFlags(Field::AllowGrouped); + field->setDescription(i18n("A table for the cast members, along with the roles they play")); + list.append(field); + + field = new Field(QString::fromLatin1("director"), i18n("Director")); + field->setCategory(i18n(video_people)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("producer"), i18n("Producer")); + field->setCategory(i18n(video_people)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("writer"), i18n("Writer")); + field->setCategory(i18n(video_people)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("composer"), i18n("Composer")); + field->setCategory(i18n(video_people)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatName); + list.append(field); + + field = new Field(QString::fromLatin1("studio"), i18n("Studio")); + field->setCategory(i18n(video_people)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("language"), i18n("Language Tracks")); + field->setCategory(i18n(video_features)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("subtitle"), i18n("Subtitle Languages")); + field->setCategory(i18n(video_features)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("audio-track"), i18n("Audio Tracks")); + field->setCategory(i18n(video_features)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("running-time"), i18n("Running Time"), Field::Number); + field->setCategory(i18n(video_features)); + field->setDescription(i18n("The running time of the video (in minutes)")); + list.append(field); + + field = new Field(QString::fromLatin1("aspect-ratio"), i18n("Aspect Ratio")); + field->setCategory(i18n(video_features)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("widescreen"), i18n("Widescreen"), Field::Bool); + field->setCategory(i18n(video_features)); + list.append(field); + + QStringList color; + color << i18n("Color") << i18n("Black & White"); + field = new Field(QString::fromLatin1("color"), i18n("Color Mode"), color); + field->setCategory(i18n(video_features)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("directors-cut"), i18n("Director's Cut"), Field::Bool); + field->setCategory(i18n(video_features)); + list.append(field); + + field = new Field(QString::fromLatin1("plot"), i18n("Plot Summary"), Field::Para); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Personal Rating"), Field::Rating); + field->setCategory(i18n(video_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(video_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(video_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(video_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("loaned"), i18n("Loaned"), Field::Bool); + field->setCategory(i18n(video_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("keyword"), i18n("Keywords")); + field->setCategory(i18n(video_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowMultiple | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("cover"), i18n("Cover"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + list.append(field); + + return list; +} + +int VideoCollection::sameEntry(Data::EntryPtr entry1_, Data::EntryPtr entry2_) const { + // not enough for title to be equal, must also have another field + // ever possible for a studio to do two movies with identical titles? + int res = 3*Entry::compareValues(entry1_, entry2_, QString::fromLatin1("title"), this); +// if(res == 0) { +// myDebug() << "VideoCollection::sameEntry() - different titles for " << entry1_->title() << " vs. " +// << entry2_->title() << endl; +// } + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("year"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("director"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("studio"), this); + res += Entry::compareValues(entry1_, entry2_, QString::fromLatin1("medium"), this); + return res; +} + +#include "videocollection.moc" diff --git a/src/collections/videocollection.h b/src/collections/videocollection.h new file mode 100644 index 0000000..58f729c --- /dev/null +++ b/src/collections/videocollection.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef VIDEOCOLLECTION_H +#define VIDEOCOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A collection for videos. + * + * It has the following standard attributes: + * @li Title + * @li Year + * @li Genre + * @li Medium + * @li Comments + * + * @author Robby Stephenson + */ +class VideoCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields Whether to add the default attributes + * @param title The title of the collection + */ + VideoCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Video; } + virtual int sameEntry(Data::EntryPtr, Data::EntryPtr) const; + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/collections/winecollection.cpp b/src/collections/winecollection.cpp new file mode 100644 index 0000000..31bc5d0 --- /dev/null +++ b/src/collections/winecollection.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + copyright : (C) 2003-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 "winecollection.h" + +#include <klocale.h> + +namespace { + static const char* wine_general = I18N_NOOP("General"); + static const char* wine_personal = I18N_NOOP("Personal"); +} + +using Tellico::Data::WineCollection; + +WineCollection::WineCollection(bool addFields_, const QString& title_ /*=null*/) + : Collection(title_.isEmpty() ? i18n("My Wines") : title_) { + if(addFields_) { + addFields(defaultFields()); + } + setDefaultGroupField(QString::fromLatin1("type")); +} + +Tellico::Data::FieldVec WineCollection::defaultFields() { + FieldVec list; + FieldPtr field; + + field = new Field(QString::fromLatin1("title"), i18n("Title"), Field::Dependent); + field->setCategory(i18n(wine_general)); + field->setDescription(QString::fromLatin1("%{vintage} %{producer} %{varietal}")); + field->setFlags(Field::NoDelete); + list.append(field); + + field = new Field(QString::fromLatin1("producer"), i18n("Producer")); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("appellation"), i18n("Appellation")); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("varietal"), i18n("Varietal")); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("vintage"), i18n("Vintage"), Field::Number); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + QStringList type; + type << i18n("Red Wine") << i18n("White Wine") << i18n("Sparkling Wine"); + field = new Field(QString::fromLatin1("type"), i18n("Type"), type); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("country"), i18n("Country")); + field->setCategory(i18n(wine_general)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + field->setFormatFlag(Field::FormatPlain); + list.append(field); + + field = new Field(QString::fromLatin1("pur_date"), i18n("Purchase Date")); + field->setCategory(i18n(wine_personal)); + field->setFormatFlag(Field::FormatDate); + list.append(field); + + field = new Field(QString::fromLatin1("pur_price"), i18n("Purchase Price")); + field->setCategory(i18n(wine_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("location"), i18n("Location")); + field->setCategory(i18n(wine_personal)); + field->setFlags(Field::AllowCompletion | Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("quantity"), i18n("Quantity"), Field::Number); + field->setCategory(i18n(wine_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("drink-by"), i18n("Drink By"), Field::Number); + field->setCategory(i18n(wine_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("rating"), i18n("Rating"), Field::Rating); + field->setCategory(i18n(wine_personal)); + field->setFlags(Field::AllowGrouped); + list.append(field); + + field = new Field(QString::fromLatin1("gift"), i18n("Gift"), Field::Bool); + field->setCategory(i18n(wine_personal)); + list.append(field); + + field = new Field(QString::fromLatin1("label"), i18n("Label Image"), Field::Image); + list.append(field); + + field = new Field(QString::fromLatin1("comments"), i18n("Comments"), Field::Para); + list.append(field); + + return list; +} + +#include "winecollection.moc" diff --git a/src/collections/winecollection.h b/src/collections/winecollection.h new file mode 100644 index 0000000..236a9cd --- /dev/null +++ b/src/collections/winecollection.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef WINECOLLECTION_H +#define WINECOLLECTION_H + +#include "../collection.h" + +namespace Tellico { + namespace Data { + +/** + * A wine collection. + * + * It has the following standard attributes: + * @li Title + * @li Artist + * @li Album + * @li Year + * @li Genre + * @li Comments + * + * @author Robby Stephenson + */ +class WineCollection : public Collection { +Q_OBJECT + +public: + /** + * The constructor + * + * @param addFields A boolean indicating whether the default attributes should be added + * @param title The title of the collection + */ + WineCollection(bool addFields, const QString& title = QString::null); + + virtual Type type() const { return Wine; } + + static FieldVec defaultFields(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/commands/Makefile.am b/src/commands/Makefile.am new file mode 100644 index 0000000..95147c2 --- /dev/null +++ b/src/commands/Makefile.am @@ -0,0 +1,29 @@ +AM_CPPFLAGS = $(all_includes) + +noinst_LIBRARIES = libcommands.a +libcommands_a_SOURCES = \ + addentries.cpp modifyentries.cpp removeentries.cpp \ + addloans.cpp modifyloans.cpp removeloans.cpp \ + fieldcommand.cpp filtercommand.cpp reorderfields.cpp \ + group.cpp collectioncommand.cpp renamecollection.cpp \ + updateentries.cpp + +libcommands_a_METASOURCES = AUTO +KDE_OPTIONS = noautodist +EXTRA_DIST = \ + addentries.h addentries.cpp \ + modifyentries.h modifyentries.cpp \ + removeentries.h removeentries.cpp \ + addloans.h addloans.cpp \ + modifyloans.h modifyloans.cpp \ + removeloans.h removeloans.cpp \ + fieldcommand.h fieldcommand.cpp \ + filtercommand.h filtercommand.cpp \ + reorderfields.h reorderfields.cpp \ + group.h group.cpp \ + collectioncommand.h collectioncommand.cpp \ + renamecollection.h renamecollection.cpp \ + updateentries.h updateentries.cpp + + +CLEANFILES = *~ diff --git a/src/commands/addentries.cpp b/src/commands/addentries.cpp new file mode 100644 index 0000000..6cd7636 --- /dev/null +++ b/src/commands/addentries.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + copyright : (C) 2005-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 "addentries.h" +#include "../collection.h" +#include "../controller.h" +#include "../datavectors.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::AddEntries; + +AddEntries::AddEntries(Data::CollPtr coll_, const Data::EntryVec& entries_) + : KCommand() + , m_coll(coll_) + , m_entries(entries_) +{ +} + +void AddEntries::execute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + + m_coll->addEntries(m_entries); + // now check for default values + Data::FieldVec fields = m_coll->fields(); + for(Data::FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) { + const QString defaultValue = field->defaultValue(); + if(!defaultValue.isEmpty()) { + for(Data::EntryVec::Iterator entry = m_entries.begin(); entry != m_entries.end(); ++entry) { + if(entry->field(field).isEmpty()) { + entry->setField(field->name(), defaultValue); + } + } + } + } + Controller::self()->addedEntries(m_entries); +} + +void AddEntries::unexecute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + + m_coll->removeEntries(m_entries); + Controller::self()->removedEntries(m_entries); +} + +QString AddEntries::name() const { + return m_entries.count() > 1 ? i18n("Add Entries") + : i18n("Add (Entry Title)", "Add %1").arg(m_entries.begin()->title()); +} diff --git a/src/commands/addentries.h b/src/commands/addentries.h new file mode 100644 index 0000000..54d8e29 --- /dev/null +++ b/src/commands/addentries.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ADDENTRIES_H +#define TELLICO_ADDENTRIES_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class AddEntries : public KCommand { + +public: + AddEntries(Data::CollPtr coll, const Data::EntryVec& entries); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::CollPtr m_coll; + Data::EntryVec m_entries; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/addloans.cpp b/src/commands/addloans.cpp new file mode 100644 index 0000000..dab67d7 --- /dev/null +++ b/src/commands/addloans.cpp @@ -0,0 +1,110 @@ +/*************************************************************************** + copyright : (C) 2005-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 "addloans.h" +#include "../document.h" +#include "../entry.h" +#include "../collection.h" +#include "../controller.h" +#include "../calendarhandler.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::AddLoans; + +AddLoans::AddLoans(Data::BorrowerPtr borrower_, Data::LoanVec loans_, bool addToCalendar_) + : KCommand() + , m_borrower(borrower_) + , m_loans(loans_) + , m_addedLoanField(false) + , m_addToCalendar(addToCalendar_) +{ +} + +void AddLoans::execute() { + if(!m_borrower || m_loans.isEmpty()) { + return; + } + + // if the borrower is empty, assume it's getting added, otherwise it's being modified + bool wasEmpty = m_borrower->isEmpty(); + + // if there's no loaned field, we'll add one + bool loanExisted = m_loans.begin()->entry()->collection()->hasField(QString::fromLatin1("loaned")); + m_addedLoanField = false; // assume we didn't add the field yet + + // add the loans to the borrower + for(Data::LoanVec::Iterator loan = m_loans.begin(); loan != m_loans.end(); ++loan) { + m_borrower->addLoan(loan); + Data::Document::self()->checkOutEntry(loan->entry()); + Data::EntryVec vec; + vec.append(loan->entry()); + Controller::self()->modifiedEntries(vec); + } + if(!loanExisted) { + Data::CollPtr c = m_loans.begin()->entry()->collection(); + Data::FieldPtr f = c->fieldByName(QString::fromLatin1("loaned")); + if(f) { + // notify everything that a new field was added + Controller::self()->addedField(c, f); + m_addedLoanField = true; + } + } + if(m_addToCalendar) { + CalendarHandler::addLoans(m_loans); + } + if(wasEmpty) { + m_loans.begin()->entry()->collection()->addBorrower(m_borrower); + Controller::self()->addedBorrower(m_borrower); + } else { + // don't have to do anything to the document, it just holds a pointer + myDebug() << "AddLoansCommand::execute() - modifying an existing borrower! " << endl; + Controller::self()->modifiedBorrower(m_borrower); + } +} + +void AddLoans::unexecute() { + if(!m_borrower) { + return; + } + + // remove the loans from the borrower + for(Data::LoanVec::Iterator loan = m_loans.begin(); loan != m_loans.end(); ++loan) { + m_borrower->removeLoan(loan); + Data::Document::self()->checkInEntry(loan->entry()); + Data::EntryVec vec; + vec.append(loan->entry()); + Controller::self()->modifiedEntries(vec); + } + if(m_addedLoanField) { + Data::CollPtr c = m_loans.begin()->entry()->collection(); + Data::FieldPtr f = c->fieldByName(QString::fromLatin1("loaned")); + if(f) { + c->removeField(f); + Controller::self()->removedField(c, f); + } + } + if(m_addToCalendar) { + CalendarHandler::removeLoans(m_loans); + } + // the borrower object is kept in the document, it's just empty + // it won't get saved in the document file + // here, just notify everybody that it changed + Controller::self()->modifiedBorrower(m_borrower); +} + +QString AddLoans::name() const { + return m_loans.count() > 1 ? i18n("Check-out Items") + : i18n("Check-out (Entry Title)", "Check-out %1").arg(m_loans.begin()->entry()->title()); +} diff --git a/src/commands/addloans.h b/src/commands/addloans.h new file mode 100644 index 0000000..b80cbc3 --- /dev/null +++ b/src/commands/addloans.h @@ -0,0 +1,47 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ADDLOANS_H +#define TELLICO_ADDLOANS_H + +#include "../borrower.h" +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class AddLoans : public KCommand { + +public: + AddLoans(Data::BorrowerPtr borrower, Data::LoanVec loans, bool addToCalendar); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::BorrowerPtr m_borrower; + Data::LoanVec m_loans; + bool m_addedLoanField : 1; + bool m_addToCalendar : 1; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/collectioncommand.cpp b/src/commands/collectioncommand.cpp new file mode 100644 index 0000000..8955ea0 --- /dev/null +++ b/src/commands/collectioncommand.cpp @@ -0,0 +1,126 @@ +/*************************************************************************** + copyright : (C) 2005-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 "collectioncommand.h" +#include "../collection.h" +#include "../document.h" +#include "../controller.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::CollectionCommand; + +CollectionCommand::CollectionCommand(Mode mode_, Data::CollPtr origColl_, Data::CollPtr newColl_) + : KCommand() + , m_mode(mode_) + , m_origColl(origColl_) + , m_newColl(newColl_) + , m_cleanup(DoNothing) +{ +#ifndef NDEBUG +// just some sanity checking + if(m_origColl == 0 || m_newColl == 0) { + myDebug() << "CollectionCommand() - null collection pointer" << endl; + } +#endif +} + +CollectionCommand::~CollectionCommand() { + switch(m_cleanup) { + case ClearOriginal: + m_origColl->clear(); + break; + case ClearNew: + m_newColl->clear(); + break; + default: + break; + } +} + +void CollectionCommand::execute() { + if(!m_origColl || !m_newColl) { + return; + } + + switch(m_mode) { + case Append: + copyFields(); + Data::Document::self()->appendCollection(m_newColl); + Controller::self()->slotCollectionModified(m_origColl); + break; + + case Merge: + copyFields(); + m_mergePair = Data::Document::self()->mergeCollection(m_newColl); + Controller::self()->slotCollectionModified(m_origColl); + break; + + case Replace: + // replaceCollection() makes the URL = "Unknown" + m_origURL = Data::Document::self()->URL(); + Data::Document::self()->replaceCollection(m_newColl); + Controller::self()->slotCollectionDeleted(m_origColl); + Controller::self()->slotCollectionAdded(m_newColl); + m_cleanup = ClearOriginal; + break; + } +} + +void CollectionCommand::unexecute() { + if(!m_origColl || !m_newColl) { + return; + } + + switch(m_mode) { + case Append: + Data::Document::self()->unAppendCollection(m_newColl, m_origFields); + Controller::self()->slotCollectionModified(m_origColl); + break; + + case Merge: + Data::Document::self()->unMergeCollection(m_newColl, m_origFields, m_mergePair); + Controller::self()->slotCollectionModified(m_origColl); + break; + + case Replace: + Data::Document::self()->replaceCollection(m_origColl); + Data::Document::self()->setURL(m_origURL); + Controller::self()->slotCollectionDeleted(m_newColl); + Controller::self()->slotCollectionAdded(m_origColl); + m_cleanup = ClearNew; + break; + } +} + +QString CollectionCommand::name() const { + switch(m_mode) { + case Append: + return i18n("Append Collection"); + case Merge: + return i18n("Merge Collection"); + case Replace: + return i18n("Replace Collection"); + } + // hush warnings + return QString::null; +} + +void CollectionCommand::copyFields() { + m_origFields.clear(); + Data::FieldVec fieldsToCopy = m_origColl->fields(); + for(Data::FieldVec::Iterator field = fieldsToCopy.begin(); field != fieldsToCopy.end(); ++field) { + m_origFields.append(new Data::Field(*field)); + } +} diff --git a/src/commands/collectioncommand.h b/src/commands/collectioncommand.h new file mode 100644 index 0000000..ad0b2f4 --- /dev/null +++ b/src/commands/collectioncommand.h @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_COLLECTIONCOMMAND_H +#define TELLICO_COLLECTIONCOMMAND_H + +#include "../datavectors.h" + +#include <kcommand.h> +#include <kurl.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class CollectionCommand : public KCommand { +public: + enum Mode { + Append, + Merge, + Replace + }; + + CollectionCommand(Mode mode, Data::CollPtr currentColl, Data::CollPtr newColl); + ~CollectionCommand(); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + void copyFields(); + + Mode m_mode; + Data::CollPtr m_origColl; + Data::CollPtr m_newColl; + + KURL m_origURL; + Data::FieldVec m_origFields; + Data::MergePair m_mergePair; + // for the Replace case, the collection that got replaced needs to be cleared + enum CleanupMode { + DoNothing, ClearOriginal, ClearNew + }; + CleanupMode m_cleanup; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/fieldcommand.cpp b/src/commands/fieldcommand.cpp new file mode 100644 index 0000000..6902e15 --- /dev/null +++ b/src/commands/fieldcommand.cpp @@ -0,0 +1,112 @@ +/*************************************************************************** + copyright : (C) 2005-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 "fieldcommand.h" +#include "../collection.h" +#include "../controller.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::FieldCommand; + +FieldCommand::FieldCommand(Mode mode_, Data::CollPtr coll_, + Data::FieldPtr activeField_, Data::FieldPtr oldField_/*=0*/) + : KCommand() + , m_mode(mode_) + , m_coll(coll_) + , m_activeField(activeField_) + , m_oldField(oldField_) +{ + if(!m_coll) { + myDebug() << "FieldCommand() - null collection pointer" << endl; + } else if(!m_activeField) { + myDebug() << "FieldCommand() - null active field pointer" << endl; + } +#ifndef NDEBUG +// just some sanity checking + if(m_mode == FieldAdd && m_oldField != 0) { + myDebug() << "FieldCommand() - adding field, but pointers are wrong" << endl; + } else if(m_mode == FieldModify && m_oldField == 0) { + myDebug() << "FieldCommand() - modifying field, but pointers are wrong" << endl; + } else if(m_mode == FieldRemove && m_oldField != 0) { + myDebug() << "FieldCommand() - removing field, but pointers are wrong" << endl; + } +#endif +} + +void FieldCommand::execute() { + if(!m_coll || !m_activeField) { + return; + } + + switch(m_mode) { + case FieldAdd: + // if there's currently a field in the collection with the same name, it will get overwritten + // so save a pointer to it here, the collection should not delete it + m_oldField = m_coll->fieldByName(m_activeField->name()); + m_coll->addField(m_activeField); + Controller::self()->addedField(m_coll, m_activeField); + break; + + case FieldModify: + m_coll->modifyField(m_activeField); + Controller::self()->modifiedField(m_coll, m_oldField, m_activeField); + break; + + case FieldRemove: + m_coll->removeField(m_activeField); + Controller::self()->removedField(m_coll, m_activeField); + break; + } +} + +void FieldCommand::unexecute() { + if(!m_coll || !m_activeField) { + return; + } + + switch(m_mode) { + case FieldAdd: + m_coll->removeField(m_activeField); + Controller::self()->removedField(m_coll, m_activeField); + if(m_oldField) { + m_coll->addField(m_oldField); + Controller::self()->addedField(m_coll, m_oldField); + } + break; + + case FieldModify: + m_coll->modifyField(m_oldField); + Controller::self()->modifiedField(m_coll, m_activeField, m_oldField); + break; + + case FieldRemove: + m_coll->addField(m_activeField); + Controller::self()->addedField(m_coll, m_activeField); + break; + } +} + +QString FieldCommand::name() const { + switch(m_mode) { + case FieldAdd: + return i18n("Add %1 Field").arg(m_activeField->title()); + case FieldModify: + return i18n("Modify %1 Field").arg(m_activeField->title()); + case FieldRemove: + return i18n("Delete %1 Field").arg(m_activeField->title()); + } + // hush warnings + return QString::null; +} diff --git a/src/commands/fieldcommand.h b/src/commands/fieldcommand.h new file mode 100644 index 0000000..0e5706f --- /dev/null +++ b/src/commands/fieldcommand.h @@ -0,0 +1,52 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FIELDCOMMAND_H +#define TELLICO_FIELDCOMMAND_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class FieldCommand : public KCommand { + +public: + enum Mode { + FieldAdd, + FieldModify, + FieldRemove + }; + + FieldCommand(Mode mode, Data::CollPtr coll, Data::FieldPtr activeField, Data::FieldPtr oldField=0); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Mode m_mode; + Data::CollPtr m_coll; + Data::FieldPtr m_activeField; + Data::FieldPtr m_oldField; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/filtercommand.cpp b/src/commands/filtercommand.cpp new file mode 100644 index 0000000..ba205e5 --- /dev/null +++ b/src/commands/filtercommand.cpp @@ -0,0 +1,106 @@ +/*************************************************************************** + copyright : (C) 2005-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 "filtercommand.h" +#include "../document.h" +#include "../collection.h" +#include "../controller.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::FilterCommand; + +FilterCommand::FilterCommand(Mode mode_, FilterPtr activeFilter_, FilterPtr oldFilter_/*=0*/) + : KCommand() + , m_mode(mode_) + , m_activeFilter(activeFilter_) + , m_oldFilter(oldFilter_) +{ + if(!m_activeFilter) { + myDebug() << "FilterCommand() - null active filter pointer" << endl; + } +#ifndef NDEBUG +// just some sanity checking + if(m_mode == FilterAdd && m_oldFilter != 0) { + myDebug() << "FilterCommand() - adding field, but pointers are wrong" << endl; + } else if(m_mode == FilterModify && m_oldFilter == 0) { + myDebug() << "FilterCommand() - modifying field, but pointers are wrong" << endl; + } else if(m_mode == FilterRemove && m_oldFilter != 0) { + myDebug() << "FilterCommand() - removing field, but pointers are wrong" << endl; + } +#endif +} + +void FilterCommand::execute() { + if(!m_activeFilter) { + return; + } + + switch(m_mode) { + case FilterAdd: + Data::Document::self()->collection()->addFilter(m_activeFilter); + Controller::self()->addedFilter(m_activeFilter); + break; + + case FilterModify: + Data::Document::self()->collection()->removeFilter(m_oldFilter); + Controller::self()->removedFilter(m_oldFilter); + Data::Document::self()->collection()->addFilter(m_activeFilter); + Controller::self()->addedFilter(m_activeFilter); + break; + + case FilterRemove: + Data::Document::self()->collection()->removeFilter(m_activeFilter); + Controller::self()->removedFilter(m_activeFilter); + break; + } +} + +void FilterCommand::unexecute() { + if(!m_activeFilter) { + return; + } + + switch(m_mode) { + case FilterAdd: + Data::Document::self()->collection()->removeFilter(m_activeFilter); + Controller::self()->removedFilter(m_activeFilter); + break; + + case FilterModify: + Data::Document::self()->collection()->removeFilter(m_activeFilter); + Controller::self()->removedFilter(m_activeFilter); + Data::Document::self()->collection()->addFilter(m_oldFilter); + Controller::self()->addedFilter(m_oldFilter); + break; + + case FilterRemove: + Data::Document::self()->collection()->addFilter(m_activeFilter); + Controller::self()->addedFilter(m_activeFilter); + break; + } +} + +QString FilterCommand::name() const { + switch(m_mode) { + case FilterAdd: + return i18n("Add Filter"); + case FilterModify: + return i18n("Modify Filter"); + case FilterRemove: + return i18n("Delete Filter"); + } + // hush warnings + return QString::null; +} diff --git a/src/commands/filtercommand.h b/src/commands/filtercommand.h new file mode 100644 index 0000000..93aa629 --- /dev/null +++ b/src/commands/filtercommand.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FILTERCOMMAND_H +#define TELLICO_FILTERCOMMAND_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class FilterCommand : public KCommand { + +public: + enum Mode { + FilterAdd, + FilterModify, + FilterRemove + }; + + FilterCommand(Mode mode, FilterPtr activeFilter, FilterPtr oldFilter=0); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Mode m_mode; + FilterPtr m_activeFilter; + FilterPtr m_oldFilter; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/group.cpp b/src/commands/group.cpp new file mode 100644 index 0000000..fd9cd76 --- /dev/null +++ b/src/commands/group.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** + copyright : (C) 2005-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 "group.h" + +using Tellico::Command::Group; + +QString Group::name() const { + if(m_commands.count() == 1) { + return m_commands.getFirst()->name(); + } + return KMacroCommand::name(); +} diff --git a/src/commands/group.h b/src/commands/group.h new file mode 100644 index 0000000..dc87bf9 --- /dev/null +++ b/src/commands/group.h @@ -0,0 +1,38 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_COMMAND_GROUP_H +#define TELLICO_COMMAND_GROUP_H + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class Group : public KMacroCommand { + +public: + Group(const QString& name) : KMacroCommand(name) {} + + virtual QString name() const; + + bool isEmpty() const { return m_commands.count() == 0; } +}; + + } // end namespace +} + +#endif diff --git a/src/commands/modifyentries.cpp b/src/commands/modifyentries.cpp new file mode 100644 index 0000000..895348e --- /dev/null +++ b/src/commands/modifyentries.cpp @@ -0,0 +1,88 @@ +/*************************************************************************** + copyright : (C) 2005-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 "modifyentries.h" +#include "../collection.h" +#include "../controller.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::ModifyEntries; + +ModifyEntries::ModifyEntries(Data::CollPtr coll_, const Data::EntryVec& oldEntries_, const Data::EntryVec& newEntries_) + : KCommand() + , m_coll(coll_) + , m_oldEntries(oldEntries_) + , m_entries(newEntries_) + , m_needToSwap(false) +{ +#ifndef NDEBUG + if(m_oldEntries.count() != m_entries.count()) { + kdWarning() << "ModifyEntriesCommand() - unequal number of entries" << endl; + } +#endif +} + +void ModifyEntries::execute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + if(m_needToSwap) { + swapValues(); + m_needToSwap = false; + } + // loans expose a field named "loaned", and the user might modify that without + // checking in the loan, so verify that. Heavy-handed, yes... + const QString loaned = QString::fromLatin1("loaned"); + bool hasLoanField = m_coll->hasField(loaned); + for(Data::EntryVecIt entry = m_entries.begin(); hasLoanField && entry != m_entries.end(); ++entry) { + if(entry->field(loaned).isEmpty()) { + Data::EntryVec notLoaned; + notLoaned.append(entry); + Controller::self()->slotCheckIn(notLoaned); + } + } + m_coll->updateDicts(m_entries); + Controller::self()->modifiedEntries(m_entries); +} + +void ModifyEntries::unexecute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + swapValues(); + m_needToSwap = true; + m_coll->updateDicts(m_entries); + Controller::self()->modifiedEntries(m_entries); + //TODO: need to tell edit dialog that it's not modified +} + +QString ModifyEntries::name() const { + return m_entries.count() > 1 ? i18n("Modify Entries") + : i18n("Modify (Entry Title)", "Modify %1").arg(m_entries.begin()->title()); +} + +void ModifyEntries::swapValues() { + // since things like the detailedlistview and the entryiconview hold pointers to the entries + // can't just call Controller::modifiedEntry() on the old pointers + for(size_t i = 0; i < m_entries.count(); ++i) { + // need to swap entry values, not just pointers + // the id gets reset when copying, so need to keep it + long id = m_entries.at(i)->id(); + Data::Entry tmp(*m_entries.at(i)); // tmp id becomes -1 + *m_entries.at(i) = *m_oldEntries.at(i); // id becomes -1 + m_entries.at(i)->setId(id); // id becomes what was originally + *m_oldEntries.at(i) = tmp; // id becomes -1 + } +} diff --git a/src/commands/modifyentries.h b/src/commands/modifyentries.h new file mode 100644 index 0000000..4dfbda1 --- /dev/null +++ b/src/commands/modifyentries.h @@ -0,0 +1,48 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_MODIFYENTRIES_H +#define TELLICO_MODIFYENTRIES_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class ModifyEntries : public KCommand { + +public: + ModifyEntries(Data::CollPtr coll, const Data::EntryVec& oldEntries, const Data::EntryVec& newEntries); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + void swapValues(); + + Data::CollPtr m_coll; + Data::EntryVec m_oldEntries; + Data::EntryVec m_entries; + bool m_needToSwap : 1; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/modifyloans.cpp b/src/commands/modifyloans.cpp new file mode 100644 index 0000000..f8976e4 --- /dev/null +++ b/src/commands/modifyloans.cpp @@ -0,0 +1,76 @@ +/*************************************************************************** + copyright : (C) 2005-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 "modifyloans.h" +#include "../document.h" +#include "../entry.h" +#include "../controller.h" +#include "../calendarhandler.h" + +#include <klocale.h> + +using Tellico::Command::ModifyLoans; + +ModifyLoans::ModifyLoans(Data::LoanPtr oldLoan_, Data::LoanPtr newLoan_, bool addToCalendar_) + : KCommand() + , m_oldLoan(oldLoan_) + , m_newLoan(newLoan_) + , m_addToCalendar(addToCalendar_) +{ +} + +void ModifyLoans::execute() { + if(!m_oldLoan || !m_newLoan) { + return; + } + + Data::BorrowerPtr b = m_oldLoan->borrower(); + b->removeLoan(m_oldLoan); + b->addLoan(m_newLoan); + Controller::self()->modifiedBorrower(b); + + if(m_addToCalendar && !m_oldLoan->inCalendar()) { + Data::LoanVec loans; + loans.append(m_newLoan); + CalendarHandler::addLoans(loans); + } else if(!m_addToCalendar && m_oldLoan->inCalendar()) { + Data::LoanVec loans; + loans.append(m_newLoan); // CalendarHandler checks via uid + CalendarHandler::removeLoans(loans); + } +} + +void ModifyLoans::unexecute() { + if(!m_oldLoan || !m_newLoan) { + return; + } + + Data::BorrowerPtr b = m_oldLoan->borrower(); + b->removeLoan(m_newLoan); + b->addLoan(m_oldLoan); + Controller::self()->modifiedBorrower(b); + + if(m_addToCalendar && !m_oldLoan->inCalendar()) { + Data::LoanVec loans; + loans.append(m_newLoan); + CalendarHandler::removeLoans(loans); + } else if(!m_addToCalendar && m_oldLoan->inCalendar()) { + Data::LoanVec loans; + loans.append(m_oldLoan); + CalendarHandler::addLoans(loans); + } +} + +QString ModifyLoans::name() const { + return i18n("Modify Loan"); +} diff --git a/src/commands/modifyloans.h b/src/commands/modifyloans.h new file mode 100644 index 0000000..6f531ce --- /dev/null +++ b/src/commands/modifyloans.h @@ -0,0 +1,45 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_MODIFYLOANS_H +#define TELLICO_MODIFYLOANS_H + +#include "../borrower.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class ModifyLoans : public KCommand { + +public: + ModifyLoans(Data::LoanPtr oldLoan, Data::LoanPtr newLoan, bool addToCalendar); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::LoanPtr m_oldLoan; + Data::LoanPtr m_newLoan; + bool m_addToCalendar : 1; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/removeentries.cpp b/src/commands/removeentries.cpp new file mode 100644 index 0000000..8d77fb6 --- /dev/null +++ b/src/commands/removeentries.cpp @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2005-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 "removeentries.h" +#include "../collection.h" +#include "../controller.h" + +#include <klocale.h> + +using Tellico::Command::RemoveEntries; + +RemoveEntries::RemoveEntries(Data::CollPtr coll_, const Data::EntryVec& entries_) + : KCommand() + , m_coll(coll_) + , m_entries(entries_) +{ +} + +void RemoveEntries::execute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + + m_coll->removeEntries(m_entries); + Controller::self()->removedEntries(m_entries); +} + +void RemoveEntries::unexecute() { + if(!m_coll || m_entries.isEmpty()) { + return; + } + + m_coll->addEntries(m_entries); + Controller::self()->addedEntries(m_entries); +} + +QString RemoveEntries::name() const { + return m_entries.count() > 1 ? i18n("Delete Entries") + : i18n("Delete (Entry Title)", "Delete %1").arg(m_entries.begin()->title()); +} diff --git a/src/commands/removeentries.h b/src/commands/removeentries.h new file mode 100644 index 0000000..71c5ed4 --- /dev/null +++ b/src/commands/removeentries.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_REMOVEENTRIES_H +#define TELLICO_REMOVEENTRIES_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class RemoveEntries : public KCommand { + +public: + RemoveEntries(Data::CollPtr coll, const Data::EntryVec& entries); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::CollPtr m_coll; + Data::EntryVec m_entries; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/removeloans.cpp b/src/commands/removeloans.cpp new file mode 100644 index 0000000..5ceadc9 --- /dev/null +++ b/src/commands/removeloans.cpp @@ -0,0 +1,81 @@ +/*************************************************************************** + copyright : (C) 2005-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 "removeloans.h" +#include "../document.h" +#include "../entry.h" +#include "../controller.h" +#include "../calendarhandler.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::RemoveLoans; + +RemoveLoans::RemoveLoans(Data::LoanVec loans_) + : KCommand() + , m_loans(loans_) +{ +} + +void RemoveLoans::execute() { + if(m_loans.isEmpty()) { + return; + } + + // not all of the loans might be in the calendar + Data::LoanVec calLoans; + // remove the loans from the borrowers + for(Data::LoanVec::Iterator loan = m_loans.begin(); loan != m_loans.end(); ++loan) { + if(loan->inCalendar()) { + calLoans.append(loan); + } + loan->borrower()->removeLoan(loan); + Data::Document::self()->checkInEntry(loan->entry()); + Data::EntryVec vec; + vec.append(loan->entry()); + Controller::self()->modifiedEntries(vec); + Controller::self()->modifiedBorrower(loan->borrower()); + } + if(!calLoans.isEmpty()) { + CalendarHandler::removeLoans(calLoans); + } +} + +void RemoveLoans::unexecute() { + if(m_loans.isEmpty()) { + return; + } + + // not all of the loans might be in the calendar + Data::LoanVec calLoans; + for(Data::LoanVec::Iterator loan = m_loans.begin(); loan != m_loans.end(); ++loan) { + if(loan->inCalendar()) { + calLoans.append(loan); + } + loan->borrower()->addLoan(loan); + Data::Document::self()->checkOutEntry(loan->entry()); + Data::EntryVec vec; + vec.append(loan->entry()); + Controller::self()->modifiedEntries(vec); + Controller::self()->modifiedBorrower(loan->borrower()); + } + if(!calLoans.isEmpty()) { + CalendarHandler::addLoans(calLoans); + } +} + +QString RemoveLoans::name() const { + return m_loans.count() > 1 ? i18n("Check-in Entries") + : i18n("Check-in (Entry Title)", "Check-in %1").arg(m_loans.begin()->entry()->title()); +} diff --git a/src/commands/removeloans.h b/src/commands/removeloans.h new file mode 100644 index 0000000..5b578b7 --- /dev/null +++ b/src/commands/removeloans.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_REMOVELOANS_H +#define TELLICO_REMOVELOANS_H + +#include "../borrower.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class RemoveLoans : public KCommand { + +public: + RemoveLoans(Data::LoanVec loans); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::LoanVec m_loans; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/renamecollection.cpp b/src/commands/renamecollection.cpp new file mode 100644 index 0000000..b2fcc39 --- /dev/null +++ b/src/commands/renamecollection.cpp @@ -0,0 +1,46 @@ +/*************************************************************************** + copyright : (C) 2005-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 "renamecollection.h" +#include "../document.h" +#include "../collection.h" + +#include <klocale.h> + +using Tellico::Command::RenameCollection; + +RenameCollection::RenameCollection(Data::CollPtr coll_, const QString& newTitle_) + : KCommand() + , m_coll(coll_) + , m_newTitle(newTitle_) +{ +} + +void RenameCollection::execute() { + if(!m_coll) { + return; + } + m_oldTitle = m_coll->title(); + Data::Document::self()->renameCollection(m_newTitle); +} + +void RenameCollection::unexecute() { + if(!m_coll) { + return; + } + Data::Document::self()->renameCollection(m_oldTitle); +} + +QString RenameCollection::name() const { + return i18n("Rename Collection"); +} diff --git a/src/commands/renamecollection.h b/src/commands/renamecollection.h new file mode 100644 index 0000000..bdbb64c --- /dev/null +++ b/src/commands/renamecollection.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_RENAMECOLLECTION_H +#define TELLICO_RENAMECOLLECTION_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** +@author Robby Stephenson +*/ +class RenameCollection : public KCommand { +public: + RenameCollection(Data::CollPtr coll, const QString& newTitle); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::CollPtr m_coll; + QString m_oldTitle; + QString m_newTitle; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/commands/reorderfields.cpp b/src/commands/reorderfields.cpp new file mode 100644 index 0000000..0c9f7fe --- /dev/null +++ b/src/commands/reorderfields.cpp @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2005-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 "reorderfields.h" +#include "../collection.h" +#include "../controller.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +using Tellico::Command::ReorderFields; + +ReorderFields::ReorderFields(Data::CollPtr coll_, const Data::FieldVec& oldFields_, + const Data::FieldVec& newFields_) + : KCommand() + , m_coll(coll_) + , m_oldFields(oldFields_) + , m_newFields(newFields_) +{ + if(!m_coll) { + myDebug() << "ReorderFieldsCommand() - null collection pointer" << endl; + } else if(m_oldFields.count() != m_newFields.count()) { + myDebug() << "ReorderFieldsCommand() - unequal number of fields" << endl; + } +} + +void ReorderFields::execute() { + if(!m_coll) { + return; + } + m_coll->reorderFields(m_newFields); + Controller::self()->reorderedFields(m_coll); +} + +void ReorderFields::unexecute() { + if(!m_coll) { + return; + } + m_coll->reorderFields(m_oldFields); + Controller::self()->reorderedFields(m_coll); +} + +QString ReorderFields::name() const { + return i18n("Reorder Fields"); +} diff --git a/src/commands/reorderfields.h b/src/commands/reorderfields.h new file mode 100644 index 0000000..6a59050 --- /dev/null +++ b/src/commands/reorderfields.h @@ -0,0 +1,45 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_REORDERFIELDS_H +#define TELLICO_REORDERFIELDS_H + +#include "../datavectors.h" + +#include <kcommand.h> + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class ReorderFields : public KCommand { + +public: + ReorderFields(Data::CollPtr coll, const Data::FieldVec& oldFields, const Data::FieldVec& newFields); + + virtual void execute(); + virtual void unexecute(); + virtual QString name() const; + +private: + Data::CollPtr m_coll; + Data::FieldVec m_oldFields; + Data::FieldVec m_newFields; +}; + + } // end namespace +} + +#endif diff --git a/src/commands/updateentries.cpp b/src/commands/updateentries.cpp new file mode 100644 index 0000000..cb2f850 --- /dev/null +++ b/src/commands/updateentries.cpp @@ -0,0 +1,94 @@ +/*************************************************************************** + copyright : (C) 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 "updateentries.h" +#include "fieldcommand.h" +#include "modifyentries.h" +#include "../collection.h" +#include "../tellico_kernel.h" +#include "../document.h" + +#include <klocale.h> + +using Tellico::Command::UpdateEntries; + +namespace Tellico { + namespace Command { + +class MergeEntries : public KCommand { +public: + MergeEntries(Data::EntryPtr currEntry_, Data::EntryPtr newEntry_, bool overWrite_) : KCommand() + , m_oldEntry(new Data::Entry(*currEntry_)) { + // we merge the entries here instead of in execute() because this + // command is never called without also calling ModifyEntries() + // which takes care of copying the entry values + Data::Collection::mergeEntry(currEntry_, newEntry_, overWrite_); + } + + virtual void execute() {} // does nothing + virtual void unexecute() {} // does nothing + virtual QString name() const { return QString(); } + Data::EntryPtr oldEntry() const { return m_oldEntry; } + +private: + Data::EntryPtr m_oldEntry; +}; + } +} + +UpdateEntries::UpdateEntries(Data::CollPtr coll_, Data::EntryPtr oldEntry_, Data::EntryPtr newEntry_, bool overWrite_) + : Group(i18n("Modify (Entry Title)", "Modify %1").arg(newEntry_->title())) + , m_coll(coll_) + , m_oldEntry(oldEntry_) + , m_newEntry(newEntry_) + , m_overWrite(overWrite_) +{ +} + +void UpdateEntries::execute() { + if(isEmpty()) { + // add commands + // do this here instead of the constructor because several UpdateEntries may be in one command + // and I don't want to add new fields multiple times + + QPair<Data::FieldVec, Data::FieldVec> p = Kernel::self()->mergeFields(m_coll, + m_newEntry->collection()->fields(), + m_newEntry); + Data::FieldVec modifiedFields = p.first; + Data::FieldVec addedFields = p.second; + + for(Data::FieldVec::Iterator field = modifiedFields.begin(); field != modifiedFields.end(); ++field) { + if(m_coll->hasField(field->name())) { + addCommand(new FieldCommand(FieldCommand::FieldModify, m_coll, + field, m_coll->fieldByName(field->name()))); + } + } + + for(Data::FieldVec::Iterator field = addedFields.begin(); field != addedFields.end(); ++field) { + addCommand(new FieldCommand(FieldCommand::FieldAdd, m_coll, field)); + } + + // MergeEntries copies values from m_newEntry into m_oldEntry + // m_oldEntry is in the current collection + // m_newEntry isn't... + MergeEntries* cmd = new MergeEntries(m_oldEntry, m_newEntry, m_overWrite); + addCommand(cmd); + // cmd->oldEntry() returns a copy of m_oldEntry before values were merged + // m_oldEntry has new values + // in the ModifyEntries command, the second entry should be owned by the current + // collection and contain the updated values + // the first one is not owned by current collection + addCommand(new ModifyEntries(m_coll, cmd->oldEntry(), m_oldEntry)); + } + Group::execute(); +} diff --git a/src/commands/updateentries.h b/src/commands/updateentries.h new file mode 100644 index 0000000..feb80e7 --- /dev/null +++ b/src/commands/updateentries.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_UPDATEENTRIES_H +#define TELLICO_UPDATEENTRIES_H + +#include "group.h" +#include "../datavectors.h" + +namespace Tellico { + namespace Command { + +/** + * @author Robby Stephenson + */ +class UpdateEntries : public Group { + +public: + UpdateEntries(Data::CollPtr coll, Data::EntryPtr oldEntry, Data::EntryPtr newEntry, bool overWrite); + + virtual void execute(); + +private: + Data::CollPtr m_coll; + Data::EntryPtr m_oldEntry; + Data::EntryPtr m_newEntry; + bool m_overWrite : 1; +}; + + } // end namespace +} + +#endif diff --git a/src/configdialog.cpp b/src/configdialog.cpp new file mode 100644 index 0000000..e05766d --- /dev/null +++ b/src/configdialog.cpp @@ -0,0 +1,1060 @@ +/*************************************************************************** + 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 "configdialog.h" +#include "field.h" +#include "collection.h" +#include "collectionfactory.h" +#include "fetch/execexternalfetcher.h" +#include "fetch/fetchmanager.h" +#include "fetch/configwidget.h" +#include "controller.h" +#include "fetcherconfigdialog.h" +#include "tellico_kernel.h" +#include "latin1literal.h" +#include "tellico_utils.h" +#include "core/tellico_config.h" +#include "imagefactory.h" +#include "gui/combobox.h" +#include "gui/previewdialog.h" +#include "newstuff/dialog.h" +#include "../tellico_debug.h" + +#include <klineedit.h> +#include <klocale.h> +#include <kconfig.h> +#include <kstandarddirs.h> +#include <knuminput.h> +#include <kpushbutton.h> +#include <kiconloader.h> +#include <ksortablevaluelist.h> +#include <kaccelmanager.h> +#include <khtmlview.h> +#include <kfiledialog.h> +#include <kinputdialog.h> +#include <kfontcombo.h> +#include <kcolorcombo.h> + +#include <qsize.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qptrlist.h> +#include <qpixmap.h> +#include <qgrid.h> +#include <qwhatsthis.h> +#include <qregexp.h> +#include <qhgroupbox.h> +#include <qvgroupbox.h> +#include <qpushbutton.h> +#include <qvbox.h> +#include <qhbox.h> +#include <qfileinfo.h> +#include <qradiobutton.h> +#include <qvbuttongroup.h> + +namespace { + static const int CONFIG_MIN_WIDTH = 640; + static const int CONFIG_MIN_HEIGHT = 420; +} + +using Tellico::SourceListViewItem; +using Tellico::ConfigDialog; + +SourceListViewItem::SourceListViewItem(KListView* parent_, const GeneralFetcherInfo& info_, + const QString& groupName_) + : KListViewItem(parent_, info_.name), m_info(info_), + m_configGroup(groupName_), m_newSource(groupName_.isNull()), m_fetcher(0) { + QPixmap pix = Fetch::Manager::fetcherIcon(info_.type); + if(!pix.isNull()) { + setPixmap(0, pix); + } +} + +SourceListViewItem::SourceListViewItem(KListView* parent_, QListViewItem* after_, + const GeneralFetcherInfo& info_, const QString& groupName_) + : KListViewItem(parent_, after_, info_.name), m_info(info_), + m_configGroup(groupName_), m_newSource(groupName_.isNull()), m_fetcher(0) { + QPixmap pix = Fetch::Manager::fetcherIcon(info_.type); + if(!pix.isNull()) { + setPixmap(0, pix); + } +} + +void SourceListViewItem::setFetcher(Fetch::Fetcher::Ptr fetcher) { + m_fetcher = fetcher; + QPixmap pix = Fetch::Manager::fetcherIcon(fetcher.data()); + if(!pix.isNull()) { + setPixmap(0, pix); + } +} + +ConfigDialog::ConfigDialog(QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(IconList, i18n("Configure Tellico"), Help|Ok|Apply|Cancel|Default, + Ok, parent_, name_, true, false) + , m_modifying(false) + , m_okClicked(false) { + setupGeneralPage(); + setupPrintingPage(); + setupTemplatePage(); + setupFetchPage(); + + updateGeometry(); + QSize s = sizeHint(); + resize(QMAX(s.width(), CONFIG_MIN_WIDTH), QMAX(s.height(), CONFIG_MIN_HEIGHT)); + + // purely for asthetics make all widgets line up + QPtrList<QWidget> widgets; + widgets.append(m_fontCombo); + widgets.append(m_fontSizeInput); + widgets.append(m_baseColorCombo); + widgets.append(m_textColorCombo); + widgets.append(m_highBaseColorCombo); + widgets.append(m_highTextColorCombo); + int w = 0; + for(QPtrListIterator<QWidget> it(widgets); it.current(); ++it) { + it.current()->polish(); + w = QMAX(w, it.current()->sizeHint().width()); + } + for(QPtrListIterator<QWidget> it(widgets); it.current(); ++it) { + it.current()->setMinimumWidth(w); + } + + enableButtonOK(false); + enableButtonApply(false); + + setHelp(QString::fromLatin1("general-options")); + connect(this, SIGNAL(aboutToShowPage(QWidget*)), SLOT(slotUpdateHelpLink(QWidget*))); +} + +ConfigDialog::~ConfigDialog() { + for(QPtrListIterator<Fetch::ConfigWidget> it(m_newStuffConfigWidgets); it.current(); ++it) { + it.current()->removed(); + } +} + +void ConfigDialog::slotUpdateHelpLink(QWidget* w_) { + switch(pageIndex(w_)) { + case 0: + setHelp(QString::fromLatin1("general-options")); + break; + + case 1: + setHelp(QString::fromLatin1("printing-options")); + break; + + case 2: + setHelp(QString::fromLatin1("template-options")); + break; + + case 3: + setHelp(QString::fromLatin1("internet-sources-options")); + break; + + default: + break; + } +} + +void ConfigDialog::slotOk() { + m_okClicked = true; + slotApply(); + accept(); + m_okClicked = false; +} + +void ConfigDialog::slotApply() { + emit signalConfigChanged(); + enableButtonApply(false); +} + +void ConfigDialog::slotDefault() { + // only change the defaults on the active page + Config::self()->useDefaults(true); + switch(activePageIndex()) { + case 0: + readGeneralConfig(); break; + case 1: + readPrintingConfig(); break; + case 2: + readTemplateConfig(); break; + } + Config::self()->useDefaults(false); + slotModified(); +} + +void ConfigDialog::setupGeneralPage() { + QPixmap pix = DesktopIcon(QString::fromLatin1("tellico"), KIcon::SizeMedium); + QFrame* frame = addPage(i18n("General"), i18n("General Options"), pix); + QVBoxLayout* l = new QVBoxLayout(frame, KDialog::marginHint(), KDialog::spacingHint()); + + m_cbOpenLastFile = new QCheckBox(i18n("&Reopen file at startup"), frame); + QWhatsThis::add(m_cbOpenLastFile, i18n("If checked, the file that was last open " + "will be re-opened at program start-up.")); + l->addWidget(m_cbOpenLastFile); + connect(m_cbOpenLastFile, SIGNAL(clicked()), SLOT(slotModified())); + + m_cbShowTipDay = new QCheckBox(i18n("&Show \"Tip of the Day\" at startup"), frame); + QWhatsThis::add(m_cbShowTipDay, i18n("If checked, the \"Tip of the Day\" will be " + "shown at program start-up.")); + l->addWidget(m_cbShowTipDay); + connect(m_cbShowTipDay, SIGNAL(clicked()), SLOT(slotModified())); + + QButtonGroup* imageGroup = new QVButtonGroup(i18n("Image Storage Options"), frame); + m_rbImageInFile = new QRadioButton(i18n("Store images in data file"), imageGroup); + m_rbImageInAppDir = new QRadioButton(i18n("Store images in common application directory"), imageGroup); + m_rbImageInLocalDir = new QRadioButton(i18n("Store images in directory relative to data file"), imageGroup); + QWhatsThis::add(imageGroup, i18n("Images may be saved in the data file itself, which can " + "cause Tellico to run slowly, stored in the Tellico " + "application directory, or stored in a directory in the " + "same location as the data file.")); + l->addWidget(imageGroup); + connect(imageGroup, SIGNAL(clicked(int)), SLOT(slotModified())); + + QVGroupBox* formatGroup = new QVGroupBox(i18n("Formatting Options"), frame); + l->addWidget(formatGroup); + + m_cbCapitalize = new QCheckBox(i18n("Auto capitalize &titles and names"), formatGroup); + QWhatsThis::add(m_cbCapitalize, i18n("If checked, titles and names will " + "be automatically capitalized.")); + connect(m_cbCapitalize, SIGNAL(clicked()), SLOT(slotModified())); + + m_cbFormat = new QCheckBox(i18n("Auto &format titles and names"), formatGroup); + QWhatsThis::add(m_cbFormat, i18n("If checked, titles and names will " + "be automatically formatted.")); + connect(m_cbFormat, SIGNAL(clicked()), SLOT(slotModified())); + + QGrid* g1 = new QGrid(2, formatGroup); + g1->setSpacing(5); + + QLabel* lab = new QLabel(i18n("No capitali&zation:"), g1); + m_leCapitals = new KLineEdit(g1); + lab->setBuddy(m_leCapitals); + QString whats = i18n("<qt>A list of words which should not be capitalized. Multiple values " + "should be separated by a semi-colon.</qt>"); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_leCapitals, whats); + connect(m_leCapitals, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + lab = new QLabel(i18n("Artic&les:"), g1); + m_leArticles = new KLineEdit(g1); + lab->setBuddy(m_leArticles); + whats = i18n("<qt>A list of words which should be considered as articles " + "if they are the first word in a title. Multiple values " + "should be separated by a semi-colon.</qt>"); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_leArticles, whats); + connect(m_leArticles, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + lab = new QLabel(i18n("Personal suffi&xes:"), g1); + m_leSuffixes = new KLineEdit(g1); + lab->setBuddy(m_leSuffixes); + whats = i18n("<qt>A list of suffixes which might be used in personal names. Multiple values " + "should be separated by a semi-colon.</qt>"); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_leSuffixes, whats); + connect(m_leSuffixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + lab = new QLabel(i18n("Surname &prefixes:"), g1); + m_lePrefixes = new KLineEdit(g1); + lab->setBuddy(m_lePrefixes); + whats = i18n("<qt>A list of prefixes which might be used in surnames. Multiple values " + "should be separated by a semi-colon.</qt>"); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_lePrefixes, whats); + connect(m_lePrefixes, SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + // stretch to fill lower area + l->addStretch(1); +} + +void ConfigDialog::setupPrintingPage() { + // SuSE changed the icon name on me + QPixmap pix; + KIconLoader* loader = KGlobal::iconLoader(); + if(loader) { + pix = loader->loadIcon(QString::fromLatin1("printer1"), KIcon::Desktop, KIcon::SizeMedium, + KIcon::DefaultState, 0, true /*canReturnNull */); + if(pix.isNull()) { + pix = loader->loadIcon(QString::fromLatin1("printer2"), KIcon::Desktop, KIcon::SizeMedium, + KIcon::DefaultState, 0, true /*canReturnNull */); + } + if(pix.isNull()) { + pix = loader->loadIcon(QString::fromLatin1("print_printer"), KIcon::Desktop, KIcon::SizeMedium); + } + } + QFrame* frame = addPage(i18n("Printing"), i18n("Printing Options"), pix); + QVBoxLayout* l = new QVBoxLayout(frame, KDialog::marginHint(), KDialog::spacingHint()); + + QVGroupBox* formatOptions = new QVGroupBox(i18n("Formatting Options"), frame); + l->addWidget(formatOptions); + + m_cbPrintFormatted = new QCheckBox(i18n("&Format titles and names"), formatOptions); + QWhatsThis::add(m_cbPrintFormatted, i18n("If checked, titles and names will be automatically formatted.")); + connect(m_cbPrintFormatted, SIGNAL(clicked()), SLOT(slotModified())); + + m_cbPrintHeaders = new QCheckBox(i18n("&Print field headers"), formatOptions); + QWhatsThis::add(m_cbPrintHeaders, i18n("If checked, the field names will be printed as table headers.")); + connect(m_cbPrintHeaders, SIGNAL(clicked()), SLOT(slotModified())); + + QHGroupBox* groupOptions = new QHGroupBox(i18n("Grouping Options"), frame); + l->addWidget(groupOptions); + + m_cbPrintGrouped = new QCheckBox(i18n("&Group the entries"), groupOptions); + QWhatsThis::add(m_cbPrintGrouped, i18n("If checked, the entries will be grouped by the selected field.")); + connect(m_cbPrintGrouped, SIGNAL(clicked()), SLOT(slotModified())); + + QVGroupBox* imageOptions = new QVGroupBox(i18n("Image Options"), frame); + l->addWidget(imageOptions); + + QGrid* grid = new QGrid(3, imageOptions); + grid->setSpacing(5); + + QLabel* lab = new QLabel(i18n("Maximum image &width:"), grid); + m_imageWidthBox = new KIntSpinBox(0, 999, 1, 50, 10, grid); + m_imageWidthBox->setSuffix(QString::fromLatin1(" px")); + lab->setBuddy(m_imageWidthBox); + (void) new QWidget(grid); + QString whats = i18n("The maximum width of the images in the printout. The aspect ration is preserved."); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_imageWidthBox, whats); + connect(m_imageWidthBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); + // QSpinBox doesn't emit valueChanged if you edit the value with + // the lineEdit until you change the keyboard focus + connect(m_imageWidthBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + lab = new QLabel(i18n("&Maximum image height:"), grid); + m_imageHeightBox = new KIntSpinBox(0, 999, 1, 50, 10, grid); + m_imageHeightBox->setSuffix(QString::fromLatin1(" px")); + lab->setBuddy(m_imageHeightBox); + (void) new QWidget(grid); + whats = i18n("The maximum height of the images in the printout. The aspect ration is preserved."); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_imageHeightBox, whats); + connect(m_imageHeightBox, SIGNAL(valueChanged(int)), SLOT(slotModified())); + // QSpinBox doesn't emit valueChanged if you edit the value with + // the lineEdit until you change the keyboard focus + connect(m_imageHeightBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SLOT(slotModified())); + + // stretch to fill lower area + l->addStretch(1); +} + +void ConfigDialog::setupTemplatePage() { + QPixmap pix = DesktopIcon(QString::fromLatin1("looknfeel"), KIcon::SizeMedium); + QFrame* frame = addPage(i18n("Templates"), i18n("Template Options"), pix); + QVBoxLayout* l = new QVBoxLayout(frame, KDialog::marginHint(), KDialog::spacingHint()); + + QGridLayout* gridLayout = new QGridLayout(l); + gridLayout->setSpacing(KDialogBase::spacingHint()); + + int row = -1; + // so I can reuse an i18n string, a plain label can't have an '&' + QLabel* lab = new QLabel(i18n("Collection &type:").remove('&'), frame); + gridLayout->addWidget(lab, ++row, 0); + const int collType = Kernel::self()->collectionType(); + lab = new QLabel(CollectionFactory::nameMap()[collType], frame); + gridLayout->addMultiCellWidget(lab, row, row, 1, 2); + + lab = new QLabel(i18n("Template:"), frame); + m_templateCombo = new GUI::ComboBox(frame); + connect(m_templateCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_templateCombo); + QString whats = i18n("Select the template to use for the current type of collections. " + "Not all templates will use the font and color settings."); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_templateCombo, whats); + gridLayout->addWidget(lab, ++row, 0); + gridLayout->addWidget(m_templateCombo, row, 1); + + KPushButton* btn = new KPushButton(i18n("&Preview..."), frame); + QWhatsThis::add(btn, i18n("Show a preview of the template")); + btn->setIconSet(SmallIconSet(QString::fromLatin1("viewmag"))); + gridLayout->addWidget(btn, row, 2); + connect(btn, SIGNAL(clicked()), SLOT(slotShowTemplatePreview())); + + // so the button is squeezed small + gridLayout->setColStretch(0, 10); + gridLayout->setColStretch(1, 10); + + loadTemplateList(); + +// QLabel* l1 = new QLabel(i18n("The options below will be passed to the template, but not " +// "all templates will use them. Some fonts and colors may be " +// "specified directly in the template."), frame); +// l1->setTextFormat(Qt::RichText); +// l->addWidget(l1); + + QGroupBox* fontGroup = new QGroupBox(0, Qt::Vertical, i18n("Font Options"), frame); + l->addWidget(fontGroup); + + row = -1; + QGridLayout* fontLayout = new QGridLayout(fontGroup->layout()); + fontLayout->setSpacing(KDialogBase::spacingHint()); + + lab = new QLabel(i18n("Font:"), fontGroup); + fontLayout->addWidget(lab, ++row, 0); + m_fontCombo = new KFontCombo(fontGroup); + fontLayout->addWidget(m_fontCombo, row, 1); + connect(m_fontCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_fontCombo); + whats = i18n("This font is passed to the template used in the Entry View."); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_fontCombo, whats); + + fontLayout->addWidget(new QLabel(i18n("Size:"), fontGroup), ++row, 0); + m_fontSizeInput = new KIntNumInput(fontGroup); + m_fontSizeInput->setRange(5, 30); // 30 is same max as konq config + m_fontSizeInput->setSuffix(QString::fromLatin1("pt")); + fontLayout->addWidget(m_fontSizeInput, row, 1); + connect(m_fontSizeInput, SIGNAL(valueChanged(int)), SLOT(slotModified())); + lab->setBuddy(m_fontSizeInput); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_fontSizeInput, whats); + + QGroupBox* colGroup = new QGroupBox(0, Qt::Vertical, i18n("Color Options"), frame); + l->addWidget(colGroup); + + row = -1; + QGridLayout* colLayout = new QGridLayout(colGroup->layout()); + colLayout->setSpacing(KDialogBase::spacingHint()); + + lab = new QLabel(i18n("Background color:"), colGroup); + colLayout->addWidget(lab, ++row, 0); + m_baseColorCombo = new KColorCombo(colGroup); + colLayout->addWidget(m_baseColorCombo, row, 1); + connect(m_baseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_baseColorCombo); + whats = i18n("This color is passed to the template used in the Entry View."); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_baseColorCombo, whats); + + lab = new QLabel(i18n("Text color:"), colGroup); + colLayout->addWidget(lab, ++row, 0); + m_textColorCombo = new KColorCombo(colGroup); + colLayout->addWidget(m_textColorCombo, row, 1); + connect(m_textColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_textColorCombo); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_textColorCombo, whats); + + lab = new QLabel(i18n("Highlight color:"), colGroup); + colLayout->addWidget(lab, ++row, 0); + m_highBaseColorCombo = new KColorCombo(colGroup); + colLayout->addWidget(m_highBaseColorCombo, row, 1); + connect(m_highBaseColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_highBaseColorCombo); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_highBaseColorCombo, whats); + + lab = new QLabel(i18n("Highlighted text color:"), colGroup); + colLayout->addWidget(lab, ++row, 0); + m_highTextColorCombo = new KColorCombo(colGroup); + colLayout->addWidget(m_highTextColorCombo, row, 1); + connect(m_highTextColorCombo, SIGNAL(activated(int)), SLOT(slotModified())); + lab->setBuddy(m_highTextColorCombo); + QWhatsThis::add(lab, whats); + QWhatsThis::add(m_highTextColorCombo, whats); + + QVGroupBox* groupBox = new QVGroupBox(i18n("Manage Templates"), frame); + l->addWidget(groupBox); + + QHBox* box1 = new QHBox(groupBox); + box1->setSpacing(KDialog::spacingHint()); + + KPushButton* b1 = new KPushButton(i18n("Install..."), box1); + b1->setIconSet(SmallIconSet(QString::fromLatin1("add"))); + connect(b1, SIGNAL(clicked()), SLOT(slotInstallTemplate())); + whats = i18n("Click to install a new template directly."); + QWhatsThis::add(b1, whats); + + KPushButton* b2 = new KPushButton(i18n("Download..."), box1); + b2->setIconSet(SmallIconSet(QString::fromLatin1("knewstuff"))); + connect(b2, SIGNAL(clicked()), SLOT(slotDownloadTemplate())); + whats = i18n("Click to download additional templates via the Internet."); + QWhatsThis::add(b2, whats); + + KPushButton* b3 = new KPushButton(i18n("Delete..."), box1); + b3->setIconSet(SmallIconSet(QString::fromLatin1("remove"))); + connect(b3, SIGNAL(clicked()), SLOT(slotDeleteTemplate())); + whats = i18n("Click to select and remove installed templates."); + QWhatsThis::add(b3, whats); + + // stretch to fill lower area + l->addStretch(1); + + KAcceleratorManager::manage(frame); +} + +void ConfigDialog::setupFetchPage() { + QPixmap pix = DesktopIcon(QString::fromLatin1("network"), KIcon::SizeMedium); + QFrame* frame = addPage(i18n("Data Sources"), i18n("Data Source Options"), pix); + QHBoxLayout* l = new QHBoxLayout(frame, KDialog::marginHint(), KDialog::spacingHint()); + + QVBoxLayout* leftLayout = new QVBoxLayout(l); + m_sourceListView = new KListView(frame); + m_sourceListView->addColumn(i18n("Source")); + m_sourceListView->setResizeMode(QListView::LastColumn); + m_sourceListView->setSorting(-1); // no sorting + m_sourceListView->setSelectionMode(QListView::Single); + leftLayout->addWidget(m_sourceListView, 1); + connect(m_sourceListView, SIGNAL(selectionChanged(QListViewItem*)), SLOT(slotSelectedSourceChanged(QListViewItem*))); + connect(m_sourceListView, SIGNAL(doubleClicked(QListViewItem*, const QPoint&, int)), SLOT(slotModifySourceClicked())); + + QHBox* hb = new QHBox(frame); + leftLayout->addWidget(hb); + hb->setSpacing(KDialog::spacingHint()); + m_moveUpSourceBtn = new KPushButton(i18n("Move &Up"), hb); + m_moveUpSourceBtn->setIconSet(SmallIconSet(QString::fromLatin1("up"))); + QWhatsThis::add(m_moveUpSourceBtn, i18n("The order of the data sources sets the order " + "that Tellico uses when entries are automatically updated.")); + m_moveDownSourceBtn = new KPushButton(i18n("Move &Down"), hb); + m_moveDownSourceBtn->setIconSet(SmallIconSet(QString::fromLatin1("down"))); + QWhatsThis::add(m_moveDownSourceBtn, i18n("The order of the data sources sets the order " + "that Tellico uses when entries are automatically updated.")); + + // these icons are rather arbitrary, but seem to vaguely fit + QVBoxLayout* vlay = new QVBoxLayout(l); + KPushButton* newSourceBtn = new KPushButton(i18n("&New..."), frame); + newSourceBtn->setIconSet(SmallIconSet(QString::fromLatin1("filenew"))); + QWhatsThis::add(newSourceBtn, i18n("Click to add a new data source.")); + m_modifySourceBtn = new KPushButton(i18n("&Modify..."), frame); + m_modifySourceBtn->setIconSet(SmallIconSet(QString::fromLatin1("network"))); + QWhatsThis::add(m_modifySourceBtn, i18n("Click to modify the selected data source.")); + m_removeSourceBtn = new KPushButton(i18n("&Delete"), frame); + m_removeSourceBtn->setIconSet(SmallIconSet(QString::fromLatin1("remove"))); + QWhatsThis::add(m_removeSourceBtn, i18n("Click to delete the selected data source.")); + m_newStuffBtn = new KPushButton(i18n("Download..."), frame); + m_newStuffBtn->setIconSet(SmallIconSet(QString::fromLatin1("knewstuff"))); + QWhatsThis::add(m_newStuffBtn, i18n("Click to download additional data sources via the Internet.")); + +#if !KDE_IS_VERSION(3,3,90) + // only available in KDE 3.4 and up + m_newStuffBtn->setEnabled(false); +#endif + + vlay->addWidget(newSourceBtn); + vlay->addWidget(m_modifySourceBtn); + vlay->addWidget(m_removeSourceBtn); + // separate newstuff button from the rest + vlay->addSpacing(2 * KDialog::spacingHint()); + vlay->addWidget(m_newStuffBtn); + vlay->addStretch(1); + + connect(newSourceBtn, SIGNAL(clicked()), SLOT(slotNewSourceClicked())); + connect(m_modifySourceBtn, SIGNAL(clicked()), SLOT(slotModifySourceClicked())); + connect(m_moveUpSourceBtn, SIGNAL(clicked()), SLOT(slotMoveUpSourceClicked())); + connect(m_moveDownSourceBtn, SIGNAL(clicked()), SLOT(slotMoveDownSourceClicked())); + connect(m_removeSourceBtn, SIGNAL(clicked()), SLOT(slotRemoveSourceClicked())); + connect(m_newStuffBtn, SIGNAL(clicked()), SLOT(slotNewStuffClicked())); + + KAcceleratorManager::manage(frame); +} + +void ConfigDialog::readConfiguration() { + m_modifying = true; + + readGeneralConfig(); + readPrintingConfig(); + readTemplateConfig(); + readFetchConfig(); + + m_modifying = false; +} + +void ConfigDialog::readGeneralConfig() { + m_cbShowTipDay->setChecked(Config::showTipOfDay()); + m_cbOpenLastFile->setChecked(Config::reopenLastFile()); + switch(Config::imageLocation()) { + case Config::ImagesInFile: m_rbImageInFile->setChecked(true); break; + case Config::ImagesInAppDir: m_rbImageInAppDir->setChecked(true); break; + case Config::ImagesInLocalDir: m_rbImageInLocalDir->setChecked(true); break; + } + + bool autoCapitals = Config::autoCapitalization(); + m_cbCapitalize->setChecked(autoCapitals); + + bool autoFormat = Config::autoFormat(); + m_cbFormat->setChecked(autoFormat); + + const QRegExp comma(QString::fromLatin1("\\s*,\\s*")); + const QString semicolon = QString::fromLatin1("; "); + + m_leCapitals->setText(Config::noCapitalizationString().replace(comma, semicolon)); + m_leArticles->setText(Config::articlesString().replace(comma, semicolon)); + m_leSuffixes->setText(Config::nameSuffixesString().replace(comma, semicolon)); + m_lePrefixes->setText(Config::surnamePrefixesString().replace(comma, semicolon)); +} + +void ConfigDialog::readPrintingConfig() { + m_cbPrintHeaders->setChecked(Config::printFieldHeaders()); + m_cbPrintFormatted->setChecked(Config::printFormatted()); + m_cbPrintGrouped->setChecked(Config::printGrouped()); + m_imageWidthBox->setValue(Config::maxImageWidth()); + m_imageHeightBox->setValue(Config::maxImageHeight()); +} + +void ConfigDialog::readTemplateConfig() { + // entry template selection + const int collType = Kernel::self()->collectionType(); + QString file = Config::templateName(collType); + file.replace('_', ' '); + QString fileContext = file + QString::fromLatin1(" XSL Template"); + m_templateCombo->setCurrentItem(i18n(fileContext.utf8(), file.utf8())); + + m_fontCombo->setCurrentFont(Config::templateFont(collType).family()); + m_fontSizeInput->setValue(Config::templateFont(collType).pointSize()); + m_baseColorCombo->setColor(Config::templateBaseColor(collType)); + m_textColorCombo->setColor(Config::templateTextColor(collType)); + m_highBaseColorCombo->setColor(Config::templateHighlightedBaseColor(collType)); + m_highTextColorCombo->setColor(Config::templateHighlightedTextColor(collType)); +} + +void ConfigDialog::readFetchConfig() { + m_sourceListView->clear(); + m_configWidgets.clear(); + + Fetch::FetcherVec fetchers = Fetch::Manager::self()->fetchers(); + for(Fetch::FetcherVec::Iterator it = fetchers.begin(); it != fetchers.end(); ++it) { + GeneralFetcherInfo info(it->type(), it->source(), it->updateOverwrite()); + SourceListViewItem* item = new SourceListViewItem(m_sourceListView, m_sourceListView->lastItem(), info); + item->setFetcher(it.data()); + // grab the config widget, taking ownership + Fetch::ConfigWidget* cw = it->configWidget(this); + if(cw) { // might return 0 when no widget available for fetcher type + m_configWidgets.insert(item, cw); + // there's weird layout bug if it's not hidden + cw->hide(); + } + kapp->processEvents(); + } + + if(m_sourceListView->childCount() == 0) { + m_modifySourceBtn->setEnabled(false); + m_removeSourceBtn->setEnabled(false); + } else { + // go ahead and select the first one + m_sourceListView->setSelected(m_sourceListView->firstChild(), true); + } +} + +void ConfigDialog::saveConfiguration() { + Config::setShowTipOfDay(m_cbShowTipDay->isChecked()); + + int imageLocation; + if(m_rbImageInFile->isChecked()) { + imageLocation = Config::ImagesInFile; + } else if(m_rbImageInAppDir->isChecked()) { + imageLocation = Config::ImagesInAppDir; + } else { + imageLocation = Config::ImagesInLocalDir; + } + Config::setImageLocation(imageLocation); + Config::setReopenLastFile(m_cbOpenLastFile->isChecked()); + + Config::setAutoCapitalization(m_cbCapitalize->isChecked()); + Config::setAutoFormat(m_cbFormat->isChecked()); + + const QRegExp semicolon(QString::fromLatin1("\\s*;\\s*")); + const QChar comma = ','; + + Config::setNoCapitalizationString(m_leCapitals->text().replace(semicolon, comma)); + Config::setArticlesString(m_leArticles->text().replace(semicolon, comma)); + Data::Field::articlesUpdated(); + Config::setNameSuffixesString(m_leSuffixes->text().replace(semicolon, comma)); + Config::setSurnamePrefixesString(m_lePrefixes->text().replace(semicolon, comma)); + + Config::setPrintFieldHeaders(m_cbPrintHeaders->isChecked()); + Config::setPrintFormatted(m_cbPrintFormatted->isChecked()); + Config::setPrintGrouped(m_cbPrintGrouped->isChecked()); + Config::setMaxImageWidth(m_imageWidthBox->value()); + Config::setMaxImageHeight(m_imageHeightBox->value()); + + // entry template selection + const int collType = Kernel::self()->collectionType(); + Config::setTemplateName(collType, m_templateCombo->currentData().toString()); + QFont font(m_fontCombo->currentFont(), m_fontSizeInput->value()); + Config::setTemplateFont(collType, font); + Config::setTemplateBaseColor(collType, m_baseColorCombo->color()); + Config::setTemplateTextColor(collType, m_textColorCombo->color()); + Config::setTemplateHighlightedBaseColor(collType, m_highBaseColorCombo->color()); + Config::setTemplateHighlightedTextColor(collType, m_highTextColorCombo->color()); + + // first, tell config widgets they got deleted + for(QPtrListIterator<Fetch::ConfigWidget> it(m_removedConfigWidgets); it.current(); ++it) { + it.current()->removed(); + } + m_removedConfigWidgets.clear(); + + KConfig* masterConfig = KGlobal::config(); + + bool reloadFetchers = false; + int count = 0; // start group numbering at 0 + for(QListViewItemIterator it(m_sourceListView); it.current(); ++it, ++count) { + SourceListViewItem* item = static_cast<SourceListViewItem*>(it.current()); + Fetch::ConfigWidget* cw = m_configWidgets[item]; + if(!cw || (!cw->shouldSave() && !item->isNewSource())) { + continue; + } + m_newStuffConfigWidgets.removeRef(cw); + QString group = QString::fromLatin1("Data Source %1").arg(count); + // in case we later change the order, clear the group now + masterConfig->deleteGroup(group); + KConfigGroup configGroup(masterConfig, group); + configGroup.writeEntry("Name", item->text(0)); + configGroup.writeEntry("Type", item->fetchType()); + configGroup.writeEntry("UpdateOverwrite", item->updateOverwrite()); + cw->saveConfig(configGroup); + item->setNewSource(false); + // in case the ordering changed + item->setConfigGroup(group); + reloadFetchers = true; + } + // now update total number of sources + KConfigGroup sourceGroup(masterConfig, "Data Sources"); + sourceGroup.writeEntry("Sources Count", count); + // and purge old config groups + QString group = QString::fromLatin1("Data Source %1").arg(count); + while(masterConfig->hasGroup(group)) { + masterConfig->deleteGroup(group); + ++count; + group = QString::fromLatin1("Data Source %1").arg(count); + } + + masterConfig->sync(); + Config::writeConfig(); + + QString s = m_sourceListView->selectedItem() ? m_sourceListView->selectedItem()->text(0) : QString(); + if(reloadFetchers) { + Fetch::Manager::self()->loadFetchers(); + Controller::self()->updatedFetchers(); + // reload fetcher items if OK was not clicked + // meaning apply was clicked + if(!m_okClicked) { + readFetchConfig(); + if(!s.isEmpty()) { + for(QListViewItemIterator it(m_sourceListView); it.current(); ++it) { + if(it.current()->text(0) == s) { + m_sourceListView->setSelected(it.current(), true); + m_sourceListView->ensureItemVisible(it.current()); + break; + } + } + } + } + } +} + +void ConfigDialog::slotModified() { + if(m_modifying) { + return; + } + enableButtonOK(true); + enableButtonApply(true); +} + +void ConfigDialog::slotNewSourceClicked() { + FetcherConfigDialog dlg(this); + if(dlg.exec() != QDialog::Accepted) { + return; + } + + Fetch::Type type = dlg.sourceType(); + if(type == Fetch::Unknown) { + return; + } + + GeneralFetcherInfo info(type, dlg.sourceName(), dlg.updateOverwrite()); + SourceListViewItem* item = new SourceListViewItem(m_sourceListView, m_sourceListView->lastItem(), info); + m_sourceListView->ensureItemVisible(item); + m_sourceListView->setSelected(item, true); + Fetch::ConfigWidget* cw = dlg.configWidget(); + if(cw) { + cw->setAccepted(true); + cw->slotSetModified(); + cw->reparent(this, QPoint()); // keep the config widget around + m_configWidgets.insert(item, cw); + } + m_modifySourceBtn->setEnabled(true); + m_removeSourceBtn->setEnabled(true); + slotModified(); // toggle apply button +} + +void ConfigDialog::slotModifySourceClicked() { + SourceListViewItem* item = static_cast<SourceListViewItem*>(m_sourceListView->selectedItem()); + if(!item) { + return; + } + + Fetch::ConfigWidget* cw = 0; + if(m_configWidgets.contains(item)) { + cw = m_configWidgets[item]; + } + if(!cw) { + // no config widget for this one + // might be because support was compiled out + myDebug() << "ConfigDialog::slotModifySourceClicked() - no config widget for source " << item->text(0) << endl; + return; + } + FetcherConfigDialog dlg(item->text(0), item->fetchType(), item->updateOverwrite(), cw, this); + + if(dlg.exec() == QDialog::Accepted) { + cw->setAccepted(true); // mark to save + QString newName = dlg.sourceName(); + if(newName != item->text(0)) { + item->setText(0, newName); + cw->slotSetModified(); + } + item->setUpdateOverwrite(dlg.updateOverwrite()); + slotModified(); // toggle apply button + } + cw->reparent(this, QPoint()); // keep the config widget around +} + +void ConfigDialog::slotRemoveSourceClicked() { + SourceListViewItem* item = static_cast<SourceListViewItem*>(m_sourceListView->selectedItem()); + if(!item) { + return; + } + + Fetch::ConfigWidget* cw = m_configWidgets[item]; + if(cw) { + m_removedConfigWidgets.append(cw); + // it gets deleted by the parent + } + m_configWidgets.remove(item); + delete item; + m_sourceListView->setSelected(m_sourceListView->currentItem(), true); + slotModified(); // toggle apply button +} + +void ConfigDialog::slotMoveUpSourceClicked() { + QListViewItem* item = m_sourceListView->selectedItem(); + if(!item) { + return; + } + SourceListViewItem* prev = static_cast<SourceListViewItem*>(item->itemAbove()); // could be 0 + if(prev) { + GeneralFetcherInfo info(prev->fetchType(), prev->text(0), prev->updateOverwrite()); + SourceListViewItem* newItem = new SourceListViewItem(m_sourceListView, item, info, prev->configGroup()); + newItem->setFetcher(prev->fetcher()); + Fetch::ConfigWidget* cw = m_configWidgets[prev]; + m_configWidgets.remove(prev); + m_configWidgets.insert(newItem, cw); + delete prev; + slotModified(); // toggle apply button + slotSelectedSourceChanged(item); + } +} + +void ConfigDialog::slotMoveDownSourceClicked() { + SourceListViewItem* item = static_cast<SourceListViewItem*>(m_sourceListView->selectedItem()); + if(!item) { + return; + } + QListViewItem* next = item->nextSibling(); // could be 0 + if(next) { + GeneralFetcherInfo info(item->fetchType(), item->text(0), item->updateOverwrite()); + SourceListViewItem* newItem = new SourceListViewItem(m_sourceListView, next, info, item->configGroup()); + newItem->setFetcher(item->fetcher()); + Fetch::ConfigWidget* cw = m_configWidgets[item]; + m_configWidgets.remove(item); + m_configWidgets.insert(newItem, cw); + delete item; + slotModified(); // toggle apply button + m_sourceListView->setSelected(newItem, true); + } +} + +void ConfigDialog::slotSelectedSourceChanged(QListViewItem* item_) { + m_moveUpSourceBtn->setEnabled(item_ && item_->itemAbove()); + m_moveDownSourceBtn->setEnabled(item_ && item_->nextSibling()); +} + +void ConfigDialog::slotNewStuffClicked() { + NewStuff::Dialog dlg(NewStuff::DataScript, this); + dlg.exec(); + + QPtrList<NewStuff::DataSourceInfo> infoList = dlg.dataSourceInfo(); + for(QPtrListIterator<NewStuff::DataSourceInfo> it(infoList); it.current(); ++it) { + const NewStuff::DataSourceInfo& info = *it.current(); + Fetch::ExecExternalFetcher::ConfigWidget* cw = 0; + SourceListViewItem* item = 0; + + // yes, this is checking if item exists + if(info.isUpdate && (item = findItem(info.sourceExec))) { + m_sourceListView->setSelected(item, true); + cw = dynamic_cast<Fetch::ExecExternalFetcher::ConfigWidget*>(m_configWidgets[item]); + } else { + cw = new Fetch::ExecExternalFetcher::ConfigWidget(this); + m_newStuffConfigWidgets.append(cw); + + GeneralFetcherInfo fetchInfo(Fetch::ExecExternal, info.sourceName, false); + item = new SourceListViewItem(m_sourceListView, m_sourceListView->lastItem(), fetchInfo); + m_configWidgets.insert(item, cw); + } + + if(!cw) { + continue; + } + + KConfig spec(info.specFile, false, false); + cw->readConfig(&spec); + cw->slotSetModified(); + cw->setAccepted(true); + + if(item) { + m_sourceListView->setSelected(item, true); + m_sourceListView->ensureItemVisible(item); + } + } + + if(infoList.count() > 0) { + m_modifySourceBtn->setEnabled(true); + m_removeSourceBtn->setEnabled(true); + slotModified(); // toggle apply button + } +} + +Tellico::SourceListViewItem* ConfigDialog::findItem(const QString& path_) const { + if(path_.isEmpty()) { + kdWarning() << "ConfigDialog::findItem() - empty path" << endl; + return 0; + } + + // this is a bit ugly, loop over all items, find the execexternal one + // that matches the path + for(QListViewItemIterator it(m_sourceListView); it.current(); ++it) { + SourceListViewItem* item = static_cast<SourceListViewItem*>(it.current()); + if(item->fetchType() != Fetch::ExecExternal) { + continue; + } + Fetch::ExecExternalFetcher* f = dynamic_cast<Fetch::ExecExternalFetcher*>(item->fetcher().data()); + if(f && f->execPath() == path_) { + return item; + } + } + myDebug() << "ConfigDialog::findItem() - no matching item found" << endl; + return 0; +} + +void ConfigDialog::slotShowTemplatePreview() { + GUI::PreviewDialog* dlg = new GUI::PreviewDialog(this); + + const QString templateName = m_templateCombo->currentData().toString(); + dlg->setXSLTFile(templateName + QString::fromLatin1(".xsl")); + + StyleOptions options; + options.fontFamily = m_fontCombo->currentFont(); + options.fontSize = m_fontSizeInput->value(); + options.baseColor = m_baseColorCombo->color(); + options.textColor = m_textColorCombo->color(); + options.highlightedTextColor = m_highTextColorCombo->color(); + options.highlightedBaseColor = m_highBaseColorCombo->color(); + dlg->setXSLTOptions(options); + + Data::CollPtr c = CollectionFactory::collection(Kernel::self()->collectionType(), true); + Data::EntryPtr e = new Data::Entry(c); + for(Data::FieldVec::ConstIterator f = c->fields().begin(); f != c->fields().end(); ++f) { + if(f->name() == Latin1Literal("title")) { + e->setField(f->name(), m_templateCombo->currentText()); + } else if(f->type() == Data::Field::Image) { + continue; + } else if(f->type() == Data::Field::Choice) { + e->setField(f->name(), f->allowed().front()); + } else if(f->type() == Data::Field::Number) { + e->setField(f->name(), QString::fromLatin1("1")); + } else if(f->type() == Data::Field::Bool) { + e->setField(f->name(), QString::fromLatin1("true")); + } else if(f->type() == Data::Field::Rating) { + e->setField(f->name(), QString::fromLatin1("5")); + } else { + e->setField(f->name(), f->title()); + } + } + + dlg->showEntry(e); + dlg->show(); + // dlg gets deleted by itself + // the finished() signal is connected in its constructor to delayedDestruct +} + +void ConfigDialog::loadTemplateList() { + QStringList files = KGlobal::dirs()->findAllResources("appdata", QString::fromLatin1("entry-templates/*.xsl"), + false, true); + KSortableValueList<QString, QString> templates; + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + QFileInfo fi(*it); + QString file = fi.fileName().section('.', 0, -2); + QString name = file; + name.replace('_', ' '); + QString title = i18n((name + QString::fromLatin1(" XSL Template")).utf8(), name.utf8()); + templates.insert(title, file); + } + templates.sort(); + + QString s = m_templateCombo->currentText(); + m_templateCombo->clear(); + for(KSortableValueList<QString, QString>::iterator it2 = templates.begin(); it2 != templates.end(); ++it2) { + m_templateCombo->insertItem((*it2).index(), (*it2).value()); + } + m_templateCombo->setCurrentItem(s); +} + +void ConfigDialog::slotInstallTemplate() { + QString filter = i18n("*.xsl|XSL Files (*.xsl)") + '\n'; + filter += i18n("*.tar.gz *.tgz|Template Packages (*.tar.gz)") + '\n'; + filter += i18n("*|All Files"); + + KURL u = KFileDialog::getOpenURL(QString::null, filter, this); + if(u.isEmpty() || !u.isValid()) { + return; + } + + NewStuff::Manager man(this); + if(man.installTemplate(u)) { + loadTemplateList(); + } +} + +void ConfigDialog::slotDownloadTemplate() { + NewStuff::Dialog dlg(NewStuff::EntryTemplate, this); + dlg.exec(); + loadTemplateList(); +} + +void ConfigDialog::slotDeleteTemplate() { + QDir dir(Tellico::saveLocation(QString::fromLatin1("entry-templates/"))); + dir.setNameFilter(QString::fromLatin1("*.xsl")); + dir.setFilter(QDir::Files | QDir::Writable); + QStringList files = dir.entryList(); + QMap<QString, QString> nameFileMap; + for(QStringList::Iterator it = files.begin(); it != files.end(); ++it) { + (*it).truncate((*it).length()-4); // remove ".xsl" + QString name = (*it); + name.replace('_', ' '); + nameFileMap.insert(name, *it); + } + bool ok; + QString name = KInputDialog::getItem(i18n("Delete Template"), + i18n("Select template to delete:"), + nameFileMap.keys(), 0, false, &ok, this); + if(ok && !name.isEmpty()) { + QString file = nameFileMap[name]; + NewStuff::Manager man(this); + man.removeTemplate(file); + loadTemplateList(); + } +} + +#include "configdialog.moc" diff --git a/src/configdialog.h b/src/configdialog.h new file mode 100644 index 0000000..64b5dbf --- /dev/null +++ b/src/configdialog.h @@ -0,0 +1,226 @@ +/**************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef CONFIGDIALOG_H +#define CONFIGDIALOG_H + +#include "fetch/fetcher.h" + +#include <kdialogbase.h> +#include <klistview.h> + +class KConfig; +class KLineEdit; +class KIntSpinBox; +class KPushButton; +class KIntNumInput; +class KFontCombo; +class KColorCombo; + +class QCheckBox; +class QRadioButton; + +namespace Tellico { + class SourceListViewItem; + namespace Fetch { + class ConfigWidget; + } + namespace GUI { + class ComboBox; + } + +/** + * The configuration dialog class allows the user to change the global application + * preferences. + * + * @author Robby Stephenson + */ +class ConfigDialog : public KDialogBase { +Q_OBJECT + +public: + /** + * The constructor sets up the Tabbed dialog pages. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + ConfigDialog(QWidget* parent, const char* name=0); + virtual ~ConfigDialog(); + + /** + * Reads the current configuration. Only the options which are not saved somewhere + * else are read at this point. + * + * @param config A pointer to the KConfig object + */ + void readConfiguration(); + /** + * Saves the configuration. @ref KConfigBase::sync is called. This method is called + * from the main Tellico object. + * + * @param config A pointer to the KConfig object + */ + void saveConfiguration(); + +signals: + /** + * Emitted whenever the Ok or Apply button is clicked. + */ + void signalConfigChanged(); + +private slots: + /** + * Called when anything gets changed + */ + void slotModified(); + /** + * Called when the Ok button is clicked. + */ + void slotOk(); + /** + * Called when the Apply button is clicked. + */ + void slotApply(); + /** + * Called when the Default button is clicked. + */ + void slotDefault(); + /** + * Update the help link for a page. + * + QCheckBox* m_cbWriteImagesInFile; * @param w The page + */ + void slotUpdateHelpLink(QWidget* w); + /** + * Create a new Internet source + */ + void slotNewSourceClicked(); + /** + * Modify a Internet source + */ + void slotModifySourceClicked(); + /** + * Remove a Internet source + */ + void slotRemoveSourceClicked(); + void slotSelectedSourceChanged(QListViewItem* item); + void slotMoveUpSourceClicked(); + void slotMoveDownSourceClicked(); + void slotNewStuffClicked(); + void slotShowTemplatePreview(); + void slotInstallTemplate(); + void slotDownloadTemplate(); + void slotDeleteTemplate(); + +private: + /** + * Sets-up the page for the general options. + */ + void setupGeneralPage(); + void readGeneralConfig(); + /** + * Sets-up the page for printing options. + */ + void setupPrintingPage(); + void readPrintingConfig(); + /** + * Sets-up the page for template options. + */ + void setupTemplatePage(); + void readTemplateConfig(); + void setupFetchPage(); + /** + * Load fetcher config + */ + void readFetchConfig(); + + SourceListViewItem* findItem(const QString& path) const; + void loadTemplateList(); + + bool m_modifying; + bool m_okClicked; + + QRadioButton* m_rbImageInFile; + QRadioButton* m_rbImageInAppDir; + QRadioButton* m_rbImageInLocalDir; + QCheckBox* m_cbOpenLastFile; + QCheckBox* m_cbShowTipDay; + QCheckBox* m_cbCapitalize; + QCheckBox* m_cbFormat; + KLineEdit* m_leCapitals; + KLineEdit* m_leArticles; + KLineEdit* m_leSuffixes; + KLineEdit* m_lePrefixes; + + QCheckBox* m_cbPrintHeaders; + QCheckBox* m_cbPrintFormatted; + QCheckBox* m_cbPrintGrouped; + KIntSpinBox* m_imageWidthBox; + KIntSpinBox* m_imageHeightBox; + + GUI::ComboBox* m_templateCombo; + KPushButton* m_previewButton; + KFontCombo* m_fontCombo; + KIntNumInput* m_fontSizeInput; + KColorCombo* m_baseColorCombo; + KColorCombo* m_textColorCombo; + KColorCombo* m_highBaseColorCombo; + KColorCombo* m_highTextColorCombo; + + KListView* m_sourceListView; + QMap<SourceListViewItem*, Fetch::ConfigWidget*> m_configWidgets; + QPtrList<Fetch::ConfigWidget> m_newStuffConfigWidgets; + QPtrList<Fetch::ConfigWidget> m_removedConfigWidgets; + KPushButton* m_modifySourceBtn; + KPushButton* m_moveUpSourceBtn; + KPushButton* m_moveDownSourceBtn; + KPushButton* m_removeSourceBtn; + KPushButton* m_newStuffBtn; +}; + +class GeneralFetcherInfo { +public: + GeneralFetcherInfo(Fetch::Type t, const QString& n, bool o) : type(t), name(n), updateOverwrite(o) {} + Fetch::Type type; + QString name; + bool updateOverwrite : 1; +}; + +class SourceListViewItem : public KListViewItem { +public: + SourceListViewItem(KListView* parent, const GeneralFetcherInfo& info, + const QString& groupName = QString::null); + + SourceListViewItem(KListView* parent, QListViewItem* after, + const GeneralFetcherInfo& info, const QString& groupName = QString::null); + + void setConfigGroup(const QString& s) { m_configGroup = s; } + const QString& configGroup() const { return m_configGroup; } + const Fetch::Type& fetchType() const { return m_info.type; } + void setUpdateOverwrite(bool b) { m_info.updateOverwrite = b; } + bool updateOverwrite() const { return m_info.updateOverwrite; } + void setNewSource(bool b) { m_newSource = b; } + bool isNewSource() const { return m_newSource; } + void setFetcher(Fetch::Fetcher::Ptr fetcher); + Fetch::Fetcher::Ptr fetcher() const { return m_fetcher; } + +private: + GeneralFetcherInfo m_info; + QString m_configGroup; + bool m_newSource : 1; + Fetch::Fetcher::Ptr m_fetcher; +}; + +} // end namespace +#endif diff --git a/src/controller.cpp b/src/controller.cpp new file mode 100644 index 0000000..1b3e29b --- /dev/null +++ b/src/controller.cpp @@ -0,0 +1,762 @@ +/*************************************************************************** + copyright : (C) 2003-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 "controller.h" +#include "mainwindow.h" +#include "groupview.h" +#include "detailedlistview.h" +#include "entryeditdialog.h" +#include "viewstack.h" +#include "entryview.h" +#include "entryiconview.h" +#include "entry.h" +#include "field.h" +#include "filter.h" +#include "filterdialog.h" +#include "tellico_kernel.h" +#include "latin1literal.h" +#include "collection.h" +#include "document.h" +#include "borrower.h" +#include "filterview.h" +#include "loanview.h" +#include "entryitem.h" +#include "gui/tabcontrol.h" +#include "calendarhandler.h" +#include "tellico_debug.h" +#include "groupiterator.h" +#include "tellico_utils.h" +#include "entryupdater.h" +#include "entrymerger.h" + +#include <klocale.h> +#include <kmessagebox.h> +#include <kaction.h> +#include <ktoolbarbutton.h> + +#include <qpopupmenu.h> + +using Tellico::Controller; + +Controller* Controller::s_self = 0; + +Controller::Controller(MainWindow* parent_, const char* name_) + : QObject(parent_, name_), m_mainWindow(parent_), m_working (false), m_widgetWithSelection(0) { +} + +void Controller::addObserver(Observer* obs) { + m_observers.push_back(obs); +} + +void Controller::removeObserver(Observer* obs) { + m_observers.remove(obs); +} + +Tellico::GroupIterator Controller::groupIterator() const { + return GroupIterator(m_mainWindow->m_groupView); +} + +QString Controller::groupBy() const { + return m_mainWindow->m_groupView->groupBy(); +} + +QStringList Controller::expandedGroupBy() const { + QStringList g = groupBy(); + // special case for pseudo-group + if(g[0] == Data::Collection::s_peopleGroupName) { + g.clear(); + Data::FieldVec fields = Data::Document::self()->collection()->peopleFields(); + for(Data::FieldVec::Iterator it = fields.begin(); it != fields.end(); ++it) { + g << it->name(); + } + } + // special case for no groups + if(g[0].isEmpty()) { + g.clear(); + } + return g; +} + +QStringList Controller::sortTitles() const { + QStringList list; + list << m_mainWindow->m_detailedView->sortColumnTitle1(); + list << m_mainWindow->m_detailedView->sortColumnTitle2(); + list << m_mainWindow->m_detailedView->sortColumnTitle3(); + return list; +} + +QStringList Controller::visibleColumns() const { + return m_mainWindow->m_detailedView->visibleColumns(); +} + +Tellico::Data::EntryVec Controller::visibleEntries() { + return m_mainWindow->m_detailedView->visibleEntries(); +} + +void Controller::slotCollectionAdded(Data::CollPtr coll_) { +// myDebug() << "Controller::slotCollectionAdded()" << endl; + // at start-up, this might get called too early, so check and bail + if(!m_mainWindow->m_groupView) { + return; + } + + // do this first because the group view will need it later + m_mainWindow->readCollectionOptions(coll_); + m_mainWindow->slotUpdateToolbarIcons(); + m_mainWindow->updateEntrySources(); // has to be called before all the addCollection() + // calls in the widgets since they may want menu updates + +// blockAllSignals(true); + m_mainWindow->m_detailedView->addCollection(coll_); + m_mainWindow->m_groupView->addCollection(coll_); + m_mainWindow->m_editDialog->setLayout(coll_); + if(!coll_->filters().isEmpty()) { + m_mainWindow->addFilterView(); + m_mainWindow->m_filterView->addCollection(coll_); + m_mainWindow->m_viewTabs->setTabBarHidden(false); + } + if(!coll_->borrowers().isEmpty()) { + m_mainWindow->addLoanView(); + m_mainWindow->m_loanView->addCollection(coll_); + m_mainWindow->m_viewTabs->setTabBarHidden(false); + } +// blockAllSignals(false); + + m_mainWindow->slotStatusMsg(i18n("Ready.")); + + m_selectedEntries.clear(); + m_mainWindow->slotEntryCount(); + + emit collectionAdded(coll_->type()); + + updateActions(); + + connect(coll_, SIGNAL(signalGroupsModified(Tellico::Data::CollPtr, PtrVector<Tellico::Data::EntryGroup>)), + m_mainWindow->m_groupView, SLOT(slotModifyGroups(Tellico::Data::CollPtr, PtrVector<Tellico::Data::EntryGroup>))); + + connect(coll_, SIGNAL(signalRefreshField(Tellico::Data::FieldPtr)), + this, SLOT(slotRefreshField(Tellico::Data::FieldPtr))); +} + +void Controller::slotCollectionModified(Data::CollPtr coll_) { + // easiest thing is to signal collection deleted, then added? + // FIXME: Signals for delete collection and then added are yucky + slotCollectionDeleted(coll_); + slotCollectionAdded(coll_); +} + +void Controller::slotCollectionDeleted(Data::CollPtr coll_) { +// myDebug() << "Controller::slotCollectionDeleted()" << endl; + + blockAllSignals(true); + m_mainWindow->saveCollectionOptions(coll_); + m_mainWindow->m_groupView->removeCollection(coll_); + if(m_mainWindow->m_filterView) { + m_mainWindow->m_filterView->clear(); + } + if(m_mainWindow->m_loanView) { + m_mainWindow->m_loanView->clear(); + } + m_mainWindow->m_detailedView->removeCollection(coll_); + m_mainWindow->m_viewStack->clear(); + blockAllSignals(false); + + // disconnect all signals from the collection + // this is needed because the Collection::appendCollection() and mergeCollection() + // functions signal collection deleted then added for the same collection + coll_->disconnect(); +} + +void Controller::addedEntries(Data::EntryVec entries_) { + blockAllSignals(true); + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->addEntries(entries_); + } + m_mainWindow->slotQueueFilter(); + blockAllSignals(false); +} + +void Controller::modifiedEntries(Data::EntryVec entries_) { + // when a new document is being loaded, loans are added to borrowers, which + // end up calling Entry::checkIn() which called Document::saveEntry() which calls here + // ignore that + if(!m_mainWindow->m_initialized) { + return; + } + blockAllSignals(true); + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->modifyEntries(entries_); + } + m_mainWindow->m_viewStack->entryView()->slotRefresh(); // special case + m_mainWindow->slotQueueFilter(); + blockAllSignals(false); +} + +void Controller::removedEntries(Data::EntryVec entries_) { + blockAllSignals(true); + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->removeEntries(entries_); + } + for(Data::EntryVecIt it = entries_.begin(); it != entries_.end(); ++it) { + m_selectedEntries.remove(it); + m_currentEntries.remove(it); + } + if(m_currentEntries.isEmpty()) { + m_mainWindow->m_viewStack->entryView()->clear(); + } + m_mainWindow->slotEntryCount(); + m_mainWindow->slotQueueFilter(); + blockAllSignals(false); +} + +void Controller::addedField(Data::CollPtr coll_, Data::FieldPtr field_) { + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->addField(coll_, field_); + } + m_mainWindow->m_viewStack->refresh(); + m_mainWindow->slotUpdateCollectionToolBar(coll_); + m_mainWindow->slotQueueFilter(); +} + +void Controller::removedField(Data::CollPtr coll_, Data::FieldPtr field_) { +// myDebug() << "Controller::removedField() - " << field_->name() << endl; + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->removeField(coll_, field_); + } + m_mainWindow->m_viewStack->refresh(); + m_mainWindow->slotUpdateCollectionToolBar(coll_); + m_mainWindow->slotQueueFilter(); +} + +void Controller::modifiedField(Data::CollPtr coll_, Data::FieldPtr oldField_, Data::FieldPtr newField_) { + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->modifyField(coll_, oldField_, newField_); + } + m_mainWindow->m_viewStack->refresh(); + m_mainWindow->slotUpdateCollectionToolBar(coll_); + m_mainWindow->slotQueueFilter(); +} + +void Controller::reorderedFields(Data::CollPtr coll_) { + m_mainWindow->m_editDialog->setLayout(coll_); + m_mainWindow->m_detailedView->reorderFields(coll_->fields()); + m_mainWindow->slotUpdateCollectionToolBar(coll_); + m_mainWindow->m_viewStack->refresh(); +} + +void Controller::slotClearSelection() { + if(m_working) { + return; + } + + m_working = true; + blockAllSignals(true); + + m_mainWindow->m_detailedView->clearSelection(); + m_mainWindow->m_groupView->clearSelection(); + if(m_mainWindow->m_loanView) { + m_mainWindow->m_loanView->clearSelection(); + } +// m_mainWindow->m_editDialog->clear(); let this stay +// m_mainWindow->m_viewStack->clear(); let this stay + + blockAllSignals(false); + + m_selectedEntries.clear(); + updateActions(); + m_mainWindow->slotEntryCount(); + m_working = false; +} + +void Controller::slotUpdateSelection(QWidget* widget_, const Data::EntryVec& entries_) { + if(m_working) { + return; + } + m_working = true; + + if(widget_) { + m_widgetWithSelection = widget_; + } +// myDebug() << "Controller::slotUpdateSelection() entryList - " << entries_.count() << endl; + + blockAllSignals(true); +// in the list view and group view, if entries are selected in one, clear selection in other + if(m_widgetWithSelection != m_mainWindow->m_detailedView) { + m_mainWindow->m_detailedView->clearSelection(); + } + if(m_widgetWithSelection != m_mainWindow->m_groupView) { + m_mainWindow->m_groupView->clearSelection(); + } + if(m_mainWindow->m_filterView && m_widgetWithSelection != m_mainWindow->m_filterView) { + m_mainWindow->m_filterView->clearSelection(); + } + if(m_mainWindow->m_loanView && m_widgetWithSelection != m_mainWindow->m_loanView) { + m_mainWindow->m_loanView->clearSelection(); + } + if(m_widgetWithSelection != m_mainWindow->m_editDialog) { + m_mainWindow->m_editDialog->setContents(entries_); + } + // only show first one + if(m_widgetWithSelection && m_widgetWithSelection != m_mainWindow->m_viewStack->iconView()) { + if(entries_.count() > 1) { + m_mainWindow->m_viewStack->showEntries(entries_); + } else if(entries_.count() > 0) { + m_mainWindow->m_viewStack->showEntry(entries_[0]); + } + } + blockAllSignals(false); + + m_selectedEntries = entries_; + updateActions(); + m_mainWindow->slotEntryCount(); + m_working = false; +} + +void Controller::slotGoPrevEntry() { + goEntrySibling(PrevEntry); +} + +void Controller::slotGoNextEntry() { + goEntrySibling(NextEntry); +} + +void Controller::goEntrySibling(EntryDirection dir_) { + // if there are currently multiple selected, then do nothing + if(m_selectedEntries.count() != 1) { + return; + } + // find the widget that has an entry selected + GUI::ListView* view = ::qt_cast<GUI::ListView*>(m_widgetWithSelection); + if(!view) { + return; + } + + GUI::ListViewItemList items = view->selectedItems(); + if(items.count() != 1) { + return; + } + GUI::ListViewItem* item = items.first(); + if(item->isEntryItem()) { + bool looped = false; + // check sanity + if(m_selectedEntries.front() != static_cast<EntryItem*>(item)->entry()) { + myDebug() << "Controller::slotGoNextEntry() - entries don't match!" << endl; + } + GUI::ListViewItem* nextItem = static_cast<GUI::ListViewItem*>(dir_ == PrevEntry + ? item->itemAbove() + : item->itemBelow()); + if(!nextItem) { + // cycle through + nextItem = static_cast<GUI::ListViewItem*>(dir_ == PrevEntry + ? view->lastItem() + : view->firstChild()); + looped = true; + } + while(!nextItem->isVisible()) { + nextItem = static_cast<GUI::ListViewItem*>(dir_ == PrevEntry + ? nextItem->itemAbove() + : nextItem->itemBelow()); + } + while(nextItem && !nextItem->isEntryItem()) { + nextItem->setOpen(true); // have to be open to find the next one + nextItem = static_cast<GUI::ListViewItem*>(dir_ == PrevEntry + ? nextItem->itemAbove() + : nextItem->itemBelow()); + if(!nextItem && !looped) { + // cycle through + nextItem = static_cast<GUI::ListViewItem*>(dir_ == PrevEntry + ? view->lastItem() + : view->firstChild()); + looped = true; + } + } + if(nextItem) { + Data::EntryPtr e = static_cast<EntryItem*>(nextItem)->entry(); + view->blockSignals(true); + view->setSelected(item, false); + view->setSelected(nextItem, true); + view->ensureItemVisible(nextItem); + view->blockSignals(false); + slotUpdateSelection(view, e); + } + } +} + +void Controller::slotUpdateCurrent(const Data::EntryVec& entries_) { + if(m_working) { + return; + } + m_working = true; + + blockAllSignals(true); + m_mainWindow->m_viewStack->showEntries(entries_); + blockAllSignals(false); + + m_currentEntries = entries_; + m_working = false; +} + +void Controller::slotUpdateSelectedEntries(const QString& source_) { + if(m_selectedEntries.isEmpty()) { + return; + } + + // it deletes itself when done + // signal mapper strings can't be empty, "_all" is set in mainwindow + if(source_.isEmpty() || source_ == Latin1Literal("_all")) { + new EntryUpdater(m_selectedEntries.front()->collection(), m_selectedEntries, this); + } else { + new EntryUpdater(source_, m_selectedEntries.front()->collection(), m_selectedEntries, this); + } +} + +void Controller::slotDeleteSelectedEntries() { + if(m_selectedEntries.isEmpty()) { + return; + } + + m_working = true; + + // confirm delete + if(m_selectedEntries.count() == 1) { + QString str = i18n("Do you really want to delete this entry?"); + QString dontAsk = QString::fromLatin1("DeleteEntry"); + int ret = KMessageBox::warningContinueCancel(Kernel::self()->widget(), str, i18n("Delete Entry"), + KGuiItem(i18n("&Delete"), QString::fromLatin1("editdelete")), dontAsk); + if(ret != KMessageBox::Continue) { + return; + } + } else { + QStringList names; + for(Data::EntryVecIt entry = m_selectedEntries.begin(); entry != m_selectedEntries.end(); ++entry) { + names += entry->title(); + } + QString str = i18n("Do you really want to delete these entries?"); + // historically called DeleteMultipleBooks, don't change + QString dontAsk = QString::fromLatin1("DeleteMultipleBooks"); + int ret = KMessageBox::warningContinueCancelList(Kernel::self()->widget(), str, names, + i18n("Delete Multiple Entries"), + KGuiItem(i18n("&Delete"), QString::fromLatin1("editdelete")), dontAsk); + if(ret != KMessageBox::Continue) { + return; + } + } + + GUI::CursorSaver cs; + Kernel::self()->removeEntries(m_selectedEntries); + updateActions(); + + m_working = false; + + // special case, the detailed list view selects the next item, so handle that +// Data::EntryList newList; +// for(GUI::ListViewItemListIt it(m_mainWindow->m_detailedView->selectedItems()); it.current(); ++it) { +// newList.append(static_cast<EntryItem*>(it.current())->entry()); +// } +// slotUpdateSelection(m_mainWindow->m_detailedView, newList); + slotClearSelection(); +} + +void Controller::slotMergeSelectedEntries() { + // merge requires at least 2 entries + if(m_selectedEntries.count() < 2) { + return; + } + + new EntryMerger(m_selectedEntries, this); +} + +void Controller::slotRefreshField(Data::FieldPtr field_) { +// myDebug() << "Controller::slotRefreshField()" << endl; + // group view only needs to refresh if it's the title + if(field_->name() == Latin1Literal("title")) { + m_mainWindow->m_groupView->populateCollection(); + } + m_mainWindow->m_detailedView->slotRefresh(); + m_mainWindow->m_viewStack->refresh(); +} + +void Controller::slotCopySelectedEntries() { + if(m_selectedEntries.isEmpty()) { + return; + } + + // keep copy of selected entries + Data::EntryVec old = m_selectedEntries; + + GUI::CursorSaver cs; + // need to create copies + Data::EntryVec entries; + for(Data::EntryVecIt it = m_selectedEntries.begin(); it != m_selectedEntries.end(); ++it) { + entries.append(new Data::Entry(*it)); + } + Kernel::self()->addEntries(entries, false); + slotUpdateSelection(0, old); +} + +void Controller::blockAllSignals(bool block_) const { +// sanity check + if(!m_mainWindow->m_initialized) { + return; + } + m_mainWindow->m_detailedView->blockSignals(block_); + m_mainWindow->m_groupView->blockSignals(block_); + if(m_mainWindow->m_loanView) { + m_mainWindow->m_loanView->blockSignals(block_); + } + if(m_mainWindow->m_filterView) { + m_mainWindow->m_filterView->blockSignals(block_); + } + m_mainWindow->m_editDialog->blockSignals(block_); + m_mainWindow->m_viewStack->iconView()->blockSignals(block_); +} + +void Controller::slotUpdateFilter(FilterPtr filter_) { +// myDebug() << "Controller::slotUpdateFilter()" << endl; + blockAllSignals(true); + + // the view takes over ownership of the filter + if(filter_ && !filter_->isEmpty()) { + // clear the icon view selection only + // the detailed view takes care of itself + m_mainWindow->m_viewStack->iconView()->clearSelection(); + m_selectedEntries.clear(); + } + updateActions(); + + m_mainWindow->m_detailedView->setFilter(filter_); // takes ownership + + blockAllSignals(false); + + m_mainWindow->slotEntryCount(); +} + +void Controller::editEntry(Data::EntryPtr) const { + m_mainWindow->slotShowEntryEditor(); +} + +void Controller::plugCollectionActions(QPopupMenu* popup_) { + if(!popup_) { + return; + } + + m_mainWindow->action("coll_rename_collection")->plug(popup_); + m_mainWindow->action("coll_fields")->plug(popup_); + m_mainWindow->action("change_entry_grouping")->plug(popup_); +} + +void Controller::plugEntryActions(QPopupMenu* popup_) { + if(!popup_) { + return; + } + +// m_mainWindow->m_newEntry->plug(popup_); + m_mainWindow->m_editEntry->plug(popup_); + m_mainWindow->m_copyEntry->plug(popup_); + m_mainWindow->m_deleteEntry->plug(popup_); + m_mainWindow->m_mergeEntry->plug(popup_); + m_mainWindow->m_updateEntryMenu->plug(popup_); + // there's a bug in KActionMenu with KXMLGUIFactory::plugActionList + // pluging the menu action isn't enough to have the popup get populated + plugUpdateMenu(popup_); + popup_->insertSeparator(); + m_mainWindow->m_checkOutEntry->plug(popup_); +} + +void Controller::plugUpdateMenu(QPopupMenu* popup_) { + QPopupMenu* updatePopup = 0; + const uint count = popup_->count(); + for(uint i = 0; i < count; ++i) { + QMenuItem* item = popup_->findItem(popup_->idAt(i)); + if(item && item->text() == m_mainWindow->m_updateEntryMenu->text()) { + updatePopup = item->popup(); + break; + } + } + + if(!updatePopup) { + return; + } + + // I can't figure out why the actions get duplicated, but they do + // so clear them all + m_mainWindow->m_updateAll->unplug(updatePopup); + for(QPtrListIterator<KAction> it(m_mainWindow->m_fetchActions); it.current(); ++it) { + it.current()->unplug(updatePopup); + } + + // clear separator, too + updatePopup->clear(); + + m_mainWindow->m_updateAll->plug(updatePopup); + updatePopup->insertSeparator(); + for(QPtrListIterator<KAction> it(m_mainWindow->m_fetchActions); it.current(); ++it) { + it.current()->plug(updatePopup); + } +} + +void Controller::updateActions() const { + bool emptySelection = m_selectedEntries.isEmpty(); + m_mainWindow->stateChanged(QString::fromLatin1("empty_selection"), + emptySelection ? KXMLGUIClient::StateNoReverse : KXMLGUIClient::StateReverse); + for(QPtrListIterator<KAction> it(m_mainWindow->m_fetchActions); it.current(); ++it) { + it.current()->setEnabled(!emptySelection); + } + //only enable citation items when it's a bibliography + bool isBibtex = Kernel::self()->collectionType() == Data::Collection::Bibtex; + if(isBibtex) { + m_mainWindow->action("cite_clipboard")->setEnabled(!emptySelection); + m_mainWindow->action("cite_lyxpipe")->setEnabled(!emptySelection); + m_mainWindow->action("cite_openoffice")->setEnabled(!emptySelection); + } + m_mainWindow->m_checkInEntry->setEnabled(canCheckIn()); + + if(m_selectedEntries.count() < 2) { + m_mainWindow->m_editEntry->setText(i18n("&Edit Entry...")); + m_mainWindow->m_copyEntry->setText(i18n("D&uplicate Entry")); + m_mainWindow->m_updateEntryMenu->setText(i18n("&Update Entry")); + m_mainWindow->m_deleteEntry->setText(i18n("&Delete Entry")); + m_mainWindow->m_mergeEntry->setEnabled(false); + } else { + m_mainWindow->m_editEntry->setText(i18n("&Edit Entries...")); + m_mainWindow->m_copyEntry->setText(i18n("D&uplicate Entries")); + m_mainWindow->m_updateEntryMenu->setText(i18n("&Update Entries")); + m_mainWindow->m_deleteEntry->setText(i18n("&Delete Entries")); + m_mainWindow->m_mergeEntry->setEnabled(true); + } +} + +void Controller::addedBorrower(Data::BorrowerPtr borrower_) { + m_mainWindow->addLoanView(); // just in case + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->addBorrower(borrower_); + } + m_mainWindow->m_viewTabs->setTabBarHidden(false); +} + +void Controller::modifiedBorrower(Data::BorrowerPtr borrower_) { + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->modifyBorrower(borrower_); + } + hideTabs(); +} + +void Controller::addedFilter(FilterPtr filter_) { + m_mainWindow->addFilterView(); // just in case + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->addFilter(filter_); + } + m_mainWindow->m_viewTabs->setTabBarHidden(false); +} + +void Controller::removedFilter(FilterPtr filter_) { + for(ObserverVec::Iterator it = m_observers.begin(); it != m_observers.end(); ++it) { + it->removeFilter(filter_); + } + hideTabs(); +} + +void Controller::slotCheckOut() { + if(m_selectedEntries.isEmpty()) { + return; + } + + Data::EntryVec loanedEntries = m_selectedEntries; + + // check to see if any of the entries are already on-loan, and warn user + QMap<QString, Data::EntryPtr> alreadyLoaned; + const Data::BorrowerVec& borrowers = Data::Document::self()->collection()->borrowers(); + for(Data::BorrowerVec::ConstIterator it = borrowers.begin(); it != borrowers.end(); ++it) { + const Data::LoanVec& loans = it->loans(); + for(Data::LoanVec::ConstIterator it2 = loans.begin(); it2 != loans.end(); ++it2) { + if(m_selectedEntries.contains(it2->entry())) { + alreadyLoaned.insert(it2->entry()->title(), it2->entry()); + } + } + } + if(!alreadyLoaned.isEmpty()) { + KMessageBox::informationList(Kernel::self()->widget(), + i18n("The following items are already loaned, but Tellico " + "does not currently support lending an item multiple " + "times. They will be removed from the list of items " + "to lend."), + alreadyLoaned.keys()); + QMapConstIterator<QString, Data::EntryPtr> it = alreadyLoaned.constBegin(); + QMapConstIterator<QString, Data::EntryPtr> end = alreadyLoaned.constEnd(); + for( ; it != end; ++it) { + loanedEntries.remove(it.data()); + } + if(loanedEntries.isEmpty()) { + return; + } + } + + if(Kernel::self()->addLoans(loanedEntries)) { + m_mainWindow->m_checkInEntry->setEnabled(true); + } +} + +void Controller::slotCheckIn() { + slotCheckIn(m_selectedEntries); +} + +void Controller::slotCheckIn(const Data::EntryVec& entries_) { + if(entries_.isEmpty()) { + return; + } + + Data::LoanVec loans; + for(Data::EntryVec::ConstIterator it = entries_.begin(); it != entries_.end(); ++it) { + // these have to be in the loop since if a borrower gets empty + // it will be deleted, so the vector could change, for every entry iterator + Data::BorrowerVec vec = Data::Document::self()->collection()->borrowers(); + // vec.end() must be in the loop, do NOT cache the value, it could change! + for(Data::BorrowerVec::Iterator bIt = vec.begin(); bIt != vec.end(); ++bIt) { + Data::LoanPtr l = bIt->loan(it.data()); + if(l) { + loans.append(l); + // assume it's only loaned once + break; + } + } + } + + if(Kernel::self()->removeLoans(loans)) { + m_mainWindow->m_checkInEntry->setEnabled(false); + } + hideTabs(); +} + +void Controller::hideTabs() const { + if((!m_mainWindow->m_filterView || m_mainWindow->m_filterView->childCount() == 0) && + (!m_mainWindow->m_loanView || m_mainWindow->m_loanView->childCount() == 0)) { + m_mainWindow->m_viewTabs->showPage(m_mainWindow->m_groupView); + m_mainWindow->m_viewTabs->setTabBarHidden(true); + } +} + +inline +bool Controller::canCheckIn() const { + for(Data::EntryVec::ConstIterator entry = m_selectedEntries.begin(); entry != m_selectedEntries.end(); ++entry) { + if(entry->field(QString::fromLatin1("loaned")) == Latin1Literal("true")) { + return true; + } + } + return false; +} + +void Controller::updatedFetchers() { + m_mainWindow->updateEntrySources(); +} + +#include "controller.moc" diff --git a/src/controller.h b/src/controller.h new file mode 100644 index 0000000..58c3917 --- /dev/null +++ b/src/controller.h @@ -0,0 +1,175 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOCONTROLLER_H +#define TELLICOCONTROLLER_H + +class QPopupMenu; + +namespace Tellico { + class MainWindow; + class GroupView; + class GroupIterator; + namespace Data { + class Collection; + } +} + +#include "entry.h" + +#include <qobject.h> + +namespace Tellico { +class Observer; + +/** + * @author Robby Stephenson + */ +class Controller : public QObject { +Q_OBJECT + +public: + static Controller* self() { return s_self; } + /** + * Initializes the singleton. Should just be called once, from Tellico::MainWindow + */ + static void init(MainWindow* parent, const char* name=0) { + if(!s_self) s_self = new Controller(parent, name); + } + + const Data::EntryVec& selectedEntries() const { return m_selectedEntries; } + Data::EntryVec visibleEntries(); + + void editEntry(Data::EntryPtr) const; + void hideTabs() const; + /** + * Plug the default collection actions into a widget + */ + void plugCollectionActions(QPopupMenu* popup); + /** + * Plug the default entry actions into a widget + */ + void plugEntryActions(QPopupMenu* popup); + void updateActions() const; + + GroupIterator groupIterator() const; + /** + * Returns the name of the field being used to group the entries. + * That field name may not be an actual field in the collection, since + * pseudo-groups like _people exist. + */ + QString groupBy() const; + /** + * Returns a list of the fields being used to group the entries. + * For ordinary fields, the list has a single item, the field name. + * For the pseudo-group _people, all the people fields are included. + */ + QStringList expandedGroupBy() const; + /** + * Returns a list of the titles of the fields being used to sort the entries in the detailed column view. + */ + QStringList sortTitles() const; + /** + * Returns the title of the fields currently visible in the detailed column view. + */ + QStringList visibleColumns() const; + + void addObserver(Observer* obs); + void removeObserver(Observer* obs); + + void addedField(Data::CollPtr coll, Data::FieldPtr field); + void modifiedField(Data::CollPtr coll, Data::FieldPtr oldField, Data::FieldPtr newField); + void removedField(Data::CollPtr coll, Data::FieldPtr field); + + void addedEntries(Data::EntryVec entries); + void modifiedEntries(Data::EntryVec entries); + void removedEntries(Data::EntryVec entries); + + void addedBorrower(Data::BorrowerPtr borrower); + void modifiedBorrower(Data::BorrowerPtr borrower); + + void addedFilter(FilterPtr filter); + void removedFilter(FilterPtr filter); + + void reorderedFields(Data::CollPtr coll); + void updatedFetchers(); + +public slots: + /** + * When a collection is added to the document, certain actions need to be taken + * by the parent app. The collection toolbar is updated, the entry count is set, and + * the collection's modified signal is connected to the @ref GroupView widget. + * + * @param coll A pointer to the collection being added + */ + void slotCollectionAdded(Tellico::Data::CollPtr coll); + void slotCollectionModified(Tellico::Data::CollPtr coll); + /** + * Removes a collection from all the widgets + * + * @param coll A pointer to the collection being added + */ + void slotCollectionDeleted(Tellico::Data::CollPtr coll); + void slotRefreshField(Tellico::Data::FieldPtr field); + + void slotClearSelection(); + /** + * Updates the widgets when entries are selected. + * + * param widget A pointer to the widget where the entries were selected + * @param widget The widget doing the selecting, if NULL, then use previous + * @param entries The list of selected entries + */ + void slotUpdateSelection(QWidget* widget, const Tellico::Data::EntryVec& entries); + void slotUpdateCurrent(const Tellico::Data::EntryVec& entries); + void slotCopySelectedEntries(); + void slotUpdateSelectedEntries(const QString& source); + void slotDeleteSelectedEntries(); + void slotMergeSelectedEntries(); + void slotUpdateFilter(Tellico::FilterPtr filter); + void slotCheckOut(); + void slotCheckIn(); + void slotCheckIn(const Data::EntryVec& entries); + void slotGoPrevEntry(); + void slotGoNextEntry(); + +signals: + void collectionAdded(int collType); + +private: + static Controller* s_self; + Controller(MainWindow* parent, const char* name); + + void blockAllSignals(bool block) const; + bool canCheckIn() const; + void plugUpdateMenu(QPopupMenu* popup); + enum EntryDirection { PrevEntry, NextEntry }; + void goEntrySibling(EntryDirection dir); + + MainWindow* m_mainWindow; + + bool m_working; + + typedef PtrVector<Tellico::Observer> ObserverVec; + ObserverVec m_observers; + + /** + * Keep track of the selected entries so that a top-level delete has something for reference + */ + Data::EntryVec m_selectedEntries; + Data::EntryVec m_currentEntries; + QWidget* m_widgetWithSelection; +}; + +} // end namespace +#endif diff --git a/src/core/Makefile.am b/src/core/Makefile.am new file mode 100644 index 0000000..2d61d84 --- /dev/null +++ b/src/core/Makefile.am @@ -0,0 +1,27 @@ +AM_CPPFLAGS = $(all_includes) + +noinst_LIBRARIES = libcore.a +libcore_a_SOURCES = dcopinterface.cpp dcopinterface.skel drophandler.cpp \ + netaccess.cpp tellico_config.cpp tellico_config.kcfgc tellico_config_addons.cpp + +libcore_a_METASOURCES = AUTO + +KDE_OPTIONS = noautodist + +EXTRA_DIST = \ +tellico_config.kcfg tellico_config.kcfgc \ +tellico_config_addons.h tellico_config_addons.cpp \ +dcopinterface.h dcopinterface.cpp \ +netaccess.h netaccess.cpp \ +drophandler.h drophandler.cpp \ +tellico-rename.upd tellico.upd \ +tellico-1-3-update.pl + +kde_kcfg_DATA = tellico_config.kcfg + +updatedir = $(kde_datadir)/kconf_update +update_DATA = tellico-rename.upd tellico.upd +update_SCRIPTS = tellico-1-3-update.pl + +CLEANFILES = *~ *.loT tellico_config.h tellico_config.cpp + diff --git a/src/core/dcopinterface.cpp b/src/core/dcopinterface.cpp new file mode 100644 index 0000000..f375b6b --- /dev/null +++ b/src/core/dcopinterface.cpp @@ -0,0 +1,171 @@ +/*************************************************************************** + copyright : (C) 2004-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 "dcopinterface.h" +#include "../latin1literal.h" +#include "../controller.h" +#include "../tellico_kernel.h" +#include "../document.h" +#include "../collection.h" +#include "../translators/bibtexhandler.h" + +using Tellico::ApplicationInterface; +using Tellico::CollectionInterface; + +Tellico::Import::Action ApplicationInterface::actionType(const QString& actionName) { + QString name = actionName.lower(); + if(name == Latin1Literal("append")) { + return Import::Append; + } else if(name == Latin1Literal("merge")) { + return Import::Merge; + } + return Import::Replace; +} + +QValueList<long> ApplicationInterface::selectedEntries() const { + QValueList<long> ids; + Data::EntryVec entries = Controller::self()->selectedEntries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + ids << entry->id(); + } + return ids; +} + +QValueList<long> ApplicationInterface::filteredEntries() const { + QValueList<long> ids; + Data::EntryVec entries = Controller::self()->visibleEntries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + ids << entry->id(); + } + return ids; +} + +long CollectionInterface::addEntry() { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return -1; + } + Data::EntryPtr entry = new Data::Entry(coll); + Kernel::self()->addEntries(entry, false); + return entry->id(); +} + +bool CollectionInterface::removeEntry(long id_) { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return false; + } + Data::EntryPtr entry = coll->entryById(id_); + if(!entry) { + return false; + } + Kernel::self()->removeEntries(entry); + return coll->entryById(id_) == 0; +} + +QStringList CollectionInterface::values(const QString& fieldName_) const { + QStringList results; + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return results; + } + Data::FieldPtr field = coll->fieldByName(fieldName_); + if(!field) { + field = coll->fieldByTitle(fieldName_); + } + if(!field) { + return results; + } + Data::EntryVec entries = Controller::self()->selectedEntries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + results += entry->fields(field, false); + } + return results; +} + +QStringList CollectionInterface::values(long id_, const QString& fieldName_) const { + QStringList results; + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return results; + } + Data::FieldPtr field = coll->fieldByName(fieldName_); + if(!field) { + field = coll->fieldByTitle(fieldName_); + } + if(!field) { + return results; + } + Data::EntryPtr entry = coll->entryById(id_); + if(entry) { + results += entry->fields(field, false); + } + return results; +} + +QStringList CollectionInterface::bibtexKeys() const { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll || coll->type() != Data::Collection::Bibtex) { + return QStringList(); + } + return BibtexHandler::bibtexKeys(Controller::self()->selectedEntries()); +} + +QString CollectionInterface::bibtexKey(long id_) const { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll || coll->type() != Data::Collection::Bibtex) { + return QString(); + } + Data::EntryPtr entry = coll->entryById(id_); + if(!entry) { + return QString(); + } + return BibtexHandler::bibtexKeys(entry).first(); +} + +bool CollectionInterface::setFieldValue(long id_, const QString& fieldName_, const QString& value_) { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return false; + } + Data::EntryPtr entry = coll->entryById(id_); + if(!entry) { + return false; + } + Data::EntryPtr oldEntry = new Data::Entry(*entry); + if(!entry->setField(fieldName_, value_)) { + return false; + } + Kernel::self()->modifyEntries(oldEntry, entry); + return true; +} + +bool CollectionInterface::addFieldValue(long id_, const QString& fieldName_, const QString& value_) { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return false; + } + Data::EntryPtr entry = coll->entryById(id_); + if(!entry) { + return false; + } + Data::EntryPtr oldEntry = new Data::Entry(*entry); + QStringList values = entry->fields(fieldName_, false); + QStringList newValues = values; + newValues << value_; + if(!entry->setField(fieldName_, newValues.join(QString::fromLatin1("; ")))) { + return false; + } + Kernel::self()->modifyEntries(oldEntry, entry); + return true; +} diff --git a/src/core/dcopinterface.h b/src/core/dcopinterface.h new file mode 100644 index 0000000..018de80 --- /dev/null +++ b/src/core/dcopinterface.h @@ -0,0 +1,85 @@ +/*************************************************************************** + copyright : (C) 2004-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DCOPINTERFACE_H +#define TELLICO_DCOPINTERFACE_H + +#include "../translators/translators.h" + +#include <dcopobject.h> +#include <kurl.h> + +#include <qstringlist.h> // used in generated dcopinterface_skel.cpp + +namespace Tellico { + +class ApplicationInterface : public DCOPObject { +K_DCOP +k_dcop: + bool importTellico(const QString& file, const QString& action) + { return importFile(Import::TellicoXML, KURL::fromPathOrURL(file), actionType(action)); } + bool importBibtex(const QString& file, const QString& action) + { return importFile(Import::Bibtex, KURL::fromPathOrURL(file), actionType(action)); } + bool importMODS(const QString& file, const QString& action) + { return importFile(Import::MODS, KURL::fromPathOrURL(file), actionType(action)); } + bool importRIS(const QString& file, const QString& action) + { return importFile(Import::RIS, KURL::fromPathOrURL(file), actionType(action)); } + + bool exportXML(const QString& file) + { return exportCollection(Export::TellicoXML, KURL::fromPathOrURL(file)); } + bool exportZip(const QString& file) + { return exportCollection(Export::TellicoZip, KURL::fromPathOrURL(file)); } + bool exportBibtex(const QString& file) + { return exportCollection(Export::Bibtex, KURL::fromPathOrURL(file)); } + bool exportHTML(const QString& file) + { return exportCollection(Export::HTML, KURL::fromPathOrURL(file)); } + bool exportCSV(const QString& file) + { return exportCollection(Export::CSV, KURL::fromPathOrURL(file)); } + bool exportPilotDB(const QString& file) + { return exportCollection(Export::PilotDB, KURL::fromPathOrURL(file)); } + + QValueList<long> selectedEntries() const; + QValueList<long> filteredEntries() const; + + virtual void openFile(const QString& file) = 0; + virtual void setFilter(const QString& text) = 0; + virtual bool showEntry(long id) = 0; + +protected: + ApplicationInterface() : DCOPObject("tellico") {} + virtual bool importFile(Import::Format format, const KURL& url, Import::Action action) = 0; + virtual bool exportCollection(Export::Format format, const KURL& url) = 0; + +private: + Import::Action actionType(const QString& actionName); +}; + +class CollectionInterface : public DCOPObject { +K_DCOP +k_dcop: + CollectionInterface() : DCOPObject("collection") {} + + long addEntry(); + bool removeEntry(long entryID); + + QStringList values(const QString& fieldName) const; + QStringList values(long entryID, const QString& fieldName) const; + QStringList bibtexKeys() const; + QString bibtexKey(long entryID) const; + + bool setFieldValue(long entryID, const QString& fieldName, const QString& value); + bool addFieldValue(long entryID, const QString& fieldName, const QString& value); +}; + +} // end namespace +#endif diff --git a/src/core/dcopinterface_skel.cpp b/src/core/dcopinterface_skel.cpp new file mode 100644 index 0000000..9edf4ad --- /dev/null +++ b/src/core/dcopinterface_skel.cpp @@ -0,0 +1,374 @@ +/**************************************************************************** +** +** DCOP Skeleton generated by dcopidl2cpp from dcopinterface.kidl +** +** WARNING! All changes made in this file will be lost! +** +*****************************************************************************/ + +#include "./dcopinterface.h" + +#include <kdatastream.h> +#include <qasciidict.h> + +namespace Tellico { + +static const int ApplicationInterface_fhash = 17; +static const char* const ApplicationInterface_ftable[16][3] = { + { "bool", "importTellico(QString,QString)", "importTellico(QString file,QString action)" }, + { "bool", "importBibtex(QString,QString)", "importBibtex(QString file,QString action)" }, + { "bool", "importMODS(QString,QString)", "importMODS(QString file,QString action)" }, + { "bool", "importRIS(QString,QString)", "importRIS(QString file,QString action)" }, + { "bool", "exportXML(QString)", "exportXML(QString file)" }, + { "bool", "exportZip(QString)", "exportZip(QString file)" }, + { "bool", "exportBibtex(QString)", "exportBibtex(QString file)" }, + { "bool", "exportHTML(QString)", "exportHTML(QString file)" }, + { "bool", "exportCSV(QString)", "exportCSV(QString file)" }, + { "bool", "exportPilotDB(QString)", "exportPilotDB(QString file)" }, + { "QValueList<long int>", "selectedEntries()", "selectedEntries()" }, + { "QValueList<long int>", "filteredEntries()", "filteredEntries()" }, + { "void", "openFile(QString)", "openFile(QString file)" }, + { "void", "setFilter(QString)", "setFilter(QString text)" }, + { "bool", "showEntry(long int)", "showEntry(long int id)" }, + { 0, 0, 0 } +}; +static const int ApplicationInterface_ftable_hiddens[15] = { + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, +}; + +bool ApplicationInterface::process(const QCString &fun, const QByteArray &data, QCString& replyType, QByteArray &replyData) +{ + static QAsciiDict<int>* fdict = 0; + if ( !fdict ) { + fdict = new QAsciiDict<int>( ApplicationInterface_fhash, true, false ); + for ( int i = 0; ApplicationInterface_ftable[i][1]; i++ ) + fdict->insert( ApplicationInterface_ftable[i][1], new int( i ) ); + } + int* fp = fdict->find( fun ); + switch ( fp?*fp:-1) { + case 0: { // bool importTellico(QString,QString) + QString arg0; + QString arg1; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + replyType = ApplicationInterface_ftable[0][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << importTellico(arg0, arg1 ); + } break; + case 1: { // bool importBibtex(QString,QString) + QString arg0; + QString arg1; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + replyType = ApplicationInterface_ftable[1][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << importBibtex(arg0, arg1 ); + } break; + case 2: { // bool importMODS(QString,QString) + QString arg0; + QString arg1; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + replyType = ApplicationInterface_ftable[2][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << importMODS(arg0, arg1 ); + } break; + case 3: { // bool importRIS(QString,QString) + QString arg0; + QString arg1; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + replyType = ApplicationInterface_ftable[3][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << importRIS(arg0, arg1 ); + } break; + case 4: { // bool exportXML(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[4][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportXML(arg0 ); + } break; + case 5: { // bool exportZip(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[5][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportZip(arg0 ); + } break; + case 6: { // bool exportBibtex(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[6][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportBibtex(arg0 ); + } break; + case 7: { // bool exportHTML(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[7][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportHTML(arg0 ); + } break; + case 8: { // bool exportCSV(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[8][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportCSV(arg0 ); + } break; + case 9: { // bool exportPilotDB(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[9][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << exportPilotDB(arg0 ); + } break; + case 10: { // QValueList<long int> selectedEntries() + replyType = ApplicationInterface_ftable[10][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << selectedEntries( ); + } break; + case 11: { // QValueList<long int> filteredEntries() + replyType = ApplicationInterface_ftable[11][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << filteredEntries( ); + } break; + case 12: { // void openFile(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[12][0]; + openFile(arg0 ); + } break; + case 13: { // void setFilter(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[13][0]; + setFilter(arg0 ); + } break; + case 14: { // bool showEntry(long int) + long int arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = ApplicationInterface_ftable[14][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << showEntry(arg0 ); + } break; + default: + return DCOPObject::process( fun, data, replyType, replyData ); + } + return true; +} + +QCStringList ApplicationInterface::interfaces() +{ + QCStringList ifaces = DCOPObject::interfaces(); + ifaces += "Tellico::ApplicationInterface"; + return ifaces; +} + +QCStringList ApplicationInterface::functions() +{ + QCStringList funcs = DCOPObject::functions(); + for ( int i = 0; ApplicationInterface_ftable[i][2]; i++ ) { + if (ApplicationInterface_ftable_hiddens[i]) + continue; + QCString func = ApplicationInterface_ftable[i][0]; + func += ' '; + func += ApplicationInterface_ftable[i][2]; + funcs << func; + } + return funcs; +} + +} // namespace + +#include <kdatastream.h> +#include <qasciidict.h> + +namespace Tellico { + +static const int CollectionInterface_fhash = 11; +static const char* const CollectionInterface_ftable[9][3] = { + { "long int", "addEntry()", "addEntry()" }, + { "bool", "removeEntry(long int)", "removeEntry(long int entryID)" }, + { "QStringList", "values(QString)", "values(QString fieldName)" }, + { "QStringList", "values(long int,QString)", "values(long int entryID,QString fieldName)" }, + { "QStringList", "bibtexKeys()", "bibtexKeys()" }, + { "QString", "bibtexKey(long int)", "bibtexKey(long int entryID)" }, + { "bool", "setFieldValue(long int,QString,QString)", "setFieldValue(long int entryID,QString fieldName,QString value)" }, + { "bool", "addFieldValue(long int,QString,QString)", "addFieldValue(long int entryID,QString fieldName,QString value)" }, + { 0, 0, 0 } +}; +static const int CollectionInterface_ftable_hiddens[8] = { + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, +}; + +bool CollectionInterface::process(const QCString &fun, const QByteArray &data, QCString& replyType, QByteArray &replyData) +{ + static QAsciiDict<int>* fdict = 0; + if ( !fdict ) { + fdict = new QAsciiDict<int>( CollectionInterface_fhash, true, false ); + for ( int i = 0; CollectionInterface_ftable[i][1]; i++ ) + fdict->insert( CollectionInterface_ftable[i][1], new int( i ) ); + } + int* fp = fdict->find( fun ); + switch ( fp?*fp:-1) { + case 0: { // long int addEntry() + replyType = CollectionInterface_ftable[0][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << addEntry( ); + } break; + case 1: { // bool removeEntry(long int) + long int arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = CollectionInterface_ftable[1][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << removeEntry(arg0 ); + } break; + case 2: { // QStringList values(QString) + QString arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = CollectionInterface_ftable[2][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << values(arg0 ); + } break; + case 3: { // QStringList values(long int,QString) + long int arg0; + QString arg1; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + replyType = CollectionInterface_ftable[3][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << values(arg0, arg1 ); + } break; + case 4: { // QStringList bibtexKeys() + replyType = CollectionInterface_ftable[4][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << bibtexKeys( ); + } break; + case 5: { // QString bibtexKey(long int) + long int arg0; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + replyType = CollectionInterface_ftable[5][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << bibtexKey(arg0 ); + } break; + case 6: { // bool setFieldValue(long int,QString,QString) + long int arg0; + QString arg1; + QString arg2; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + if (arg.atEnd()) return false; + arg >> arg2; + replyType = CollectionInterface_ftable[6][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << setFieldValue(arg0, arg1, arg2 ); + } break; + case 7: { // bool addFieldValue(long int,QString,QString) + long int arg0; + QString arg1; + QString arg2; + QDataStream arg( data, IO_ReadOnly ); + if (arg.atEnd()) return false; + arg >> arg0; + if (arg.atEnd()) return false; + arg >> arg1; + if (arg.atEnd()) return false; + arg >> arg2; + replyType = CollectionInterface_ftable[7][0]; + QDataStream _replyStream( replyData, IO_WriteOnly ); + _replyStream << addFieldValue(arg0, arg1, arg2 ); + } break; + default: + return DCOPObject::process( fun, data, replyType, replyData ); + } + return true; +} + +QCStringList CollectionInterface::interfaces() +{ + QCStringList ifaces = DCOPObject::interfaces(); + ifaces += "Tellico::CollectionInterface"; + return ifaces; +} + +QCStringList CollectionInterface::functions() +{ + QCStringList funcs = DCOPObject::functions(); + for ( int i = 0; CollectionInterface_ftable[i][2]; i++ ) { + if (CollectionInterface_ftable_hiddens[i]) + continue; + QCString func = CollectionInterface_ftable[i][0]; + func += ' '; + func += CollectionInterface_ftable[i][2]; + funcs << func; + } + return funcs; +} + +} // namespace + diff --git a/src/core/drophandler.cpp b/src/core/drophandler.cpp new file mode 100644 index 0000000..e27df9d --- /dev/null +++ b/src/core/drophandler.cpp @@ -0,0 +1,101 @@ +/*************************************************************************** + copyright : (C) 2007 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 "drophandler.h" +#include "../mainwindow.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" + +#include <kurldrag.h> +#include <kmimetype.h> + +using Tellico::DropHandler; + +DropHandler::DropHandler(QObject* parent_) : QObject(parent_) { +} + +DropHandler::~DropHandler() { +} + +// assume the object is always the main window, that's the +// only object with this event filter +bool DropHandler::eventFilter(QObject* obj_, QEvent* ev_) { + Q_UNUSED(obj_); + if(ev_->type() == QEvent::DragEnter) { + return dragEnter(static_cast<QDragEnterEvent*>(ev_)); + } else if(ev_->type() == QEvent::Drop) { + return drop(static_cast<QDropEvent*>(ev_)); + } + return false; +} + +bool DropHandler::dragEnter(QDragEnterEvent* event_) { + bool accept = KURLDrag::canDecode(event_) || QTextDrag::canDecode(event_); + if(accept) { + event_->accept(accept); + } + return accept; +} + +bool DropHandler::drop(QDropEvent* event_) { + KURL::List urls; + QString text; + + if(KURLDrag::decode(event_, urls)) { + } else if(QTextDrag::decode(event_, text) && !text.isEmpty()) { + urls << KURL(text); + } + return !urls.isEmpty() && handleURL(urls); +} + +bool DropHandler::handleURL(const KURL::List& urls_) { + bool hasUnknown = false; + KURL::List tc, pdf, bib, ris; + for(KURL::List::ConstIterator it = urls_.begin(); it != urls_.end(); ++it) { + KMimeType::Ptr ptr = KMimeType::findByURL(*it); + if(ptr->is(QString::fromLatin1("application/x-tellico"))) { + tc << *it; + } else if(ptr->is(QString::fromLatin1("application/pdf"))) { + pdf << *it; + } else if(ptr->is(QString::fromLatin1("text/x-bibtex")) || + ptr->is(QString::fromLatin1("application/x-bibtex"))) { + bib << *it; + } else if(ptr->is(QString::fromLatin1("application/x-research-info-systems"))) { + ris << *it; + } else { + myDebug() << "DropHandler::handleURL() - unrecognized type: " << ptr->name() << " (" << *it << ")" << endl; + hasUnknown = true; + } + } + MainWindow* mainWindow = ::qt_cast<MainWindow*>(Kernel::self()->widget()); + if(!mainWindow) { + myDebug() << "DropHandler::handleURL() - no main window!" << endl; + return !hasUnknown; + } + if(!tc.isEmpty()) { + mainWindow->importFile(Import::TellicoXML, tc); + } + if(!pdf.isEmpty()) { + mainWindow->importFile(Import::PDF, pdf); + } + if(!bib.isEmpty()) { + mainWindow->importFile(Import::Bibtex, bib); + } + if(!ris.isEmpty()) { + mainWindow->importFile(Import::RIS, ris); + } + // any unknown urls get passed + return !hasUnknown; +} + +#include "drophandler.moc" diff --git a/src/core/drophandler.h b/src/core/drophandler.h new file mode 100644 index 0000000..ac4d146 --- /dev/null +++ b/src/core/drophandler.h @@ -0,0 +1,39 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DROPHANDLER_H +#define TELLICO_DROPHANDLER_H + +#include <qobject.h> +#include <kurl.h> + +namespace Tellico { + +class DropHandler : public QObject { +Q_OBJECT + +public: + DropHandler(QObject* parent); + ~DropHandler(); + +protected: + bool eventFilter(QObject* object, QEvent* event); + +private: + bool dragEnter(QDragEnterEvent* event); + bool drop(QDropEvent* event); + bool handleURL(const KURL::List& urls); +}; + +} +#endif diff --git a/src/core/netaccess.cpp b/src/core/netaccess.cpp new file mode 100644 index 0000000..77b6554 --- /dev/null +++ b/src/core/netaccess.cpp @@ -0,0 +1,100 @@ +/*************************************************************************** + copyright : (C) 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 "netaccess.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" + +#include <kdeversion.h> +#include <kio/job.h> +#include <kio/netaccess.h> +#include <kio/scheduler.h> +#include <kio/previewjob.h> +#include <ktempfile.h> + +#include <qapplication.h> +#include <qfile.h> + +#include <unistd.h> // for unlink() + +using Tellico::NetAccess; + +QStringList* NetAccess::s_tmpFiles = 0; + +bool NetAccess::download(const KURL& url_, QString& target_, QWidget* window_) { + if(url_.isLocalFile()) { + return KIO::NetAccess::download(url_, target_, window_); + } + +// if(!KIO::NetAccess::exists(url_, true, window_)) { +// myDebug() << "NetAccess::download() - does not exist: " << url_ << endl; +// return false; +// } + + if(target_.isEmpty()) { + KTempFile tmpFile; + target_ = tmpFile.name(); + if(!s_tmpFiles) { + s_tmpFiles = new QStringList; + } + s_tmpFiles->append(target_); + } + + KURL dest; + dest.setPath(target_); + + KIO::Job* job = KIO::file_copy(url_, dest, -1, true /*overwrite*/, false /*resume*/, false /*showProgress*/); + return KIO::NetAccess::synchronousRun(job, window_); +} + +void NetAccess::removeTempFile(const QString& name_) { + if(!s_tmpFiles) { + return; + } + if(s_tmpFiles->contains(name_)) { + ::unlink(QFile::encodeName(name_)); + s_tmpFiles->remove(name_); + } +} + +QPixmap NetAccess::filePreview(const KURL& url, int size) { + NetAccess netaccess; + + KURL::List list; + list.append(url); + KIO::Job* previewJob = KIO::filePreview(list, size, size); + connect(previewJob, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), + &netaccess, SLOT(slotPreview(const KFileItem*, const QPixmap&))); + + KIO::NetAccess::synchronousRun(previewJob, Kernel::self()->widget()); + return netaccess.m_preview; +} + +QPixmap NetAccess::filePreview(KFileItem* item, int size) { + NetAccess netaccess; + + KFileItemList list; + list.append(item); + KIO::Job* previewJob = KIO::filePreview(list, size, size); + connect(previewJob, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), + &netaccess, SLOT(slotPreview(const KFileItem*, const QPixmap&))); + + KIO::NetAccess::synchronousRun(previewJob, Kernel::self()->widget()); + return netaccess.m_preview; +} + +void NetAccess::slotPreview(const KFileItem*, const QPixmap& pix_) { + m_preview = pix_; +} + +#include "netaccess.moc" diff --git a/src/core/netaccess.h b/src/core/netaccess.h new file mode 100644 index 0000000..df11e0d --- /dev/null +++ b/src/core/netaccess.h @@ -0,0 +1,47 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_NETACCESS_H +#define TELLICO_NETACCESS_H + +#include <qobject.h> +#include <qpixmap.h> + +class KURL; +class KFileItem; +namespace KIO { + class Job; +} + +namespace Tellico { + +class NetAccess : public QObject { +Q_OBJECT + +public: + static bool download(const KURL& u, QString& target, QWidget* window); + static void removeTempFile(const QString& name); + static QPixmap filePreview(const KURL& fileName, int size=196); + static QPixmap filePreview(KFileItem* item, int size=196); + +private slots: + void slotPreview(const KFileItem* item, const QPixmap& pix); + +private: + static QStringList* s_tmpFiles; + + QPixmap m_preview; +}; + +} +#endif diff --git a/src/core/tellico-1-3-update.pl b/src/core/tellico-1-3-update.pl new file mode 100644 index 0000000..9e5ad3b --- /dev/null +++ b/src/core/tellico-1-3-update.pl @@ -0,0 +1,24 @@ +#!/usr/bin/env perl + +my $input; +my $oldvalue; + +while( $input = <STDIN> ) +{ + chop $input; + + if( $input =~ /^Write Images In File\=(.*)/) + { + $oldvalue=$1; + if( $oldvalue eq "true" ) + { + print "Image Location=ImagesInFile\n"; + } + else + { + print "Image Location=ImageAppDir\n"; + } + } +} + +print "# DELETE [General Options]Write Images In File\n"; diff --git a/src/core/tellico-rename.upd b/src/core/tellico-rename.upd new file mode 100644 index 0000000..5e3cb11 --- /dev/null +++ b/src/core/tellico-rename.upd @@ -0,0 +1,4 @@ +# move old bookcaserc file to tellicorc +Id=tellico-rename +File=bookcaserc,tellicorc +AllGroups diff --git a/src/core/tellico.upd b/src/core/tellico.upd new file mode 100644 index 0000000..18d4220 --- /dev/null +++ b/src/core/tellico.upd @@ -0,0 +1,26 @@ +# +# config updates for Tellico 0.14 +# +Id=tellico-0.14 +File=tellicorc +# +Group=General Options +RemoveKey=Geometry +RemoveKey=Main Window Splitter Sizes +RemoveKey=Show Count +RemoveKey=Show Detail Widget +RemoveKey=Show Toolbar +RemoveKey=ToolBarPos +# +Group=Printing +RemoveKey=Print Grouped Attribute +RemoveKey=Print Fields - book +# +Group=ExportOptions +Key=FormatAttributes,FormatFields +# +# config updates for Tellico 1.3 +Id=tellico-1.3 +File=tellicorc +Options=overwrite +Script=tellico-1-3-update.pl,perl diff --git a/src/core/tellico_config.cpp b/src/core/tellico_config.cpp new file mode 100644 index 0000000..f2050f0 --- /dev/null +++ b/src/core/tellico_config.cpp @@ -0,0 +1,455 @@ +// This file is generated by kconfig_compiler from tellico_config.kcfg. +// All changes you do to this file will be lost. + +#include "tellico_config.h" + +#include <kstaticdeleter.h> + +using namespace Tellico; + +Config *Config::mSelf = 0; +static KStaticDeleter<Config> staticConfigDeleter; + +Config *Config::self() +{ + if ( !mSelf ) { + staticConfigDeleter.setObject( mSelf, new Config() ); + mSelf->readConfig(); + } + + return mSelf; +} + +Config::Config( ) + : KConfigSkeleton( QString::fromLatin1( "tellicorc" ) ) +{ + mSelf = this; + setCurrentGroup( QString::fromLatin1( "TipOfDay" ) ); + + KConfigSkeleton::ItemBool *itemShowTipOfDay; + itemShowTipOfDay = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "RunOnStart" ), mShowTipOfDay, true ); + addItem( itemShowTipOfDay, QString::fromLatin1( "showTipOfDay" ) ); + + setCurrentGroup( QString::fromLatin1( "Main Window Options" ) ); + + QValueList<int> defaultMainSplitterSizes; + + KConfigSkeleton::ItemIntList *itemMainSplitterSizes; + itemMainSplitterSizes = new KConfigSkeleton::ItemIntList( currentGroup(), QString::fromLatin1( "Main Splitter Sizes" ), mMainSplitterSizes, defaultMainSplitterSizes ); + addItem( itemMainSplitterSizes, QString::fromLatin1( "MainSplitterSizes" ) ); + QValueList<int> defaultSecondarySplitterSizes; + + KConfigSkeleton::ItemIntList *itemSecondarySplitterSizes; + itemSecondarySplitterSizes = new KConfigSkeleton::ItemIntList( currentGroup(), QString::fromLatin1( "Secondary Splitter Sizes" ), mSecondarySplitterSizes, defaultSecondarySplitterSizes ); + addItem( itemSecondarySplitterSizes, QString::fromLatin1( "SecondarySplitterSizes" ) ); + + setCurrentGroup( QString::fromLatin1( "Detailed View Options" ) ); + + KConfigSkeleton::ItemInt *itemMaxPixmapWidth; + itemMaxPixmapWidth = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "MaxPixmapWidth" ), mMaxPixmapWidth, 50 ); + addItem( itemMaxPixmapWidth, QString::fromLatin1( "MaxPixmapWidth" ) ); + KConfigSkeleton::ItemInt *itemMaxPixmapHeight; + itemMaxPixmapHeight = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "MaxPixmapHeight" ), mMaxPixmapHeight, 50 ); + addItem( itemMaxPixmapHeight, QString::fromLatin1( "MaxPixmapHeight" ) ); + + setCurrentGroup( QString::fromLatin1( "Group View Options" ) ); + + KConfigSkeleton::ItemInt *itemGroupViewSortColumn; + itemGroupViewSortColumn = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "SortColumn" ), mGroupViewSortColumn, 0 ); + addItem( itemGroupViewSortColumn, QString::fromLatin1( "groupViewSortColumn" ) ); + KConfigSkeleton::ItemBool *itemGroupViewSortAscending; + itemGroupViewSortAscending = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "SortAscending" ), mGroupViewSortAscending, true ); + addItem( itemGroupViewSortAscending, QString::fromLatin1( "groupViewSortAscending" ) ); + + setCurrentGroup( QString::fromLatin1( "Filter View Options" ) ); + + KConfigSkeleton::ItemInt *itemFilterViewSortColumn; + itemFilterViewSortColumn = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "SortColumn" ), mFilterViewSortColumn, 0 ); + addItem( itemFilterViewSortColumn, QString::fromLatin1( "filterViewSortColumn" ) ); + KConfigSkeleton::ItemBool *itemFilterViewSortAscending; + itemFilterViewSortAscending = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "SortAscending" ), mFilterViewSortAscending, true ); + addItem( itemFilterViewSortAscending, QString::fromLatin1( "filterViewSortAscending" ) ); + + setCurrentGroup( QString::fromLatin1( "Loan View Options" ) ); + + KConfigSkeleton::ItemInt *itemLoanViewSortColumn; + itemLoanViewSortColumn = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "SortColumn" ), mLoanViewSortColumn, 0 ); + addItem( itemLoanViewSortColumn, QString::fromLatin1( "loanViewSortColumn" ) ); + KConfigSkeleton::ItemBool *itemLoanViewSortAscending; + itemLoanViewSortAscending = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "SortAscending" ), mLoanViewSortAscending, true ); + addItem( itemLoanViewSortAscending, QString::fromLatin1( "loanViewSortAscending" ) ); + + setCurrentGroup( QString::fromLatin1( "Export Options - Bibtex" ) ); + + KConfigSkeleton::ItemBool *itemUseBraces; + itemUseBraces = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Use Braces" ), mUseBraces, true ); + addItem( itemUseBraces, QString::fromLatin1( "UseBraces" ) ); + + setCurrentGroup( QString::fromLatin1( "General Options" ) ); + + KConfigSkeleton::ItemBool *itemShowGroupWidget; + itemShowGroupWidget = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Show Group Widget" ), mShowGroupWidget, true ); + addItem( itemShowGroupWidget, QString::fromLatin1( "ShowGroupWidget" ) ); + KConfigSkeleton::ItemBool *itemShowEditWidget; + itemShowEditWidget = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Show Edit Widget" ), mShowEditWidget, false ); + addItem( itemShowEditWidget, QString::fromLatin1( "ShowEditWidget" ) ); + KConfigSkeleton::ItemBool *itemShowEntryView; + itemShowEntryView = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Show Entry View" ), mShowEntryView, true ); + addItem( itemShowEntryView, QString::fromLatin1( "ShowEntryView" ) ); + QValueList<KConfigSkeleton::ItemEnum::Choice> valuesImageLocation; + { + KConfigSkeleton::ItemEnum::Choice choice; + choice.name = QString::fromLatin1( "ImagesInFile" ); + valuesImageLocation.append( choice ); + } + { + KConfigSkeleton::ItemEnum::Choice choice; + choice.name = QString::fromLatin1( "ImagesInAppDir" ); + valuesImageLocation.append( choice ); + } + { + KConfigSkeleton::ItemEnum::Choice choice; + choice.name = QString::fromLatin1( "ImagesInLocalDir" ); + valuesImageLocation.append( choice ); + } + KConfigSkeleton::ItemEnum *itemImageLocation; + itemImageLocation = new KConfigSkeleton::ItemEnum( currentGroup(), QString::fromLatin1( "Image Location" ), mImageLocation, valuesImageLocation, ImagesInFile ); + addItem( itemImageLocation, QString::fromLatin1( "ImageLocation" ) ); + KConfigSkeleton::ItemBool *itemAskWriteImagesInFile; + itemAskWriteImagesInFile = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Ask Write Images In File" ), mAskWriteImagesInFile, true ); + addItem( itemAskWriteImagesInFile, QString::fromLatin1( "AskWriteImagesInFile" ) ); + KConfigSkeleton::ItemBool *itemReopenLastFile; + itemReopenLastFile = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Reopen Last File" ), mReopenLastFile, true ); + addItem( itemReopenLastFile, QString::fromLatin1( "ReopenLastFile" ) ); + KConfigSkeleton::ItemBool *itemAutoCapitalization; + itemAutoCapitalization = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Auto Capitalization" ), mAutoCapitalization, true ); + addItem( itemAutoCapitalization, QString::fromLatin1( "AutoCapitalization" ) ); + KConfigSkeleton::ItemBool *itemAutoFormat; + itemAutoFormat = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Auto Format" ), mAutoFormat, true ); + addItem( itemAutoFormat, QString::fromLatin1( "AutoFormat" ) ); + KConfigSkeleton::ItemString *itemLastOpenFile; + itemLastOpenFile = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Last Open File" ), mLastOpenFile ); + addItem( itemLastOpenFile, QString::fromLatin1( "LastOpenFile" ) ); + KConfigSkeleton::ItemString *itemNoCapitalizationString; + itemNoCapitalizationString = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "No Capitalization" ), mNoCapitalizationString, i18n("a,an,and,as,at,but,by,for,from,in,into,nor,of,off,on,onto,or,out,over,the,to,up,with") ); + addItem( itemNoCapitalizationString, QString::fromLatin1( "noCapitalizationString" ) ); + KConfigSkeleton::ItemString *itemArticlesString; + itemArticlesString = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Articles" ), mArticlesString, i18n("the") ); + addItem( itemArticlesString, QString::fromLatin1( "articlesString" ) ); + KConfigSkeleton::ItemString *itemNameSuffixesString; + itemNameSuffixesString = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Name Suffixes" ), mNameSuffixesString, i18n("jr.,jr,iii,iv") ); + addItem( itemNameSuffixesString, QString::fromLatin1( "nameSuffixesString" ) ); + KConfigSkeleton::ItemString *itemSurnamePrefixesString; + itemSurnamePrefixesString = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Surname Prefixes" ), mSurnamePrefixesString, i18n("de,van,der,van der,von") ); + addItem( itemSurnamePrefixesString, QString::fromLatin1( "surnamePrefixesString" ) ); + KConfigSkeleton::ItemInt *itemMaxIconSize; + itemMaxIconSize = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "Max Icon Size" ), mMaxIconSize, 96 ); + addItem( itemMaxIconSize, QString::fromLatin1( "MaxIconSize" ) ); + KConfigSkeleton::ItemInt *itemImageCacheSize; + itemImageCacheSize = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "Image Cache Size" ), mImageCacheSize, (10 * 1024 * 1024) ); + addItem( itemImageCacheSize, QString::fromLatin1( "ImageCacheSize" ) ); + KConfigSkeleton::ItemUInt *itemMaxCustomURLSettings; + itemMaxCustomURLSettings = new KConfigSkeleton::ItemUInt( currentGroup(), QString::fromLatin1( "Max Custom URL Settings" ), mMaxCustomURLSettings, 9 ); + addItem( itemMaxCustomURLSettings, QString::fromLatin1( "MaxCustomURLSettings" ) ); + + setCurrentGroup( QString::fromLatin1( "Printing" ) ); + + KConfigSkeleton::ItemBool *itemPrintFieldHeaders; + itemPrintFieldHeaders = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Print Field Headers" ), mPrintFieldHeaders, true ); + addItem( itemPrintFieldHeaders, QString::fromLatin1( "PrintFieldHeaders" ) ); + KConfigSkeleton::ItemBool *itemPrintFormatted; + itemPrintFormatted = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Print Formatted" ), mPrintFormatted, true ); + addItem( itemPrintFormatted, QString::fromLatin1( "PrintFormatted" ) ); + KConfigSkeleton::ItemBool *itemPrintGrouped; + itemPrintGrouped = new KConfigSkeleton::ItemBool( currentGroup(), QString::fromLatin1( "Print Grouped" ), mPrintGrouped, true ); + addItem( itemPrintGrouped, QString::fromLatin1( "PrintGrouped" ) ); + KConfigSkeleton::ItemInt *itemMaxImageWidth; + itemMaxImageWidth = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "Max Image Width" ), mMaxImageWidth, 50 ); + addItem( itemMaxImageWidth, QString::fromLatin1( "MaxImageWidth" ) ); + KConfigSkeleton::ItemInt *itemMaxImageHeight; + itemMaxImageHeight = new KConfigSkeleton::ItemInt( currentGroup(), QString::fromLatin1( "Max Image Height" ), mMaxImageHeight, 50 ); + addItem( itemMaxImageHeight, QString::fromLatin1( "MaxImageHeight" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - book" ) ); + + KConfigSkeleton::ItemString *itemTemplateBook; + itemTemplateBook = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateBook, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateBook, QString::fromLatin1( "templateBook" ) ); + KConfigSkeleton::ItemFont *itemFontBook; + itemFontBook = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontBook, KGlobalSettings::generalFont() ); + addItem( itemFontBook, QString::fromLatin1( "fontBook" ) ); + KConfigSkeleton::ItemColor *itemBaseColorBook; + itemBaseColorBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorBook, KGlobalSettings::baseColor() ); + addItem( itemBaseColorBook, QString::fromLatin1( "baseColorBook" ) ); + KConfigSkeleton::ItemColor *itemTextColorBook; + itemTextColorBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorBook, KGlobalSettings::textColor() ); + addItem( itemTextColorBook, QString::fromLatin1( "textColorBook" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorBook; + itemHighlightedBaseColorBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorBook, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorBook, QString::fromLatin1( "highlightedBaseColorBook" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorBook; + itemHighlightedTextColorBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorBook, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorBook, QString::fromLatin1( "highlightedTextColorBook" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - video" ) ); + + KConfigSkeleton::ItemString *itemTemplateVideo; + itemTemplateVideo = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateVideo, QString::fromLatin1( "Video" ) ); + addItem( itemTemplateVideo, QString::fromLatin1( "templateVideo" ) ); + KConfigSkeleton::ItemFont *itemFontVideo; + itemFontVideo = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontVideo, KGlobalSettings::generalFont() ); + addItem( itemFontVideo, QString::fromLatin1( "fontVideo" ) ); + KConfigSkeleton::ItemColor *itemBaseColorVideo; + itemBaseColorVideo = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorVideo, KGlobalSettings::baseColor() ); + addItem( itemBaseColorVideo, QString::fromLatin1( "baseColorVideo" ) ); + KConfigSkeleton::ItemColor *itemTextColorVideo; + itemTextColorVideo = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorVideo, KGlobalSettings::textColor() ); + addItem( itemTextColorVideo, QString::fromLatin1( "textColorVideo" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorVideo; + itemHighlightedBaseColorVideo = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorVideo, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorVideo, QString::fromLatin1( "highlightedBaseColorVideo" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorVideo; + itemHighlightedTextColorVideo = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorVideo, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorVideo, QString::fromLatin1( "highlightedTextColorVideo" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - album" ) ); + + KConfigSkeleton::ItemString *itemTemplateAlbum; + itemTemplateAlbum = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateAlbum, QString::fromLatin1( "Album" ) ); + addItem( itemTemplateAlbum, QString::fromLatin1( "templateAlbum" ) ); + KConfigSkeleton::ItemFont *itemFontAlbum; + itemFontAlbum = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontAlbum, KGlobalSettings::generalFont() ); + addItem( itemFontAlbum, QString::fromLatin1( "fontAlbum" ) ); + KConfigSkeleton::ItemColor *itemBaseColorAlbum; + itemBaseColorAlbum = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorAlbum, KGlobalSettings::baseColor() ); + addItem( itemBaseColorAlbum, QString::fromLatin1( "baseColorAlbum" ) ); + KConfigSkeleton::ItemColor *itemTextColorAlbum; + itemTextColorAlbum = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorAlbum, KGlobalSettings::textColor() ); + addItem( itemTextColorAlbum, QString::fromLatin1( "textColorAlbum" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorAlbum; + itemHighlightedBaseColorAlbum = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorAlbum, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorAlbum, QString::fromLatin1( "highlightedBaseColorAlbum" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorAlbum; + itemHighlightedTextColorAlbum = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorAlbum, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorAlbum, QString::fromLatin1( "highlightedTextColorAlbum" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - bibtex" ) ); + + KConfigSkeleton::ItemPath *itemLyxpipe; + itemLyxpipe = new KConfigSkeleton::ItemPath( currentGroup(), QString::fromLatin1( "lyxpipe" ), mLyxpipe, QString::fromLatin1( "$HOME/.lyx/lyxpipe" ) ); + addItem( itemLyxpipe, QString::fromLatin1( "lyxpipe" ) ); + KConfigSkeleton::ItemString *itemTemplateBibtex; + itemTemplateBibtex = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateBibtex, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateBibtex, QString::fromLatin1( "templateBibtex" ) ); + KConfigSkeleton::ItemFont *itemFontBibtex; + itemFontBibtex = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontBibtex, KGlobalSettings::generalFont() ); + addItem( itemFontBibtex, QString::fromLatin1( "fontBibtex" ) ); + KConfigSkeleton::ItemColor *itemBaseColorBibtex; + itemBaseColorBibtex = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorBibtex, KGlobalSettings::baseColor() ); + addItem( itemBaseColorBibtex, QString::fromLatin1( "baseColorBibtex" ) ); + KConfigSkeleton::ItemColor *itemTextColorBibtex; + itemTextColorBibtex = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorBibtex, KGlobalSettings::textColor() ); + addItem( itemTextColorBibtex, QString::fromLatin1( "textColorBibtex" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorBibtex; + itemHighlightedBaseColorBibtex = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorBibtex, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorBibtex, QString::fromLatin1( "highlightedBaseColorBibtex" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorBibtex; + itemHighlightedTextColorBibtex = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorBibtex, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorBibtex, QString::fromLatin1( "highlightedTextColorBibtex" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - comic" ) ); + + KConfigSkeleton::ItemString *itemTemplateComicBook; + itemTemplateComicBook = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateComicBook, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateComicBook, QString::fromLatin1( "templateComicBook" ) ); + KConfigSkeleton::ItemFont *itemFontComicBook; + itemFontComicBook = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontComicBook, KGlobalSettings::generalFont() ); + addItem( itemFontComicBook, QString::fromLatin1( "fontComicBook" ) ); + KConfigSkeleton::ItemColor *itemBaseColorComicBook; + itemBaseColorComicBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorComicBook, KGlobalSettings::baseColor() ); + addItem( itemBaseColorComicBook, QString::fromLatin1( "baseColorComicBook" ) ); + KConfigSkeleton::ItemColor *itemTextColorComicBook; + itemTextColorComicBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorComicBook, KGlobalSettings::textColor() ); + addItem( itemTextColorComicBook, QString::fromLatin1( "textColorComicBook" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorComicBook; + itemHighlightedBaseColorComicBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorComicBook, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorComicBook, QString::fromLatin1( "highlightedBaseColorComicBook" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorComicBook; + itemHighlightedTextColorComicBook = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorComicBook, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorComicBook, QString::fromLatin1( "highlightedTextColorComicBook" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - wine" ) ); + + KConfigSkeleton::ItemString *itemTemplateWine; + itemTemplateWine = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateWine, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateWine, QString::fromLatin1( "templateWine" ) ); + KConfigSkeleton::ItemFont *itemFontWine; + itemFontWine = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontWine, KGlobalSettings::generalFont() ); + addItem( itemFontWine, QString::fromLatin1( "fontWine" ) ); + KConfigSkeleton::ItemColor *itemBaseColorWine; + itemBaseColorWine = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorWine, KGlobalSettings::baseColor() ); + addItem( itemBaseColorWine, QString::fromLatin1( "baseColorWine" ) ); + KConfigSkeleton::ItemColor *itemTextColorWine; + itemTextColorWine = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorWine, KGlobalSettings::textColor() ); + addItem( itemTextColorWine, QString::fromLatin1( "textColorWine" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorWine; + itemHighlightedBaseColorWine = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorWine, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorWine, QString::fromLatin1( "highlightedBaseColorWine" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorWine; + itemHighlightedTextColorWine = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorWine, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorWine, QString::fromLatin1( "highlightedTextColorWine" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - coin" ) ); + + KConfigSkeleton::ItemString *itemTemplateCoin; + itemTemplateCoin = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateCoin, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateCoin, QString::fromLatin1( "templateCoin" ) ); + KConfigSkeleton::ItemFont *itemFontCoin; + itemFontCoin = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontCoin, KGlobalSettings::generalFont() ); + addItem( itemFontCoin, QString::fromLatin1( "fontCoin" ) ); + KConfigSkeleton::ItemColor *itemBaseColorCoin; + itemBaseColorCoin = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorCoin, KGlobalSettings::baseColor() ); + addItem( itemBaseColorCoin, QString::fromLatin1( "baseColorCoin" ) ); + KConfigSkeleton::ItemColor *itemTextColorCoin; + itemTextColorCoin = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorCoin, KGlobalSettings::textColor() ); + addItem( itemTextColorCoin, QString::fromLatin1( "textColorCoin" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorCoin; + itemHighlightedBaseColorCoin = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorCoin, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorCoin, QString::fromLatin1( "highlightedBaseColorCoin" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorCoin; + itemHighlightedTextColorCoin = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorCoin, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorCoin, QString::fromLatin1( "highlightedTextColorCoin" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - stamp" ) ); + + KConfigSkeleton::ItemString *itemTemplateStamp; + itemTemplateStamp = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateStamp, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateStamp, QString::fromLatin1( "templateStamp" ) ); + KConfigSkeleton::ItemFont *itemFontStamp; + itemFontStamp = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontStamp, KGlobalSettings::generalFont() ); + addItem( itemFontStamp, QString::fromLatin1( "fontStamp" ) ); + KConfigSkeleton::ItemColor *itemBaseColorStamp; + itemBaseColorStamp = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorStamp, KGlobalSettings::baseColor() ); + addItem( itemBaseColorStamp, QString::fromLatin1( "baseColorStamp" ) ); + KConfigSkeleton::ItemColor *itemTextColorStamp; + itemTextColorStamp = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorStamp, KGlobalSettings::textColor() ); + addItem( itemTextColorStamp, QString::fromLatin1( "textColorStamp" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorStamp; + itemHighlightedBaseColorStamp = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorStamp, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorStamp, QString::fromLatin1( "highlightedBaseColorStamp" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorStamp; + itemHighlightedTextColorStamp = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorStamp, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorStamp, QString::fromLatin1( "highlightedTextColorStamp" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - card" ) ); + + KConfigSkeleton::ItemString *itemTemplateCard; + itemTemplateCard = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateCard, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateCard, QString::fromLatin1( "templateCard" ) ); + KConfigSkeleton::ItemFont *itemFontCard; + itemFontCard = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontCard, KGlobalSettings::generalFont() ); + addItem( itemFontCard, QString::fromLatin1( "fontCard" ) ); + KConfigSkeleton::ItemColor *itemBaseColorCard; + itemBaseColorCard = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorCard, KGlobalSettings::baseColor() ); + addItem( itemBaseColorCard, QString::fromLatin1( "baseColorCard" ) ); + KConfigSkeleton::ItemColor *itemTextColorCard; + itemTextColorCard = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorCard, KGlobalSettings::textColor() ); + addItem( itemTextColorCard, QString::fromLatin1( "textColorCard" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorCard; + itemHighlightedBaseColorCard = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorCard, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorCard, QString::fromLatin1( "highlightedBaseColorCard" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorCard; + itemHighlightedTextColorCard = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorCard, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorCard, QString::fromLatin1( "highlightedTextColorCard" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - game" ) ); + + KConfigSkeleton::ItemString *itemTemplateGame; + itemTemplateGame = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateGame, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateGame, QString::fromLatin1( "templateGame" ) ); + KConfigSkeleton::ItemFont *itemFontGame; + itemFontGame = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontGame, KGlobalSettings::generalFont() ); + addItem( itemFontGame, QString::fromLatin1( "fontGame" ) ); + KConfigSkeleton::ItemColor *itemBaseColorGame; + itemBaseColorGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorGame, KGlobalSettings::baseColor() ); + addItem( itemBaseColorGame, QString::fromLatin1( "baseColorGame" ) ); + KConfigSkeleton::ItemColor *itemTextColorGame; + itemTextColorGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorGame, KGlobalSettings::textColor() ); + addItem( itemTextColorGame, QString::fromLatin1( "textColorGame" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorGame; + itemHighlightedBaseColorGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorGame, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorGame, QString::fromLatin1( "highlightedBaseColorGame" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorGame; + itemHighlightedTextColorGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorGame, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorGame, QString::fromLatin1( "highlightedTextColorGame" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - file" ) ); + + KConfigSkeleton::ItemString *itemTemplateFile; + itemTemplateFile = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateFile, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateFile, QString::fromLatin1( "templateFile" ) ); + KConfigSkeleton::ItemFont *itemFontFile; + itemFontFile = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontFile, KGlobalSettings::generalFont() ); + addItem( itemFontFile, QString::fromLatin1( "fontFile" ) ); + KConfigSkeleton::ItemColor *itemBaseColorFile; + itemBaseColorFile = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorFile, KGlobalSettings::baseColor() ); + addItem( itemBaseColorFile, QString::fromLatin1( "baseColorFile" ) ); + KConfigSkeleton::ItemColor *itemTextColorFile; + itemTextColorFile = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorFile, KGlobalSettings::textColor() ); + addItem( itemTextColorFile, QString::fromLatin1( "textColorFile" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorFile; + itemHighlightedBaseColorFile = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorFile, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorFile, QString::fromLatin1( "highlightedBaseColorFile" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorFile; + itemHighlightedTextColorFile = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorFile, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorFile, QString::fromLatin1( "highlightedTextColorFile" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - boardgame" ) ); + + KConfigSkeleton::ItemString *itemTemplateBoardGame; + itemTemplateBoardGame = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateBoardGame, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateBoardGame, QString::fromLatin1( "templateBoardGame" ) ); + KConfigSkeleton::ItemFont *itemFontBoardGame; + itemFontBoardGame = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontBoardGame, KGlobalSettings::generalFont() ); + addItem( itemFontBoardGame, QString::fromLatin1( "fontBoardGame" ) ); + KConfigSkeleton::ItemColor *itemBaseColorBoardGame; + itemBaseColorBoardGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorBoardGame, KGlobalSettings::baseColor() ); + addItem( itemBaseColorBoardGame, QString::fromLatin1( "baseColorBoardGame" ) ); + KConfigSkeleton::ItemColor *itemTextColorBoardGame; + itemTextColorBoardGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorBoardGame, KGlobalSettings::textColor() ); + addItem( itemTextColorBoardGame, QString::fromLatin1( "textColorBoardGame" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorBoardGame; + itemHighlightedBaseColorBoardGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorBoardGame, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorBoardGame, QString::fromLatin1( "highlightedBaseColorBoardGame" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorBoardGame; + itemHighlightedTextColorBoardGame = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorBoardGame, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorBoardGame, QString::fromLatin1( "highlightedTextColorBoardGame" ) ); + + setCurrentGroup( QString::fromLatin1( "Options - entry" ) ); + + KConfigSkeleton::ItemString *itemTemplateBase; + itemTemplateBase = new KConfigSkeleton::ItemString( currentGroup(), QString::fromLatin1( "Entry Template" ), mTemplateBase, QString::fromLatin1( "Fancy" ) ); + addItem( itemTemplateBase, QString::fromLatin1( "templateBase" ) ); + KConfigSkeleton::ItemFont *itemFontBase; + itemFontBase = new KConfigSkeleton::ItemFont( currentGroup(), QString::fromLatin1( "Template Font" ), mFontBase, KGlobalSettings::generalFont() ); + addItem( itemFontBase, QString::fromLatin1( "fontBase" ) ); + KConfigSkeleton::ItemColor *itemBaseColorBase; + itemBaseColorBase = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Base Color" ), mBaseColorBase, KGlobalSettings::baseColor() ); + addItem( itemBaseColorBase, QString::fromLatin1( "baseColorBase" ) ); + KConfigSkeleton::ItemColor *itemTextColorBase; + itemTextColorBase = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Text Color" ), mTextColorBase, KGlobalSettings::textColor() ); + addItem( itemTextColorBase, QString::fromLatin1( "textColorBase" ) ); + KConfigSkeleton::ItemColor *itemHighlightedBaseColorBase; + itemHighlightedBaseColorBase = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlight Color" ), mHighlightedBaseColorBase, KGlobalSettings::highlightColor() ); + addItem( itemHighlightedBaseColorBase, QString::fromLatin1( "highlightedBaseColorBase" ) ); + KConfigSkeleton::ItemColor *itemHighlightedTextColorBase; + itemHighlightedTextColorBase = new KConfigSkeleton::ItemColor( currentGroup(), QString::fromLatin1( "Template Highlighted Text Color" ), mHighlightedTextColorBase, KGlobalSettings::highlightedTextColor() ); + addItem( itemHighlightedTextColorBase, QString::fromLatin1( "highlightedTextColorBase" ) ); +} + +Config::~Config() +{ + if ( mSelf == this ) + staticConfigDeleter.setObject( mSelf, 0, false ); +} + diff --git a/src/core/tellico_config.kcfg b/src/core/tellico_config.kcfg new file mode 100644 index 0000000..8b4ed39 --- /dev/null +++ b/src/core/tellico_config.kcfg @@ -0,0 +1,477 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 + http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > + +<kcfgfile name="tellicorc"/> +<include>klocale.h</include> + +<group name="TipOfDay"> + <entry key="RunOnStart" type="Bool" name="showTipOfDay"> + <default>true</default> + </entry> +</group> + +<group name="Main Window Options"> + <entry key="Main Splitter Sizes" type="IntList"/> + <entry key="Secondary Splitter Sizes" type="IntList"/> +</group> + +<group name="Detailed View Options"> + <entry key="MaxPixmapWidth" type="Int"> + <default>50</default> + </entry> + <entry key="MaxPixmapHeight" type="Int"> + <default>50</default> + </entry> +</group> + +<group name="Group View Options"> + <entry key="SortColumn" type="Int" name="groupViewSortColumn"> + <default>0</default> + </entry> + <entry key="SortAscending" type="Bool" name="groupViewSortAscending"> + <default>true</default> + </entry> +</group> + +<group name="Filter View Options"> + <entry key="SortColumn" type="Int" name="filterViewSortColumn"> + <default>0</default> + </entry> + <entry key="SortAscending" type="Bool" name="filterViewSortAscending"> + <default>true</default> + </entry> +</group> + +<group name="Loan View Options"> + <entry key="SortColumn" type="Int" name="loanViewSortColumn"> + <default>0</default> + </entry> + <entry key="SortAscending" type="Bool" name="loanViewSortAscending"> + <default>true</default> + </entry> +</group> + +<group name="Export Options - Bibtex"> + <entry key="Use Braces" type="Bool"> + <default>true</default> + </entry> +</group> + +<group name="General Options"> + <entry key="Show Group Widget" type="Bool"> + <default>true</default> + </entry> + <entry key="Show Edit Widget" type="Bool"> + <default>false</default> + </entry> + <entry key="Show Entry View" type="Bool"> + <default>true</default> + </entry> + <entry key="Image Location" type="Enum"> + <choices> + <choice name="ImagesInFile"/> + <choice name="ImagesInAppDir"/> + <choice name="ImagesInLocalDir"/> + </choices> + <default>ImagesInFile</default> + </entry> + <entry key="Ask Write Images In File" type="Bool"> + <default>true</default> + </entry> + <entry key="Reopen Last File" type="Bool"> + <default>true</default> + </entry> + <entry key="Auto Capitalization" type="Bool"> + <default>true</default> + </entry> + <entry key="Auto Format" type="Bool"> + <default>true</default> + </entry> + <entry key="Last Open File" type="String"/> + <entry key="No Capitalization" name="noCapitalizationString" type="String"> + <default code="true">i18n("a,an,and,as,at,but,by,for,from,in,into,nor,of,off,on,onto,or,out,over,the,to,up,with")</default> + </entry> + <entry key="Articles" name="articlesString" type="String"> + <default code="true">i18n("the")</default> + </entry> + <entry key="Name Suffixes" name="nameSuffixesString" type="String"> + <default code="true">i18n("jr.,jr,iii,iv")</default> + </entry> + <entry key="Surname Prefixes" name="surnamePrefixesString" type="String"> + <default code="true">i18n("de,van,der,van der,von")</default> + </entry> + <entry key="Max Icon Size" type="Int"> + <default>96</default> + </entry> + <entry key="Image Cache Size" type="Int"> + <default code="true">(10 * 1024 * 1024)</default> + </entry> + <entry key="Max Custom URL Settings" type="UInt"> + <default>9</default> + </entry> +</group> + +<group name="Printing"> + <entry key="Print Field Headers" type="Bool"> + <default>true</default> + </entry> + <entry key="Print Formatted" type="Bool"> + <default>true</default> + </entry> + <entry key="Print Grouped" type="Bool"> + <default>true</default> + </entry> + <entry key="Max Image Width" type="Int"> + <default>50</default> + </entry> + <entry key="Max Image Height" type="Int"> + <default>50</default> + </entry> +</group> + +<group name="Options - book"> + <entry key="Entry Template" type="String" name="templateBook"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontBook"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorBook"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorBook"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorBook"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorBook"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - video"> + <entry key="Entry Template" type="String" name="templateVideo"> + <default>Video</default> + </entry> + <entry key="Template Font" type="Font" name="fontVideo"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorVideo"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorVideo"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorVideo"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorVideo"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - album"> + <entry key="Entry Template" type="String" name="templateAlbum"> + <default>Album</default> + </entry> + <entry key="Template Font" type="Font" name="fontAlbum"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorAlbum"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorAlbum"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorAlbum"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorAlbum"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - bibtex"> + <entry key="lyxpipe" type="Path"> + <default>$HOME/.lyx/lyxpipe</default> + </entry> + <entry key="Entry Template" type="String" name="templateBibtex"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontBibtex"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorBibtex"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorBibtex"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorBibtex"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorBibtex"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - comic"> + <entry key="Entry Template" type="String" name="templateComicBook"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontComicBook"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorComicBook"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorComicBook"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorComicBook"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorComicBook"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - wine"> + <entry key="Entry Template" type="String" name="templateWine"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontWine"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorWine"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorWine"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorWine"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorWine"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - coin"> + <entry key="Entry Template" type="String" name="templateCoin"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontCoin"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorCoin"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorCoin"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorCoin"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorCoin"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - stamp"> + <entry key="Entry Template" type="String" name="templateStamp"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontStamp"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorStamp"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorStamp"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorStamp"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorStamp"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - card"> + <entry key="Entry Template" type="String" name="templateCard"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontCard"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorCard"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorCard"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorCard"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorCard"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - game"> + <entry key="Entry Template" type="String" name="templateGame"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontGame"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorGame"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorGame"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorGame"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorGame"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - file"> + <entry key="Entry Template" type="String" name="templateFile"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontFile"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorFile"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorFile"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorFile"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorFile"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - boardgame"> + <entry key="Entry Template" type="String" name="templateBoardGame"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontBoardGame"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorBoardGame"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorBoardGame"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorBoardGame"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorBoardGame"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +<group name="Options - entry"> + <entry key="Entry Template" type="String" name="templateBase"> + <default>Fancy</default> + </entry> + <entry key="Template Font" type="Font" name="fontBase"> + <label>Template font</label> + <default code="true">KGlobalSettings::generalFont()</default> + </entry> + <entry key="Template Base Color" type="Color" name="baseColorBase"> + <label>Template background color</label> + <default code="true">KGlobalSettings::baseColor()</default> + </entry> + <entry key="Template Text Color" type="Color" name="textColorBase"> + <label>Template text color</label> + <default code="true">KGlobalSettings::textColor()</default> + </entry> + <entry key="Template Highlight Color" type="Color" name="highlightedBaseColorBase"> + <label>Template highlight color</label> + <default code="true">KGlobalSettings::highlightColor()</default> + </entry> + <entry key="Template Highlighted Text Color" type="Color" name="highlightedTextColorBase"> + <label>Template highlighted text color</label> + <default code="true">KGlobalSettings::highlightedTextColor()</default> + </entry> +</group> + +</kcfg> diff --git a/src/core/tellico_config.kcfgc b/src/core/tellico_config.kcfgc new file mode 100644 index 0000000..7cf7c9c --- /dev/null +++ b/src/core/tellico_config.kcfgc @@ -0,0 +1,9 @@ +# Code generation options for kconfig_compiler +File=tellico_config.kcfg +ClassName=Config +NameSpace=Tellico +Singleton=true +Mutators=true +MemberVariables=private +CustomAdditions=true +GlobalEnums=true diff --git a/src/core/tellico_config_addons.cpp b/src/core/tellico_config_addons.cpp new file mode 100644 index 0000000..9b388d5 --- /dev/null +++ b/src/core/tellico_config_addons.cpp @@ -0,0 +1,171 @@ +/*************************************************************************** + copyright : (C) 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 "tellico_config.h" +#include "../collection.h" + +#define COLL Data::Collection:: +#define CLASS Config:: +#define P1 ( +#define P2 ) +#define CASE1(a,b) case COLL b: return CLASS a ## b P1 P2; +#define CASE2(a,b,c) case COLL b: CLASS a ## b P1 c P2; break; +#define GET(name,type) \ + CASE1(name,type) +#define SET(name,type,value) \ + CASE2(name,type,value) +#define ALL_GET(name) \ + GET(name, Base) \ + GET(name, Book) \ + GET(name, Video) \ + GET(name, Album) \ + GET(name, Bibtex) \ + GET(name, ComicBook) \ + GET(name, Wine) \ + GET(name, Coin) \ + GET(name, Stamp) \ + GET(name, Card) \ + GET(name, Game) \ + GET(name, File) \ + GET(name, BoardGame) +#define ALL_SET(name,value) \ + SET(name, Base, value) \ + SET(name, Book, value) \ + SET(name, Video, value) \ + SET(name, Album, value) \ + SET(name, Bibtex, value) \ + SET(name, ComicBook, value) \ + SET(name, Wine, value) \ + SET(name, Coin, value) \ + SET(name, Stamp, value) \ + SET(name, Card, value) \ + SET(name, Game, value) \ + SET(name, File, value) \ + SET(name, BoardGame, value) + +namespace { + static const QRegExp commaSplit = QRegExp(QString::fromLatin1("\\s*,\\s*")); +} + +using Tellico::Config; + +void Config::deleteAndReset() { + delete mSelf; + mSelf = 0; +} + +QStringList Config::noCapitalizationList() { + return QStringList::split(commaSplit, Config::noCapitalizationString()); +} + +QStringList Config::articleList() { + return QStringList::split(commaSplit, Config::articlesString()); +} + +QStringList Config::nameSuffixList() { + return QStringList::split(commaSplit, Config::nameSuffixesString()); +} + +QStringList Config::surnamePrefixList() { + return QStringList::split(commaSplit, Config::surnamePrefixesString()); +} + + +QString Config::templateName(int type_) { + switch(type_) { + ALL_GET(template); + } + return QString(); +} + +QFont Config::templateFont(int type_) { + switch(type_) { + ALL_GET(font); + } + return QFont(); +} + +QColor Config::templateBaseColor(int type_) { + switch(type_) { + ALL_GET(baseColor) + } + return QColor(); +} + +QColor Config::templateTextColor(int type_) { + switch(type_) { + ALL_GET(textColor) + } + return QColor(); +} + +QColor Config::templateHighlightedBaseColor(int type_) { + switch(type_) { + ALL_GET(highlightedBaseColor) + } + return QColor(); +} + +QColor Config::templateHighlightedTextColor(int type_) { + switch(type_) { + ALL_GET(highlightedTextColor) + } + return QColor(); +} + +void Config::setTemplateName(int type_, const QString& name_) { + switch(type_) { + ALL_SET(setTemplate,name_) + } +} + +void Config::setTemplateFont(int type_, const QFont& font_) { + switch(type_) { + ALL_SET(setFont,font_) + } +} + +void Config::setTemplateBaseColor(int type_, const QColor& color_) { + switch(type_) { + ALL_SET(setBaseColor,color_) + } +} + +void Config::setTemplateTextColor(int type_, const QColor& color_) { + switch(type_) { + ALL_SET(setTextColor,color_) + } +} + +void Config::setTemplateHighlightedBaseColor(int type_, const QColor& color_) { + switch(type_) { + ALL_SET(setHighlightedBaseColor,color_) + } +} + +void Config::setTemplateHighlightedTextColor(int type_, const QColor& color_) { + switch(type_) { + ALL_SET(setHighlightedTextColor,color_) + } +} + +#undef COLL +#undef CLASS +#undef P1 +#undef P2 +#undef CASE1 +#undef CASE2 +#undef GET +#undef SET +#undef ALL_GET +#undef ALL_SET diff --git a/src/core/tellico_config_addons.h b/src/core/tellico_config_addons.h new file mode 100644 index 0000000..7573257 --- /dev/null +++ b/src/core/tellico_config_addons.h @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// this file gets included by tellico_config.h + +public: + static void deleteAndReset(); + + static QStringList noCapitalizationList(); + static QStringList articleList(); + static QStringList nameSuffixList(); + static QStringList surnamePrefixList(); + + static QString templateName(int type); + static QFont templateFont(int type); + static QColor templateBaseColor(int type); + static QColor templateTextColor(int type); + static QColor templateHighlightedBaseColor(int type); + static QColor templateHighlightedTextColor(int type); + + static void setTemplateName(int type, const QString& name); + static void setTemplateFont(int type, const QFont& font); + static void setTemplateBaseColor(int type, const QColor& color); + static void setTemplateTextColor(int type, const QColor& color); + static void setTemplateHighlightedBaseColor(int type, const QColor& color); + static void setTemplateHighlightedTextColor(int type, const QColor& color); diff --git a/src/datavectors.h b/src/datavectors.h new file mode 100644 index 0000000..9dd8257 --- /dev/null +++ b/src/datavectors.h @@ -0,0 +1,63 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef DATA_VECTORS_H +#define DATA_VECTORS_H + +#include "ptrvector.h" + +#include <qmap.h> +#include <qpair.h> + +#include <ksharedptr.h> + +namespace Tellico { + typedef QMap<QString, QString> StringMap; + + class Filter; + typedef KSharedPtr<Filter> FilterPtr; + typedef Vector<Filter> FilterVec; + + namespace Data { + class Collection; + typedef KSharedPtr<Collection> CollPtr; + typedef KSharedPtr<const Collection> ConstCollPtr; + typedef Vector<Collection> CollVec; + + class Field; + typedef KSharedPtr<Field> FieldPtr; + typedef KSharedPtr<const Field> ConstFieldPtr; + typedef Vector<Field> FieldVec; + typedef FieldVec::Iterator FieldVecIt; +// typedef Vector<ConstFieldPtr> ConstFieldVec; + + class Entry; + typedef KSharedPtr<Entry> EntryPtr; + typedef KSharedPtr<const Entry> ConstEntryPtr; + typedef Vector<Entry> EntryVec; + typedef EntryVec::Iterator EntryVecIt; + typedef Vector<const Entry> ConstEntryVec; + // complicated, I know + // first item is a vector of all entries that got added in the merge process + // second item is a pair of entries that had their track field modified + // since a music collection is the only one that would actually merge entries + typedef QValueVector< QPair<EntryPtr, QString> > PairVector; + typedef QPair<Data::EntryVec, PairVector> MergePair; + + class Borrower; + typedef KSharedPtr<Borrower> BorrowerPtr; + typedef Vector<Borrower> BorrowerVec; + } +} + +#endif diff --git a/src/detailedentryitem.cpp b/src/detailedentryitem.cpp new file mode 100644 index 0000000..0a71f25 --- /dev/null +++ b/src/detailedentryitem.cpp @@ -0,0 +1,127 @@ +/*************************************************************************** + copyright : (C) 2005-2007 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 "detailedentryitem.h" +#include "detailedlistview.h" +#include "collection.h" +#include "entry.h" +#include "tellico_utils.h" +#include "latin1literal.h" + +#include <klocale.h> +#include <kstringhandler.h> + +#include <qpainter.h> +#include <qheader.h> +#include <qdatetime.h> +#include <qtimer.h> + +namespace { + static const short ENTRY_MAX_MINUTES_MESSAGE = 5; +} + +using Tellico::DetailedEntryItem; + +DetailedEntryItem::DetailedEntryItem(DetailedListView* parent_, Data::EntryPtr entry_) + : EntryItem(parent_, entry_), m_state(Normal), m_time(0), m_timer(0) { +} + +DetailedEntryItem::~DetailedEntryItem() { + delete m_time; + m_time = 0; + delete m_timer; + m_timer = 0; +} + +void DetailedEntryItem::setState(State state_) { + if(m_state == state_) { + return; + } + m_state = state_; + + if(m_state == Normal) { + delete m_time; + m_time = 0; + delete m_timer; + m_timer = 0; + } else { + if(!m_time) { + m_time = new QTime; + } + m_time->start(); + + if(!m_timer) { + m_timer = new QTimer(); + m_timer->connect(m_timer, SIGNAL(timeout()), listView(), SLOT(triggerUpdate())); + } + m_timer->start(30 * 1000); // every 30 seconds + } + + // have to put this in a timer, or it doesn't update properly + QTimer::singleShot(500, listView(), SLOT(triggerUpdate())); +} + +void DetailedEntryItem::paintCell(QPainter* p_, const QColorGroup& cg_, + int column_, int width_, int align_) { + if(m_state == Normal) { + EntryItem::paintCell(p_, cg_, column_, width_, align_); + return; + } + + int t = m_time->elapsed()/(60 * 1000); + if(t > ENTRY_MAX_MINUTES_MESSAGE) { + setState(Normal); + t = 0; + } + + QFont f = p_->font(); + f.setBold(true); + if(m_state == New) { + f.setItalic(true); + } + p_->setFont(f); + + // taken from ListViewItem, but without line drawn to right of cell + QColorGroup cg = cg_; + const QPixmap* pm = listView()->viewport()->backgroundPixmap(); + if(pm && !pm->isNull()) { + cg.setBrush(QColorGroup::Base, QBrush(backgroundColor(column_), *pm)); + QPoint o = p_->brushOrigin(); + p_->setBrushOrigin(o.x()-listView()->contentsX(), o.y()-listView()->contentsY()); + } else { + cg.setColor(listView()->viewport()->backgroundMode() == Qt::FixedColor ? + QColorGroup::Background : QColorGroup::Base, + backgroundColor(column_)); + } + // don't call KListViewItem::paintCell() since that also does alternate painting, etc... + QListViewItem::paintCell(p_, cg, column_, width_, align_); +} + +QColor DetailedEntryItem::backgroundColor(int column_) { + GUI::ListView* lv = listView(); + if(!lv || m_state == Normal || isSelected()) { + return EntryItem::backgroundColor(column_); + } + int t = m_time->elapsed()/(60 * 1000); + if(t > ENTRY_MAX_MINUTES_MESSAGE) { + return EntryItem::backgroundColor(column_); + } + return blendColors(lv->colorGroup().highlight(), + lv->colorGroup().base(), + 80 + 20*t/ENTRY_MAX_MINUTES_MESSAGE /* percent */); + // no more than 20% of highlight color +} + +void DetailedEntryItem::paintFocus(QPainter*, const QColorGroup&, const QRect&) { +// don't paint anything +} diff --git a/src/detailedentryitem.h b/src/detailedentryitem.h new file mode 100644 index 0000000..50f0671 --- /dev/null +++ b/src/detailedentryitem.h @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2005-2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DETAILEDENTRYITEM_H +#define TELLICO_DETAILEDENTRYITEM_H + +class QTime; +class QTimer; + +#include "entryitem.h" + +namespace Tellico { + +class DetailedListView; + +/** + * @author Robby Stephenson + */ +class DetailedEntryItem : public EntryItem { +public: + enum State { Normal, New, Modified }; + + DetailedEntryItem(DetailedListView* parent, Data::EntryPtr entry); + ~DetailedEntryItem(); + + void setState(State state); + + virtual QColor backgroundColor(int column); + virtual void paintCell(QPainter* p, const QColorGroup& cg, + int column, int width, int align); + +private: + /** + * Paints a focus indicator on the rectangle (current item). Disable for current items. + */ + void paintFocus(QPainter*, const QColorGroup&, const QRect&); + + State m_state; + QTime* m_time; + QTimer* m_timer; +}; + +} + +#endif diff --git a/src/detailedlistview.cpp b/src/detailedlistview.cpp new file mode 100644 index 0000000..5c4e2d2 --- /dev/null +++ b/src/detailedlistview.cpp @@ -0,0 +1,894 @@ +/*************************************************************************** + 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 "detailedlistview.h" +#include "detailedentryitem.h" +#include "collection.h" +#include "imagefactory.h" +#include "controller.h" +#include "field.h" +#include "entry.h" +#include "gui/ratingwidget.h" +#include "tellico_debug.h" +#include "tellico_kernel.h" +#include "core/tellico_config.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kapplication.h> +#include <kaction.h> +#include <kiconloader.h> +#include <kpopupmenu.h> + +#include <qptrlist.h> +#include <qstringlist.h> +#include <qvaluelist.h> +#include <qheader.h> + +namespace { + static const int MIN_COL_WIDTH = 50; +} + +using Tellico::DetailedListView; + +DetailedListView::DetailedListView(QWidget* parent_, const char* name_/*=0*/) + : GUI::ListView(parent_, name_), m_filter(0), + m_prevSortColumn(-1), m_prev2SortColumn(-1), m_firstSection(-1), + m_pixWidth(50), m_pixHeight(50) { +// myDebug() << "DetailedListView()" << endl; + setAllColumnsShowFocus(true); + setShowSortIndicator(true); + setShadeSortColumn(true); + +// connect(this, SIGNAL(selectionChanged()), SLOT(slotSelectionChanged())); + connect(this, SIGNAL(contextMenuRequested(QListViewItem*, const QPoint&, int)), + SLOT(contextMenuRequested(QListViewItem*, const QPoint&, int))); + connect(header(), SIGNAL(indexChange(int, int, int)), + SLOT(slotUpdatePixmap())); + + // header menu + header()->setClickEnabled(true); + header()->installEventFilter(this); + connect(header(), SIGNAL(sizeChange(int, int, int)), + this, SLOT(slotCacheColumnWidth(int, int, int))); + + m_headerMenu = new KPopupMenu(this); + m_headerMenu->setCheckable(true); + m_headerMenu->insertTitle(i18n("View Columns")); + connect(m_headerMenu, SIGNAL(activated(int)), + this, SLOT(slotHeaderMenuActivated(int))); + + m_checkPix = UserIcon(QString::fromLatin1("checkmark")); +} + +DetailedListView::~DetailedListView() { +} + +void DetailedListView::addCollection(Data::CollPtr coll_) { + if(!coll_) { + return; + } + + m_imageColumns.clear(); +// myDebug() << "DetailedListView::addCollection()" << endl; + + KConfigGroup config(kapp->config(), QString::fromLatin1("Options - %1").arg(coll_->typeName())); + + QString configN; + if(coll_->type() == Data::Collection::Base) { + KURL url = Kernel::self()->URL(); + for(uint i = 0; i < Config::maxCustomURLSettings(); ++i) { + KURL u = config.readEntry(QString::fromLatin1("URL_%1").arg(i)); + if(u == url) { + configN = QString::fromLatin1("_%1").arg(i); + break; + } + } + } + + QStringList colNames = config.readListEntry("ColumnNames" + configN); + if(colNames.isEmpty()) { + colNames = QString::fromLatin1("title"); + } + + // this block compensates for the chance that the user added a field and it wasn't + // written to the widths. Also compensates for 0.5.x to 0.6.x column layout changes + QValueList<int> colWidths = config.readIntListEntry("ColumnWidths" + configN); + if(colWidths.empty()) { + colWidths.insert(colWidths.begin(), colNames.count(), -1); // automatic width + } + + QValueList<int> colOrder = config.readIntListEntry("ColumnOrder" + configN); + + // need to remove values for fields which don't exist in the current collection + QStringList newCols; + QValueList<int> newWidths, removeCols; + for(uint i = 0; i < colNames.count(); ++i) { + if(!colNames[i].isEmpty() && coll_->hasField(colNames[i])) { + newCols += colNames[i]; + newWidths += colWidths[i]; + } else { + removeCols += i; + } + } + colNames = newCols; + colWidths = newWidths; + + qHeapSort(removeCols); + // now need to shift values in the order if any columns were removed + // only need to shift by number of "holes" + QValueList<int> newOrder; + for(QValueList<int>::ConstIterator it = colOrder.begin(); it != colOrder.end(); ++it) { + if(removeCols.findIndex(*it) == -1) { + int i = *it; + for(uint j = 0; j < removeCols.count() && removeCols[j] < i; ++j) { + --i; + } + newOrder += i; + } + } + colOrder = newOrder; + + bool none = true; + Data::FieldVec fields = coll_->fields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + if(colNames.findIndex(fIt->name()) > -1 && colWidths.count() > 0) { + addField(fIt, colWidths.front()); + if(none && colWidths.front() != 0) { + none = false; + } + colWidths.pop_front(); + } else { + addField(fIt, 0); + } + } + if(none && columns() > 0 && !colNames.isEmpty()) { + showColumn(coll_->fieldNames().findIndex(colNames[0])); + } + + QValueList<int>::ConstIterator it = colOrder.begin(); + for(int i = 0; it != colOrder.end(); ++it) { + header()->moveSection(i++, *it); + } + slotUpdatePixmap(); + + int sortCol = config.readNumEntry("SortColumn" + configN, 0); + bool sortAsc = config.readBoolEntry("SortAscending" + configN, true); + setSorting(sortCol, sortAsc); + int prevSortCol = config.readNumEntry("PrevSortColumn" + configN, -1); + int prev2SortCol = config.readNumEntry("Prev2SortColumn" + configN, -1); + setPrevSortedColumn(prevSortCol, prev2SortCol); + + triggerUpdate(); + kapp->processEvents(); + setUpdatesEnabled(false); + + m_loadingCollection = true; + Data::EntryVec entries = coll_->entries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + addEntryInternal(entry); + } + m_loadingCollection = false; + + setUpdatesEnabled(true); + triggerUpdate(); +} + +void DetailedListView::slotReset() { +// myDebug() << "DetailedListView::slotReset()" << endl; + //clear() does not remove columns + clear(); +// while(columns() > 0) { +// removeColumn(0); +// } + m_filter = 0; +} + +void DetailedListView::addEntries(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + +// myDebug() << "DetailedListView::addEntry() - " << entry_->title() << endl; + + DetailedEntryItem* item = 0; + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + item = addEntryInternal(entry); + item->setState(DetailedEntryItem::New); + item->setVisible(!m_filter || m_filter->matches(entry.data())); + } + + if(isUpdatesEnabled() && item && item->isVisible()) { + sort(); + ensureItemVisible(item); + setCurrentItem(item); + if(!selectedItems().isEmpty()) { + blockSignals(true); + clearSelection(); + setSelected(item, true); + blockSignals(false); + } + } else { + triggerUpdate(); + } +} + +Tellico::DetailedEntryItem* DetailedListView::addEntryInternal(Data::EntryPtr entry_) { + if(m_entryPix.isNull()) { + m_entryPix = UserIcon(entry_->collection()->typeName()); + if(m_entryPix.isNull()) { + kdWarning() << "DetailedListView::addEntryInternal() - can't find entry pix" << endl; + } + } + + DetailedEntryItem* item = new DetailedEntryItem(this, entry_); + populateItem(item); + return item; +} + +void DetailedListView::modifyEntries(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + + DetailedEntryItem* item = 0; + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + item = locateItem(entry.data()); + if(!item) { + kdWarning() << "DetailedListView::modifyEntries() - no item found for " << entry->title() << endl; + continue; + } + + populateItem(item); + item->setState(DetailedEntryItem::Modified); + item->setVisible(!m_filter || m_filter->matches(entry.data())); + } + + if(isUpdatesEnabled() && item && item->isVisible()) { + sort(); + } + + if(item && !item->isSelected() && !selectedItems().isEmpty()) { + blockSignals(true); + clearSelection(); + setSelected(item, true); + blockSignals(false); + } +} + +void DetailedListView::removeEntries(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + +// myDebug() << "DetailedListView::removeEntries() - " << entry_->title() << endl; + + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + delete locateItem(entry); + } + // update is required + triggerUpdate(); +} + +void DetailedListView::removeCollection(Data::CollPtr coll_) { + if(!coll_) { + kdWarning() << "DetailedListView::removeCollection() - null coll pointer!" << endl; + return; + } + +// myDebug() << "DetailedListView::removeCollection() - " << coll_->title() << endl; + + clear(); + while(columns() > 0) { + removeColumn(0); + } + + m_headerMenu->clear(); + m_headerMenu->insertTitle(i18n("View Columns")); + + m_columnWidths.clear(); + clearComparisons(); + m_entryPix = QPixmap(); + + // clear the filter, too + m_filter = 0; +} + +void DetailedListView::populateColumn(int col_) { + if(childCount() == 0) { + return; + } +// myDebug() << "DetailedListView::populateColumn() - " << columnText(col_) << endl; + DetailedEntryItem* item = static_cast<DetailedEntryItem*>(firstChild()); + Data::FieldPtr field = item->entry()->collection()->fieldByTitle(columnText(col_)); + for( ; item; item = static_cast<DetailedEntryItem*>(item->nextSibling())) { + setPixmapAndText(item, col_, field); + } + m_isDirty[col_] = false; +} + +void DetailedListView::populateItem(DetailedEntryItem* item_) { + Data::EntryPtr entry = item_->entry(); + if(!entry) { + return; + } + + for(int colNum = 0; colNum < columns(); ++colNum) { + if(columnWidth(colNum) > 0) { + Data::FieldPtr field = entry->collection()->fieldByTitle(columnText(colNum)); + if(!field) { + kdWarning() << "DetailedListView::populateItem() - no field found for " << columnText(colNum) << endl; + continue; + } + setPixmapAndText(item_, colNum, field); + } else { + m_isDirty[colNum] = true; + } + } +} + +void DetailedListView::contextMenuRequested(QListViewItem* item_, const QPoint& point_, int) { + if(!item_) { + return; + } + KPopupMenu menu(this); + Controller::self()->plugEntryActions(&menu); + menu.exec(point_); +} + +// don't shadow QListView::setSelected +void DetailedListView::setEntrySelected(Data::EntryPtr entry_) { +// myDebug() << "DetailedListView::setEntrySelected()" << endl; + // if entry_ is null pointer, just return + if(!entry_) { + return; + } + + DetailedEntryItem* item = locateItem(entry_); + + blockSignals(true); + clearSelection(); + setSelected(item, true); + setCurrentItem(item); + blockSignals(false); + ensureItemVisible(item); +} + +Tellico::DetailedEntryItem* DetailedListView::locateItem(Data::EntryPtr entry_) { + for(QListViewItemIterator it(this); it.current(); ++it) { + DetailedEntryItem* item = static_cast<DetailedEntryItem*>(it.current()); + if(item->entry() == entry_) { + return item; + } + } + + return 0; +} + +bool DetailedListView::eventFilter(QObject* obj_, QEvent* ev_) { + if(ev_->type() == QEvent::MouseButtonPress + && static_cast<QMouseEvent*>(ev_)->button() == Qt::RightButton + && obj_ == header()) { + m_headerMenu->popup(static_cast<QMouseEvent*>(ev_)->globalPos()); + return true; + } + return GUI::ListView::eventFilter(obj_, ev_); +} + +void DetailedListView::slotHeaderMenuActivated(int id_) { +// myDebug() << "DetailedListView::slotHeaderMenuActivated() - " << m_headerMenu->text(id_) << endl; + bool checked = m_headerMenu->isItemChecked(id_); + checked = !checked; // toggle + m_headerMenu->setItemChecked(id_, checked); + + int col = m_headerMenu->indexOf(id_) - 1; // subtract 1 because there's a title item + + if(checked) { // add a column + showColumn(col); + } else { + hideColumn(col); + } + slotUpdatePixmap(); +} + +void DetailedListView::slotRefresh() { + if(childCount() == 0) { + return; + } + + // the algorithm here is to iterate over each column, then over every list item + Data::CollPtr coll = static_cast<DetailedEntryItem*>(firstChild())->entry()->collection(); + Data::FieldPtr field; + DetailedEntryItem* item; + + for(int colNum = 0; colNum < columns(); ++colNum) { + field = coll->fieldByTitle(columnText(colNum)); + + // iterate over all items + + for(QListViewItemIterator it(this); it.current(); ++it) { + item = static_cast<DetailedEntryItem*>(it.current()); + + setPixmapAndText(item, colNum, field); + + // if we're doing this for the first time, go ahead and pass through filter + if(colNum == 0) { + if(m_filter && !m_filter->matches(item->entry())) { + item->setVisible(false); + } else { + item->setVisible(true); + } + } + } + } +} + +void DetailedListView::slotRefreshImages() { + for(QValueVector<int>::const_iterator it = m_imageColumns.begin(); it != m_imageColumns.end(); ++it) { + if(columnWidth(*it) > 0) { + populateColumn(*it); + } + } +} + +void DetailedListView::setPixmapAndText(DetailedEntryItem* item_, int col_, Data::FieldPtr field_) { + if(!item_ || !field_) { + return; + } + + // if the bool is not empty, show the checkmark pixmap + if(field_->type() == Data::Field::Bool) { + const QString value = item_->entry()->field(field_); + item_->setPixmap(col_, value.isEmpty() ? QPixmap() : m_checkPix); + item_->setText(col_, QString::null); + } else if(field_->type() == Data::Field::Image) { + // if we're currently loading a collection + // don't load the image just yet, it'll get refreshed later + if(m_loadingCollection || columnWidth(col_) == 0) { + item_->setPixmap(col_, QPixmap()); + item_->setText(col_, QString::null); + } else { + item_->setPixmap(col_, ImageFactory::pixmap(item_->entry()->field(field_), m_pixWidth, m_pixHeight)); + item_->setText(col_, QString::null); + } + } else if(field_->type() == Data::Field::Rating) { + item_->setPixmap(col_, GUI::RatingWidget::pixmap(item_->entry()->field(field_))); + item_->setText(col_, QString::null); + } else { // for everything else, there's no pixmap, unless it's the first column + item_->setPixmap(col_, col_ == m_firstSection ? m_entryPix : QPixmap()); + item_->setText(col_, item_->entry()->formattedField(field_)); + } + item_->widthChanged(col_); +} + +void DetailedListView::showColumn(int col_) { +// myDebug() << "DetailedListView::showColumn() - " << columnText(col_) << endl; + int w = m_columnWidths[col_]; // this should be safe - all were initialized to 0 + if(w == 0) { + setColumnWidthMode(col_, QListView::Maximum); + for(QListViewItemIterator it(this); it.current(); ++it) { + w = QMAX(it.current()->width(fontMetrics(), this, col_), w); + } + w = QMAX(w, MIN_COL_WIDTH); + } else { + setColumnWidthMode(col_, QListView::Manual); + } + + setColumnWidth(col_, w); + if(m_isDirty[col_]) { + populateColumn(col_); + } + header()->setResizeEnabled(true, col_); + triggerUpdate(); +} + +void DetailedListView::hideColumn(int col_) { +// myDebug() << "DetailedListView::hideColumn() - " << columnText(col_) << endl; + setColumnWidthMode(col_, QListView::Manual); + setColumnWidth(col_, 0); + header()->setResizeEnabled(false, col_); + + // special case for images, I don't want all the items to be tall, so remove pixmaps + if(childCount() > 0) { + Data::EntryPtr entry = static_cast<DetailedEntryItem*>(firstChild())->entry(); + if(entry) { + Data::FieldPtr field = entry->collection()->fieldByTitle(columnText(col_)); + if(field && field->type() == Data::Field::Image) { + m_isDirty[col_] = true; + for(QListViewItemIterator it(this); it.current(); ++it) { + it.current()->setPixmap(col_, QPixmap()); + } + } + } + } + + triggerUpdate(); +} + +void DetailedListView::slotCacheColumnWidth(int section_, int oldSize_, int newSize_) { + // if the old size was 0, update the menu + if(oldSize_ == 0 && newSize_ > 0) { + m_headerMenu->setItemChecked(m_headerMenu->idAt(section_+1), true); // add 1 for title item + } + + if(newSize_ > 0) { + m_columnWidths[section_] = newSize_; + } + setColumnWidthMode(section_, QListView::Manual); +} + +void DetailedListView::setFilter(FilterPtr filter_) { + if(m_filter.data() != filter_) { // might just be updating + m_filter = filter_; + } +// clearSelection(); + + int count = 0; + // iterate over all items + DetailedEntryItem* item; + for(QListViewItemIterator it(this); it.current(); ++it) { + item = static_cast<DetailedEntryItem*>(it.current()); + if(m_filter && !m_filter->matches(item->entry())) { + item->setVisible(false); + setSelected(item, false); + } else { + item->setVisible(true); + ++count; + } + } + m_visibleItems = count; +} + +void DetailedListView::addField(Data::CollPtr, Data::FieldPtr field) { + addField(field, 0); /* field is hidden by default */ +} + +void DetailedListView::addField(Data::FieldPtr field_, int width_) { +// myDebug() << "DetailedListView::addField() - " << field_->title() << endl; + int col = addColumn(field_->title()); + + // Bools, images, and numbers should be centered + if(field_->type() == Data::Field::Bool + || field_->type() == Data::Field::Number + || field_->type() == Data::Field::Image) { + setColumnAlignment(col, Qt::AlignHCenter | Qt::AlignVCenter); + if(field_->type() == Data::Field::Image) { + m_imageColumns.push_back(col); + } + } else { + setColumnAlignment(col, Qt::AlignLeft | Qt::AlignVCenter); + } + + // width might be -1, which means set the width to maximum + // but m_columnWidths is the cached width, so just set it to 0 + m_columnWidths.push_back(QMAX(width_, 0)); + + m_isDirty.push_back(true); + + int id = m_headerMenu->insertItem(field_->title()); + if(width_ == 0) { + m_headerMenu->setItemChecked(id, false); + hideColumn(col); + } else { + m_headerMenu->setItemChecked(id, true); + showColumn(col); + } + setComparison(col, ListViewComparison::create(field_)); + resetComparisons(); +} + +void DetailedListView::modifyField(Tellico::Data::CollPtr, Data::FieldPtr oldField_, Data::FieldPtr newField_) { + int sec; // I need it for after the loop + for(sec = 0; sec < columns(); ++sec) { + if(header()->label(sec) == oldField_->title()) { + break; + } + } + + // I thought this would have to be mapped to index, but not the case + setColumnText(sec, newField_->title()); + if(newField_->type() == Data::Field::Bool + || newField_->type() == Data::Field::Number + || newField_->type() == Data::Field::Image) { + setColumnAlignment(sec, Qt::AlignHCenter | Qt::AlignVCenter); + if(oldField_->type() == Data::Field::Image) { + QValueVector<int>::iterator it = qFind(m_imageColumns.begin(), m_imageColumns.end(), sec); + if(it != m_imageColumns.end()) { + m_imageColumns.erase(it); + } + } + if(newField_->type() == Data::Field::Image) { + m_imageColumns.push_back(sec); + } + } else { + setColumnAlignment(sec, Qt::AlignLeft | Qt::AlignVCenter); + } + m_headerMenu->changeItem(m_headerMenu->idAt(sec+1), newField_->title()); // add 1 since menu has title + setComparison(sec, ListViewComparison::create(newField_)); + resetComparisons(); +} + +void DetailedListView::removeField(Tellico::Data::CollPtr, Data::FieldPtr field_) { +// myDebug() << "DetailedListView::removeField() - " << field_->name() << endl; + + int sec; // I need it for after the loop + for(sec = 0; sec < columns(); ++sec) { + if(header()->label(sec) == field_->title()) { +// myDebug() << "Removing section " << sec << endl; + break; + } + } + + if(sec == columns()) { + kdWarning() << "DetailedListView::removeField() - no column named " << field_->title() << endl; + return; + } + + m_headerMenu->removeItem(m_headerMenu->idAt(sec+1)); // add 1 since menu has title + + m_columnWidths.erase(&m_columnWidths[sec]); + m_isDirty.erase(&m_isDirty[sec]); + + // I thought this would have to be mapped to index, but not the case + removeComparison(sec); // must be before removeColumn(); + removeColumn(sec); + + // sometimes resizeEnabled gets messed up + for(int i = sec; i < columns(); ++i) { + header()->setResizeEnabled(columnWidth(i) > 0, header()->mapToSection(i)); + } + resetComparisons(); + slotUpdatePixmap(); + triggerUpdate(); +} + +void DetailedListView::reorderFields(const Data::FieldVec& fields_) { +// myDebug() << "DetailedListView::reorderFields()" << endl; + // find the first out of place field + int sec = 0; + Data::FieldVec::ConstIterator it = fields_.begin(); + for(sec = 0; it != fields_.end() && sec < columns(); ++sec, ++it) { + if(header()->label(sec) != it->title()) { + break; + } + } + + QStringList visible = visibleColumns(); + for( ; it != fields_.end() && sec < columns(); ++sec, ++it) { + header()->setLabel(sec, it->title()); + bool isVisible = (visible.findIndex(it->title()) > -1); + m_headerMenu->changeItem(m_headerMenu->idAt(sec+1), it->title()); + m_headerMenu->setItemChecked(m_headerMenu->idAt(sec+1), isVisible); + m_columnWidths[sec] = 0; + if(it->type() == Data::Field::Bool + || it->type() == Data::Field::Number + || it->type() == Data::Field::Image) { + setColumnAlignment(sec, Qt::AlignHCenter | Qt::AlignVCenter); + } else { + setColumnAlignment(sec, Qt::AlignLeft | Qt::AlignVCenter); + } + + if(isVisible) { + showColumn(sec); + } else { + hideColumn(sec); + } + } + + slotRefresh(); + slotUpdatePixmap(); + triggerUpdate(); +} + +int DetailedListView::prevSortedColumn() const { + return m_prevSortColumn; +} + +int DetailedListView::prev2SortedColumn() const { + return m_prev2SortColumn; +} + +void DetailedListView::setPrevSortedColumn(int prev1_, int prev2_) { + m_prevSortColumn = prev1_; + m_prev2SortColumn = prev2_; +} + +void DetailedListView::setSorting(int column_, bool ascending_/*=true*/) { +// DEBUG_BLOCK; + if(column_ != columnSorted()) { + m_prev2SortColumn = m_prevSortColumn; + m_prevSortColumn = columnSorted(); + } + GUI::ListView::setSorting(column_, ascending_); +} + +void DetailedListView::updateFirstSection() { + for(int numCol = 0; numCol < columns(); ++numCol) { + if(columnWidth(header()->mapToSection(numCol)) > 0) { + m_firstSection = header()->mapToSection(numCol); + break; + } + } +} + +void DetailedListView::slotUpdatePixmap() { + int oldSection = m_firstSection; + updateFirstSection(); + if(childCount() == 0 || oldSection == m_firstSection) { + return; + } + + Data::EntryPtr entry = static_cast<DetailedEntryItem*>(firstChild())->entry(); + if(!entry) { + return; + } + + Data::FieldPtr field1 = entry->collection()->fieldByTitle(columnText(oldSection)); + Data::FieldPtr field2 = entry->collection()->fieldByTitle(columnText(m_firstSection)); + if(!field1 || !field2) { + kdWarning() << "DetailedListView::slotUpdatePixmap() - no field found." << endl; + return; + } + + for(QListViewItemIterator it(this); it.current(); ++it) { + setPixmapAndText(static_cast<DetailedEntryItem*>(it.current()), oldSection, field1); + setPixmapAndText(static_cast<DetailedEntryItem*>(it.current()), m_firstSection, field2); + } +} + +void DetailedListView::saveConfig(Tellico::Data::CollPtr coll_, int configIndex_) { + KConfigGroup config(kapp->config(), QString::fromLatin1("Options - %1").arg(coll_->typeName())); + + // all of this is to have custom settings on a per file basis + QString configN; + if(coll_->type() == Data::Collection::Base) { + QValueList<ConfigInfo> info; + for(uint i = 0; i < Config::maxCustomURLSettings(); ++i) { + KURL u = config.readEntry(QString::fromLatin1("URL_%1").arg(i)); + if(!u.isEmpty() && static_cast<int>(i) != configIndex_) { + configN = QString::fromLatin1("_%1").arg(i); + ConfigInfo ci; + ci.cols = config.readListEntry("ColumnNames" + configN); + ci.widths = config.readIntListEntry("ColumnWidths" + configN); + ci.order = config.readIntListEntry("ColumnOrder" + configN); + ci.colSorted = config.readNumEntry("SortColumn" + configN); + ci.ascSort = config.readBoolEntry("SortAscending" + configN); + ci.prevSort = config.readNumEntry("PrevSortColumn" + configN); + ci.prev2Sort = config.readNumEntry("Prev2SortColumn" + configN); + info.append(ci); + } + } + // subtract one since we're writing the current settings, too + uint limit = QMIN(info.count(), Config::maxCustomURLSettings()-1); + for(uint i = 0; i < limit; ++i) { + // starts at one since the current config will be written below + configN = QString::fromLatin1("_%1").arg(i+1); + config.writeEntry("ColumnNames" + configN, info[i].cols); + config.writeEntry("ColumnWidths" + configN, info[i].widths); + config.writeEntry("ColumnOrder" + configN, info[i].order); + config.writeEntry("SortColumn" + configN, info[i].colSorted); + config.writeEntry("SortAscending" + configN, info[i].ascSort); + config.writeEntry("PrevSortColumn" + configN, info[i].prevSort); + config.writeEntry("Prev2SortColumn" + configN, info[i].prev2Sort); + } + configN = QString::fromLatin1("_0"); + } + + QValueList<int> widths, order; + for(int i = 0; i < columns(); ++i) { + if(columnWidthMode(i) == QListView::Manual) { + widths += columnWidth(i); + } else { + widths += -1; // Maximum width mode + } + order += header()->mapToIndex(i); + } + + config.writeEntry("ColumnWidths" + configN, widths); + config.writeEntry("ColumnOrder" + configN, order); + config.writeEntry("SortColumn" + configN, columnSorted()); + config.writeEntry("SortAscending" + configN, ascendingSort()); + config.writeEntry("PrevSortColumn" + configN, prevSortedColumn()); + config.writeEntry("Prev2SortColumn" + configN, prev2SortedColumn()); + + QStringList colNames; + for(int col = 0; col < columns(); ++col) { + colNames += coll_->fieldNameByTitle(columnText(col)); + } + config.writeEntry("ColumnNames" + configN, colNames); +} + +QString DetailedListView::sortColumnTitle1() const { + return columnSorted() == -1 ? QString::null : header()->label(columnSorted()); +} + +QString DetailedListView::sortColumnTitle2() const { + return prevSortedColumn() == -1 ? QString::null : header()->label(prevSortedColumn()); +} + +QString DetailedListView::sortColumnTitle3() const { + return prev2SortedColumn() == -1 ? QString::null : header()->label(prev2SortedColumn()); +} + +QStringList DetailedListView::visibleColumns() const { + QStringList titles; + for(int i = 0; i < columns(); ++i) { + if(columnWidth(header()->mapToSection(i)) > 0) { + titles << columnText(header()->mapToSection(i)); + } + } + return titles; +} + +// can't be const +Tellico::Data::EntryVec DetailedListView::visibleEntries() { + // We could just return the full collection entry list if the filter is 0 + // but printing depends on the sorted order + Data::EntryVec entries; + for(QListViewItemIterator it(this); it.current(); ++it) { + if(it.current()->isVisible()) { + entries.append(static_cast<DetailedEntryItem*>(it.current())->entry()); + } + } + return entries; +} + +void DetailedListView::selectAllVisible() { + blockSignals(true); + for(QListViewItemIterator it(this); it.current(); ++it) { + if(it.current()->isVisible()) { + setSelected(it.current(), true); + } + } + blockSignals(false); + // FIXME: not right with MultiSelectionListView + slotSelectionChanged(); +} + +void DetailedListView::resetEntryStatus() { + for(QListViewItemIterator it(this); it.current(); ++it) { + static_cast<DetailedEntryItem*>(it.current())->setState(DetailedEntryItem::Normal); + } + triggerUpdate(); +} + +int DetailedListView::compare(int col_, const GUI::ListViewItem* item1_, GUI::ListViewItem* item2_, bool asc_) { + DetailedEntryItem* item2 = static_cast<DetailedEntryItem*>(item2_); + int res = 0; + return (res = compareColumn(col_, item1_, item2, asc_)) != 0 ? res : + (res = compareColumn(m_prevSortColumn, item1_, item2, asc_)) != 0 ? res : + (res = compareColumn(m_prev2SortColumn, item1_, item2, asc_)) != 0 ? res : 0; +} + +int DetailedListView::compareColumn(int col, const GUI::ListViewItem* item1, GUI::ListViewItem* item2, bool asc) { + return GUI::ListView::compare(col, item1, item2, asc); +} + +void DetailedListView::resetComparisons() { + // this is only allowed when the view is not empty, so we can grab a collection ptr + if(childCount() == 0) { + return; + } + Data::CollPtr coll = static_cast<DetailedEntryItem*>(firstChild())->entry()->collection(); + if(!coll) { + return; + } + for(int i = 0; i < columns(); ++i) { + Data::FieldPtr f = coll->fieldByTitle(header()->label(i)); + if(f) { + setComparison(i, ListViewComparison::create(f)); + } + } +} + +#include "detailedlistview.moc" diff --git a/src/detailedlistview.h b/src/detailedlistview.h new file mode 100644 index 0000000..c6c7299 --- /dev/null +++ b/src/detailedlistview.h @@ -0,0 +1,214 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef DETAILEDLISTVIEW_H +#define DETAILEDLISTVIEW_H + +#include "gui/listview.h" +#include "observer.h" +#include "filter.h" + +#include <qstringlist.h> +#include <qpixmap.h> +#include <qvaluevector.h> + +class KPopupMenu; + +namespace Tellico { + class DetailedEntryItem; + +/** + * The DetailedListView class shows detailed information about entries in the + * collection. + * + * @author Robby Stephenson + */ +class DetailedListView : public GUI::ListView, public Observer { +Q_OBJECT + +public: + /** + * The constructor initializes the popup menu, but no columns are inserted. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + DetailedListView(QWidget* parent, const char* name=0); + ~DetailedListView(); + + /** + * Event filter used to popup the menu + */ + bool eventFilter(QObject* obj, QEvent* ev); + /** + * Selects the item which refers to a certain entry. + * + * @param entry A pointer to the entry + */ + void setEntrySelected(Data::EntryPtr entry); + void setFilter(FilterPtr filter); + FilterPtr filter() { return m_filter; } + + int prevSortedColumn() const; + int prev2SortedColumn() const; + virtual void setSorting(int column, bool ascending = true); + void setPrevSortedColumn(int prev1, int prev2); + QString sortColumnTitle1() const; + QString sortColumnTitle2() const; + QString sortColumnTitle3() const; + QStringList visibleColumns() const; + Data::EntryVec visibleEntries(); + + /** + * @param coll A pointer to the collection + */ + void addCollection(Data::CollPtr coll); + /** + * Removes all items which refers to a entry within a collection. + * + * @param coll A pointer to the collection + */ + void removeCollection(Data::CollPtr coll); + + /** + * Adds a new list item showing the details for a entry. + * + * @param entry A pointer to the entry + */ + virtual void addEntries(Data::EntryVec entries); + /** + * Modifies any item which refers to a entry, resetting the column contents. + * + * @param entry A pointer to the entry + */ + virtual void modifyEntries(Data::EntryVec entries); + /** + * Removes any item which refers to a certain entry. + * + * @param entry A pointer to the entry + */ + virtual void removeEntries(Data::EntryVec entries); + + virtual void addField(Data::CollPtr, Data::FieldPtr field); + void addField(Data::FieldPtr field, int width); + virtual void modifyField(Data::CollPtr, Data::FieldPtr oldField, Data::FieldPtr newField); + virtual void removeField(Data::CollPtr, Data::FieldPtr field); + + void reorderFields(const Data::FieldVec& fields); + /** + * saveConfig is only needed for custom collections */ + void saveConfig(Data::CollPtr coll, int saveConfig); + /** + * Select all visible items. + */ + void selectAllVisible(); + int visibleItems() const { return m_filter ? m_visibleItems : childCount(); } + /** + * Set max size of pixmaps. + * + * @param width Width + * @param height Height + */ + void setPixmapSize(int width, int height) { m_pixWidth = width; m_pixHeight = height; } + void resetEntryStatus(); + + virtual int compare(int col, const GUI::ListViewItem* item1, GUI::ListViewItem* item2, bool asc); + +public slots: + /** + * Resets the list view, clearing and deleting all items. + */ + void slotReset(); + /** + * Refreshes the view, repopulating all items. + */ + void slotRefresh(); + /** + * Refreshes all images only. + */ + void slotRefreshImages(); + +private: + DetailedEntryItem* addEntryInternal(Data::EntryPtr entry); + int compareColumn(int col, const GUI::ListViewItem* item1, GUI::ListViewItem* item2, bool asc); + + /** + * A helper method to populate an item. The column text is initialized by pulling + * the contents from the entry pointer of the item, so it should properly be set + * before this method is called. + * + * @param item A pointer to the item + */ + void populateItem(DetailedEntryItem* item); + void populateColumn(int col); + void setPixmapAndText(DetailedEntryItem* item, int col, Data::FieldPtr field); + + /** + * A helper method to locate any item which refers to a certain entry. If none + * is found, a NULL pointer is returned. + * + * @param entry A pointer to the entry + * @return A pointer to the item + */ + DetailedEntryItem* locateItem(Data::EntryPtr entry); + void showColumn(int col); + void hideColumn(int col); + void updateFirstSection(); + void resetComparisons(); + +private slots: + /** + * Handles the appearance of the popup menu. + * + * @param item A pointer to the list item underneath the mouse + * @param point The location point + * @param col The column number, not currently used + */ + void contextMenuRequested(QListViewItem* item, const QPoint& point, int col); + void slotHeaderMenuActivated(int id); + void slotCacheColumnWidth(int section, int oldSize, int newSize); + /** + * Slot to update the position of the pixmap + */ + void slotUpdatePixmap(); + +private: + struct ConfigInfo { + QStringList cols; + QValueList<int> widths; + QValueList<int> order; + int colSorted; + bool ascSort : 1; + int prevSort; + int prev2Sort; + }; + + KPopupMenu* m_headerMenu; + QValueVector<int> m_columnWidths; + QValueVector<bool> m_isDirty; + QValueVector<int> m_imageColumns; + bool m_loadingCollection; + QPixmap m_entryPix; + QPixmap m_checkPix; + + FilterPtr m_filter; + int m_prevSortColumn; + int m_prev2SortColumn; + int m_firstSection; + int m_visibleItems; + int m_pixWidth; + int m_pixHeight; +}; + +} // end namespace; +#endif diff --git a/src/document.cpp b/src/document.cpp new file mode 100644 index 0000000..6163af8 --- /dev/null +++ b/src/document.cpp @@ -0,0 +1,679 @@ +/*************************************************************************** + 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/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 <kmessagebox.h> +#include <klocale.h> +#include <kglobal.h> +#include <kapplication.h> + +#include <qregexp.h> +#include <qtimer.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() : QObject(), 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; + m_importer = new Import::TellicoImporter(url_, m_loadAllImages); + + 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; + QTimer::singleShot(500, this, SLOT(slotLoadAllImages())); + } else { + emit signalCollectionImagesLoaded(m_coll); + } + return true; +} + +bool Document::saveModified() { + bool completed = true; + + if(m_isModified) { + MainWindow* app = static_cast<MainWindow*>(Kernel::self()->widget()); + QString 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 KSharedPtr + 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); + + QStringList 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 QString track = QString::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 QString& 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 QString loaned = QString::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, QString::fromLatin1("true")); + EntryVec vec; + vec.append(entry_); + m_coll->updateDicts(vec); +} + +void Document::checkInEntry(Data::EntryPtr entry_) { + if(!entry_) { + return; + } + + const QString loaned = QString::fromLatin1("loaned"); + if(!m_coll->hasField(loaned)) { + return; + } + entry_->setField(loaned, QString::null); + m_coll->updateDicts(EntryVec(entry_)); +} + +void Document::renameCollection(const QString& 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() { + QString 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 + QMAX(1, m_coll->entryCount()/80); // add 1 since it could round off + uint j = 1; + + QString oldLocalDir = ImageFactory::localDir(); + ImageFactory::setLocalDirectory(localDir_); + + ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_); + + QString 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; + QString 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, QString::null); + 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); + } + + QStringList 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; + } + QString 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) { + QString id = e->field(f->name()); + if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) { + imagesToRemove.add(id); + } + } + } + + const QStringList realImagesToRemove = imagesToRemove.toList(); + for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) { + ImageFactory::removeImage(*it, false); // doesn't delete, just remove link + } +} + +#include "document.moc" diff --git a/src/document.h b/src/document.h new file mode 100644 index 0000000..82d74f7 --- /dev/null +++ b/src/document.h @@ -0,0 +1,237 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DOCUMENT_H +#define TELLICO_DOCUMENT_H + +#include <config.h> + +#include "datavectors.h" +#include "filter.h" + +#include <kurl.h> + +#include <qobject.h> +#include <qguardedptr.h> + +namespace Tellico { + namespace Import { + class TellicoImporter; + } + namespace Data { + +/** + * The Document contains everything needed to deal with the contents, thus separated from + * the viewer, the Tellico object. It can take of opening and saving documents, and contains + * a list of the collections in the document. + * + * @author Robby Stephenson + */ +class Document : public QObject { +Q_OBJECT + +public: + static Document* self() { if(!s_self) s_self = new Document(); return s_self; } + + /** + * Sets the URL associated with the document. + * + * @param url The URL + */ + void setURL(const KURL& url); + /** + * Checks the modified flag, which indicates if the document has changed since the + * last save. + * + * @return A boolean indicating the modified status + */ + bool isModified() const { return m_isModified; } + /** + * Sets whether all images are loaded from file or not + */ + void setLoadAllImages(bool loadAll) { m_loadAllImages = loadAll; } + /** + * Returns the current url associated with the document + * + * @return The url + */ + const KURL& URL() const { return m_url; } + /** + * Initializes a new document. The signalNewDoc() signal is emitted. The return + * value is currently always true, but should indicate whether or not a new document + * was correctly initialized. + * + * @param type The type of collection to add + * @return A boolean indicating success + */ + bool newDocument(int type); + /** + * Open a document given a specified location. If, for whatever reason, the file + * cannot be opened, a proper message box is shown, indicating the problem. The + * signalNewDoc() signal is made once the file contents have been confirmed. + * + * @param url The location to open + * @return A boolean indicating success + */ + bool openDocument(const KURL& url); + /** + * Checks to see if the document has been modified before deleting the contents. + * If it has, then a message box asks the user if the document should be saved, + * and then acts on the result. + * + * @return A boolean indicating success + */ + bool saveModified(); + /** + * Saves the document contents to a file. + * + * @param url The location to save the file + * @return A boolean indicating success + */ + bool saveDocument(const KURL& url); + /** + * Closes the document, deleting the contents. The return value is presently always true. + * + * @return A boolean indicating success + */ + bool closeDocument(); + /** + * Deletes the contents of the document. A signalCollectionDeleted() will be sent for every + * collection in the document. + */ + void deleteContents(); + /** + * Returns a pointer to the document collection + * + * @return The collection + */ + CollPtr collection() const; + /** + * Returns true if there are no entries. A doc with an empty collection is still empty. + */ + bool isEmpty() const; + /** + * Appends the contents of another collection to the current one. The collections must be the + * same type. Fields which are in the current collection are left alone. Fields + * in the appended collection not in the current one are added. Entrys in the appended collection + * are added to the current one. + * + * @param coll A pointer to the appended collection. + */ + void appendCollection(CollPtr coll); + /** + * Merges another collection into this one. The collections must be the same type. Fields in the + * current collection are left alone. Fields not in the current are added. The merging is slow + * since each entry in @p coll must be compared to ever entry in the current collection. + * + * @param coll A pointer to the collection to be merged. + * @return A QPair of the merged entries, see note in datavectors.h + */ + MergePair mergeCollection(CollPtr coll); + /** + * Replace the current collection with a new one. Effectively, this is equivalent to opening + * a new file containing this collection. + * + * @param coll A Pointer to the new collection, the document takes ownership. + */ + void replaceCollection(CollPtr coll); + void unAppendCollection(CollPtr coll, FieldVec origFields); + void unMergeCollection(CollPtr coll, FieldVec origFields_, MergePair entryPair); + /** + * Attempts to load an image from the document file + */ + bool loadImage(const QString& id); + bool loadAllImagesNow() const; + bool allImagesOnDisk() const { return m_allImagesOnDisk; } + int imageCount() const; + EntryVec filteredEntries(Filter::Ptr filter) const; + /** + * Sort entries according to current detailed view + */ + EntryVec sortEntries(EntryVec entries) const; + + void renameCollection(const QString& newTitle); + + void checkInEntry(EntryPtr entry); + void checkOutEntry(EntryPtr entry); + + /** + * The second entry vector contains entries with images which should not be removed + * in addition to those already in the collection + */ + void removeImagesNotInCollection(EntryVec entries, EntryVec entriesToKeep); + void cancelImageWriting() { m_cancelImageWriting = true; } + +public slots: + /** + * Sets the modified flag. If it is true, the signalModified signal is made. + * + * @param m A boolean indicating the current modified status + */ + void slotSetModified(bool m=true); + void slotDocumentRestored(); + +signals: + /** + * Signals that the document has been modified. + */ + void signalModified(bool modified); + /** + * Signals that a status message should be shown. + * + * @param str The message + */ + void signalStatusMsg(const QString& str); + /** + * Signals that all images in the loaded file have been loaded + * into memory or onto the disk + */ + void signalCollectionImagesLoaded(Tellico::Data::CollPtr coll); + +private slots: + /** + * Does an initial loading of all images, used for writing + * images to temp dir initially + */ + void slotLoadAllImages(); + +private: + static Document* s_self; + + /** + * Writes all images in the current collection to the cache directory + * if cacheDir = LocalDir, then url will be used and must not be empty + */ + void writeAllImages(int cacheDir, const KURL& url=KURL()); + bool pruneImages(); + + // make all constructors private + Document(); + Document(const Document& doc); + Document& operator=(const Document&); + ~Document(); + + CollPtr m_coll; + bool m_isModified : 1; + bool m_loadAllImages : 1; + KURL m_url; + bool m_validFile : 1; + QGuardedPtr<Import::TellicoImporter> m_importer; + bool m_cancelImageWriting : 1; + int m_fileFormat; + bool m_allImagesOnDisk : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/entry.cpp b/src/entry.cpp new file mode 100644 index 0000000..a558ba2 --- /dev/null +++ b/src/entry.cpp @@ -0,0 +1,466 @@ +/*************************************************************************** + 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 "entry.h" +#include "collection.h" +#include "field.h" +#include "translators/bibtexhandler.h" // needed for BibtexHandler::cleanText() +#include "document.h" +#include "tellico_debug.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "latin1literal.h" +#include "../isbnvalidator.h" +#include "../lccnvalidator.h" + +#include <klocale.h> + +#include <qregexp.h> + +using Tellico::Data::Entry; +using Tellico::Data::EntryGroup; + +EntryGroup::EntryGroup(const QString& group, const QString& field) + : QObject(), EntryVec(), m_group(Tellico::shareString(group)), m_field(Tellico::shareString(field)) { +} + +EntryGroup::~EntryGroup() { + // need a copy since we remove ourselves + EntryVec vec = *this; + for(Data::EntryVecIt entry = vec.begin(); entry != vec.end(); ++entry) { + entry->removeFromGroup(this); + } +} + +bool Entry::operator==(const Entry& e1) { +// special case for file catalog, just check the url + if(m_coll && m_coll->type() == Collection::File && + e1.m_coll && e1.m_coll->type() == Collection::File) { + // don't forget case where both could have empty urls + // but different values for other fields + QString u = field(QString::fromLatin1("url")); + if(!u.isEmpty()) { + // versions before 1.2.7 could have saved the url without the protocol + bool b = KURL::fromPathOrURL(u) == KURL::fromPathOrURL(e1.field(QString::fromLatin1("url"))); + if(b) { + return true; + } else { + Data::FieldPtr f = m_coll->fieldByName(QString::fromLatin1("url")); + if(f && f->property(QString::fromLatin1("relative")) == Latin1Literal("true")) { + return KURL(Document::self()->URL(), u) == KURL::fromPathOrURL(e1.field(QString::fromLatin1("url"))); + } + } + } + } + if(e1.m_fields.count() != m_fields.count()) { + return false; + } + for(StringMap::ConstIterator it = e1.m_fields.begin(); it != e1.m_fields.end(); ++it) { + if(!m_fields.contains(it.key()) || m_fields[it.key()] != it.data()) { + return false; + } + } + return true; +} + +Entry::Entry(CollPtr coll_) : KShared(), m_coll(coll_), m_id(-1) { +#ifndef NDEBUG + if(!coll_) { + kdWarning() << "Entry() - null collection pointer!" << endl; + } +#endif +} + +Entry::Entry(CollPtr coll_, int id_) : KShared(), m_coll(coll_), m_id(id_) { +#ifndef NDEBUG + if(!coll_) { + kdWarning() << "Entry() - null collection pointer!" << endl; + } +#endif +} + +Entry::Entry(const Entry& entry_) : + KShared(entry_), + m_coll(entry_.m_coll), + m_id(-1), + m_fields(entry_.m_fields), + m_formattedFields(entry_.m_formattedFields) { +} + +Entry& Entry::operator=(const Entry& other_) { + if(this == &other_) return *this; + +// myDebug() << "Entry::operator=()" << endl; + static_cast<KShared&>(*this) = static_cast<const KShared&>(other_); + m_coll = other_.m_coll; + m_id = other_.m_id; + m_fields = other_.m_fields; + m_formattedFields = other_.m_formattedFields; + return *this; +} + +Entry::~Entry() { +} + +Tellico::Data::CollPtr Entry::collection() const { + return m_coll; +} + +void Entry::setCollection(CollPtr coll_) { + if(coll_ == m_coll) { + myDebug() << "Entry::setCollection() - already belongs to collection!" << endl; + return; + } + // special case adding a book to a bibtex collection + // it would be better to do this in a real OOO way, but this should work + const bool addEntryType = m_coll->type() == Collection::Book && + coll_->type() == Collection::Bibtex && + !m_coll->hasField(QString::fromLatin1("entry-type")); + m_coll = coll_; + m_id = -1; + // set this after changing the m_coll pointer since setField() checks field validity + if(addEntryType) { + setField(QString::fromLatin1("entry-type"), QString::fromLatin1("book")); + } +} + +QString Entry::title() const { + return formattedField(QString::fromLatin1("title")); +} + +QString Entry::field(Data::FieldPtr field_, bool formatted_/*=false*/) const { + return field(field_->name(), formatted_); +} + +QString Entry::field(const QString& fieldName_, bool formatted_/*=false*/) const { + if(formatted_) { + return formattedField(fieldName_); + } + + FieldPtr f = m_coll->fieldByName(fieldName_); + if(!f) { + return QString::null; + } + if(f->type() == Field::Dependent) { + return dependentValue(this, f->description(), false); + } + + if(!m_fields.isEmpty() && m_fields.contains(fieldName_)) { + return m_fields[fieldName_]; + } + return QString::null; +} + +QString Entry::formattedField(Data::FieldPtr field_) const { + return formattedField(field_->name()); +} + +QString Entry::formattedField(const QString& fieldName_) const { + FieldPtr f = m_coll->fieldByName(fieldName_); + if(!f) { + return QString::null; + } + + Field::FormatFlag flag = f->formatFlag(); + if(f->type() == Field::Dependent) { + if(flag == Field::FormatNone) { + return dependentValue(this, f->description(), false); + } else { + // format sub fields and whole string + return Field::format(dependentValue(this, f->description(), true), flag); + } + } + + // if auto format is not set or FormatNone, then just return the value + if(flag == Field::FormatNone) { + return field(fieldName_); + } + + if(m_formattedFields.isEmpty() || !m_formattedFields.contains(fieldName_)) { + QString value = field(fieldName_); + if(!value.isEmpty()) { + // special for Bibtex collections + if(m_coll->type() == Collection::Bibtex) { + BibtexHandler::cleanText(value); + } + value = Field::format(value, flag); + m_formattedFields.insert(fieldName_, value); + } + return value; + } + // otherwise, just look it up + return m_formattedFields[fieldName_]; +} + +QStringList Entry::fields(Data::FieldPtr field_, bool formatted_) const { + return fields(field_->name(), formatted_); +} + +QStringList Entry::fields(const QString& field_, bool formatted_) const { + QString s = formatted_ ? formattedField(field_) : field(field_); + if(s.isEmpty()) { + return QStringList(); + } + return Field::split(s, true); +} + +bool Entry::setField(Data::FieldPtr field_, const QString& value_) { + return setField(field_->name(), value_); +} + +bool Entry::setField(const QString& name_, const QString& value_) { + if(name_.isEmpty()) { + kdWarning() << "Entry::setField() - empty field name for value: " << value_ << endl; + return false; + } + // an empty value means remove the field + if(value_.isEmpty()) { + if(!m_fields.isEmpty() && m_fields.contains(name_)) { + m_fields.remove(name_); + } + invalidateFormattedFieldValue(name_); + return true; + } + +#ifndef NDEBUG + if(m_coll && (m_coll->fields().count() == 0 || !m_coll->hasField(name_))) { + myDebug() << "Entry::setField() - unknown collection entry field - " + << name_ << endl; + return false; + } +#endif + + if(m_coll && !m_coll->isAllowed(name_, value_)) { + myDebug() << "Entry::setField() - for " << name_ + << ", value is not allowed - " << value_ << endl; + return false; + } + + Data::FieldPtr f = m_coll->fieldByName(name_); + if(!f) { + return false; + } + + // the string store is probable only useful for fields with auto-completion or choice/number/bool + if(!(f->flags() & Field::AllowMultiple) && + ((f->type() == Field::Choice || f->type() == Field::Bool || f->type() == Field::Number) || + (f->type() == Field::Line && (f->flags() & Field::AllowCompletion)))) { + m_fields.insert(Tellico::shareString(name_), Tellico::shareString(value_)); + } else { + m_fields.insert(Tellico::shareString(name_), value_); + } + invalidateFormattedFieldValue(name_); + return true; +} + +bool Entry::addToGroup(EntryGroup* group_) { + if(!group_ || m_groups.contains(group_)) { + return false; + } + + m_groups.push_back(group_); + group_->append(this); +// m_coll->groupModified(group_); + return true; +} + +bool Entry::removeFromGroup(EntryGroup* group_) { + // if the removal isn't successful, just return + bool success = m_groups.remove(group_); + success = success && group_->remove(this); +// myDebug() << "Entry::removeFromGroup() - removing from group - " +// << group_->fieldName() << "::" << group_->groupName() << endl; + if(success) { +// m_coll->groupModified(group_); + } else { + myDebug() << "Entry::removeFromGroup() failed! " << endl; + } + return success; +} + +void Entry::clearGroups() { + m_groups.clear(); +} + +// this function gets called before m_groups is updated. In fact, it is used to +// update that list. This is the function that actually parses the field values +// and returns the list of the group names. +QStringList Entry::groupNamesByFieldName(const QString& fieldName_) const { +// myDebug() << "Entry::groupsByfieldName() - " << fieldName_ << endl; + FieldPtr f = m_coll->fieldByName(fieldName_); + + // easy if not allowing multiple values + if(!(f->flags() & Field::AllowMultiple)) { + QString value = formattedField(fieldName_); + if(value.isEmpty()) { + return i18n(Collection::s_emptyGroupTitle); + } else { + return value; + } + } + + QStringList groups = fields(fieldName_, true); + if(groups.isEmpty()) { + return i18n(Collection::s_emptyGroupTitle); + } else if(f->type() == Field::Table) { + // quick hack for tables, how often will a user have "::" in their value? + // only use first column for group + QStringList::Iterator it = groups.begin(); + while(it != groups.end()) { + (*it) = (*it).section(QString::fromLatin1("::"), 0, 0); + if((*it).isEmpty()) { + it = groups.remove(it); // points to next in list + } else { + ++it; + } + } + } + return groups; +} + +bool Entry::isOwned() { + return (m_coll && m_id > -1 && m_coll->entryCount() > 0 && m_coll->entries().contains(this)); +} + +// a null string means invalidate all +void Entry::invalidateFormattedFieldValue(const QString& name_) { + if(name_.isNull()) { + m_formattedFields.clear(); + } else if(!m_formattedFields.isEmpty() && m_formattedFields.contains(name_)) { + m_formattedFields.remove(name_); + } +} + +// format is something like "%{year} %{author}" +QString Entry::dependentValue(ConstEntryPtr entry_, const QString& format_, bool formatted_) { + if(!entry_) { + return format_; + } + + QString result, fieldName; + FieldPtr field; + + int endPos; + int curPos = 0; + int pctPos = format_.find('%', curPos); + while(pctPos != -1 && pctPos+1 < static_cast<int>(format_.length())) { + if(format_[pctPos+1] == '{') { + endPos = format_.find('}', pctPos+2); + if(endPos > -1) { + result += format_.mid(curPos, pctPos-curPos); + fieldName = format_.mid(pctPos+2, endPos-pctPos-2); + field = entry_->collection()->fieldByName(fieldName); + if(!field) { + // allow the user to also use field titles + field = entry_->collection()->fieldByTitle(fieldName); + } + if(field) { + // don't format, just capitalize + result += entry_->field(field, formatted_); + } else if(fieldName == Latin1Literal("id")) { + result += QString::number(entry_->id()); + } else { + result += format_.mid(pctPos, endPos-pctPos+1); + } + curPos = endPos+1; + } else { + break; + } + } else { + result += format_.mid(curPos, pctPos-curPos+1); + curPos = pctPos+1; + } + pctPos = format_.find('%', curPos); + } + result += format_.mid(curPos, format_.length()-curPos); +// myDebug() << "Entry::dependentValue() - " << format_ << " = " << result << endl; + // sometimes field value might empty, resulting in multiple consecutive white spaces + // so let's simplify that... + return result.simplifyWhiteSpace(); +} + +int Entry::compareValues(EntryPtr e1, EntryPtr e2, const QString& f, ConstCollPtr c) { + return compareValues(e1, e2, c->fieldByName(f)); +} + +int Entry::compareValues(EntryPtr e1, EntryPtr e2, FieldPtr f) { + if(!e1 || !e2 || !f) { + return 0; + } + QString s1 = e1->field(f).lower(); + QString s2 = e2->field(f).lower(); + if(s1.isEmpty() || s2.isEmpty()) { + return 0; + } + // complicated string matching, here are the cases I want to match + // "bend it like beckham" == "bend it like beckham (widescreen edition)" + // "the return of the king" == "return of the king" + if(s1 == s2) { + return 5; + } + // special case for isbn + if(f->name() == Latin1Literal("isbn") && ISBNValidator::isbn10(s1) == ISBNValidator::isbn10(s2)) { + return 5; + } + if(f->name() == Latin1Literal("lccn") && LCCNValidator::formalize(s1) == LCCNValidator::formalize(s2)) { + return 5; + } + if(f->formatFlag() == Field::FormatName) { + s1 = e1->field(f, true).lower(); + s2 = e2->field(f, true).lower(); + if(s1 == s2) { + return 5; + } + } + // try removing punctuation + QRegExp notAlphaNum(QString::fromLatin1("[^\\s\\w]")); + QString s1a = s1; s1a.remove(notAlphaNum); + QString s2a = s2; s2a.remove(notAlphaNum); + if(!s1a.isEmpty() && s1a == s2a) { +// myDebug() << "match without punctuation" << endl; + return 5; + } + Field::stripArticles(s1); + Field::stripArticles(s2); + if(!s1.isEmpty() && s1 == s2) { +// myDebug() << "match without articles" << endl; + return 3; + } + // try removing everything between parentheses + QRegExp rx(QString::fromLatin1("\\s*\\(.*\\)\\s*")); + s1.remove(rx); + s2.remove(rx); + if(!s1.isEmpty() && s1 == s2) { +// myDebug() << "match without parentheses" << endl; + return 2; + } + if(f->flags() & Field::AllowMultiple) { + QStringList sl1 = e1->fields(f, false); + QStringList sl2 = e2->fields(f, false); + int matches = 0; + for(QStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) { + matches += sl2.contains(*it); + } + if(matches == 0 && f->formatFlag() == Field::FormatName) { + sl1 = e1->fields(f, true); + sl2 = e2->fields(f, true); + for(QStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) { + matches += sl2.contains(*it); + } + } + return matches; + } + return 0; +} + +#include "entry.moc" diff --git a/src/entry.h b/src/entry.h new file mode 100644 index 0000000..bff3d7d --- /dev/null +++ b/src/entry.h @@ -0,0 +1,256 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTRY_H +#define TELLICO_ENTRY_H + +#include "datavectors.h" + +#include <qstringlist.h> +#include <qstring.h> +#include <qobject.h> + +#include <functional> + +namespace Tellico { + namespace Data { + class Collection; + +/** + * The EntryGroup is simply a vector of entries which knows the name of its group, + * and the name of the field to which that group belongs. + * + * An example for a book collection would be a group of books, all written by + * David Weber. The @ref groupName() would be "Weber, David" and the + * @ref fieldName() would be "author". + * + * It's a QObject because EntryGroupItem holds a QGuardedPtr + * + * @author Robby Stephenson + */ +class EntryGroup : public QObject, public EntryVec { +Q_OBJECT + +public: + EntryGroup(const QString& group, const QString& field); + ~EntryGroup(); + + const QString& groupName() const { return m_group; } + const QString& fieldName() const { return m_field; } + +private: + QString m_group; + QString m_field; +}; + +/** + * The Entry class represents a book, a CD, or whatever is the basic entity + * in the collection. + * + * Each Entry object has a set of field values, such as title, artist, or format, + * and must belong to a collection. A unique id number identifies each entry. + * + * @see Field + * + * @author Robby Stephenson + */ +class Entry : public KShared { + +public: + /** + * The constructor, which automatically sets the id to the current number + * of entries in the collection. + * + * @param coll A pointer to the parent collection object + */ + Entry(CollPtr coll); + Entry(CollPtr coll, int id); + /** + * The copy constructor, needed since the id must be different. + */ + Entry(const Entry& entry); + /** + * The assignment operator is overloaded, since the id must be different. + */ + Entry& operator=(const Entry& other); + /** + * two entries are equal if all their field values are equal, except for + * file catalogs which match on the url only + */ + bool operator==(const Entry& other); + + ~Entry(); + + /** + * Every entry has a title. + * + * @return The entry title + */ + QString title() const; + /** + * Returns the value of the field with a given key name. If the key doesn't + * exist, the method returns @ref QString::null. + * + * @param fieldName The field name + * @param formatted Whether to format the field or not. + * @return The value of the field + */ + QString field(const QString& fieldName, bool formatted=false) const; + QString field(Data::FieldPtr field, bool formatted=false) const; + /** + * Returns the formatted value of the field with a given key name. If the + * key doesn't exist, the method returns @ref QString::null. The value is cached, + * so the first time the value is requested, @ref Field::format is called. + * The second time, that lookup isn't necessary. + * + * @param fieldName The field name + * @return The formatted value of the field + */ + QString formattedField(const QString& fieldName) const; + QString formattedField(Data::FieldPtr field) const; + /** + * Splits a field value. This is faster than calling Data::Field::split() since + * a regexp is not used, only a string. + * + * @param field The field name + * @param format Whether to format the values or not + * @return The list of field values + */ + QStringList fields(const QString& fieldName, bool formatted) const; + QStringList fields(Data::FieldPtr field, bool formatted) const; + /** + * Sets the value of an field for the entry. The method first verifies that + * the value is allowed for that particular key. + * + * @param fieldName The name of the field + * @param value The value of the field + * @return A boolean indicating whether or not the field was successfully set + */ + bool setField(const QString& fieldName, const QString& value); + bool setField(Data::FieldPtr field, const QString& value); + /** + * Returns a pointer to the parent collection of the entry. + * + * @return The collection pointer + */ + CollPtr collection() const; + /** + * Changes the collection owner of the entry + */ + void setCollection(CollPtr coll); + /** + * Returns the id of the entry + * + * @return The id + */ + long id() const { return m_id; } + void setId(long id) { m_id = id; } + /** + * Adds the entry to a group. The group list within the entry is updated + * and the entry is added to the group. + * + * @param group The group + * @return a bool indicating if it was successfully added. If the entry was already + * in the group, the return value is false + */ + bool addToGroup(EntryGroup* group); + /** + * Removes the entry from a group. The group list within the entry is updated + * and the entry is removed from the group. + * + * @param group The group + * @return a bool indicating if the group was successfully removed + */ + bool removeFromGroup(EntryGroup* group); + void clearGroups(); + /** + * Returns a list of the groups to which the entry belongs + * + * @return The list of groups + */ + const PtrVector<EntryGroup>& groups() const { return m_groups; } + /** + * Returns a list containing the names of the groups for + * a certain field to which the entry belongs + * + * @param fieldName The name of the field + * @return The list of names + */ + QStringList groupNamesByFieldName(const QString& fieldName) const; + /** + * Returns a list of all the field values contained in the entry. + * + * @return The list of field values + */ + QStringList fieldValues() const { return m_fields.values(); } + /** + * Returns a list of all the formatted field values contained in the entry. + * + * @return The list of field values + */ + QStringList formattedFieldValues() const { return m_formattedFields.values(); } + /** + * Returns a boolean indicating if the entry's parent collection recognizes + * it existence, that is, the parent collection has this entry in its list. + * + * @return Whether the entry is owned or not + */ + bool isOwned(); + /** + * Removes the formatted value of the field from the map. This should be used when + * the field's format flag has changed. + * + * @param name The name of the field that changed. QString::null means invalidate all fields. + */ + void invalidateFormattedFieldValue(const QString& name=QString::null); + + static int compareValues(EntryPtr entry1, EntryPtr entry2, FieldPtr field); + static int compareValues(EntryPtr entry1, EntryPtr entry2, const QString& field, ConstCollPtr coll); + + /** + * Construct the derived valued for an field. The format string should be + * of the form "%{name1} %{name2}" where the names are replaced by the value + * of that field for the entry. Whether or not formatting is done on the + * strings themselves should be taken into account. + * + * @param formatString The format string + * @param formatted Whether the inserted values should be formatted. + * @return The constructed field value + */ + static QString dependentValue(ConstEntryPtr e, const QString& formatString, bool formatted); + +private: + CollPtr m_coll; + long m_id; + StringMap m_fields; + mutable StringMap m_formattedFields; + PtrVector<EntryGroup> m_groups; +}; + +class EntryCmp : public std::binary_function<EntryPtr, EntryPtr, bool> { + +public: + EntryCmp(const QString& field) : m_field(field) {} + + bool operator()(EntryPtr e1, EntryPtr e2) const { + return e1->field(m_field) < e2->field(m_field); + } + +private: + QString m_field; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/entryeditdialog.cpp b/src/entryeditdialog.cpp new file mode 100644 index 0000000..db99d7f --- /dev/null +++ b/src/entryeditdialog.cpp @@ -0,0 +1,757 @@ +/*************************************************************************** + 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 "entryeditdialog.h" +#include "gui/tabcontrol.h" +#include "collection.h" +#include "controller.h" +#include "field.h" +#include "entry.h" +#include "tellico_utils.h" +#include "tellico_kernel.h" +#include "tellico_debug.h" +#include "latin1literal.h" + +#include <klocale.h> +#include <kmessagebox.h> +#include <kaccelmanager.h> +#include <kiconloader.h> +#include <kdeversion.h> +#include <kpushbutton.h> +#include <kaccel.h> + +#include <qlayout.h> +#include <qstringlist.h> +#include <qpushbutton.h> +#include <qvaluevector.h> +#include <qvbox.h> +#include <qobjectlist.h> +#include <qtabbar.h> +#include <qstyle.h> +#include <qapplication.h> + +namespace { + // must be an even number + static const int NCOLS = 2; // number of columns of GUI::FieldWidgets +} + +using Tellico::EntryEditDialog; + +EntryEditDialog::EntryEditDialog(QWidget* parent_, const char* name_) + : KDialogBase(parent_, name_, false, i18n("Edit Entry"), Help|User1|User2|User3|Apply|Close, User1, false, + KGuiItem(i18n("&New Entry"))), + m_currColl(0), + m_tabs(new GUI::TabControl(this)), + m_modified(false), + m_isOrphan(false), + m_isWorking(false), + m_needReset(false) { + setMainWidget(m_tabs); + + m_prevBtn = User3; + m_nextBtn = User2; + m_newBtn = User1; + m_saveBtn = Apply; + KGuiItem save = KStdGuiItem::save(); + save.setText(i18n("Sa&ve Entry")); + setButtonGuiItem(m_saveBtn, save); + enableButton(m_saveBtn, false); + + connect(this, SIGNAL(applyClicked()), SLOT(slotHandleSave())); + connect(this, SIGNAL(user1Clicked()), SLOT(slotHandleNew())); + connect(this, SIGNAL(user2Clicked()), SLOT(slotGoNextEntry())); + connect(this, SIGNAL(user3Clicked()), SLOT(slotGoPrevEntry())); + + KGuiItem prev; + prev.setIconName(QString::fromLatin1(QApplication::reverseLayout() ? "forward" : "back")); + prev.setToolTip(i18n("Go to the previous entry in the collection")); + prev.setWhatsThis(prev.toolTip()); + + KGuiItem next; + next.setIconName(QString::fromLatin1(QApplication::reverseLayout() ? "back" : "forward")); + next.setToolTip(i18n("Go to the next entry in the collection")); + next.setWhatsThis(next.toolTip()); + + setButtonGuiItem(m_nextBtn, next); + setButtonGuiItem(m_prevBtn, prev); + + KAccel* accel = new KAccel(this); + accel->insert(QString::fromLatin1("Go Prev"), QString(), prev.toolTip(), Qt::Key_PageUp, + Controller::self(), SLOT(slotGoPrevEntry())); + accel->insert(QString::fromLatin1("Go Next"), QString(), next.toolTip(), Qt::Key_PageDown, + Controller::self(), SLOT(slotGoNextEntry())); + + setHelp(QString::fromLatin1("entry-editor")); + + resize(configDialogSize(QString::fromLatin1("Edit Dialog Options"))); +} + +void EntryEditDialog::slotClose() { + // check to see if an entry should be saved before hiding + // block signals so the entry view and selection isn't cleared + if(queryModified()) { + hide(); +// blockSignals(true); +// slotHandleNew(); +// blockSignals(false); + } +} + +void EntryEditDialog::slotReset() { +// myDebug() << "EntryEditDialog::slotReset()" << endl; + if(m_isWorking) { + return; + } + + enableButton(m_saveBtn, false); + setButtonText(m_saveBtn, i18n("Sa&ve Entry")); + m_currColl = 0; + m_currEntries.clear(); + + m_modified = false; + + m_tabs->clear(); // GUI::FieldWidgets get deleted here + m_widgetDict.clear(); +} + +void EntryEditDialog::setLayout(Data::CollPtr coll_) { + if(!coll_ || m_isWorking) { + return; + } +// myDebug() << "EntryEditDialog::setLayout()" << endl; + + actionButton(m_newBtn)->setIconSet(UserIconSet(coll_->typeName())); + + setUpdatesEnabled(false); + if(m_tabs->count() > 0) { +// myDebug() << "EntryEditDialog::setLayout() - resetting contents." << endl; + slotReset(); + } + m_isWorking = true; + + m_currColl = coll_; + + int maxHeight = 0; + QPtrList<QWidget> gridList; + bool noChoices = true; + + bool focusedFirst = false; + QStringList catList = m_currColl->fieldCategories(); + for(QStringList::ConstIterator catIt = catList.begin(); catIt != catList.end(); ++catIt) { + Data::FieldVec fields = m_currColl->fieldsByCategory(*catIt); + if(fields.isEmpty()) { // sanity check + continue; + } + + // if this layout model is changed, be sure to check slotUpdateField() + QWidget* page = new QWidget(m_tabs); + // (parent, margin, spacing) + QVBoxLayout* boxLayout = new QVBoxLayout(page, 0, 0); + + QWidget* grid = new QWidget(page); + gridList.append(grid); + // (parent, nrows, ncols, margin, spacing) + // spacing gets a bit weird, if there are absolutely no Choice fields, + // then spacing should be 5, which is set later + QGridLayout* layout = new QGridLayout(grid, 0, NCOLS, 8, 2); + // keramik styles make big widget, cut down the spacing a bit + if(QCString(style().name()).lower().find("keramik", 0, false) > -1) { + layout->setSpacing(0); + } + + boxLayout->addWidget(grid, 0); + // those with multiple, get a stretch + if(fields.count() > 1 || !fields[0]->isSingleCategory()) { + boxLayout->addStretch(1); + } + + // keep track of which should expand + QValueVector<bool> expands(NCOLS, false); + QValueVector<int> maxWidth(NCOLS, 0); + + Data::FieldVec::Iterator it = fields.begin(); // needed later + for(int count = 0; it != fields.end(); ++it) { + Data::FieldPtr field = it; + // ReadOnly and Dependent fields don't get widgets + if(field->type() == Data::Field::ReadOnly || field->type() == Data::Field::Dependent) { + continue; + } + if(field->type() == Data::Field::Choice) { + noChoices = false; + } + + GUI::FieldWidget* widget = GUI::FieldWidget::create(field, grid); + if(!widget) { + continue; + } + connect(widget, SIGNAL(modified()), SLOT(slotSetModified())); + if(!focusedFirst && widget->isFocusEnabled()) { + widget->setFocus(); + focusedFirst = true; + } + + int r = count/NCOLS; + int c = count%NCOLS; + layout->addWidget(widget, r, c); + layout->setRowStretch(r, 1); + + m_widgetDict.insert(QString::number(m_currColl->id()) + field->name(), widget); + + maxWidth[count%NCOLS] = QMAX(maxWidth[count%NCOLS], widget->labelWidth()); + if(widget->expands()) { + expands[count%NCOLS] = true; + } + widget->updateGeometry(); + if(!field->isSingleCategory()) { + maxHeight = QMAX(maxHeight, widget->minimumSizeHint().height()); + } + ++count; + } + + // now, the labels in a column should all be the same width + it = fields.begin(); + for(int count = 0; it != fields.end(); ++it) { + GUI::FieldWidget* widget = m_widgetDict.find(QString::number(m_currColl->id()) + it->name()); + if(widget) { + widget->setLabelWidth(maxWidth[count%NCOLS]); + ++count; + } + } + + // update stretch factors for columns with a line edit + for(int col = 0; col < NCOLS; ++col) { + if(expands[col]) { + layout->setColStretch(col, 1); + } + } + + m_tabs->addTab(page, *catIt); + } + + // Now, go through and set all the field widgets to the same height + for(QPtrListIterator<QWidget> it(gridList); it.current(); ++it) { + QGridLayout* l = static_cast<QGridLayout*>(it.current()->layout()); + if(noChoices) { + l->setSpacing(5); + } + for(int row = 0; row < l->numRows() && it.current()->children()->count() > 1; ++row) { + l->addRowSpacing(row, maxHeight); + } + // I don't want anything to be hidden, Keramik has a bug if I don't do this + it.current()->setMinimumHeight(it.current()->sizeHint().height()); + // the parent of the grid is the page that got added to the tabs + it.current()->parentWidget()->layout()->invalidate(); + it.current()->parentWidget()->setMinimumHeight(it.current()->parentWidget()->sizeHint().height()); + } + + setUpdatesEnabled(true); +// this doesn't seem to work +// setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); +// so do this instead + layout()->invalidate(); // needed so the sizeHint() gets recalculated + m_tabs->setMinimumHeight(m_tabs->minimumSizeHint().height()); + m_tabs->setMinimumWidth(m_tabs->sizeHint().width()); + + // update keyboard accels + // only want to manage tabBar() + KAcceleratorManager::manage(m_tabs->tabBar()); + + m_tabs->setCurrentPage(0); + + m_isWorking = false; + slotHandleNew(); + m_modified = false; // because the year is inserted +} + +void EntryEditDialog::slotHandleNew() { + if(!m_currColl || !queryModified()) { + return; + } + + m_tabs->setCurrentPage(0); + m_tabs->setFocusToFirstChild(); + clear(); + m_isWorking = true; // clear() will get called again + Controller::self()->slotClearSelection(); + m_isWorking = false; + + enableButton(m_prevBtn, false); + enableButton(m_nextBtn, false); + + Data::EntryPtr entry = new Data::Entry(m_currColl); + m_currEntries.append(entry); + m_isOrphan = true; +} + +void EntryEditDialog::slotHandleSave() { + if(!m_currColl || m_isWorking) { + return; + } + + m_isWorking = true; + + if(m_currEntries.isEmpty()) { + myDebug() << "EntryEditDialog::slotHandleSave() - creating new entry" << endl; + m_currEntries.append(new Data::Entry(m_currColl)); + m_isOrphan = true; + } + + // add a message box if multiple items are selected + if(m_currEntries.count() > 1) { + QStringList names; + for(Data::EntryVec::ConstIterator entry = m_currEntries.constBegin(); entry != m_currEntries.constEnd(); ++entry) { + names += entry->title(); + } + QString str(i18n("Do you really want to modify these entries?")); + QString dontAsk = QString::fromLatin1("SaveMultipleBooks"); // don't change 'books', invisible anyway + int ret = KMessageBox::questionYesNoList(this, str, names, i18n("Modify Multiple Entries"), + KStdGuiItem::yes(), KStdGuiItem::no(), dontAsk); + if(ret != KMessageBox::Yes) { + m_isWorking = false; + return; + } + } + + GUI::CursorSaver cs; + + Data::EntryVec oldEntries; + Data::FieldVec fields = m_currColl->fields(); + Data::FieldVec fieldsRequiringValues; + // boolean to keep track if any field gets changed + bool modified = false; + for(Data::EntryVecIt entry = m_currEntries.begin(); entry != m_currEntries.end(); ++entry) { + // if the entry is owned, then we're modifying an existing entry, keep a copy of the old one + if(entry->isOwned()) { + oldEntries.append(new Data::Entry(*entry)); + } + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + QString key = QString::number(m_currColl->id()) + fIt->name(); + GUI::FieldWidget* widget = m_widgetDict.find(key); + if(widget && widget->isEnabled()) { + QString temp = widget->text(); + // ok to set field empty string, just not all of them + if(modified == false && entry->field(fIt) != temp) { + modified = true; + } + entry->setField(fIt, temp); + if(temp.isEmpty()) { + QString prop = fIt->property(QString::fromLatin1("required")).lower(); + if(prop == Latin1Literal("1") || prop == Latin1Literal("true")) { + fieldsRequiringValues.append(fIt.data()); + } + } + } + } + } + + if(!fieldsRequiringValues.isEmpty()) { + GUI::CursorSaver cs2(Qt::arrowCursor); + QString str = i18n("A value is required for the following fields. Do you want to continue?"); + QStringList titles; + for(Data::FieldVecIt it = fieldsRequiringValues.begin(); it != fieldsRequiringValues.end(); ++it) { + titles << it->title(); + } + QString dontAsk = QString::fromLatin1("SaveWithoutRequired"); + int ret = KMessageBox::questionYesNoList(this, str, titles, i18n("Modify Entries"), + KStdGuiItem::yes(), KStdGuiItem::no(), dontAsk); + if(ret != KMessageBox::Yes) { + m_isWorking = false; + return; + } + } + + // if something was not empty, signal a save + if(modified) { + m_isOrphan = false; + if(oldEntries.isEmpty()) { + Kernel::self()->addEntries(m_currEntries, false); + } else { + Kernel::self()->modifyEntries(oldEntries, m_currEntries); + } + if(!m_currEntries.isEmpty() && !m_currEntries[0]->title().isEmpty()) { + setCaption(i18n("Edit Entry") + QString::fromLatin1(" - ") + m_currEntries[0]->title()); + } + } + + m_modified = false; + m_isWorking = false; + enableButton(m_saveBtn, false); +// slotHandleNew(); +} + +void EntryEditDialog::clear() { + if(m_isWorking) { + return; + } +// myDebug() << "EntryEditDialog::clear()" << endl; + + m_isWorking = true; + // clear the widgets + for(QDictIterator<GUI::FieldWidget> it(m_widgetDict); it.current(); ++it) { + it.current()->setEnabled(true); + it.current()->clear(); + it.current()->insertDefault(); + } + + setCaption(i18n("Edit Entry")); + + if(m_isOrphan) { + if(m_currEntries.count() > 1) { + kdWarning() << "EntryEditDialog::clear() - is an orphan, but more than one" << endl; + } + m_isOrphan = false; + } + m_currEntries.clear(); + + setButtonText(m_saveBtn, i18n("Sa&ve Entry")); + enableButton(m_saveBtn, false); + + m_modified = false; + m_isWorking = false; +} + +void EntryEditDialog::setContents(Data::EntryVec entries_) { + // this slot might get called if we try to save multiple items, so just return + if(m_isWorking) { + return; + } + + if(entries_.isEmpty()) { +// myDebug() << "EntryEditDialog::setContents() - empty list" << endl; + if(queryModified()) { + blockSignals(true); + slotHandleNew(); + blockSignals(false); + } + return; + } + +// myDebug() << "EntryEditDialog::setContents() - " << entries_.count() << " entries" << endl; + + // if some entries get selected in one view, then in another, don't reset + if(!m_needReset && entries_ == m_currEntries) { + return; + } + m_needReset = false; + + // first set contents to first item + setContents(entries_.front()); + // something weird...if list count can actually be 1 before the setContents call + // and 0 after it. Why is that? It's const! + if(entries_.count() < 2) { + return; + } + + // multiple entries, so don't set caption + setCaption(i18n("Edit Entries")); + + m_currEntries = entries_; + m_isWorking = true; + blockSignals(true); + + Data::EntryVec::ConstIterator entry; + Data::FieldVec fields = m_currColl->fields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + QString key = QString::number(m_currColl->id()) + fIt->name(); + GUI::FieldWidget* widget = m_widgetDict.find(key); + if(!widget) { // probably read-only + continue; + } + widget->editMultiple(true); + + QString value = entries_[0]->field(fIt); + entry = entries_.constBegin(); + for(++entry; entry != entries_.constEnd(); ++entry) { // skip checking the first one + if(entry->field(fIt) != value) { + widget->setEnabled(false); + break; + } + } + } // end field loop + + blockSignals(false); + m_isWorking = false; + + // can't go to next entry if multiple are selected + enableButton(m_prevBtn, false); + enableButton(m_nextBtn, false); + setButtonText(m_saveBtn, i18n("Sa&ve Entries")); +} + +void EntryEditDialog::setContents(Data::EntryPtr entry_) { + bool ok = queryModified(); + if(!ok || m_isWorking) { + return; + } + + if(!entry_) { + myDebug() << "EntryEditDialog::setContents() - null entry pointer" << endl; + slotHandleNew(); + return; + } + +// myDebug() << "EntryEditDialog::setContents() - " << entry_->title() << endl; + blockSignals(true); + clear(); + blockSignals(false); + + m_isWorking = true; + m_currEntries.append(entry_); + + if(!entry_->title().isEmpty()) { + setCaption(i18n("Edit Entry") + QString::fromLatin1(" - ") + entry_->title()); + } + + if(m_currColl != entry_->collection()) { + myDebug() << "EntryEditDialog::setContents() - collections don't match" << endl; + m_currColl = entry_->collection(); + } + +// m_tabs->showTab(0); + + slotSetModified(false); + + Data::FieldVec fields = m_currColl->fields(); + for(Data::FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) { + QString key = QString::number(m_currColl->id()) + field->name(); + GUI::FieldWidget* widget = m_widgetDict.find(key); + if(!widget) { // is probably read-only + continue; + } + + widget->setText(entry_->field(field)); + widget->setEnabled(true); + widget->editMultiple(false); + } // end field loop + + enableButton(m_prevBtn, true); + enableButton(m_nextBtn, true); + if(entry_->isOwned()) { + setButtonText(m_saveBtn, i18n("Sa&ve Entry")); + enableButton(m_saveBtn, m_modified); + } else { + slotSetModified(true); + } + m_isWorking = false; +} + +void EntryEditDialog::removeField(Data::CollPtr, Data::FieldPtr field_) { + if(!field_) { + return; + } + +// myDebug() << "EntryEditDialog::removeField - name = " << field_->name() << endl; + QString key = QString::number(m_currColl->id()) + field_->name(); + GUI::FieldWidget* widget = m_widgetDict.find(key); + if(widget) { + m_widgetDict.remove(key); + // if this is the last field in the category, need to remove the tab page + // this function is called after the field has been removed from the collection, + // so the category should be gone from the category list + if(m_currColl->fieldCategories().findIndex(field_->category()) == -1) { +// myDebug() << "last field in the category" << endl; + // fragile, widget's parent is the grid, whose parent is the tab page + QWidget* w = widget->parentWidget()->parentWidget(); + m_tabs->removePage(w); + delete w; // automatically deletes child widget + } else { + // much of this replicates code in setLayout() + QGridLayout* layout = static_cast<QGridLayout*>(widget->parentWidget()->layout()); + delete widget; // automatically removes from layout + + QValueVector<bool> expands(NCOLS, false); + QValueVector<int> maxWidth(NCOLS, 0); + + Data::FieldVec vec = m_currColl->fieldsByCategory(field_->category()); + Data::FieldVec::Iterator it = vec.begin(); + for(int count = 0; it != vec.end(); ++it) { + GUI::FieldWidget* widget = m_widgetDict.find(QString::number(m_currColl->id()) + it->name()); + if(widget) { + layout->remove(widget); + layout->addWidget(widget, count/NCOLS, count%NCOLS); + + maxWidth[count%NCOLS] = QMAX(maxWidth[count%NCOLS], widget->labelWidth()); + if(widget->expands()) { + expands[count%NCOLS] = true; + } + widget->updateGeometry(); + ++count; + } + } + + // now, the labels in a column should all be the same width + it = vec.begin(); + for(int count = 0; it != vec.end(); ++it) { + GUI::FieldWidget* widget = m_widgetDict.find(QString::number(m_currColl->id()) + it->name()); + if(widget) { + widget->setLabelWidth(maxWidth[count%NCOLS]); + ++count; + } + } + + // update stretch factors for columns with a line edit + for(int col = 0; col < NCOLS; ++col) { + if(expands[col]) { + layout->setColStretch(col, 1); + } + } + } + } +} + +void EntryEditDialog::updateCompletions(Data::EntryPtr entry_) { +#ifndef NDEBUG + if(m_currColl != entry_->collection()) { + myDebug() << "EntryEditDialog::updateCompletions - inconsistent collection pointers!" << endl; +// m_currColl = entry_->collection(); + } +#endif + + Data::FieldVec fields = m_currColl->fields(); + for(Data::FieldVec::Iterator it = fields.begin(); it != fields.end(); ++it) { + if(it->type() != Data::Field::Line + || !(it->flags() & Data::Field::AllowCompletion)) { + continue; + } + + QString key = QString::number(m_currColl->id()) + it->name(); + GUI::FieldWidget* widget = m_widgetDict.find(key); + if(!widget) { + continue; + } + if(it->flags() & Data::Field::AllowMultiple) { + QStringList items = entry_->fields(it, false); + for(QStringList::ConstIterator it = items.begin(); it != items.end(); ++it) { + widget->addCompletionObjectItem(*it); + } + } else { + widget->addCompletionObjectItem(entry_->field(it->name())); + } + } +} + +void EntryEditDialog::slotSetModified(bool mod_/*=true*/) { + m_modified = mod_; + enableButton(m_saveBtn, mod_); +} + +bool EntryEditDialog::queryModified() { +// myDebug() << "EntryEditDialog::queryModified() - modified is " << (m_modified?"true":"false") << endl; + bool ok = true; + // assume that if the dialog is hidden, we shouldn't ask the user to modify changes + if(!isShown()) { + m_modified = false; + } + if(m_modified) { + QString str(i18n("The current entry has been modified.\n" + "Do you want to enter the changes?")); + KGuiItem item = KStdGuiItem::save(); + item.setText(i18n("Save Entry")); + int want_save = KMessageBox::warningYesNoCancel(this, str, i18n("Unsaved Changes"), + item, KStdGuiItem::discard()); + switch(want_save) { + case KMessageBox::Yes: + slotHandleSave(); + ok = true; + break; + + case KMessageBox::No: + m_modified = false; + ok = true; + break; + + case KMessageBox::Cancel: + ok = false; + break; + } + } + return ok; +} + +// modified fields will always have the same name +void EntryEditDialog::modifyField(Data::CollPtr coll_, Data::FieldPtr oldField_, Data::FieldPtr newField_) { +// myDebug() << "EntryEditDialog::slotUpdateField() - " << newField_->name() << endl; + + if(coll_ != m_currColl) { + myDebug() << "EntryEditDialog::slotUpdateField() - wrong collection pointer!" << endl; + m_currColl = coll_; + } + + // if the field type changed, go ahead and redo the whole layout + // also if the category changed for a non-single field, since a new tab must be created + if(oldField_->type() != newField_->type() + || (oldField_->category() != newField_->category() && !newField_->isSingleCategory())) { + bool modified = m_modified; + setLayout(coll_); + setContents(m_currEntries); + m_modified = modified; + return; + } + + QString key = QString::number(coll_->id()) + oldField_->name(); + GUI::FieldWidget* widget = m_widgetDict[key]; + if(widget) { + widget->updateField(oldField_, newField_); + // need to update label widths + if(newField_->title() != oldField_->title()) { + int maxWidth = 0; + QObjectList* childList = widget->parentWidget()->queryList("Tellico::GUI::FieldWidget", 0, false, false); + QObjectListIt it(*childList); + for(it.toFirst(); it.current(); ++it) { + maxWidth = QMAX(maxWidth, static_cast<GUI::FieldWidget*>(it.current())->labelWidth()); + } + for(it.toFirst(); it.current(); ++it) { + static_cast<GUI::FieldWidget*>(it.current())->setLabelWidth(maxWidth); + } + delete childList; + } + // this is very fragile! + // field widgets's parent is the grid, whose parent is the tab page + // this is for singleCategory fields + if(newField_->category() != oldField_->category()) { + m_tabs->setTabLabel(widget->parentWidget()->parentWidget(), newField_->category()); + } + } +} + +void EntryEditDialog::addEntries(Data::EntryVec entries_) { + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + updateCompletions(entry); + } +} + +void EntryEditDialog::modifyEntries(Data::EntryVec entries_) { + bool updateContents = false; + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + updateCompletions(entry); + if(!updateContents && m_currEntries.contains(entry)) { + updateContents = true; + } + } + if(updateContents) { + m_needReset = true; + setContents(m_currEntries); + } +} + +void EntryEditDialog::slotGoPrevEntry() { + queryModified(); + Controller::self()->slotGoPrevEntry(); +} + +void EntryEditDialog::slotGoNextEntry() { + queryModified(); + Controller::self()->slotGoNextEntry(); +} + +#include "entryeditdialog.moc" diff --git a/src/entryeditdialog.h b/src/entryeditdialog.h new file mode 100644 index 0000000..2d71b63 --- /dev/null +++ b/src/entryeditdialog.h @@ -0,0 +1,149 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef ENTRYEDITDIALOG_H +#define ENTRYEDITDIALOG_H + +class KPushButton; + +#include "observer.h" +#include "gui/fieldwidget.h" + +#include <kdialogbase.h> + +#include <qdict.h> + +namespace Tellico { + namespace GUI { + class TabControl; + } + +/** + * @author Robby Stephenson + */ +class EntryEditDialog : public KDialogBase, public Observer { +Q_OBJECT + +// needed for completion object support +friend class GUI::FieldWidget; + +public: + EntryEditDialog(QWidget* parent, const char* name); + + /** + * Checks to see if any data needs to be saved. Returns @p true if it's ok to continue with + * saving or closing the widget. + * + * @return Returns @p true if either the data has not been modified or the user to save or discard the new data. + */ + bool queryModified(); + /** + * Deletes and resets the layout of the tabs. + * + * @param coll A pointer to the collection whose fields should be used for setting up the layout + */ + void setLayout(Data::CollPtr coll); + /** + * Sets the contents of the input controls to match the contents of a list of entries. + * + * @param list A list of the entries. The data in the first one will be inserted in the controls, and + * the widgets will be enabled or not, depending on whether the rest of the entries match the first one. + */ + void setContents(Data::EntryVec entries); + /** + * Clears all of the input controls in the widget. The pointer to the + * current entry is nullified, but not the pointer to the current collection. + */ + void clear(); + + virtual void addEntries(Data::EntryVec entries); + virtual void modifyEntries(Data::EntryVec entries); + + virtual void addField(Data::CollPtr coll, Data::FieldPtr) { setLayout(coll); } + /** + * Updates a widget when its field has been modified. The category may have changed, completions may have + * been added or removed, or what-have-you. + * + * @param coll A pointer to the parent collection + * @param oldField A pointer to the old field, which should have the same name as the new one + * @param newField A pointer to the new field + */ + virtual void modifyField(Data::CollPtr coll, Data::FieldPtr oldField, Data::FieldPtr newField); + /** + * Removes a field from the editor. + * + * @param field The field to be removed + */ + virtual void removeField(Data::CollPtr, Data::FieldPtr field); + +public slots: + /** + * Called when the Close button is clicked. It just hides the dialog. + */ + virtual void slotClose(); + /** + * Resets the widget, deleting all of its contents + */ + void slotReset(); + /** + * Handles clicking the New button. The old entry pointer is destroyed and a + * new one is created, but not added to any collection. + */ + void slotHandleNew(); + /** + * Handles clicking the Save button. All the values in the entry widgets are + * copied into the entry object. @ref signalSaveEntry is made. The widget is cleared, + * and the first tab is shown. + */ + void slotHandleSave(); + /** + * This slot is called whenever anything is modified. It's public so I can call it + * from a @ref FieldEditWidget. + */ + void slotSetModified(bool modified=true); + +private slots: + void slotGoPrevEntry(); + void slotGoNextEntry(); + +private: + /** + * Sets the contents of the input controls to match the contents of a entry. + * + * @param entry A pointer to the entry + * @param highlight An optional string to highlight + */ + void setContents(Data::EntryPtr entry); + /** + * Updates the completion objects in the edit boxes to include values + * contained in a certain entry. + * + * @param entry A pointer to the entry + */ + void updateCompletions(Data::EntryPtr entry); + + Data::CollPtr m_currColl; + Data::EntryVec m_currEntries; + GUI::TabControl* m_tabs; + QDict<GUI::FieldWidget> m_widgetDict; + + ButtonCode m_saveBtn, m_newBtn, m_nextBtn, m_prevBtn; + + bool m_modified; + bool m_isOrphan; + bool m_isWorking; + bool m_needReset; +}; + +} // end namespace +#endif diff --git a/src/entrygroupitem.cpp b/src/entrygroupitem.cpp new file mode 100644 index 0000000..f2de60d --- /dev/null +++ b/src/entrygroupitem.cpp @@ -0,0 +1,140 @@ +/*************************************************************************** + copyright : (C) 2005-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 "entrygroupitem.h" +#include "entry.h" +#include "collection.h" +#include "gui/ratingwidget.h" +#include "tellico_debug.h" +#include "latin1literal.h" +#include "core/tellico_config.h" + +#include <kiconloader.h> +#include <klocale.h> + +#include <qpixmap.h> +#include <qpainter.h> + +using Tellico::EntryGroupItem; + +EntryGroupItem::EntryGroupItem(GUI::ListView* parent_, Data::EntryGroup* group_, int fieldType_) + : GUI::CountedItem(parent_), m_group(group_), m_fieldType(fieldType_) { + setText(0, group_->groupName()); + m_emptyGroup = group_->groupName() == i18n(Data::Collection::s_emptyGroupTitle); +} + +QPixmap EntryGroupItem::ratingPixmap() { + if(!m_group) { + return QPixmap(); + } + QPixmap stars = GUI::RatingWidget::pixmap(m_group->groupName()); + if(m_pix.isNull() && stars.isNull()) { + m_emptyGroup = true; + return QPixmap(); + } + + QPixmap newPix(m_pix.width() + 4 + stars.width(), QMAX(m_pix.height(), stars.height())); + newPix.fill(isSelected() ? listView()->colorGroup().highlight() : backgroundColor(0)); + QPainter p(&newPix); + if(!m_pix.isNull()) { + p.drawPixmap(0, 0, m_pix); + } + if(!stars.isNull()) { + p.drawPixmap(m_pix.width() + 4, 0, stars); + } + p.end(); + return newPix; +} + +void EntryGroupItem::setPixmap(int col, const QPixmap& pix) { + m_pix = pix; + GUI::CountedItem::setPixmap(col, m_pix); +} + +void EntryGroupItem::paintCell(QPainter* p_, const QColorGroup& cg_, + int column_, int width_, int align_) { + if(column_> 0 || m_fieldType != Data::Field::Rating || m_emptyGroup) { + return GUI::CountedItem::paintCell(p_, cg_, column_, width_, align_); + } + + QString oldText = text(column_); + // "\t\t" is the flag to not paint the item + // CountedItem already uses "\t" + if(oldText == Latin1Literal("\t\t")) { + return; + } + + setText(column_, QString::fromLatin1("\t\t")); + GUI::CountedItem::setPixmap(column_, ratingPixmap()); + GUI::CountedItem::paintCell(p_, cg_, column_, width_, align_); +// GUI::CountedItem::setPixmap(column_, m_pix); + setText(column_, oldText); +} + +// prepend a tab character to always sort the empty group name first +// also check for surname prefixes +QString EntryGroupItem::key(int col_, bool) const { + if(col_ > 0) { + return text(col_); + } + + if((m_fieldType == Data::Field::Rating || text(col_).isEmpty()) + && pixmap(col_) && !pixmap(col_)->isNull()) { + // a little weird, sort for width, too, in case of rating widget + // but sort reverse by width + return QChar('\t') + QString::number(1000-pixmap(col_)->width()); + } else if(text(col_) == i18n(Data::Collection::s_emptyGroupTitle)) { + return QChar('\t'); + } + + if(m_text.isEmpty() || m_text != text(col_)) { + m_text = text(col_); + if(Config::autoFormat()) { + const QStringList prefixes = Config::surnamePrefixList(); + // build a regexp to match surname prefixes + // complicated by fact that prefix could have an apostrophe + QString tokens; + for(QStringList::ConstIterator it = prefixes.begin(); + it != prefixes.end(); + ++it) { + tokens += (*it); + if(!(*it).endsWith(QChar('\''))) { + tokens += QString::fromLatin1("\\s"); + } + // if it's not the last item, add a pipe + if((*it) != prefixes.last()) { + tokens += QChar('|'); + } + } + QRegExp rx(QString::fromLatin1("^(") + tokens + QChar(')'), false); + // expensive + if(rx.search(m_text) > -1) { + m_key = m_text.mid(rx.matchedLength()); + } else { + m_key = m_text; + } + } else { + m_key = m_text; + } + } + + return m_key; +} + +int EntryGroupItem::count() const { + return m_group ? m_group->count() : GUI::CountedItem::count(); +} + +Tellico::Data::EntryVec EntryGroupItem::entries() const { + return m_group ? *m_group : Data::EntryVec(); +} diff --git a/src/entrygroupitem.h b/src/entrygroupitem.h new file mode 100644 index 0000000..827ce8d --- /dev/null +++ b/src/entrygroupitem.h @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef ENTRYGROUPITEM_H +#define ENTRYGROUPITEM_H + +#include "gui/counteditem.h" + +#include <qpixmap.h> +#include <qguardedptr.h> + +namespace Tellico { + namespace Data { + class EntryGroup; + } + +/** + * @author Robby Stephenson + */ +class EntryGroupItem : public GUI::CountedItem { +public: + EntryGroupItem(GUI::ListView* parent, Data::EntryGroup* group, int fieldType); + + virtual bool isEntryGroupItem() const { return true; } + Data::EntryGroup* group() const { return m_group; } + void setGroup(Data::EntryGroup* group) { m_group = group; } + + QPixmap ratingPixmap(); + + virtual void setPixmap(int col, const QPixmap& pix); + virtual void paintCell(QPainter* p, const QColorGroup& cg, + int column, int width, int align); + /** + * Returns the key for sorting the listitems. The text used for an empty + * value should be sorted first, so the returned key is "\t". Since the text may + * have the number of entries or something added to the name, only check if the + * text begins with the empty name. Maybe there should be something better. + * + * @param col The column number + * @return The key + */ + virtual QString key(int col, bool) const; + + virtual int count() const; + virtual Data::EntryVec entries() const; + +private: + QGuardedPtr<Data::EntryGroup> m_group; + int m_fieldType; + QPixmap m_pix; + bool m_emptyGroup : 1; + +// since I do an expensive RegExp match for the surname prefixes, I want to +// cache the text and the resulting key + mutable QString m_text; + mutable QString m_key; +}; + +} + +#endif diff --git a/src/entryiconfactory.cpp b/src/entryiconfactory.cpp new file mode 100644 index 0000000..0f4696d --- /dev/null +++ b/src/entryiconfactory.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 2005-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 "entryiconfactory.h" +#include "tellico_kernel.h" + +#include <kiconloader.h> +#include <kimageeffect.h> + +#include <qimage.h> + +using Tellico::EntryIconFactory; + +EntryIconFactory::EntryIconFactory(int size_) : QIconFactory(), m_size(size_) { + setAutoDelete(true); +} + +QPixmap* EntryIconFactory::createPixmap(const QIconSet&, QIconSet::Size, QIconSet::Mode, QIconSet::State) { + QPixmap entryPix = UserIcon(Kernel::self()->collectionTypeName()); + // if we're 22x22 or smaller, just use entry icon + if(m_size < 23) { + QImage entryImg = entryPix.convertToImage(); + entryPix.convertFromImage(entryImg.smoothScale(m_size, m_size, QImage::ScaleMin), 0); + return new QPixmap(entryPix); + } + + QPixmap newPix = BarIcon(QString::fromLatin1("mime_empty"), m_size); + QImage newImg = newPix.convertToImage(); +// QImage blend; Not exactly sure what the coordinates mean, but this seems to work ok. + KImageEffect::blendOnLower(m_size/4, m_size/4, entryPix.convertToImage(), newImg); + newPix.convertFromImage(newImg, 0); + return new QPixmap(newPix); +} diff --git a/src/entryiconfactory.h b/src/entryiconfactory.h new file mode 100644 index 0000000..d5461a3 --- /dev/null +++ b/src/entryiconfactory.h @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTRYICONFACTORY_H +#define TELLICO_ENTRYICONFACTORY_H + +#include <qiconset.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class EntryIconFactory : public QIconFactory { +public: + EntryIconFactory(int size); + + virtual QPixmap* createPixmap(const QIconSet&, QIconSet::Size, QIconSet::Mode, QIconSet::State); + +private: + int m_size; +}; + +} + +#endif diff --git a/src/entryiconview.cpp b/src/entryiconview.cpp new file mode 100644 index 0000000..2acd870 --- /dev/null +++ b/src/entryiconview.cpp @@ -0,0 +1,444 @@ +/*************************************************************************** + copyright : (C) 2002-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 "entryiconview.h" +#include "collection.h" +#include "collectionfactory.h" +#include "imagefactory.h" +#include "controller.h" +#include "entry.h" +#include "field.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "listviewcomparison.h" + +#include <kpopupmenu.h> +#include <kstringhandler.h> +#include <kiconloader.h> +#include <kwordwrap.h> +#include <kimageeffect.h> +#include <klocale.h> + +#include <qpainter.h> + +namespace { + static const int MIN_ENTRY_ICON_SIZE = 32; + static const int MAX_ENTRY_ICON_SIZE = 128; + static const int ENTRY_ICON_SIZE_PAD = 6; + static const int ENTRY_ICON_SHADOW_RIGHT = 1; + static const int ENTRY_ICON_SHADOW_BOTTOM = 1; +} + +using Tellico::EntryIconView; +using Tellico::EntryIconViewItem; + +EntryIconView::EntryIconView(QWidget* parent_, const char* name_/*=0*/) + : KIconView(parent_, name_), m_coll(0), m_maxAllowedIconWidth(MAX_ENTRY_ICON_SIZE), + m_maxIconWidth(MIN_ENTRY_ICON_SIZE), m_maxIconHeight(MIN_ENTRY_ICON_SIZE), + m_comparison(0) { + setAutoArrange(true); + setSorting(true); + setItemsMovable(false); + setSelectionMode(QIconView::Extended); + setResizeMode(QIconView::Adjust); + setMode(KIconView::Select); + setSpacing(4); +// setWordWrapIconText(false); + + m_defaultPixmaps.setAutoDelete(true); + + connect(this, SIGNAL(selectionChanged()), SLOT(slotSelectionChanged())); + connect(this, SIGNAL(doubleClicked(QIconViewItem*)), SLOT(slotDoubleClicked(QIconViewItem*))); + connect(this, SIGNAL(contextMenuRequested(QIconViewItem*, const QPoint&)), + SLOT(slotShowContextMenu(QIconViewItem*, const QPoint&))); +} + +EntryIconView::~EntryIconView() { + delete m_comparison; + m_comparison = 0; +} + +EntryIconViewItem* EntryIconView::firstItem() const { + return static_cast<EntryIconViewItem*>(KIconView::firstItem()); +} + +void EntryIconView::findImageField() { + m_imageField.truncate(0); + if(!m_coll) { + return; + } + const Data::FieldVec& fields = m_coll->imageFields(); + if(!fields.isEmpty()) { + m_imageField = fields[0]->name(); + } +// myDebug() << "EntryIconView::findImageField() - image field = " << m_imageField << endl; +} + +const QString& EntryIconView::imageField() { + return m_imageField; +} + +const QString& EntryIconView::sortField() { + return m_comparison ? m_comparison->fieldName() : QString::null; +} + +const QPixmap& EntryIconView::defaultPixmap() { + QPixmap* pix = m_defaultPixmaps[m_coll->type()]; + if(pix) { + return *pix; + } + KIconLoader* loader = KGlobal::instance()->iconLoader(); + QPixmap tmp = loader->loadIcon(QString::fromLatin1("nocover_") + CollectionFactory::typeName(m_coll->type()), + KIcon::User, 0, KIcon::DefaultState, 0, true /*canReturnNull */); + if(tmp.isNull()) { + myLog() << "EntryIconView::defaultPixmap() - null nocover image, loading tellico.png" << endl; + tmp = UserIcon(QString::fromLatin1("tellico")); + } + if(QMAX(tmp.width(), tmp.height()) > static_cast<int>(MIN_ENTRY_ICON_SIZE)) { + tmp.convertFromImage(tmp.convertToImage().smoothScale(m_maxIconWidth, m_maxIconHeight, QImage::ScaleMin)); + } + pix = new QPixmap(tmp); + m_defaultPixmaps.insert(m_coll->type(), pix); + return *pix; +} + +void EntryIconView::setMaxAllowedIconWidth(int width_) { + m_maxAllowedIconWidth = QMAX(MIN_ENTRY_ICON_SIZE, QMIN(MAX_ENTRY_ICON_SIZE, width_)); + setMaxItemWidth(m_maxAllowedIconWidth + 2*ENTRY_ICON_SIZE_PAD); + m_defaultPixmaps.clear(); + refresh(); +} + +void EntryIconView::fillView() { + setSorting(false); + setGridX(m_maxAllowedIconWidth + 2*ENTRY_ICON_SIZE_PAD); // set default spacing initially + + GUI::CursorSaver cs(Qt::waitCursor); + + bool allDefaultImages = true; + m_maxIconWidth = QMAX(MIN_ENTRY_ICON_SIZE, m_maxIconWidth); + m_maxIconHeight = QMAX(MIN_ENTRY_ICON_SIZE, m_maxIconHeight); + EntryIconViewItem* item; + for(Data::EntryVecIt it = m_entries.begin(); it != m_entries.end(); ++it) { + item = new EntryIconViewItem(this, it); + m_maxIconWidth = QMAX(m_maxIconWidth, item->width()); + m_maxIconHeight = QMAX(m_maxIconHeight, item->pixmapRect().height()); + if(item->usesImage()) { + allDefaultImages = false; + } + } + if(allDefaultImages) { + m_maxIconWidth = m_maxAllowedIconWidth; + m_maxIconHeight = m_maxAllowedIconWidth; + } + // if both width and height are min, then that means there are no images + m_defaultPixmaps.clear(); + // now reset size of all default pixmaps + for(item = firstItem(); item; item = item->nextItem()) { + if(!item->usesImage()) { + item->updatePixmap(); + } + } + setGridX(m_maxIconWidth + 2*ENTRY_ICON_SIZE_PAD); // the pad is so the text can be wider than the icon + setGridY(m_maxIconHeight + fontMetrics().height()); + sort(); + setSorting(true); +} + +void EntryIconView::refresh() { + if(!m_coll) { + return; + } + showEntries(m_entries); +} + +void EntryIconView::clear() { + KIconView::clear(); + m_coll = 0; + m_entries.clear(); + m_imageField.truncate(0); +} + +void EntryIconView::showEntries(const Data::EntryVec& entries_) { + setUpdatesEnabled(false); + KIconView::clear(); // don't call EntryIconView::clear() since that clears the entries_ ref + if(entries_.isEmpty()) { + return; + } + m_coll = entries_[0]->collection(); + m_entries = entries_; + findImageField(); + fillView(); + setUpdatesEnabled(true); +} + +void EntryIconView::addEntries(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + if(!m_coll) { + m_coll = entries_[0]->collection(); + } + // since the view probably doesn't show all the current entries + // only add the new ones if the count is the total + if(m_entries.count() + entries_.count() < m_coll->entryCount()) { + return; + } + int w = MIN_ENTRY_ICON_SIZE; + int h = MIN_ENTRY_ICON_SIZE; + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + m_entries.append(entry); + EntryIconViewItem* item = new EntryIconViewItem(this, entry); + w = QMAX(w, item->width()); + h = QMAX(h, item->pixmapRect().height()); + } + if(w > m_maxIconWidth || h > m_maxIconHeight) { + refresh(); + } +} + +void EntryIconView::modifyEntries(Data::EntryVec entries_) { + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + EntryIconViewItem* item = 0; + for(EntryIconViewItem* it = firstItem(); it; it = it->nextItem()) { + if(it->entry() == entry) { + item = it; + break; + } + } + if(!item) { + continue; + } + item->update(); + } +} + +void EntryIconView::removeEntries(Data::EntryVec entries_) { + for(Data::EntryVecIt entry = entries_.begin(); entry != entries_.end(); ++entry) { + m_entries.remove(entry); + } + bool found = false; + EntryIconViewItem* item = firstItem(); + while(item) { + if(entries_.contains(item->entry())) { + EntryIconViewItem* prev = item; + item = item->nextItem(); + delete prev; + found = true; + } else { + item = item->nextItem(); + } + } + if(found) { + arrangeItemsInGrid(); + } +} + +void EntryIconView::slotSelectionChanged() { + Data::EntryVec entries; + const QPtrList<EntryIconViewItem>& items = selectedItems(); + for(QPtrListIterator<EntryIconViewItem> it(items); it.current(); ++it) { + entries.append(it.current()->entry()); + } + Controller::self()->slotUpdateSelection(this, entries); +} + +void EntryIconView::slotDoubleClicked(QIconViewItem* item_) { + EntryIconViewItem* i = static_cast<EntryIconViewItem*>(item_); + if(i) { + Controller::self()->editEntry(i->entry()); + } +} + +void EntryIconView::updateSelected(EntryIconViewItem* item_, bool s_) const { + if(s_) { + m_selectedItems.append(item_); + } else { + m_selectedItems.removeRef(item_); + } +} + +void EntryIconView::slotShowContextMenu(QIconViewItem* item_, const QPoint& point_) { + KPopupMenu menu(this); + + // only insert entry items if one is selected + if(item_) { + Controller::self()->plugEntryActions(&menu); + menu.insertSeparator(); + } + + KPopupMenu sortMenu(&menu); + const QStringList titles = m_coll->fieldTitles(); + for(QStringList::ConstIterator it = titles.begin(); it != titles.end(); ++it) { + sortMenu.insertItem(*it); + } + connect(&sortMenu, SIGNAL(activated(int)), SLOT(slotSortMenuActivated(int))); + + menu.insertItem(i18n("&Sort By"), &sortMenu); + menu.exec(point_); +} + +void EntryIconView::slotSortMenuActivated(int id_) { + const KPopupMenu* menu = ::qt_cast<KPopupMenu*>(sender()); + if(menu) { + QString title = menu->text(id_); + Data::FieldPtr f = m_coll->fieldByTitle(title); + if(f) { + delete m_comparison; + m_comparison = ListViewComparison::create(f); + sort(); + } + } +} + +int EntryIconView::compare(const EntryIconViewItem* item1, EntryIconViewItem* item2) { + if(m_comparison) { + return m_comparison->compare(item1, item2); + } + return 0; +} + +/* *********************************************************** */ + +EntryIconViewItem::EntryIconViewItem(EntryIconView* parent_, Data::EntryPtr entry_) + : KIconViewItem(parent_, entry_->title()), m_entry(entry_), m_usesImage(false) { + setDragEnabled(false); + const QString& imageField = parent_->imageField(); + if(!imageField.isEmpty()) { + QPixmap p = ImageFactory::pixmap(m_entry->field(imageField), + parent_->maxAllowedIconWidth(), + parent_->maxAllowedIconWidth()); + if(!p.isNull()) { + setPixmap(p); + m_usesImage = true; + } + } +} + +EntryIconViewItem::~EntryIconViewItem() { + // be sure to remove from selected list when it's deleted + EntryIconView* view = iconView(); + if(view) { + view->updateSelected(this, false); + } +} + +void EntryIconViewItem::setSelected(bool s_) { + setSelected(s_, false); +} + +void EntryIconViewItem::setSelected(bool s_, bool cb_) { + EntryIconView* view = iconView(); + if(!view) { + return; + } + if(s_ != isSelected()) { + view->updateSelected(this, s_); + KIconViewItem::setSelected(s_, cb_); + } +} + +void EntryIconViewItem::updatePixmap() { + EntryIconView* view = iconView(); + const QString& imageField = view->imageField(); + m_usesImage = false; + if(imageField.isEmpty()) { + setPixmap(view->defaultPixmap()); + } else { + QPixmap p = ImageFactory::pixmap(m_entry->field(imageField), + view->maxAllowedIconWidth(), + view->maxAllowedIconWidth()); + if(p.isNull()) { + setPixmap(view->defaultPixmap()); + } else { + setPixmap(p); + m_usesImage = true; + calcRect(); + } + } +} + +void EntryIconViewItem::update() { + setText(m_entry->title()); + updatePixmap(); + iconView()->arrangeItemsInGrid(); +} + +void EntryIconViewItem::calcRect(const QString& text_) { + KIconViewItem::calcRect(text_); + QRect r = pixmapRect(); + r.rRight() += ENTRY_ICON_SHADOW_RIGHT; + r.rBottom() += ENTRY_ICON_SHADOW_BOTTOM; + setPixmapRect(r); +} + +void EntryIconViewItem::paintItem(QPainter* p_, const QColorGroup &cg_) { + p_->save(); + paintPixmap(p_, cg_); + paintText(p_, cg_); + p_->restore(); +} + +void EntryIconViewItem::paintFocus(QPainter*, const QColorGroup&) { + // don't draw anything +} + +void EntryIconViewItem::paintPixmap(QPainter* p_, const QColorGroup& cg_) { + // only draw the shadow if there's an image + // oh, and don't draw it if it's a file catalog, it doesn't look right + if(m_usesImage && !isSelected() && m_entry->collection()->type() != Data::Collection::File) { + // pixmapRect() already includes shadow size, so shift the rect by that amount + QRect r = pixmapRect(false); + r.setLeft(r.left() + ENTRY_ICON_SHADOW_RIGHT); + r.setTop(r.top() + ENTRY_ICON_SHADOW_BOTTOM); + QColor c = Tellico::blendColors(iconView()->backgroundColor(), Qt::black, 10); + p_->fillRect(r, c); + } + KIconViewItem::paintPixmap(p_, cg_); +} + +void EntryIconViewItem::paintText(QPainter* p_, const QColorGroup &cg_) { + QRect tr = textRect(false); + int textX = tr.x() + 2; + int textY = tr.y(); + + if(isSelected()) { + p_->setBrush(QBrush(cg_.highlight())); + p_->setPen(QPen(cg_.highlight())); + p_->drawRoundRect(tr, 1000/tr.width(), 1000/tr.height()); + p_->setPen(QPen(cg_.highlightedText())); + } else { + if(iconView()->itemTextBackground() != NoBrush) { + p_->fillRect(tr, iconView()->itemTextBackground()); + } + p_->setPen(cg_.text()); + } + + int align = iconView()->itemTextPos() == QIconView::Bottom ? AlignHCenter : AlignAuto; + wordWrap()->drawText(p_, textX, textY, align | KWordWrap::Truncate); +} + +QString EntryIconViewItem::key() const { + const QString& sortField = iconView()->sortField(); + if(sortField.isEmpty()) { + return KIconViewItem::key(); + } + return m_entry->field(sortField); +} + +int EntryIconViewItem::compare(QIconViewItem* item_) const { + int res = iconView()->compare(this, static_cast<EntryIconViewItem*>(item_)); + return res == 0 ? KIconViewItem::compare(item_) : res; +} + +#include "entryiconview.moc" diff --git a/src/entryiconview.h b/src/entryiconview.h new file mode 100644 index 0000000..335818c --- /dev/null +++ b/src/entryiconview.h @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2002-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOENTRYICONVIEW_H +#define TELLICOENTRYICONVIEW_H + +#include "observer.h" +#include "entry.h" + +#include <kiconview.h> + +#include <qintdict.h> + +namespace Tellico { + class EntryIconViewItem; + namespace Data { + class Collection; + } + class ListViewComparison; + +/** + * @author Robby Stephenson + */ +class EntryIconView : public KIconView, public Observer { +Q_OBJECT + +friend class EntryIconViewItem; + +public: + EntryIconView(QWidget* parent, const char* name = 0); + ~EntryIconView(); + + EntryIconViewItem* firstItem() const; + + virtual void clear(); + void refresh(); + void showEntries(const Data::EntryVec& entries); + /** + * Adds a new list item showing the details for a entry. + * + * @param entry A pointer to the entry + */ + virtual void addEntries(Data::EntryVec entries); + virtual void modifyEntries(Data::EntryVec entries); + virtual void removeEntries(Data::EntryVec entries); + + const QString& imageField(); + const QString& sortField(); + void setMaxAllowedIconWidth(int width); + int maxAllowedIconWidth() const { return m_maxAllowedIconWidth; } + + const QPixmap& defaultPixmap(); + /** + * Returns a list of the currently selected items; + * + * @return The list of selected items + */ + const QPtrList<EntryIconViewItem>& selectedItems() const { return m_selectedItems; } + + int compare(const EntryIconViewItem* item1, EntryIconViewItem* item2); + +private slots: + void slotSelectionChanged(); + void slotDoubleClicked(QIconViewItem* item); + void slotShowContextMenu(QIconViewItem* item, const QPoint& point); + void slotSortMenuActivated(int id); + +private: + /** + * Updates the pointer list. + * + * @param item The item being selected or deselected + * @param s Selected or not + */ + void updateSelected(EntryIconViewItem* item, bool s) const; + mutable QPtrList<EntryIconViewItem> m_selectedItems; + + void findImageField(); + void fillView(); + + Data::CollPtr m_coll; + Data::EntryVec m_entries; + QString m_imageField; + QIntDict<QPixmap> m_defaultPixmaps; + int m_maxAllowedIconWidth; + int m_maxIconWidth; + int m_maxIconHeight; + ListViewComparison* m_comparison; +}; + +class EntryIconViewItem : public KIconViewItem { +public: + EntryIconViewItem(EntryIconView* parent, Data::EntryPtr entry); + ~EntryIconViewItem(); + + EntryIconView* iconView() const { return static_cast<EntryIconView*>(KIconViewItem::iconView()); } + EntryIconViewItem* nextItem() const { return static_cast<EntryIconViewItem*>(KIconViewItem::nextItem()); } + + Data::EntryPtr entry() const { return m_entry; } + virtual void setSelected(bool s, bool cb); + virtual void setSelected(bool s); + virtual QString key() const; + virtual int compare(QIconViewItem* item_) const; + + bool usesImage() const { return m_usesImage; } + void updatePixmap(); + + void update(); + +protected: + virtual void calcRect(const QString& text = QString::null); + virtual void paintItem(QPainter* p, const QColorGroup& cg); + virtual void paintFocus(QPainter* p, const QColorGroup& cg); + void paintPixmap(QPainter* p, const QColorGroup& cg); + void paintText(QPainter* p, const QColorGroup& cg); + +private: + Data::EntryPtr m_entry; + bool m_usesImage : 1; +}; + +} // end namespace +#endif diff --git a/src/entryitem.cpp b/src/entryitem.cpp new file mode 100644 index 0000000..0079d44 --- /dev/null +++ b/src/entryitem.cpp @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2003-2007 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 "entryitem.h" +#include "entry.h" +#include "gui/counteditem.h" +#include "collection.h" +#include "controller.h" + +#include <kiconloader.h> + +using Tellico::EntryItem; + +EntryItem::EntryItem(GUI::ListView* parent, Data::EntryPtr entry) + : GUI::ListViewItem(parent), m_entry(entry), m_isDetailedList(true) { +} + +EntryItem::EntryItem(GUI::CountedItem* parent_, Data::EntryPtr entry_) + : GUI::ListViewItem(parent_), m_entry(entry_), m_isDetailedList(false) { + setText(0, m_entry->title()); + setPixmap(0, UserIcon(entry_->collection()->typeName())); +} + +Tellico::Data::EntryPtr const EntryItem::entry() const { + return m_entry; +} + +QString EntryItem::key(int col_, bool) const { + // first column is always title unless it's a detailed list + // detailed list takes care of things on its own + bool checkArticles = (!m_isDetailedList && col_ == 0); + // there's some sort of painting bug if the key is identical for multiple entries + // probably a null string in the group view. TODO + // don't add the entry id if it's a detailed view cause that messes up secondary sorting + QString key = (checkArticles ? Data::Field::sortKeyTitle(text(col_)) : text(col_)); + return (m_isDetailedList ? key : key + QString::number(m_entry->id())); +} + +void EntryItem::doubleClicked() { + Controller::self()->editEntry(m_entry); +} + +Tellico::Data::EntryVec EntryItem::entries() const { + return Data::EntryVec(entry()); +} diff --git a/src/entryitem.h b/src/entryitem.h new file mode 100644 index 0000000..4e529c8 --- /dev/null +++ b/src/entryitem.h @@ -0,0 +1,83 @@ +/*************************************************************************** + copyright : (C) 2001-2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTRYITEM_H +#define TELLICO_ENTRYITEM_H + +#include "gui/listview.h" +#include "datavectors.h" + +namespace Tellico { + namespace GUI { + class CountedItem; + } + +/** + * The EntryItem is a subclass of ListViewItem containing a pointer to an Entry. + * + * The entry pointer allows easy access to listview items which refer to a certain entry. + * + * @see Entry + * + * @author Robby Stephenson + */ +class EntryItem : public GUI::ListViewItem { +public: + /** + * This constructor is for items which are direct children of a ListView object, + * which is just the @ref DetailedListView. + * + * @param parent A pointer to the parent + * @param entry A pointer to the entry to which the item refers + */ + EntryItem(GUI::ListView* parent, Data::EntryPtr entry); + /** + * This constructor is for items which have other KListViewItems as parents. It + * initializes the text in the first column, as well. + * + * @param parent A pointer to the parent + * @param text The text in the first column + * @param entry A pointer to the entry to which the item refers + */ + EntryItem(GUI::CountedItem* parent, Data::EntryPtr entry); + + virtual bool isEntryItem() const { return true; } + + /** + * Returns the key for the list item. The key is just the text, unless there is none, + * in which case a tab character is returned if there is a non-null pixmap. + * + * @param col Column to compare + * @return The key string + */ + virtual QString key(int col, bool) const; + /** + * Returns a const pointer to the entry to which the item refers + * + * @return The entry pointer + */ + Data::EntryPtr const entry() const; + + virtual void doubleClicked(); + virtual Data::EntryVec entries() const; + +private: + Data::EntryPtr m_entry; + // if the parent is a DetailedListView + // this way, I don't have to call listView()->isA("Tellico::DetailedListView") every time + // when I want to do funky comparisons + bool m_isDetailedList : 1; +}; + +} // end namespace +#endif diff --git a/src/entrymerger.cpp b/src/entrymerger.cpp new file mode 100644 index 0000000..4767026 --- /dev/null +++ b/src/entrymerger.cpp @@ -0,0 +1,107 @@ +/*************************************************************************** + copyright : (C) 2007 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 "entrymerger.h" +#include "entry.h" +#include "collection.h" +#include "tellico_kernel.h" +#include "controller.h" +#include "progressmanager.h" +#include "statusbar.h" +#include "tellico_debug.h" + +#include <klocale.h> + +#include <qtimer.h> + +using Tellico::EntryMerger; + +EntryMerger::EntryMerger(Data::EntryVec entries_, QObject* parent_) + : QObject(parent_), m_entriesToCheck(entries_), m_origCount(entries_.count()), m_cancelled(false) { + + m_entriesLeft = m_entriesToCheck; + Kernel::self()->beginCommandGroup(i18n("Merge Entries")); + + QString label = i18n("Merging entries..."); + ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/); + item.setTotalSteps(m_origCount); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + + // done if no entries to merge + if(m_origCount < 2) { + QTimer::singleShot(500, this, SLOT(slotCleanup())); + } else { + slotStartNext(); // starts fetching + } +} + +void EntryMerger::slotStartNext() { + QString statusMsg = i18n("Total merged/scanned entries: %1/%2") + .arg(m_entriesToRemove.count()) + .arg(m_origCount - m_entriesToCheck.count()); + StatusBar::self()->setStatus(statusMsg); + ProgressManager::self()->setProgress(this, m_origCount - m_entriesToCheck.count()); + + Data::EntryPtr baseEntry = m_entriesToCheck.front(); + Data::EntryVec::Iterator it = m_entriesToCheck.begin(); + ++it; // skip checking against first + for( ; it != m_entriesToCheck.end(); ++it) { + bool match = cleanMerge(baseEntry, &*it); + if(!match) { + int score = baseEntry->collection()->sameEntry(baseEntry, &*it); + match = score >= Data::Collection::ENTRY_PERFECT_MATCH; + } + if(match) { + bool merge_ok = baseEntry->collection()->mergeEntry(baseEntry, &*it, false /*overwrite*/, true /*askUser*/); + if(merge_ok) { + m_entriesToRemove.append(it); + m_entriesLeft.remove(it); + } + } + } + m_entriesToCheck.remove(baseEntry); + + if(m_cancelled || m_entriesToCheck.count() < 2) { + QTimer::singleShot(0, this, SLOT(slotCleanup())); + } else { + QTimer::singleShot(0, this, SLOT(slotStartNext())); + } +} + +void EntryMerger::slotCancel() { + m_cancelled = true; +} + +void EntryMerger::slotCleanup() { + Kernel::self()->removeEntries(m_entriesToRemove); + Controller::self()->slotUpdateSelection(0, m_entriesLeft); + StatusBar::self()->clearStatus(); + ProgressManager::self()->setDone(this); + Kernel::self()->endCommandGroup(); + deleteLater(); +} + +bool EntryMerger::cleanMerge(Data::EntryPtr e1, Data::EntryPtr e2) const { + // figure out if there's a clean merge possible + Data::FieldVec fields = e1->collection()->fields(); + for(Data::FieldVecIt it = fields.begin(); it != fields.end(); ++it) { + QString val1 = e1->field(it); + QString val2 = e2->field(it); + if(val1 != val2 && !val1.isEmpty() && !val2.isEmpty()) { + return false; + } + } + return true; +} + +#include "entrymerger.moc" diff --git a/src/entrymerger.h b/src/entrymerger.h new file mode 100644 index 0000000..46e23ca --- /dev/null +++ b/src/entrymerger.h @@ -0,0 +1,52 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTRYMERGER_H +#define TELLICO_ENTRYMERGER_H + +#include "datavectors.h" + +#include <qobject.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class EntryMerger : public QObject { +Q_OBJECT +public: + EntryMerger(Data::EntryVec entries, QObject* parent); + +public slots: + void slotCancel(); + +private slots: + void slotStartNext(); + void slotCleanup(); + +private: + // if a clean merge is possible + bool cleanMerge(Data::EntryPtr entry1, Data::EntryPtr entry2) const; + bool askUser(Data::EntryPtr entry1, Data::EntryPtr entry2); + + Data::EntryVec m_entriesToCheck; + Data::EntryVec m_entriesToRemove; + Data::EntryVec m_entriesLeft; + int m_origCount; + bool m_cancelled; +}; + +} // end namespace + +#endif diff --git a/src/entryupdater.cpp b/src/entryupdater.cpp new file mode 100644 index 0000000..5c3ba2d --- /dev/null +++ b/src/entryupdater.cpp @@ -0,0 +1,273 @@ +/*************************************************************************** + copyright : (C) 2005-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 "entryupdater.h" +#include "entry.h" +#include "collection.h" +#include "tellico_kernel.h" +#include "tellico_debug.h" +#include "progressmanager.h" +#include "statusbar.h" +#include "gui/richtextlabel.h" +#include "document.h" + +#include <kdialogbase.h> +#include <klocale.h> +#include <klistview.h> +#include <kiconloader.h> + +#include <qvbox.h> +#include <qtimer.h> + +namespace { + static const int CHECK_COLLECTION_IMAGES_STEP_SIZE = 10; +} + +using Tellico::EntryUpdater; + +// for each entry, we loop over all available fetchers +// then we loop over all entries +EntryUpdater::EntryUpdater(Data::CollPtr coll_, Data::EntryVec entries_, QObject* parent_) + : QObject(parent_), m_coll(coll_), m_entriesToUpdate(entries_), m_cancelled(false) { + // for now, we're assuming all entries are same collection type + m_fetchers = Fetch::Manager::self()->createUpdateFetchers(m_coll->type()); + for(Fetch::FetcherVec::Iterator it = m_fetchers.begin(); it != m_fetchers.end(); ++it) { + connect(it.data(), SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*)), + SLOT(slotResult(Tellico::Fetch::SearchResult*))); + connect(it.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher::Ptr)), + SLOT(slotDone())); + } + init(); +} + +EntryUpdater::EntryUpdater(const QString& source_, Data::CollPtr coll_, Data::EntryVec entries_, QObject* parent_) + : QObject(parent_) + , m_coll(coll_) + , m_entriesToUpdate(entries_) + , m_cancelled(false) { + // for now, we're assuming all entries are same collection type + Fetch::Fetcher::Ptr f = Fetch::Manager::self()->createUpdateFetcher(m_coll->type(), source_); + if(f) { + m_fetchers.append(f); + connect(f, SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*)), + SLOT(slotResult(Tellico::Fetch::SearchResult*))); + connect(f, SIGNAL(signalDone(Tellico::Fetch::Fetcher::Ptr)), + SLOT(slotDone())); + } + init(); +} + +EntryUpdater::~EntryUpdater() { + for(ResultList::Iterator res = m_results.begin(); res != m_results.end(); ++res) { + delete (*res).first; + } +} + +void EntryUpdater::init() { + m_fetchIndex = 0; + m_origEntryCount = m_entriesToUpdate.count(); + QString label; + if(m_entriesToUpdate.count() == 1) { + label = i18n("Updating %1...").arg(m_entriesToUpdate.front()->title()); + } else { + label = i18n("Updating entries..."); + } + Kernel::self()->beginCommandGroup(i18n("Update Entries")); + ProgressItem& item = ProgressManager::self()->newProgressItem(this, label, true /*canCancel*/); + item.setTotalSteps(m_fetchers.count() * m_origEntryCount); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + + // done if no fetchers available + if(m_fetchers.isEmpty()) { + QTimer::singleShot(500, this, SLOT(slotCleanup())); + } else { + slotStartNext(); // starts fetching + } +} + +void EntryUpdater::slotStartNext() { + StatusBar::self()->setStatus(i18n("Updating <b>%1</b>...").arg(m_entriesToUpdate.front()->title())); + ProgressManager::self()->setProgress(this, m_fetchers.count() * (m_origEntryCount - m_entriesToUpdate.count()) + m_fetchIndex); + + Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex]; +// myDebug() << "EntryUpdater::slotDone() - starting " << f->source() << endl; + f->updateEntry(m_entriesToUpdate.front()); +} + +void EntryUpdater::slotDone() { + if(m_cancelled) { + myLog() << "EntryUpdater::slotDone() - cancelled" << endl; + QTimer::singleShot(500, this, SLOT(slotCleanup())); + return; + } + + if(!m_results.isEmpty()) { + handleResults(); + } + + m_results.clear(); + ++m_fetchIndex; +// myDebug() << "EntryUpdater::slotDone() " << m_fetchIndex << endl; + if(m_fetchIndex == static_cast<int>(m_fetchers.count())) { + m_fetchIndex = 0; + // we've gone through the loop for the first entry in the vector + // pop it and move on + m_entriesToUpdate.remove(m_entriesToUpdate.begin()); + // if there are no more entries, and this is the last fetcher, time to delete + if(m_entriesToUpdate.isEmpty()) { + QTimer::singleShot(500, this, SLOT(slotCleanup())); + return; + } + } + kapp->processEvents(); + // so the entry updater can clean up a bit + QTimer::singleShot(500, this, SLOT(slotStartNext())); +} + +void EntryUpdater::slotResult(Fetch::SearchResult* result_) { + if(!result_ || m_cancelled) { + return; + } + +// myDebug() << "EntryUpdater::slotResult() - " << result_->title << " [" << result_->fetcher->source() << "]" << endl; + m_results.append(UpdateResult(result_, m_fetchers[m_fetchIndex]->updateOverwrite())); + Data::EntryPtr e = result_->fetchEntry(); + if(e) { + m_fetchedEntries.append(e); + int match = m_coll->sameEntry(m_entriesToUpdate.front(), e); + if(match > Data::Collection::ENTRY_PERFECT_MATCH) { + result_->fetcher->stop(); + } + } + kapp->processEvents(); +} + +void EntryUpdater::slotCancel() { +// myDebug() << "EntryUpdater::slotCancel()" << endl; + m_cancelled = true; + Fetch::Fetcher::Ptr f = m_fetchers[m_fetchIndex]; + if(f) { + f->stop(); // ends up calling slotDone(); + } else { + slotDone(); + } +} + +void EntryUpdater::handleResults() { + Data::EntryPtr entry = m_entriesToUpdate.front(); + int best = 0; + ResultList matches; + for(ResultList::Iterator res = m_results.begin(); res != m_results.end(); ++res) { + Data::EntryPtr e = (*res).first->fetchEntry(); + if(!e) { + continue; + } + m_fetchedEntries.append(e); + int match = m_coll->sameEntry(entry, e); + if(match) { +// myDebug() << e->title() << " matches by " << match << endl; + } + if(match > best) { + best = match; + matches.clear(); + matches.append(*res); + } else if(match == best && best > 0) { + matches.append(*res); + } + } + if(best < Data::Collection::ENTRY_GOOD_MATCH) { + if(best > 0) { + myDebug() << "no good match (score > 10), best match = " << best << " (" << matches.count() << " matches)" << endl; + } + return; + } +// myDebug() << "best match = " << best << " (" << matches.count() << " matches)" << endl; + UpdateResult match(0, true); + if(matches.count() == 1) { + match = matches.front(); + } else if(matches.count() > 1) { + match = askUser(matches); + } + // askUser() could come back with nil + if(match.first) { + mergeCurrent(match.first->fetchEntry(), match.second); + } +} + +Tellico::EntryUpdater::UpdateResult EntryUpdater::askUser(ResultList results) { + KDialogBase dlg(Kernel::self()->widget(), "entry updater dialog", + true, i18n("Select Match"), KDialogBase::Ok|KDialogBase::Cancel); + QVBox* box = new QVBox(&dlg); + box->setSpacing(10); + + QHBox* hbox = new QHBox(box); + hbox->setSpacing(10); + QLabel* icon = new QLabel(hbox); + icon->setPixmap(KGlobal::iconLoader()->loadIcon(QString::fromLatin1("network"), KIcon::Panel, 64)); + QString s = i18n("<qt><b>%1</b> returned multiple results which could match <b>%2</b>, " + "the entry currently in the collection. Please select the correct match.</qt>") + .arg(m_fetchers[m_fetchIndex]->source()) + .arg(m_entriesToUpdate.front()->field(QString::fromLatin1("title"))); + GUI::RichTextLabel* l = new GUI::RichTextLabel(s, hbox); + hbox->setStretchFactor(l, 100); + + KListView* view = new KListView(box); + view->setShowSortIndicator(true); + view->setAllColumnsShowFocus(true); + view->setResizeMode(QListView::AllColumns); + view->setMinimumWidth(640); + view->addColumn(i18n("Title")); + view->addColumn(i18n("Description")); + QMap<KListViewItem*, UpdateResult> map; + for(ResultList::Iterator res = results.begin(); res != results.end(); ++res) { + map.insert(new KListViewItem(view, (*res).first->fetchEntry()->title(), (*res).first->desc), *res); + } + + dlg.setMainWidget(box); + if(dlg.exec() != QDialog::Accepted) { + return UpdateResult(0, false); + } + KListViewItem* item = static_cast<KListViewItem*>(view->selectedItem()); + if(!item) { + return UpdateResult(0, false); + } + return map[item]; +} + +void EntryUpdater::mergeCurrent(Data::EntryPtr entry_, bool overWrite_) { + Data::EntryPtr currEntry = m_entriesToUpdate.front(); + if(entry_) { + m_matchedEntries.append(entry_); + Kernel::self()->updateEntry(currEntry, entry_, overWrite_); + if(m_entriesToUpdate.count() % CHECK_COLLECTION_IMAGES_STEP_SIZE == 1) { + // I don't want to remove any images in the entries that are getting + // updated since they'll reference them later and the command isn't + // executed until the command history group is finished + // so remove pointers to matched entries + Data::EntryVec nonUpdatedEntries = m_fetchedEntries; + for(Data::EntryVecIt match = m_matchedEntries.begin(); match != m_matchedEntries.end(); ++match) { + nonUpdatedEntries.remove(match); + } + Data::Document::self()->removeImagesNotInCollection(nonUpdatedEntries, m_matchedEntries); + } + } +} + +void EntryUpdater::slotCleanup() { + StatusBar::self()->clearStatus(); + ProgressManager::self()->setDone(this); + Kernel::self()->endCommandGroup(); + deleteLater(); +} + +#include "entryupdater.moc" diff --git a/src/entryupdater.h b/src/entryupdater.h new file mode 100644 index 0000000..ddf4aa4 --- /dev/null +++ b/src/entryupdater.h @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTRYUPDATER_H +#define TELLICO_ENTRYUPDATER_H + +#include "datavectors.h" +#include "fetch/fetchmanager.h" + +#include <qpair.h> +#include <qvaluelist.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class EntryUpdater : public QObject { +Q_OBJECT +public: + EntryUpdater(Data::CollPtr coll, Data::EntryVec entries, QObject* parent); + EntryUpdater(const QString& fetcher, Data::CollPtr coll, Data::EntryVec entries, QObject* parent); + ~EntryUpdater(); + +public slots: + void slotResult(Tellico::Fetch::SearchResult* result); + void slotCancel(); + +private slots: + void slotStartNext(); + void slotDone(); + void slotCleanup(); + +private: + typedef QPair<Fetch::SearchResult*, bool> UpdateResult; + typedef QValueList<UpdateResult> ResultList; + + void init(); + void handleResults(); + UpdateResult askUser(ResultList results); + void mergeCurrent(Data::EntryPtr entry, bool overwrite); + + Data::CollPtr m_coll; + Data::EntryVec m_entriesToUpdate; + Data::EntryVec m_fetchedEntries; + Data::EntryVec m_matchedEntries; + Fetch::FetcherVec m_fetchers; + int m_fetchIndex; + int m_origEntryCount; + ResultList m_results; + bool m_cancelled : 1; +}; + +} // end namespace + +#endif diff --git a/src/entryview.cpp b/src/entryview.cpp new file mode 100644 index 0000000..c4b0d38 --- /dev/null +++ b/src/entryview.cpp @@ -0,0 +1,364 @@ +/*************************************************************************** + copyright : (C) 2003-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 "entryview.h" +#include "entry.h" +#include "field.h" +#include "filehandler.h" +#include "translators/xslthandler.h" +#include "translators/tellicoxmlexporter.h" +#include "collection.h" +#include "imagefactory.h" +#include "tellico_kernel.h" +#include "tellico_utils.h" +#include "core/tellico_config.h" +#include "newstuff/manager.h" +#include "document.h" +#include "latin1literal.h" +#include "../core/drophandler.h" + +#include <kstandarddirs.h> +#include <krun.h> +#include <kmessagebox.h> +#include <khtmlview.h> +#include <dom/dom_element.h> +#include <kapplication.h> +#include <ktempfile.h> +#include <klocale.h> + +#include <qfile.h> + +using Tellico::EntryView; + +EntryView::EntryView(QWidget* parent_, const char* name_) : KHTMLPart(parent_, name_), + m_entry(0), m_handler(0), m_run(0), m_tempFile(0), m_useGradientImages(true), m_checkCommonFile(true) { + setJScriptEnabled(false); + setJavaEnabled(false); + setMetaRefreshEnabled(false); + setPluginsEnabled(false); + clear(); // needed for initial layout + + view()->setAcceptDrops(true); + DropHandler* drophandler = new DropHandler(this); + view()->installEventFilter(drophandler); + + connect(browserExtension(), SIGNAL(openURLRequest(const KURL&, const KParts::URLArgs&)), + SLOT(slotOpenURL(const KURL&))); + connect(kapp, SIGNAL(kdisplayPaletteChanged()), SLOT(slotResetColors())); +} + +EntryView::~EntryView() { + if(m_run) { + m_run->abort(); + } + delete m_handler; + m_handler = 0; + delete m_tempFile; + m_tempFile = 0; +} + +void EntryView::clear() { + m_entry = 0; + + // just clear the view + begin(); + if(!m_textToShow.isEmpty()) { + write(m_textToShow); + } + end(); + view()->layout(); // I need this because some of the margins and widths may get messed up +} + +void EntryView::showEntry(Data::EntryPtr entry_) { + if(!entry_) { + clear(); + return; + } + + m_textToShow = QString(); +#if 0 + kdWarning() << "EntryView::showEntry() - turn me off!" << endl; + m_entry = 0; + setXSLTFile(m_xsltFile); +#endif + if(!m_handler || !m_handler->isValid()) { + setXSLTFile(m_xsltFile); + } + + m_entry = entry_; + + // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found + // by simply using a relative path in the xslt file + KURL u; + u.setPath(m_xsltFile); + begin(u); + + Export::TellicoXMLExporter exporter(entry_->collection()); + exporter.setEntries(entry_); + long opt = exporter.options(); + // verify images for the view + opt |= Export::ExportVerifyImages; + // on second thought, don't auto-format everything, just clean it +// if(Data::Field::autoFormat()) { +// opt = Export::ExportFormatted; +// } + if(entry_->collection()->type() == Data::Collection::Bibtex) { + opt |= Export::ExportClean; + } + exporter.setOptions(opt); + QDomDocument dom = exporter.exportXML(); + +// myDebug() << dom.toString() << endl; +#if 0 + kdWarning() << "EntryView::showEntry() - turn me off!" << endl; + QFile f1(QString::fromLatin1("/tmp/test.xml")); + if(f1.open(IO_WriteOnly)) { + QTextStream t(&f1); + t << dom.toString(); + } + f1.close(); +#endif + + QString html = m_handler->applyStylesheet(dom.toString()); + // write out image files + Data::FieldVec fields = entry_->collection()->imageFields(); + for(Data::FieldVec::Iterator field = fields.begin(); field != fields.end(); ++field) { + QString id = entry_->field(field); + if(id.isEmpty()) { + continue; + } + if(Data::Document::self()->allImagesOnDisk()) { + ImageFactory::writeCachedImage(id, ImageFactory::DataDir); + } else { + ImageFactory::writeCachedImage(id, ImageFactory::TempDir); + } + } + +#if 0 + kdWarning() << "EntryView::showEntry() - turn me off!" << endl; + QFile f2(QString::fromLatin1("/tmp/test.html")); + if(f2.open(IO_WriteOnly)) { + QTextStream t(&f2); + t << html; + } + f2.close(); +#endif + +// myDebug() << html << endl; + write(html); + end(); + // not need anymore? + view()->layout(); // I need this because some of the margins and widths may get messed up +} + +void EntryView::showText(const QString& text_) { + m_textToShow = text_; + begin(); + write(text_); + end(); +} + +void EntryView::setXSLTFile(const QString& file_) { + QString oldFile = m_xsltFile; + // if starts with slash, then absolute path + if(file_.at(0) == '/') { + m_xsltFile = file_; + } else { + const QString templateDir = QString::fromLatin1("entry-templates/"); + m_xsltFile = locate("appdata", templateDir + file_); + if(m_xsltFile.isEmpty()) { + if(!file_.isEmpty()) { + kdWarning() << "EntryView::setXSLTFile() - can't locate " << file_ << endl; + } + m_xsltFile = locate("appdata", templateDir + QString::fromLatin1("Fancy.xsl")); + if(m_xsltFile.isEmpty()) { + QString str = QString::fromLatin1("<qt>"); + str += i18n("Tellico is unable to locate the default entry stylesheet."); + str += QChar(' '); + str += i18n("Please check your installation."); + str += QString::fromLatin1("</qt>"); + KMessageBox::error(view(), str); + clear(); + return; + } + } + } + + const int type = m_entry ? m_entry->collection()->type() : Kernel::self()->collectionType(); + + // we need to know if the colors changed from last time, in case + // we need to do that ugly hack to reload the cache + bool reloadImages = m_useGradientImages; + // if m_useGradientImages is false, then we don't even need to check + // if there's no handler, there there's _no way_ to check + if(m_handler && reloadImages) { + // the only two colors that matter for the gradients are the base color + // and highlight base color + const QCString& oldBase = m_handler->param("bgcolor"); + const QCString& oldHigh = m_handler->param("color2"); + // remember the string params have apostrophes on either side, so we can start search at pos == 1 + reloadImages = oldBase.find(Config::templateBaseColor(type).name().latin1(), 1) == -1 + || oldHigh.find(Config::templateHighlightedBaseColor(type).name().latin1(), 1) == -1; + } + + if(!m_handler || m_xsltFile != oldFile) { + delete m_handler; + // must read the file name to get proper context + m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); + if(m_checkCommonFile && !m_handler->isValid()) { + NewStuff::Manager::checkCommonFile(); + m_checkCommonFile = false; + delete m_handler; + m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile)); + } + if(!m_handler->isValid()) { + kdWarning() << "EntryView::setXSLTFile() - invalid xslt handler" << endl; + clear(); + delete m_handler; + m_handler = 0; + return; + } + } + + m_handler->addStringParam("font", Config::templateFont(type).family().latin1()); + m_handler->addStringParam("fontsize", QCString().setNum(Config::templateFont(type).pointSize())); + m_handler->addStringParam("bgcolor", Config::templateBaseColor(type).name().latin1()); + m_handler->addStringParam("fgcolor", Config::templateTextColor(type).name().latin1()); + m_handler->addStringParam("color1", Config::templateHighlightedTextColor(type).name().latin1()); + m_handler->addStringParam("color2", Config::templateHighlightedBaseColor(type).name().latin1()); + + if(Data::Document::self()->allImagesOnDisk()) { + m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::dataDir())); + } else { + m_handler->addStringParam("imgdir", QFile::encodeName(ImageFactory::tempDir())); + } + + // look for a file that gets installed to know the installation directory + QString appdir = KGlobal::dirs()->findResourceDir("appdata", QString::fromLatin1("pics/tellico.png")); + m_handler->addStringParam("datadir", QFile::encodeName(appdir)); + + // if we don't have to reload the images, then just show the entry and we're done + if(!reloadImages) { + showEntry(m_entry); + return; + } + + // now, have to recreate images and refresh khtml cache + resetColors(); +} + +void EntryView::slotRefresh() { + setXSLTFile(m_xsltFile); + showEntry(m_entry); + view()->repaint(); +} + +// do some contortions in case the url is relative +// need to interpret it relative to document URL instead of xslt file +// the current node under the mouse vould be the text node inside +// the anchor node, so iterate up the parents +void EntryView::slotOpenURL(const KURL& url_) { + if(url_.protocol() == Latin1Literal("tc")) { + // handle this internally + emit signalAction(url_); + return; + } + + KURL u = url_; + for(DOM::Node node = nodeUnderMouse(); !node.isNull(); node = node.parentNode()) { + if(node.nodeType() == DOM::Node::ELEMENT_NODE && static_cast<DOM::Element>(node).tagName() == "a") { + QString href = static_cast<DOM::Element>(node).getAttribute("href").string(); + if(!href.isEmpty() && KURL::isRelativeURL(href)) { + // interpet url relative to document url + u = KURL(Kernel::self()->URL(), href); + } + break; + } + } + // open the url, m_run gets auto-deleted + m_run = new KRun(u); +} + +void EntryView::slotReloadEntry() { + // this slot should only be connected in setXSLTFile() + // must disconnect the signal first, otherwise, get an infinite loop + disconnect(SIGNAL(completed())); + closeURL(); // this is needed to stop everything, for some reason + view()->setUpdatesEnabled(true); + + if(m_entry) { + showEntry(m_entry); + } else { + // setXSLTFile() writes some html to clear the image cache + // but we don't want to see that, so just clear everything + clear(); + } + delete m_tempFile; + m_tempFile = 0; +} + +void EntryView::setXSLTOptions(const StyleOptions& opt_) { + m_handler->addStringParam("font", opt_.fontFamily.latin1()); + m_handler->addStringParam("fontsize", QCString().setNum(opt_.fontSize)); + m_handler->addStringParam("bgcolor", opt_.baseColor.name().latin1()); + m_handler->addStringParam("fgcolor", opt_.textColor.name().latin1()); + m_handler->addStringParam("color1", opt_.highlightedTextColor.name().latin1()); + m_handler->addStringParam("color2", opt_.highlightedBaseColor.name().latin1()); + m_handler->addStringParam("imgdir", QFile::encodeName(opt_.imgDir)); +} + + +void EntryView::slotResetColors() { + // this will delete and reread the default colors, assuming they changed + // better to do this elsewhere, but do it here for now + Config::deleteAndReset(); + delete m_handler; m_handler = 0; + setXSLTFile(m_xsltFile); +} + +void EntryView::resetColors() { + ImageFactory::createStyleImages(); // recreate gradients + + QString dir = m_handler ? m_handler->param("imgdir") : QString(); + if(dir.isEmpty()) { + dir = Data::Document::self()->allImagesOnDisk() ? ImageFactory::dataDir() : ImageFactory::tempDir(); + } else { + // it's a string param, so it has quotes on both sides + dir = dir.mid(1); + dir.truncate(dir.length()-1); + } + + // this is a rather bad hack to get around the fact that the image cache is not reloaded when + // the gradient files are changed on disk. Setting the URLArgs for write() calls doesn't seem to + // work. So force a reload with a temp file, then catch the completed signal and repaint + QString s = QString::fromLatin1("<html><body><img src=\"%1\"><img src=\"%2\"></body></html>") + .arg(dir + QString::fromLatin1("gradient_bg.png")) + .arg(dir + QString::fromLatin1("gradient_header.png")); + + delete m_tempFile; + m_tempFile = new KTempFile; + m_tempFile->setAutoDelete(true); + *m_tempFile->textStream() << s; + m_tempFile->file()->close(); // have to close it + + KParts::URLArgs args = browserExtension()->urlArgs(); + args.reload = true; // tell the cache to reload images + browserExtension()->setURLArgs(args); + + // don't flicker + view()->setUpdatesEnabled(false); + openURL(m_tempFile->name()); + connect(this, SIGNAL(completed()), SLOT(slotReloadEntry())); +} + +#include "entryview.moc" diff --git a/src/entryview.h b/src/entryview.h new file mode 100644 index 0000000..ddca518 --- /dev/null +++ b/src/entryview.h @@ -0,0 +1,106 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef ENTRYVIEW_H +#define ENTRYVIEW_H + +class KRun; +class KTempFile; + +#include "datavectors.h" + +#include <khtml_part.h> + +#include <qguardedptr.h> + +namespace Tellico { + class XSLTHandler; + class ImageFactory; + class StyleOptions; + +/** + * @author Robby Stephenson + */ +class EntryView : public KHTMLPart { +Q_OBJECT + +public: + /** + * The EntryView shows a HTML representation of the data in the entry. + * + * @param parent QWidget parent + * @param name QObject name + */ + EntryView(QWidget* parent, const char* name=0); + /** + */ + virtual ~EntryView(); + + /** + * Uses the xslt handler to convert an entry to html, and then writes that html to the view + * + * @param entry The entry to show + */ + void showEntry(Data::EntryPtr entry); + void showText(const QString& text); + + /** + * Clear the widget and set Entry pointer to NULL + */ + void clear(); + /** + * Sets the XSLT file. If the file name does not start with a back-slash, then the + * standard directories are searched. + * + * @param file The XSLT file name + */ + void setXSLTFile(const QString& file); + void setXSLTOptions(const StyleOptions& options); + void setUseGradientImages(bool b) { m_useGradientImages = b; } + +signals: + void signalAction(const KURL& url); + +public slots: + /** + * Helper function to refresh view. + */ + void slotRefresh(); + +private slots: + /** + * Open a URL. + * + * @param url The URL to open + */ + void slotOpenURL(const KURL& url); + void slotReloadEntry(); + void slotResetColors(); + +private: + void resetColors(); + + Data::EntryPtr m_entry; + XSLTHandler* m_handler; + QString m_xsltFile; + QString m_textToShow; + + // to run any clicked processes + QGuardedPtr<KRun> m_run; + KTempFile* m_tempFile; + bool m_useGradientImages : 1; + bool m_checkCommonFile : 1; +}; + +} //end namespace +#endif diff --git a/src/exportdialog.cpp b/src/exportdialog.cpp new file mode 100644 index 0000000..7ef61a0 --- /dev/null +++ b/src/exportdialog.cpp @@ -0,0 +1,262 @@ +/*************************************************************************** + copyright : (C) 2003-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 "exportdialog.h" +#include "collection.h" +#include "filehandler.h" +#include "controller.h" +#include "document.h" + +#include "translators/exporter.h" +#include "translators/tellicoxmlexporter.h" +#include "translators/tellicozipexporter.h" +#include "translators/htmlexporter.h" +#include "translators/csvexporter.h" +#include "translators/bibtexexporter.h" +#include "translators/bibtexmlexporter.h" +#include "translators/xsltexporter.h" +#include "translators/pilotdbexporter.h" +#include "translators/alexandriaexporter.h" +#include "translators/onixexporter.h" +#include "translators/gcfilmsexporter.h" + +#include <klocale.h> +#include <kdebug.h> +#include <kglobal.h> +#include <kconfig.h> + +#include <qlayout.h> +#include <qcheckbox.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qwhatsthis.h> +#include <qtextcodec.h> + +using Tellico::ExportDialog; + +ExportDialog::ExportDialog(Export::Format format_, Data::CollPtr coll_, QWidget* parent_, const char* name_) + : KDialogBase(parent_, name_, true /*modal*/, i18n("Export Options"), Ok|Cancel), + m_format(format_), m_coll(coll_), m_exporter(exporter(format_)) { + QWidget* widget = new QWidget(this); + QVBoxLayout* topLayout = new QVBoxLayout(widget, 0, spacingHint()); + + QGroupBox* group1 = new QGroupBox(1, Qt::Horizontal, i18n("Formatting"), widget); + topLayout->addWidget(group1, 0); + m_formatFields = new QCheckBox(i18n("Format all fields"), group1); + m_formatFields->setChecked(false); + QWhatsThis::add(m_formatFields, i18n("If checked, the values of the fields will be " + "automatically formatted according to their format type.")); + m_exportSelected = new QCheckBox(i18n("Export selected entries only"), group1); + m_exportSelected->setChecked(false); + QWhatsThis::add(m_exportSelected, i18n("If checked, only the currently selected entries will " + "be exported.")); + + QButtonGroup* bg = new QButtonGroup(1, Qt::Horizontal, i18n("Encoding"), widget); + topLayout->addWidget(bg, 0); + m_encodeUTF8 = new QRadioButton(i18n("Encode in Unicode (UTF-8)"), bg); + m_encodeUTF8->setChecked(true); + QWhatsThis::add(m_encodeUTF8, i18n("Encode the exported file in Unicode (UTF-8).")); + QString localStr = i18n("Encode in user locale (%1)").arg( + QString::fromLatin1(QTextCodec::codecForLocale()->name())); + m_encodeLocale = new QRadioButton(localStr, bg); + QWhatsThis::add(m_encodeLocale, i18n("Encode the exported file in the local encoding.")); + + QWidget* w = m_exporter->widget(widget, "exporter_widget"); + if(w) { + topLayout->addWidget(w, 0); + } + + topLayout->addStretch(); + + setMainWidget(widget); + readOptions(); + // bibtex, CSV, and text are forced to locale + if(format_ == Export::Bibtex || format_ == Export::CSV || format_ == Export::Text) { + m_encodeUTF8->setEnabled(false); + m_encodeLocale->setChecked(true); +// m_encodeLocale->setEnabled(false); + } else if(format_ == Export::Alexandria || format_ == Export::PilotDB) { + bg->setEnabled(false); + } + connect(this, SIGNAL(okClicked()), SLOT(slotSaveOptions())); +} + +ExportDialog::~ExportDialog() { + delete m_exporter; + m_exporter = 0; +} + +QString ExportDialog::fileFilter() { + return m_exporter ? m_exporter->fileFilter() : QString::null; +} + +void ExportDialog::readOptions() { + KConfigGroup config(KGlobal::config(), "ExportOptions"); + bool format = config.readBoolEntry("FormatFields", false); + m_formatFields->setChecked(format); + bool selected = config.readBoolEntry("ExportSelectedOnly", false); + m_exportSelected->setChecked(selected); + bool encode = config.readBoolEntry("EncodeUTF8", true); + if(encode) { + m_encodeUTF8->setChecked(true); + } else { + m_encodeLocale->setChecked(true); + } +} + +void ExportDialog::slotSaveOptions() { + KConfig* config = KGlobal::config(); + // each exporter sets its own group + m_exporter->saveOptions(config); + + KConfigGroup configGroup(config, "ExportOptions"); + configGroup.writeEntry("FormatFields", m_formatFields->isChecked()); + configGroup.writeEntry("ExportSelectedOnly", m_exportSelected->isChecked()); + configGroup.writeEntry("EncodeUTF8", m_encodeUTF8->isChecked()); +} + +// static +Tellico::Export::Exporter* ExportDialog::exporter(Export::Format format_) { + Export::Exporter* exporter = 0; + + switch(format_) { + case Export::TellicoXML: + exporter = new Export::TellicoXMLExporter(); + break; + + case Export::TellicoZip: + exporter = new Export::TellicoZipExporter(); + break; + + case Export::HTML: + { + Export::HTMLExporter* htmlExp = new Export::HTMLExporter(); + htmlExp->setGroupBy(Controller::self()->expandedGroupBy()); + htmlExp->setSortTitles(Controller::self()->sortTitles()); + htmlExp->setColumns(Controller::self()->visibleColumns()); + exporter = htmlExp; + } + break; + + case Export::CSV: + exporter = new Export::CSVExporter(); + break; + + case Export::Bibtex: + exporter = new Export::BibtexExporter(); + break; + + case Export::Bibtexml: + exporter = new Export::BibtexmlExporter(); + break; + + case Export::XSLT: + exporter = new Export::XSLTExporter(); + break; + + case Export::PilotDB: + { + Export::PilotDBExporter* pdbExp = new Export::PilotDBExporter(); + pdbExp->setColumns(Controller::self()->visibleColumns()); + exporter = pdbExp; + } + break; + + case Export::Alexandria: + exporter = new Export::AlexandriaExporter(); + break; + + case Export::ONIX: + exporter = new Export::ONIXExporter(); + break; + + case Export::GCfilms: + exporter = new Export::GCfilmsExporter(); + break; + + default: + kdDebug() << "ExportDialog::exporter() - not implemented!" << endl; + break; + } + if(exporter) { + exporter->readOptions(KGlobal::config()); + } + return exporter; +} + +bool ExportDialog::exportURL(const KURL& url_/*=KURL()*/) const { + if(!m_exporter) { + return false; + } + + if(!url_.isEmpty() && !FileHandler::queryExists(url_)) { + return false; + } + + // exporter might need to know final URL, say for writing images or something + m_exporter->setURL(url_); + if(m_exportSelected->isChecked()) { + m_exporter->setEntries(Controller::self()->selectedEntries()); + } else { + m_exporter->setEntries(m_coll->entries()); + } + long opt = Export::ExportImages | Export::ExportComplete | Export::ExportProgress; // for now, always export images + if(m_formatFields->isChecked()) { + opt |= Export::ExportFormatted; + } + if(m_encodeUTF8->isChecked()) { + opt |= Export::ExportUTF8; + } + // since we already asked about overwriting the file, force the save + opt |= Export::ExportForce; + + m_exporter->setOptions(opt); + + return m_exporter->exec(); +} + +// static +// alexandria is exported to known directory +// all others are files +Tellico::Export::Target ExportDialog::exportTarget(Export::Format format_) { + switch(format_) { + case Export::Alexandria: + return Export::None; + default: + return Export::File; + } +} + +// static +bool ExportDialog::exportCollection(Export::Format format_, const KURL& url_) { + Export::Exporter* exp = exporter(format_); + + exp->setURL(url_); + exp->setEntries(Data::Document::self()->collection()->entries()); + + KConfigGroup config(KGlobal::config(), "ExportOptions"); + long options = 0; + if(config.readBoolEntry("FormatFields", false)) { + options |= Export::ExportFormatted; + } + if(config.readBoolEntry("EncodeUTF8", true)) { + options |= Export::ExportUTF8; + } + exp->setOptions(options | Export::ExportForce); + + bool success = exp->exec(); + delete exp; + return success; +} + +#include "exportdialog.moc" diff --git a/src/exportdialog.h b/src/exportdialog.h new file mode 100644 index 0000000..2e68ca3 --- /dev/null +++ b/src/exportdialog.h @@ -0,0 +1,65 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef EXPORTDIALOG_H +#define EXPORTDIALOG_H + +class QCheckBox; +class QRadioButton; + +#include "translators/translators.h" +#include "datavectors.h" + +#include <kdialogbase.h> +#include <kurl.h> + +namespace Tellico { + namespace Export { + class Exporter; + } + +/** + * @author Robby Stephenson + */ +class ExportDialog : public KDialogBase { +Q_OBJECT + +public: + ExportDialog(Export::Format format, Data::CollPtr coll, QWidget* parent, const char* name); + ~ExportDialog(); + + QString fileFilter(); + bool exportURL(const KURL& url=KURL()) const; + + static Export::Target exportTarget(Export::Format format); + static bool exportCollection(Export::Format format, const KURL& url); + +private slots: + void slotSaveOptions(); + +private: + static Export::Exporter* exporter(Export::Format format); + + void readOptions(); + + Export::Format m_format; + Data::CollPtr m_coll; + Export::Exporter* m_exporter; + QCheckBox* m_formatFields; + QCheckBox* m_exportSelected; + QRadioButton* m_encodeUTF8; + QRadioButton* m_encodeLocale; +}; + +} // end namespace +#endif diff --git a/src/fetch/Makefile.am b/src/fetch/Makefile.am new file mode 100644 index 0000000..fbf2ea1 --- /dev/null +++ b/src/fetch/Makefile.am @@ -0,0 +1,46 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = libfetch.a + +AM_CPPFLAGS = $(all_includes) $(LIBXML_CFLAGS) $(LIBXSLT_CFLAGS) $(YAZ_CFLAGS) + +libfetch_a_METASOURCES = AUTO + +libfetch_a_SOURCES = amazonfetcher.cpp animenfofetcher.cpp arxivfetcher.cpp \ + bibsonomyfetcher.cpp citebasefetcher.cpp configwidget.cpp crossreffetcher.cpp \ + discogsfetcher.cpp entrezfetcher.cpp execexternalfetcher.cpp fetcher.cpp fetchmanager.cpp \ + gcstarpluginfetcher.cpp googlescholarfetcher.cpp ibsfetcher.cpp imdbfetcher.cpp \ + isbndbfetcher.cpp messagehandler.cpp srufetcher.cpp yahoofetcher.cpp z3950connection.cpp \ + z3950fetcher.cpp + +####### kdevelop will overwrite this part!!! (end)############ + +SUBDIRS = scripts + +CLEANFILES = *~ + +KDE_OPTIONS = noautodist + +EXTRA_DIST = \ +fetcher.h fetcher.cpp fetchmanager.h fetchmanager.cpp \ +amazonfetcher.h amazonfetcher.cpp z3950fetcher.h z3950fetcher.cpp \ +imdbfetcher.h imdbfetcher.cpp fetch.h configwidget.h configwidget.cpp \ +entrezfetcher.h entrezfetcher.cpp \ +execexternalfetcher.h execexternalfetcher.cpp \ +messagehandler.h messagehandler.cpp \ +z3950connection.h z3950connection.cpp \ +yahoofetcher.h yahoofetcher.cpp \ +animenfofetcher.h animenfofetcher.cpp \ +ibsfetcher.h ibsfetcher.cpp \ +srufetcher.h srufetcher.cpp \ +isbndbfetcher.h isbndbfetcher.cpp \ +gcstarpluginfetcher.h gcstarpluginfetcher.cpp \ +crossreffetcher.h crossreffetcher.cpp \ +arxivfetcher.h arxivfetcher.cpp \ +citebasefetcher.h citebasefetcher.cpp \ +bibsonomyfetcher.h bibsonomyfetcher.cpp \ +googlescholarfetcher.h googlescholarfetcher.cpp \ +discogsfetcher.h discogsfetcher.cpp \ +z3950-servers.cfg + +appdir = $(kde_datadir)/tellico +app_DATA = z3950-servers.cfg diff --git a/src/fetch/amazonfetcher.cpp b/src/fetch/amazonfetcher.cpp new file mode 100644 index 0000000..36c009f --- /dev/null +++ b/src/fetch/amazonfetcher.cpp @@ -0,0 +1,937 @@ +/*************************************************************************** + copyright : (C) 2004-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 "amazonfetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../imagefactory.h" +#include "../tellico_kernel.h" +#include "../latin1literal.h" +#include "../collection.h" +#include "../document.h" +#include "../entry.h" +#include "../field.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" +#include "../isbnvalidator.h" +#include "../gui/combobox.h" + +#include <klocale.h> +#include <kio/job.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <klineedit.h> +#include <kseparator.h> +#include <kcombobox.h> +#include <kaccelmanager.h> + +#include <qdom.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qwhatsthis.h> +#include <qcheckbox.h> +#include <qfile.h> +#include <qtextcodec.h> + +namespace { + static const int AMAZON_RETURNS_PER_REQUEST = 10; + static const int AMAZON_MAX_RETURNS_TOTAL = 20; + static const char* AMAZON_ACCESS_KEY = "0834VQ4S71KYPVSYQD02"; + static const char* AMAZON_ASSOC_TOKEN = "tellico-20"; + // need to have these in the translation file + static const char* linkText = I18N_NOOP("Amazon Link"); +} + +using Tellico::Fetch::AmazonFetcher; + +// static +const AmazonFetcher::SiteData& AmazonFetcher::siteData(int site_) { + static SiteData dataVector[6] = { + { + i18n("Amazon (US)"), + "http://webservices.amazon.com/onca/xml" + }, { + i18n("Amazon (UK)"), + "http://webservices.amazon.co.uk/onca/xml" + }, { + i18n("Amazon (Germany)"), + "http://webservices.amazon.de/onca/xml" + }, { + i18n("Amazon (Japan)"), + "http://webservices.amazon.co.jp/onca/xml" + }, { + i18n("Amazon (France)"), + "http://webservices.amazon.fr/onca/xml" + }, { + i18n("Amazon (Canada)"), + "http://webservices.amazon.ca/onca/xml" + } + }; + + return dataVector[site_]; +} + +AmazonFetcher::AmazonFetcher(Site site_, QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_xsltHandler(0), m_site(site_), m_imageSize(MediumImage), + m_access(QString::fromLatin1(AMAZON_ACCESS_KEY)), + m_assoc(QString::fromLatin1(AMAZON_ASSOC_TOKEN)), m_addLinkField(true), m_limit(AMAZON_MAX_RETURNS_TOTAL), + m_countOffset(0), m_page(1), m_total(-1), m_numResults(0), m_job(0), m_started(false) { + m_name = siteData(site_).title; +} + +AmazonFetcher::~AmazonFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString AmazonFetcher::defaultName() { + return i18n("Amazon.com Web Services"); +} + +QString AmazonFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool AmazonFetcher::canFetch(int type) const { + return type == Data::Collection::Book + || type == Data::Collection::ComicBook + || type == Data::Collection::Bibtex + || type == Data::Collection::Album + || type == Data::Collection::Video + || type == Data::Collection::Game; +} + +void AmazonFetcher::readConfigHook(const KConfigGroup& config_) { + QString s = config_.readEntry("AccessKey"); + if(!s.isEmpty()) { + m_access = s; + } + s = config_.readEntry("AssocToken"); + if(!s.isEmpty()) { + m_assoc = s; + } + int imageSize = config_.readNumEntry("Image Size", -1); + if(imageSize > -1) { + m_imageSize = static_cast<ImageSize>(imageSize); + } + m_fields = config_.readListEntry("Custom Fields", QString::fromLatin1("keyword")); +} + +void AmazonFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + m_page = 1; + m_total = -1; + m_countOffset = 0; + m_numResults = 0; + doSearch(); +} + +void AmazonFetcher::continueSearch() { + m_started = true; + m_limit += AMAZON_MAX_RETURNS_TOTAL; + doSearch(); +} + +void AmazonFetcher::doSearch() { + m_data.truncate(0); + +// myDebug() << "AmazonFetcher::doSearch() - value = " << m_value << endl; +// myDebug() << "AmazonFetcher::doSearch() - getting page " << m_page << endl; + + const SiteData& data = siteData(m_site); + KURL u = data.url; + u.addQueryItem(QString::fromLatin1("Service"), QString::fromLatin1("AWSECommerceService")); + u.addQueryItem(QString::fromLatin1("AssociateTag"), m_assoc); + u.addQueryItem(QString::fromLatin1("AWSAccessKeyId"), m_access); + u.addQueryItem(QString::fromLatin1("Operation"), QString::fromLatin1("ItemSearch")); + u.addQueryItem(QString::fromLatin1("ResponseGroup"), QString::fromLatin1("Large")); + u.addQueryItem(QString::fromLatin1("ItemPage"), QString::number(m_page)); + u.addQueryItem(QString::fromLatin1("Version"), QString::fromLatin1("2007-10-29")); + + const int type = Kernel::self()->collectionType(); + switch(type) { + case Data::Collection::Book: + case Data::Collection::ComicBook: + case Data::Collection::Bibtex: + u.addQueryItem(QString::fromLatin1("SearchIndex"), QString::fromLatin1("Books")); + u.addQueryItem(QString::fromLatin1("SortIndex"), QString::fromLatin1("relevancerank")); + break; + + case Data::Collection::Album: + u.addQueryItem(QString::fromLatin1("SearchIndex"), QString::fromLatin1("Music")); + break; + + case Data::Collection::Video: + u.addQueryItem(QString::fromLatin1("SearchIndex"), QString::fromLatin1("Video")); + u.addQueryItem(QString::fromLatin1("SortIndex"), QString::fromLatin1("relevancerank")); + break; + + case Data::Collection::Game: + u.addQueryItem(QString::fromLatin1("SearchIndex"), QString::fromLatin1("VideoGames")); + break; + + case Data::Collection::Coin: + case Data::Collection::Stamp: + case Data::Collection::Wine: + case Data::Collection::Base: + case Data::Collection::Card: + default: + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + // I have not been able to find any documentation about what character set to use + // when URL encoding the search term in the Amazon REST interface. But I do know + // that utf8 DOES NOT WORK. So I'm arbitrarily using iso-8859-1, except for JP. + // Why different for JP? Well, I've not received any bug reports from that direction yet + +// QString value = KURL::decode_string(value_, 106); +// QString value = QString::fromLocal8Bit(value_.utf8()); + QString value = m_value; + // a mibenum of 106 is utf-8, 4 is iso-8859-1, 0 means use user's locale, + int mib = m_site == AmazonFetcher::JP ? 106 : 4; + + switch(m_key) { + case Title: + u.addQueryItem(QString::fromLatin1("Title"), value, mib); + break; + + case Person: + if(type == Data::Collection::Video) { + u.addQueryItem(QString::fromLatin1("Actor"), value, mib); + u.addQueryItem(QString::fromLatin1("Director"), value, mib); + } else if(type == Data::Collection::Album) { + u.addQueryItem(QString::fromLatin1("Artist"), value, mib); + } else if(type == Data::Collection::Game) { + u.addQueryItem(QString::fromLatin1("Manufacturer"), value, mib); + } else { // books and bibtex + QString s = QString::fromLatin1("author:%1 or publisher:%2").arg(value, value); +// u.addQueryItem(QString::fromLatin1("Author"), value, mib); +// u.addQueryItem(QString::fromLatin1("Publisher"), value, mib); + u.addQueryItem(QString::fromLatin1("Power"), s, mib); + } + break; + + case ISBN: + { + u.removeQueryItem(QString::fromLatin1("Operation")); + u.addQueryItem(QString::fromLatin1("Operation"), QString::fromLatin1("ItemLookup")); + + QString s = m_value; // not encValue!!! + s.remove('-'); + // ISBN only get digits or 'X', and multiple values are connected with "; " + QStringList isbns = QStringList::split(QString::fromLatin1("; "), s); + // Amazon isbn13 search is still very flaky, so if possible, we're going to convert + // all of them to isbn10. If we run into a 979 isbn13, then we're forced to do an + // isbn13 search + bool isbn13 = false; + for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ) { + if(m_value.startsWith(QString::fromLatin1("979"))) { + if(m_site == JP) { // never works for JP + kdWarning() << "AmazonFetcher:doSearch() - ISBN-13 searching not implemented for Japan" << endl; + isbns.remove(it); // automatically skips to next + continue; + } + isbn13 = true; + break; + } + ++it; + } + // if we want isbn10, then convert all + if(!isbn13) { + for(QStringList::Iterator it = isbns.begin(); it != isbns.end(); ++it) { + if((*it).length() > 12) { + (*it) = ISBNValidator::isbn10(*it); + (*it).remove('-'); + } + } + // the default search is by ASIN, which prohibits SearchIndex + u.removeQueryItem(QString::fromLatin1("SearchIndex")); + } + // limit to first 10 + while(isbns.size() > 10) { + isbns.pop_back(); + } + u.addQueryItem(QString::fromLatin1("ItemId"), isbns.join(QString::fromLatin1(","))); + if(isbn13) { + u.addQueryItem(QString::fromLatin1("IdType"), QString::fromLatin1("EAN")); + } + } + break; + + case UPC: + { + u.removeQueryItem(QString::fromLatin1("Operation")); + u.addQueryItem(QString::fromLatin1("Operation"), QString::fromLatin1("ItemLookup")); + // US allows UPC, all others are EAN + if(m_site == US) { + u.addQueryItem(QString::fromLatin1("IdType"), QString::fromLatin1("UPC")); + } else { + u.addQueryItem(QString::fromLatin1("IdType"), QString::fromLatin1("EAN")); + } + QString s = m_value; // not encValue!!! + s.remove('-'); + // limit to first 10 + s.replace(QString::fromLatin1("; "), QString::fromLatin1(",")); + s = s.section(',', 0, 9); + u.addQueryItem(QString::fromLatin1("ItemId"), s); + } + break; + + case Keyword: + u.addQueryItem(QString::fromLatin1("Keywords"), m_value, mib); + break; + + case Raw: + { + QString key = value.section('=', 0, 0).stripWhiteSpace(); + QString str = value.section('=', 1).stripWhiteSpace(); + u.addQueryItem(key, str, mib); + } + break; + + default: + kdWarning() << "AmazonFetcher::search() - key not recognized: " << m_key << endl; + stop(); + return; + } +// myDebug() << "AmazonFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void AmazonFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "AmazonFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void AmazonFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void AmazonFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "AmazonFetcher::slotComplete()" << endl; + + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "AmazonFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from amazonfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test%1.xml").arg(m_page)); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + QStringList errors; + if(m_total == -1) { + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "AmazonFetcher::slotComplete() - server did not return valid XML." << endl; + stop(); + return; + } + // find TotalResults element + // it's in the first level under the root element + //ItemSearchResponse/Items/TotalResults + QDomNode n = dom.documentElement().namedItem(QString::fromLatin1("Items")) + .namedItem(QString::fromLatin1("TotalResults")); + QDomElement e = n.toElement(); + if(!e.isNull()) { + m_total = e.text().toInt(); + } + n = dom.documentElement().namedItem(QString::fromLatin1("Items")) + .namedItem(QString::fromLatin1("Request")) + .namedItem(QString::fromLatin1("Errors")); + e = n.toElement(); + if(!e.isNull()) { + QDomNodeList nodes = e.elementsByTagName(QString::fromLatin1("Error")); + for(uint i = 0; i < nodes.count(); ++i) { + e = nodes.item(i).toElement().namedItem(QString::fromLatin1("Code")).toElement(); + if(!e.isNull() && e.text() == Latin1Literal("AWS.ECommerceService.NoExactMatches")) { + // no exact match, not a real error, so skip + continue; + } + // for some reason, Amazon will return an error simply when a valid ISBN is not found + // I really want to ignore that, so check the IsValid element in the Request element + QDomNode isValidNode = n.parentNode().namedItem(QString::fromLatin1("IsValid")); + if(m_key == ISBN && isValidNode.toElement().text().lower() == Latin1Literal("true")) { + continue; + } + e = nodes.item(i).toElement().namedItem(QString::fromLatin1("Message")).toElement(); + if(!e.isNull()) { + errors << e.text(); + } + } + } + } + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + +// QRegExp stripHTML(QString::fromLatin1("<.*>"), true); +// stripHTML.setMinimal(true); + + // assume amazon is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(!coll) { + myDebug() << "AmazonFetcher::slotComplete() - no collection pointer" << endl; + stop(); + return; + } + + if(!m_addLinkField) { + // remove amazon field if it's not to be added + coll->removeField(QString::fromLatin1("amazon")); + } + + Data::EntryVec entries = coll->entries(); + if(entries.isEmpty() && !errors.isEmpty()) { + for(QStringList::ConstIterator it = errors.constBegin(); it != errors.constEnd(); ++it) { + myDebug() << "AmazonFetcher::" << *it << endl; + } + message(errors[0], MessageHandler::Error); + stop(); + return; + } + + int count = 0; + for(Data::EntryVec::Iterator entry = entries.begin(); + m_numResults < m_limit && entry != entries.end(); + ++entry, ++count) { + if(count < m_countOffset) { + continue; + } + if(!m_started) { + // might get aborted + break; + } + + // special case book author + // amazon is really bad about not putting spaces after periods + if(coll->type() == Data::Collection::Book) { + QRegExp rx(QString::fromLatin1("\\.([^\\s])")); + QStringList values = entry->fields(QString::fromLatin1("author"), false); + for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { + (*it).replace(rx, QString::fromLatin1(". \\1")); + } + entry->setField(QString::fromLatin1("author"), values.join(QString::fromLatin1("; "))); + } + + // UK puts the year in the title for some reason + if(m_site == UK && coll->type() == Data::Collection::Video) { + QRegExp rx(QString::fromLatin1("\\[(\\d{4})\\]")); + QString t = entry->title(); + if(t.find(rx) > -1) { + QString y = rx.cap(1); + t.remove(rx).simplifyWhiteSpace(); + entry->setField(QString::fromLatin1("title"), t); + if(entry->field(QString::fromLatin1("year")).isEmpty()) { + entry->setField(QString::fromLatin1("year"), y); + } + } + } + + QString desc; + switch(coll->type()) { + case Data::Collection::Book: + case Data::Collection::ComicBook: + case Data::Collection::Bibtex: + desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + break; + + case Data::Collection::Video: + desc = entry->field(QString::fromLatin1("studio")) + + QChar('/') + + entry->field(QString::fromLatin1("director")) + + QChar('/') + + entry->field(QString::fromLatin1("year")) + + QChar('/') + + entry->field(QString::fromLatin1("medium")); + break; + + case Data::Collection::Album: + desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + case Data::Collection::Game: + desc = entry->field(QString::fromLatin1("platform")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + default: + break; + } + + // strip HTML from comments, or plot in movies + // tentatively don't do this, looks like ECS 4 cleaned everything up +/* + if(coll->type() == Data::Collection::Video) { + QString plot = entry->field(QString::fromLatin1("plot")); + plot.remove(stripHTML); + entry->setField(QString::fromLatin1("plot"), plot); + } else if(coll->type() == Data::Collection::Game) { + QString desc = entry->field(QString::fromLatin1("description")); + desc.remove(stripHTML); + entry->setField(QString::fromLatin1("description"), desc); + } else { + QString comments = entry->field(QString::fromLatin1("comments")); + comments.remove(stripHTML); + entry->setField(QString::fromLatin1("comments"), comments); + } +*/ +// myDebug() << "AmazonFetcher::slotComplete() - " << entry->title() << endl; + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + ++m_numResults; + } + + // we might have gotten aborted + if(!m_started) { + return; + } + + // are there any additional results to get? + m_hasMoreResults = m_page * AMAZON_RETURNS_PER_REQUEST < m_total; + + const int currentTotal = QMIN(m_total, m_limit); + if(m_page * AMAZON_RETURNS_PER_REQUEST < currentTotal) { + int foundCount = (m_page-1) * AMAZON_RETURNS_PER_REQUEST + coll->entryCount(); + message(i18n("Results from %1: %2/%3").arg(source()).arg(foundCount).arg(m_total), MessageHandler::Status); + ++m_page; + m_countOffset = 0; + doSearch(); + } else if(m_value.contains(';') > 9) { + search(m_key, m_value.section(';', 10)); + } else { + m_countOffset = m_entries.count() % AMAZON_RETURNS_PER_REQUEST; + if(m_countOffset == 0) { + ++m_page; // need to go to next page + } + stop(); + } +} + +Tellico::Data::EntryPtr AmazonFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + if(!entry) { + kdWarning() << "AmazonFetcher::fetchEntry() - no entry in dict" << endl; + return 0; + } + + QStringList defaultFields = customFields().keys(); + for(QStringList::Iterator it = defaultFields.begin(); it != defaultFields.end(); ++it) { + if(!m_fields.contains(*it)) { + entry->setField(*it, QString::null); + } + } + + // do what we can to remove useless keywords + const int type = Kernel::self()->collectionType(); + switch(type) { + case Data::Collection::Book: + case Data::Collection::ComicBook: + case Data::Collection::Bibtex: + { + const QString keywords = QString::fromLatin1("keyword"); + QStringList oldWords = entry->fields(keywords, false); + StringSet words; + for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { + // the amazon2tellico stylesheet separates keywords with '/' + QStringList nodes = QStringList::split('/', *it); + for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { + if(*it2 == Latin1Literal("General") || + *it2 == Latin1Literal("Subjects") || + *it2 == Latin1Literal("Par prix") || // french stuff + *it2 == Latin1Literal("Divers") || // french stuff + (*it2).startsWith(QChar('(')) || + (*it2).startsWith(QString::fromLatin1("Authors"))) { + continue; + } + words.add(*it2); + } + } + entry->setField(keywords, words.toList().join(QString::fromLatin1("; "))); + } + entry->setField(QString::fromLatin1("comments"), Tellico::decodeHTML(entry->field(QString::fromLatin1("comments")))); + break; + + case Data::Collection::Video: + { + const QString genres = QString::fromLatin1("genre"); + QStringList oldWords = entry->fields(genres, false); + StringSet words; + // only care about genres that have "Genres" in the amazon response + // and take the first word after that + for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { + if((*it).find(QString::fromLatin1("Genres")) == -1) { + continue; + } + + // the amazon2tellico stylesheet separates words with '/' + QStringList nodes = QStringList::split('/', *it); + for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { + if(*it2 != Latin1Literal("Genres")) { + continue; + } + ++it2; + if(it2 != nodes.end() && *it2 != Latin1Literal("General")) { + words.add(*it2); + } + break; // we're done + } + } + entry->setField(genres, words.toList().join(QString::fromLatin1("; "))); + // language tracks get duplicated, too + QStringList langs = entry->fields(QString::fromLatin1("language"), false); + words.clear(); + for(QStringList::ConstIterator it = langs.begin(); it != langs.end(); ++it) { + words.add(*it); + } + entry->setField(QString::fromLatin1("language"), words.toList().join(QString::fromLatin1("; "))); + } + entry->setField(QString::fromLatin1("plot"), Tellico::decodeHTML(entry->field(QString::fromLatin1("plot")))); + break; + + case Data::Collection::Album: + { + const QString genres = QString::fromLatin1("genre"); + QStringList oldWords = entry->fields(genres, false); + StringSet words; + // only care about genres that have "Styles" in the amazon response + // and take the first word after that + for(QStringList::Iterator it = oldWords.begin(); it != oldWords.end(); ++it) { + if((*it).find(QString::fromLatin1("Styles")) == -1) { + continue; + } + + // the amazon2tellico stylesheet separates words with '/' + QStringList nodes = QStringList::split('/', *it); + bool isStyle = false; + for(QStringList::Iterator it2 = nodes.begin(); it2 != nodes.end(); ++it2) { + if(!isStyle) { + if(*it2 == Latin1Literal("Styles")) { + isStyle = true; + } + continue; + } + if(*it2 != Latin1Literal("General")) { + words.add(*it2); + } + } + } + entry->setField(genres, words.toList().join(QString::fromLatin1("; "))); + } + entry->setField(QString::fromLatin1("comments"), Tellico::decodeHTML(entry->field(QString::fromLatin1("comments")))); + break; + + case Data::Collection::Game: + entry->setField(QString::fromLatin1("description"), Tellico::decodeHTML(entry->field(QString::fromLatin1("description")))); + break; + } + + // clean up the title + parseTitle(entry, type); + + // also sometimes table fields have rows but no values + Data::FieldVec fields = entry->collection()->fields(); + QRegExp blank(QString::fromLatin1("[\\s:;]+")); // only white space, column separators and row separators + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + if(fIt->type() != Data::Field::Table) { + continue; + } + if(blank.exactMatch(entry->field(fIt))) { + entry->setField(fIt, QString::null); + } + } + + KURL imageURL; + switch(m_imageSize) { + case SmallImage: + imageURL = entry->field(QString::fromLatin1("small-image")); + break; + case MediumImage: + imageURL = entry->field(QString::fromLatin1("medium-image")); + break; + case LargeImage: + imageURL = entry->field(QString::fromLatin1("large-image")); + break; + case NoImage: + default: + break; + } +// myDebug() << "AmazonFetcher::fetchEntry() - grabbing " << imageURL.prettyURL() << endl; + if(!imageURL.isEmpty()) { + QString id = ImageFactory::addImage(imageURL, true); + // FIXME: need to add cover image field to bibtex collection + if(id.isEmpty()) { + message(i18n("The cover image could not be loaded."), MessageHandler::Warning); + } else { // amazon serves up 1x1 gifs occasionally, but that's caught in the image constructor + // all relevant collection types have cover fields + entry->setField(QString::fromLatin1("cover"), id); + } + } + + // don't want to show image urls in the fetch dialog + entry->setField(QString::fromLatin1("small-image"), QString::null); + entry->setField(QString::fromLatin1("medium-image"), QString::null); + entry->setField(QString::fromLatin1("large-image"), QString::null); + return entry; +} + +void AmazonFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("amazon2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "AmazonFetcher::initXSLTHandler() - can not locate amazon2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "AmazonFetcher::initXSLTHandler() - error in amazon2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +void AmazonFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "AmazonFetcher::updateEntry()" << endl; + + int type = entry_->collection()->type(); + if(type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex) { + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(!isbn.isEmpty()) { + m_limit = 5; // no need for more + search(Fetch::ISBN, isbn); + return; + } + } else if(type == Data::Collection::Album) { + QString a = entry_->field(QString::fromLatin1("artist")); + if(!a.isEmpty()) { + search(Fetch::Person, a); + return; + } + } + + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "AmazonFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +void AmazonFetcher::parseTitle(Data::EntryPtr entry, int collType) { + Q_UNUSED(collType); + // assume that everything in brackets or parentheses is extra + QRegExp rx(QString::fromLatin1("[\\(\\[](.*)[\\)\\]]")); + rx.setMinimal(true); + QString title = entry->field(QString::fromLatin1("title")); + int pos = rx.search(title); + while(pos > -1) { + if(parseTitleToken(entry, rx.cap(1))) { + title.remove(pos, rx.matchedLength()); + --pos; // search again there + } + pos = rx.search(title, pos+1); + } + entry->setField(QString::fromLatin1("title"), title.stripWhiteSpace()); +} + +bool AmazonFetcher::parseTitleToken(Data::EntryPtr entry, const QString& token) { + // if res = true, then the token gets removed from the title + bool res = false; + if(token.find(QString::fromLatin1("widescreen"), 0, false /* case-insensitive*/) > -1 || + token.find(i18n("Widescreen"), 0, false) > -1) { + entry->setField(QString::fromLatin1("widescreen"), QString::fromLatin1("true")); + // res = true; leave it in the title + } else if(token.find(QString::fromLatin1("full screen"), 0, false) > -1) { + // skip, but go ahead and remove from title + res = true; + } + if(token.find(QString::fromLatin1("blu-ray"), 0, false) > -1) { + entry->setField(QString::fromLatin1("medium"), i18n("Blu-ray")); + res = true; + } else if(token.find(QString::fromLatin1("hd dvd"), 0, false) > -1) { + entry->setField(QString::fromLatin1("medium"), i18n("HD DVD")); + res = true; + } + if(token.find(QString::fromLatin1("director's cut"), 0, false) > -1 || + token.find(i18n("Director's Cut"), 0, false) > -1) { + entry->setField(QString::fromLatin1("directors-cut"), QString::fromLatin1("true")); + // res = true; leave it in the title + } + return res; +} + +Tellico::Fetch::ConfigWidget* AmazonFetcher::configWidget(QWidget* parent_) const { + return new AmazonFetcher::ConfigWidget(parent_, this); +} + +AmazonFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 4, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + QLabel* label = new QLabel(i18n("Co&untry: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_siteCombo = new GUI::ComboBox(optionsWidget()); + m_siteCombo->insertItem(i18n("United States"), US); + m_siteCombo->insertItem(i18n("United Kingdom"), UK); + m_siteCombo->insertItem(i18n("Germany"), DE); + m_siteCombo->insertItem(i18n("Japan"), JP); + m_siteCombo->insertItem(i18n("France"), FR); + m_siteCombo->insertItem(i18n("Canada"), CA); + connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_siteCombo, SIGNAL(activated(int)), SLOT(slotSiteChanged())); + l->addWidget(m_siteCombo, row, 1); + QString w = i18n("Amazon.com provides data from several different localized sites. Choose the one " + "you wish to use for this data source."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_siteCombo, w); + label->setBuddy(m_siteCombo); + + label = new QLabel(i18n("&Image size: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_imageCombo = new GUI::ComboBox(optionsWidget()); + m_imageCombo->insertItem(i18n("Small Image"), SmallImage); + m_imageCombo->insertItem(i18n("Medium Image"), MediumImage); + m_imageCombo->insertItem(i18n("Large Image"), LargeImage); + m_imageCombo->insertItem(i18n("No Image"), NoImage); + connect(m_imageCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + l->addWidget(m_imageCombo, row, 1); + w = i18n("The cover image may be downloaded as well. However, too many large images in the " + "collection may degrade performance."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_imageCombo, w); + label->setBuddy(m_imageCombo); + + label = new QLabel(i18n("&Associate's ID: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_assocEdit = new KLineEdit(optionsWidget()); + connect(m_assocEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_assocEdit, row, 1); + w = i18n("The associate's id identifies the person accessing the Amazon.com Web Services, and is included " + "in any links to the Amazon.com site."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_assocEdit, w); + label->setBuddy(m_assocEdit); + + l->setRowStretch(++row, 10); + + if(fetcher_) { + m_siteCombo->setCurrentData(fetcher_->m_site); + m_assocEdit->setText(fetcher_->m_assoc); + m_imageCombo->setCurrentData(fetcher_->m_imageSize); + } else { // defaults + m_assocEdit->setText(QString::fromLatin1(AMAZON_ASSOC_TOKEN)); + m_imageCombo->setCurrentData(MediumImage); + } + + addFieldsWidget(AmazonFetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); + + KAcceleratorManager::manage(optionsWidget()); +} + +void AmazonFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + int n = m_siteCombo->currentData().toInt(); + config_.writeEntry("Site", n); + QString s = m_assocEdit->text().stripWhiteSpace(); + if(!s.isEmpty()) { + config_.writeEntry("AssocToken", s); + } + n = m_imageCombo->currentData().toInt(); + config_.writeEntry("Image Size", n); + + saveFieldsConfig(config_); + slotSetModified(false); +} + +QString AmazonFetcher::ConfigWidget::preferredName() const { + return AmazonFetcher::siteData(m_siteCombo->currentData().toInt()).title; +} + +void AmazonFetcher::ConfigWidget::slotSiteChanged() { + emit signalName(preferredName()); +} + +//static +Tellico::StringMap AmazonFetcher::customFields() { + StringMap map; + map[QString::fromLatin1("keyword")] = i18n("Keywords"); + return map; +} + +#include "amazonfetcher.moc" diff --git a/src/fetch/amazonfetcher.h b/src/fetch/amazonfetcher.h new file mode 100644 index 0000000..05df8d7 --- /dev/null +++ b/src/fetch/amazonfetcher.h @@ -0,0 +1,158 @@ +/*************************************************************************** + copyright : (C) 2004-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; * + * * + ***************************************************************************/ + +#ifndef AMAZONFETCHER_H +#define AMAZONFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kurl.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +class KLineEdit; + +class QCheckBox; +class QLabel; + +namespace KIO { + class Job; +} + +namespace Tellico { + + class XSLTHandler; + namespace GUI { + class ComboBox; + } + + namespace Fetch { + +/** + * A fetcher for Amazon.com. + * + * @author Robby Stephenson + */ +class AmazonFetcher : public Fetcher { +Q_OBJECT + +public: + enum Site { + Unknown = -1, + US = 0, + UK = 1, + DE = 2, + JP = 3, + FR = 4, + CA = 5 + }; + + enum ImageSize { + SmallImage=0, + MediumImage=1, + LargeImage=2, + NoImage=3 + }; + + AmazonFetcher(Site site, QObject* parent, const char* name = 0); + virtual ~AmazonFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + // amazon can search title, person, isbn, or keyword. No Raw for now. + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == UPC || k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Amazon; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + struct SiteData { + QString title; + KURL url; + }; + static const SiteData& siteData(int site); + + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const ; + + static StringMap customFields(); + + class ConfigWidget; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + void doSearch(); + void parseTitle(Data::EntryPtr entry, int collType); + bool parseTitleToken(Data::EntryPtr entry, const QString& token); + + XSLTHandler* m_xsltHandler; + Site m_site; + ImageSize m_imageSize; + + QString m_access; + QString m_assoc; + bool m_addLinkField; + int m_limit; + int m_countOffset; + + QByteArray m_data; + int m_page; + int m_total; + int m_numResults; + QMap<int, Data::EntryPtr> m_entries; // they get modified after collection is created, so can't be const + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; + QStringList m_fields; +}; + +class AmazonFetcher::ConfigWidget : public Fetch::ConfigWidget { +Q_OBJECT + +public: + ConfigWidget(QWidget* parent_, const AmazonFetcher* fetcher = 0); + + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + +private slots: + void slotSiteChanged(); + +private: + KLineEdit* m_assocEdit; + GUI::ComboBox* m_siteCombo; + GUI::ComboBox* m_imageCombo; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/animenfofetcher.cpp b/src/fetch/animenfofetcher.cpp new file mode 100644 index 0000000..728c583 --- /dev/null +++ b/src/fetch/animenfofetcher.cpp @@ -0,0 +1,378 @@ +/*************************************************************************** + copyright : (C) 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 "animenfofetcher.h" +#include "messagehandler.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collections/videocollection.h" +#include "../entry.h" +#include "../filehandler.h" +#include "../latin1literal.h" +#include "../imagefactory.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kio/job.h> + +#include <qregexp.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qfile.h> + +//#define ANIMENFO_TEST + +namespace { + static const char* ANIMENFO_BASE_URL = "http://www.animenfo.com/search.php"; +} + +using Tellico::Fetch::AnimeNfoFetcher; + +AnimeNfoFetcher::AnimeNfoFetcher(QObject* parent_, const char* name_ /*=0*/) + : Fetcher(parent_, name_), m_started(false) { +} + +QString AnimeNfoFetcher::defaultName() { + return QString::fromLatin1("AnimeNfo.com"); +} + +QString AnimeNfoFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool AnimeNfoFetcher::canFetch(int type) const { + return type == Data::Collection::Video; +} + +void AnimeNfoFetcher::readConfigHook(const KConfigGroup& config_) { + Q_UNUSED(config_); +} + +void AnimeNfoFetcher::search(FetchKey key_, const QString& value_) { + m_started = true; + m_matches.clear(); + +#ifdef ANIMENFO_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/animenfo.html")); +#else + KURL u(QString::fromLatin1(ANIMENFO_BASE_URL)); + u.addQueryItem(QString::fromLatin1("action"), QString::fromLatin1("Go")); + u.addQueryItem(QString::fromLatin1("option"), QString::fromLatin1("keywords")); + u.addQueryItem(QString::fromLatin1("queryin"), QString::fromLatin1("anime_titles")); + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + switch(key_) { + case Keyword: + u.addQueryItem(QString::fromLatin1("query"), value_); + break; + + default: + kdWarning() << "AnimeNfoFetcher::search() - key not recognized: " << key_ << endl; + stop(); + return; + } +#endif +// myDebug() << "AnimeNfoFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void AnimeNfoFetcher::stop() { + if(!m_started) { + return; + } + + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void AnimeNfoFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void AnimeNfoFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "AnimeNfoFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "AnimeNfoFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + + QString s = Tellico::decodeHTML(QString(m_data)); + + QRegExp infoRx(QString::fromLatin1("<td\\s+[^>]*class\\s*=\\s*[\"']anime_info[\"'][^>]*>(.*)</td>"), false); + infoRx.setMinimal(true); + QRegExp anchorRx(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)</a>"), false); + anchorRx.setMinimal(true); + QRegExp yearRx(QString::fromLatin1("\\d{4}"), false); + + // search page comes in groups of threes + int n = 0; + QString u, t, y; + + for(int pos = infoRx.search(s); m_started && pos > -1; pos = infoRx.search(s, pos+1)) { + if(n == 0 && !u.isEmpty()) { + SearchResult* r = new SearchResult(this, t, y, QString()); + emit signalResultFound(r); + +#ifdef ANIMENFO_TEST + KURL url = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/animetitle.html")); +#else + KURL url(QString::fromLatin1(ANIMENFO_BASE_URL), u); + url.setQuery(QString::null); +#endif + m_matches.insert(r->uid, url); + + u.truncate(0); + t.truncate(0); + y.truncate(0); + } + switch(n) { + case 0: // title and url + { + int pos2 = anchorRx.search(infoRx.cap(1)); + if(pos2 > -1) { + u = anchorRx.cap(1); + t = anchorRx.cap(2); + } + } + break; + case 1: // don't case + break; + case 2: + if(yearRx.exactMatch(infoRx.cap(1))) { + y = infoRx.cap(1); + } + break; + } + + n = (n+1)%3; + } + + // grab last response +#ifndef ANIMENFO_TEST + if(!u.isEmpty()) { + SearchResult* r = new SearchResult(this, t, y, QString()); + emit signalResultFound(r); + KURL url(QString::fromLatin1(ANIMENFO_BASE_URL), u); + url.setQuery(QString::null); + m_matches.insert(r->uid, url); + } +#endif + stop(); +} + +Tellico::Data::EntryPtr AnimeNfoFetcher::fetchEntry(uint uid_) { + // if we already grabbed this one, then just pull it out of the dict + Data::EntryPtr entry = m_entries[uid_]; + if(entry) { + return entry; + } + + KURL url = m_matches[uid_]; + if(url.isEmpty()) { + kdWarning() << "AnimeNfoFetcher::fetchEntry() - no url in map" << endl; + return 0; + } + + QString results = Tellico::decodeHTML(FileHandler::readTextFile(url, true)); + if(results.isEmpty()) { + myDebug() << "AnimeNfoFetcher::fetchEntry() - no text results" << endl; + return 0; + } + +#if 0 + kdWarning() << "Remove debug from animenfofetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.html")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << results; + } + f.close(); +#endif + + entry = parseEntry(results); + if(!entry) { + myDebug() << "AnimeNfoFetcher::fetchEntry() - error in processing entry" << endl; + return 0; + } + m_entries.insert(uid_, entry); // keep for later + return entry; +} + +Tellico::Data::EntryPtr AnimeNfoFetcher::parseEntry(const QString& str_) { + // myDebug() << "AnimeNfoFetcher::parseEntry()" << endl; + // class might be anime_info_top + QRegExp infoRx(QString::fromLatin1("<td\\s+[^>]*class\\s*=\\s*[\"']anime_info[^>]*>(.*)</td>"), false); + infoRx.setMinimal(true); + QRegExp tagRx(QString::fromLatin1("<.*>")); + tagRx.setMinimal(true); + QRegExp anchorRx(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*[\"'](.*)[\"'][^>]*>(.*)</a>"), false); + anchorRx.setMinimal(true); + QRegExp jsRx(QString::fromLatin1("<script.*</script>"), false); + jsRx.setMinimal(true); + + QString s = str_; + s.remove(jsRx); + + Data::CollPtr coll = new Data::VideoCollection(true); + + // add new fields + Data::FieldPtr f = new Data::Field(QString::fromLatin1("origtitle"), i18n("Original Title")); + coll->addField(f); + + f = new Data::Field(QString::fromLatin1("alttitle"), i18n("Alternative Titles"), Data::Field::Table); + f->setFormatFlag(Data::Field::FormatTitle); + coll->addField(f); + + f = new Data::Field(QString::fromLatin1("distributor"), i18n("Distributor")); + f->setCategory(i18n("Other People")); + f->setFlags(Data::Field::AllowCompletion | Data::Field::AllowMultiple | Data::Field::AllowGrouped); + f->setFormatFlag(Data::Field::FormatPlain); + coll->addField(f); + + f = new Data::Field(QString::fromLatin1("episodes"), i18n("Episodes"), Data::Field::Number); + f->setCategory(i18n("Features")); + coll->addField(f); + + // map captions in HTML to field names + QMap<QString, QString> fieldMap; + fieldMap.insert(QString::fromLatin1("Title"), QString::fromLatin1("title")); + fieldMap.insert(QString::fromLatin1("Japanese Title"), QString::fromLatin1("origtitle")); + fieldMap.insert(QString::fromLatin1("Total Episodes"), QString::fromLatin1("episodes")); + fieldMap.insert(QString::fromLatin1("Genres"), QString::fromLatin1("genre")); + fieldMap.insert(QString::fromLatin1("Year Published"), QString::fromLatin1("year")); + fieldMap.insert(QString::fromLatin1("Studio"), QString::fromLatin1("studio")); + fieldMap.insert(QString::fromLatin1("US Distribution"), QString::fromLatin1("distributor")); + + Data::EntryPtr entry = new Data::Entry(coll); + + int n = 0; + QString key, value; + int oldpos = -1; + for(int pos = infoRx.search(s); pos > -1; pos = infoRx.search(s, pos+1)) { + if(n == 0 && !key.isEmpty()) { + if(fieldMap.contains(key)) { + value = value.simplifyWhiteSpace(); + if(value.length() > 2) { // might be "-" + if(key == Latin1Literal("Genres")) { + entry->setField(fieldMap[key], QStringList::split(QRegExp(QString::fromLatin1("\\s*,\\s*")), + value).join(QString::fromLatin1("; "))); + } else { + entry->setField(fieldMap[key], value); + } + } + } + key.truncate(0); + value.truncate(0); + } + switch(n) { + case 0: + key = infoRx.cap(1).remove(tagRx); + break; + case 1: + value = infoRx.cap(1).remove(tagRx); + break; + } + n = (n+1)%2; + oldpos = pos; + } + + // image + QRegExp imgRx(QString::fromLatin1("<img\\s+[^>]*src\\s*=\\s*[\"']([^>]*)[\"']\\s+[^>]*alt\\s*=\\s*[\"']%1[\"']") + .arg(entry->field(QString::fromLatin1("title"))), false); + imgRx.setMinimal(true); + int pos = imgRx.search(s); + if(pos > -1) { + KURL imgURL(QString::fromLatin1(ANIMENFO_BASE_URL), imgRx.cap(1)); + QString id = ImageFactory::addImage(imgURL, true); + if(!id.isEmpty()) { + entry->setField(QString::fromLatin1("cover"), id); + } + } + + // now look for alternative titles and plot + const QString a = QString::fromLatin1("Alternative titles"); + pos = s.find(a, oldpos+1, false); + if(pos > -1) { + pos += a.length(); + } + int pos2 = -1; + if(pos > -1) { + pos2 = s.find(QString::fromLatin1("Description"), pos+1, true); + if(pos2 > -1) { + value = s.mid(pos, pos2-pos).remove(tagRx).simplifyWhiteSpace(); + entry->setField(QString::fromLatin1("alttitle"), value); + } + } + QRegExp descRx(QString::fromLatin1("class\\s*=\\s*[\"']description[\"'][^>]*>(.*)<"), false); + descRx.setMinimal(true); + pos = descRx.search(s, QMAX(pos, pos2)); + if(pos > -1) { + entry->setField(QString::fromLatin1("plot"), descRx.cap(1).simplifyWhiteSpace()); + } + + return entry; +} + +void AnimeNfoFetcher::updateEntry(Data::EntryPtr entry_) { + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Keyword, t); + return; + } + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* AnimeNfoFetcher::configWidget(QWidget* parent_) const { + return new AnimeNfoFetcher::ConfigWidget(parent_); +} + +AnimeNfoFetcher::ConfigWidget::ConfigWidget(QWidget* parent_) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +QString AnimeNfoFetcher::ConfigWidget::preferredName() const { + return AnimeNfoFetcher::defaultName(); +} + +#include "animenfofetcher.moc" diff --git a/src/fetch/animenfofetcher.h b/src/fetch/animenfofetcher.h new file mode 100644 index 0000000..7e4028e --- /dev/null +++ b/src/fetch/animenfofetcher.h @@ -0,0 +1,86 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_ANIMENFOFETCHER_H +#define TELLICO_FETCH_ANIMENFOFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * A fetcher for animenfo.com + * + * @author Robby Stephenson + */ +class AnimeNfoFetcher : public Fetcher { +Q_OBJECT + +public: + AnimeNfoFetcher(QObject* parent, const char* name = 0); + virtual ~AnimeNfoFetcher() {} + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + // only keyword search + virtual bool canSearch(FetchKey k) const { return k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return AnimeNfo; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_); + virtual void saveConfig(KConfigGroup&) {} + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + Data::EntryPtr parseEntry(const QString& str); + + QByteArray m_data; + int m_total; + QMap<int, Data::EntryPtr> m_entries; + QMap<int, KURL> m_matches; + QGuardedPtr<KIO::Job> m_job; + + bool m_started; +// QStringList m_fields; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/arxivfetcher.cpp b/src/fetch/arxivfetcher.cpp new file mode 100644 index 0000000..442ef30 --- /dev/null +++ b/src/fetch/arxivfetcher.cpp @@ -0,0 +1,366 @@ +/*************************************************************************** + copyright : (C) 2007 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 "arxivfetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../core/netaccess.h" +#include "../imagefactory.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> + +#include <qdom.h> +#include <qlabel.h> +#include <qlayout.h> + +//#define ARXIV_TEST + +namespace { + static const int ARXIV_RETURNS_PER_REQUEST = 20; + static const char* ARXIV_BASE_URL = "http://export.arxiv.org/api/query"; +} + +using Tellico::Fetch::ArxivFetcher; + +ArxivFetcher::ArxivFetcher(QObject* parent_) + : Fetcher(parent_), m_xsltHandler(0), m_start(0), m_job(0), m_started(false) { +} + +ArxivFetcher::~ArxivFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString ArxivFetcher::defaultName() { + return i18n("arXiv.org"); +} + +QString ArxivFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool ArxivFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void ArxivFetcher::readConfigHook(const KConfigGroup&) { +} + +void ArxivFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + m_start = 0; + m_total = -1; + doSearch(); +} + +void ArxivFetcher::continueSearch() { + m_started = true; + doSearch(); +} + +void ArxivFetcher::doSearch() { + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + m_data.truncate(0); + +// myDebug() << "ArxivFetcher::search() - value = " << value_ << endl; + + KURL u = searchURL(m_key, m_value); + if(u.isEmpty()) { + stop(); + return; + } + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void ArxivFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "ArxivFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void ArxivFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void ArxivFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "ArxivFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "ArxivFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from arxivfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + + if(m_total == -1) { + QDomDocument dom; + if(!dom.setContent(m_data, true /*namespace*/)) { + kdWarning() << "ArxivFetcher::slotComplete() - server did not return valid XML." << endl; + return; + } + // total is top level element, with attribute totalResultsAvailable + QDomNodeList list = dom.elementsByTagNameNS(QString::fromLatin1("http://a9.com/-/spec/opensearch/1.1/"), + QString::fromLatin1("totalResults")); + if(list.count() > 0) { + m_total = list.item(0).toElement().text().toInt(); + } + } + + // assume result is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + + if(!coll) { + myDebug() << "ArxivFetcher::slotComplete() - no valid result" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + + m_start = m_entries.count(); + m_hasMoreResults = m_start < m_total; + stop(); // required +} + +Tellico::Data::EntryPtr ArxivFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + // if URL but no cover image, fetch it + if(!entry->field(QString::fromLatin1("url")).isEmpty()) { + Data::CollPtr coll = entry->collection(); + Data::FieldPtr field = coll->fieldByName(QString::fromLatin1("cover")); + if(!field && !coll->imageFields().isEmpty()) { + field = coll->imageFields().front(); + } else if(!field) { + field = new Data::Field(QString::fromLatin1("cover"), i18n("Front Cover"), Data::Field::Image); + coll->addField(field); + } + if(entry->field(field).isEmpty()) { + QPixmap pix = NetAccess::filePreview(entry->field(QString::fromLatin1("url"))); + if(!pix.isNull()) { + QString id = ImageFactory::addImage(pix, QString::fromLatin1("PNG")); + if(!id.isEmpty()) { + entry->setField(field, id); + } + } + } + } + return entry; +} + +void ArxivFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("arxiv2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "ArxivFetcher::initXSLTHandler() - can not locate arxiv2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "ArxivFetcher::initXSLTHandler() - error in arxiv2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +KURL ArxivFetcher::searchURL(FetchKey key_, const QString& value_) const { + KURL u(QString::fromLatin1(ARXIV_BASE_URL)); + u.addQueryItem(QString::fromLatin1("start"), QString::number(m_start)); + u.addQueryItem(QString::fromLatin1("max_results"), QString::number(ARXIV_RETURNS_PER_REQUEST)); + + // quotes should be used if spaces are present, just use all the time + QString quotedValue = '"' + value_ + '"'; + switch(key_) { + case Title: + u.addQueryItem(QString::fromLatin1("search_query"), QString::fromLatin1("ti:%1").arg(quotedValue)); + break; + + case Person: + u.addQueryItem(QString::fromLatin1("search_query"), QString::fromLatin1("au:%1").arg(quotedValue)); + break; + + case Keyword: + // keyword gets to use all the words without being quoted + u.addQueryItem(QString::fromLatin1("search_query"), QString::fromLatin1("all:%1").arg(value_)); + break; + + case ArxivID: + { + // remove prefix and/or version number + QString value = value_; + value.remove(QRegExp(QString::fromLatin1("^arxiv:"), false)); + value.remove(QRegExp(QString::fromLatin1("v\\d+$"))); + u.addQueryItem(QString::fromLatin1("search_query"), QString::fromLatin1("id:%1").arg(value)); + } + break; + + default: + kdWarning() << "ArxivFetcher::search() - key not recognized: " << m_key << endl; + return KURL(); + } + +#ifdef ARXIV_TEST + u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/arxiv.xml")); +#endif + myDebug() << "ArxivFetcher::search() - url: " << u.url() << endl; + return u; +} + +void ArxivFetcher::updateEntry(Data::EntryPtr entry_) { + QString id = entry_->field(QString::fromLatin1("arxiv")); + if(!id.isEmpty()) { + search(Fetch::ArxivID, id); + return; + } + + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "ArxivFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +void ArxivFetcher::updateEntrySynchronous(Data::EntryPtr entry) { + if(!entry) { + return; + } + QString arxiv = entry->field(QString::fromLatin1("arxiv")); + if(arxiv.isEmpty()) { + return; + } + + KURL u = searchURL(ArxivID, arxiv); + QString xml = FileHandler::readTextFile(u, true, true); + if(xml.isEmpty()) { + return; + } + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + return; + } + } + + // assume result is always utf-8 + QString str = m_xsltHandler->applyStylesheet(xml); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(coll && coll->entryCount() > 0) { + myLog() << "ArxivFetcher::updateEntrySynchronous() - found Arxiv result, merging" << endl; + Data::Collection::mergeEntry(entry, coll->entries().front(), false /*overwrite*/); + // the arxiv id might have a version# + entry->setField(QString::fromLatin1("arxiv"), + coll->entries().front()->field(QString::fromLatin1("arxiv"))); + } +} + +Tellico::Fetch::ConfigWidget* ArxivFetcher::configWidget(QWidget* parent_) const { + return new ArxivFetcher::ConfigWidget(parent_, this); +} + +ArxivFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ArxivFetcher*) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +void ArxivFetcher::ConfigWidget::saveConfig(KConfigGroup&) { +} + +QString ArxivFetcher::ConfigWidget::preferredName() const { + return ArxivFetcher::defaultName(); +} + +#include "arxivfetcher.moc" diff --git a/src/fetch/arxivfetcher.h b/src/fetch/arxivfetcher.h new file mode 100644 index 0000000..bce5f9d --- /dev/null +++ b/src/fetch/arxivfetcher.h @@ -0,0 +1,93 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_ARXIVFETCHER_H +#define TELLICO_FETCH_ARXIVFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace Tellico { + + class XSLTHandler; + + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class ArxivFetcher : public Fetcher { +Q_OBJECT + +public: + ArxivFetcher(QObject* parent); + ~ArxivFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword || k == ArxivID; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Arxiv; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + virtual void updateEntrySynchronous(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const ArxivFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + KURL searchURL(FetchKey key, const QString& value) const; + void doSearch(); + + XSLTHandler* m_xsltHandler; + int m_start; + int m_total; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } +} +#endif diff --git a/src/fetch/bibsonomyfetcher.cpp b/src/fetch/bibsonomyfetcher.cpp new file mode 100644 index 0000000..faa48a4 --- /dev/null +++ b/src/fetch/bibsonomyfetcher.cpp @@ -0,0 +1,209 @@ +/*************************************************************************** + copyright : (C) 2007 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 "bibsonomyfetcher.h" +#include "messagehandler.h" +#include "../translators/bibteximporter.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../core/netaccess.h" +#include "../filehandler.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +#include <qlabel.h> +#include <qlayout.h> + +namespace { + // always bibtex + static const char* BIBSONOMY_BASE_URL = "http://bibsonomy.org"; + static const int BIBSONOMY_MAX_RESULTS = 20; +} + +using Tellico::Fetch::BibsonomyFetcher; + +BibsonomyFetcher::BibsonomyFetcher(QObject* parent_) + : Fetcher(parent_), m_job(0), m_started(false) { +} + +BibsonomyFetcher::~BibsonomyFetcher() { +} + +QString BibsonomyFetcher::defaultName() { + return QString::fromLatin1("Bibsonomy"); +} + +QString BibsonomyFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool BibsonomyFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void BibsonomyFetcher::readConfigHook(const KConfigGroup&) { +} + +void BibsonomyFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + m_data.truncate(0); + +// myDebug() << "BibsonomyFetcher::search() - value = " << value_ << endl; + + KURL u = searchURL(m_key, m_value); + if(u.isEmpty()) { + stop(); + return; + } + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void BibsonomyFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "BibsonomyFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void BibsonomyFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void BibsonomyFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "BibsonomyFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "BibsonomyFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + + Import::BibtexImporter imp(QString::fromUtf8(m_data, m_data.size())); + Data::CollPtr coll = imp.collection(); + + if(!coll) { + myDebug() << "BibsonomyFetcher::slotComplete() - no valid result" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + + stop(); // required +} + +Tellico::Data::EntryPtr BibsonomyFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +KURL BibsonomyFetcher::searchURL(FetchKey key_, const QString& value_) const { + KURL u(QString::fromLatin1(BIBSONOMY_BASE_URL)); + u.setPath(QString::fromLatin1("/bib/")); + + switch(key_) { + case Person: + u.addPath(QString::fromLatin1("author/%1").arg(value_)); + break; + + case Keyword: + u.addPath(QString::fromLatin1("search/%1").arg(value_)); + break; + + default: + kdWarning() << "BibsonomyFetcher::search() - key not recognized: " << m_key << endl; + return KURL(); + } + + u.addQueryItem(QString::fromLatin1("items"), QString::number(BIBSONOMY_MAX_RESULTS)); + myDebug() << "BibsonomyFetcher::search() - url: " << u.url() << endl; + return u; +} + +void BibsonomyFetcher::updateEntry(Data::EntryPtr entry_) { + QString title = entry_->field(QString::fromLatin1("title")); + if(!title.isEmpty()) { + search(Fetch::Keyword, title); + return; + } + + myDebug() << "BibsonomyFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* BibsonomyFetcher::configWidget(QWidget* parent_) const { + return new BibsonomyFetcher::ConfigWidget(parent_, this); +} + +BibsonomyFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const BibsonomyFetcher*) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +void BibsonomyFetcher::ConfigWidget::saveConfig(KConfigGroup&) { +} + +QString BibsonomyFetcher::ConfigWidget::preferredName() const { + return BibsonomyFetcher::defaultName(); +} + +#include "bibsonomyfetcher.moc" diff --git a/src/fetch/bibsonomyfetcher.h b/src/fetch/bibsonomyfetcher.h new file mode 100644 index 0000000..fc59928 --- /dev/null +++ b/src/fetch/bibsonomyfetcher.h @@ -0,0 +1,82 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_BIBSONOMYFETCHER_H +#define TELLICO_FETCH_BIBSONOMYFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class BibsonomyFetcher : public Fetcher { +Q_OBJECT + +public: + BibsonomyFetcher(QObject* parent); + ~BibsonomyFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + + virtual bool canSearch(FetchKey k) const { return k == Person || k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Bibsonomy; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const BibsonomyFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + KURL searchURL(FetchKey key, const QString& value) const; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } +} +#endif diff --git a/src/fetch/citebasefetcher.cpp b/src/fetch/citebasefetcher.cpp new file mode 100644 index 0000000..798d690 --- /dev/null +++ b/src/fetch/citebasefetcher.cpp @@ -0,0 +1,248 @@ +/*************************************************************************** + copyright : (C) 2007 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 "citebasefetcher.h" +#include "messagehandler.h" +#include "../translators/bibteximporter.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../core/netaccess.h" +#include "../filehandler.h" +#include "../tellico_debug.h" + +#include <klocale.h> + +#include <qlabel.h> +#include <qlayout.h> + +// #define CITEBASE_TEST + +namespace { + // always bibtex + static const char* CITEBASE_BASE_URL = "http://www.citebase.org/openurl/?url_ver=Z39.88-2004&svc_id=bibtex"; +} + +using Tellico::Fetch::CitebaseFetcher; + +CitebaseFetcher::CitebaseFetcher(QObject* parent_) + : Fetcher(parent_), m_job(0), m_started(false) { +} + +CitebaseFetcher::~CitebaseFetcher() { +} + +QString CitebaseFetcher::defaultName() { + return QString::fromLatin1("Citebase"); +} + +QString CitebaseFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool CitebaseFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void CitebaseFetcher::readConfigHook(const KConfigGroup&) { +} + +void CitebaseFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + m_data.truncate(0); + +// myDebug() << "CitebaseFetcher::search() - value = " << value_ << endl; + + KURL u = searchURL(m_key, m_value); + if(u.isEmpty()) { + stop(); + return; + } + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void CitebaseFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "CitebaseFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void CitebaseFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void CitebaseFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "CitebaseFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "CitebaseFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from citebasefetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.bib")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + Import::BibtexImporter imp(QString::fromUtf8(m_data, m_data.size())); + Data::CollPtr coll = imp.collection(); + + if(!coll) { + myDebug() << "CitebaseFetcher::slotComplete() - no valid result" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + + stop(); // required +} + +Tellico::Data::EntryPtr CitebaseFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +KURL CitebaseFetcher::searchURL(FetchKey key_, const QString& value_) const { + KURL u(QString::fromLatin1(CITEBASE_BASE_URL)); + + switch(key_) { + case ArxivID: + { + // remove prefix and/or version number + QString value = value_; + value.remove(QRegExp(QString::fromLatin1("^arxiv:"), false)); + value.remove(QRegExp(QString::fromLatin1("v\\d+$"))); + u.addQueryItem(QString::fromLatin1("rft_id"), QString::fromLatin1("oai:arXiv.org:%1").arg(value)); + } + break; + + default: + kdWarning() << "CitebaseFetcher::search() - key not recognized: " << m_key << endl; + return KURL(); + } + +#ifdef CITEBASE_TEST + u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/citebase.bib")); +#endif + myDebug() << "CitebaseFetcher::search() - url: " << u.url() << endl; + return u; +} + +void CitebaseFetcher::updateEntry(Data::EntryPtr entry_) { + QString arxiv = entry_->field(QString::fromLatin1("arxiv")); + if(!arxiv.isEmpty()) { + search(Fetch::ArxivID, arxiv); + return; + } + + myDebug() << "CitebaseFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +void CitebaseFetcher::updateEntrySynchronous(Data::EntryPtr entry) { + if(!entry) { + return; + } + QString arxiv = entry->field(QString::fromLatin1("arxiv")); + if(arxiv.isEmpty()) { + return; + } + + KURL u = searchURL(ArxivID, arxiv); + QString bibtex = FileHandler::readTextFile(u, true); + if(bibtex.isEmpty()) { + return; + } + + // assume result is always utf-8 + Import::BibtexImporter imp(bibtex); + Data::CollPtr coll = imp.collection(); + if(coll && coll->entryCount() > 0) { + myLog() << "CitebaseFetcher::updateEntrySynchronous() - found arxiv result, merging" << endl; + Data::Collection::mergeEntry(entry, coll->entries().front(), false /*overwrite*/); + } +} + +Tellico::Fetch::ConfigWidget* CitebaseFetcher::configWidget(QWidget* parent_) const { + return new CitebaseFetcher::ConfigWidget(parent_, this); +} + +CitebaseFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const CitebaseFetcher*) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +void CitebaseFetcher::ConfigWidget::saveConfig(KConfigGroup&) { +} + +QString CitebaseFetcher::ConfigWidget::preferredName() const { + return CitebaseFetcher::defaultName(); +} + +#include "citebasefetcher.moc" diff --git a/src/fetch/citebasefetcher.h b/src/fetch/citebasefetcher.h new file mode 100644 index 0000000..a292107 --- /dev/null +++ b/src/fetch/citebasefetcher.h @@ -0,0 +1,83 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_CITEBASEFETCHER_H +#define TELLICO_FETCH_CITEBASEFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class CitebaseFetcher : public Fetcher { +Q_OBJECT + +public: + CitebaseFetcher(QObject* parent); + ~CitebaseFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + + virtual bool canSearch(FetchKey k) const { return k == ArxivID; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Citebase; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + virtual void updateEntrySynchronous(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const CitebaseFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + KURL searchURL(FetchKey key, const QString& value) const; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } +} +#endif diff --git a/src/fetch/configwidget.cpp b/src/fetch/configwidget.cpp new file mode 100644 index 0000000..c7b3b59 --- /dev/null +++ b/src/fetch/configwidget.cpp @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2003-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 "configwidget.h" + +#include <kconfig.h> +#include <klocale.h> +#include <kaccelmanager.h> + +#include <qvgroupbox.h> +#include <qlayout.h> + +using Tellico::Fetch::ConfigWidget; + +ConfigWidget::ConfigWidget(QWidget* parent_) : QWidget(parent_), m_modified(false), m_accepted(false) { + QHBoxLayout* boxLayout = new QHBoxLayout(this); + boxLayout->setSpacing(10); + + QGroupBox* vbox = new QVGroupBox(i18n("Source Options"), this); + boxLayout->addWidget(vbox, 10 /*stretch*/); + + m_optionsWidget = new QWidget(vbox); +} + +void ConfigWidget::addFieldsWidget(const StringMap& customFields_, const QStringList& fieldsToAdd_) { + if(customFields_.isEmpty()) { + return; + } + + QVGroupBox* box = new QVGroupBox(i18n("Available Fields"), this); + static_cast<QBoxLayout*>(layout())->addWidget(box); + for(StringMap::ConstIterator it = customFields_.begin(); it != customFields_.end(); ++it) { + QCheckBox* cb = new QCheckBox(it.data(), box); + m_fields.insert(it.key(), cb); + if(fieldsToAdd_.contains(it.key())) { + cb->setChecked(true); + } + connect(cb, SIGNAL(clicked()), SLOT(slotSetModified())); + } + + KAcceleratorManager::manage(this); + + return; +} + +void ConfigWidget::saveFieldsConfig(KConfigGroup& config_) const { + QStringList fields; + for(QDictIterator<QCheckBox> it(m_fields); it.current(); ++it) { + if(it.current()->isChecked()) { + fields << it.currentKey(); + } + } + config_.writeEntry(QString::fromLatin1("Custom Fields"), fields); +} + +#include "configwidget.moc" diff --git a/src/fetch/configwidget.h b/src/fetch/configwidget.h new file mode 100644 index 0000000..9f18f83 --- /dev/null +++ b/src/fetch/configwidget.h @@ -0,0 +1,78 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FETCHCONFIGWIDGET_H +#define FETCHCONFIGWIDGET_H + +#include "../datavectors.h" + +#include <qwidget.h> +#include <qdict.h> +#include <qcheckbox.h> + +class KConfigGroup; +class QStringList; + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class ConfigWidget : public QWidget { +Q_OBJECT + +public: + ConfigWidget(QWidget* parent); + virtual ~ConfigWidget() {} + + void setAccepted(bool accepted_) { m_accepted = accepted_; } + bool shouldSave() const { return m_modified && m_accepted; } + /** + * Saves any configuration options. The config group must be + * set before calling this function. + * + * @param config_ The KConfig pointer + */ + virtual void saveConfig(KConfigGroup& config) = 0; + /** + * Called when a fetcher data source is removed. Useful for any cleanup work necessary. + * The ExecExternalFetcher might need to remove the script, for example. + * Because of the way the ConfigDialog is setup, easier to have that in the ConfigWidget + * class than in the Fetcher class itself + */ + virtual void removed() {} + virtual QString preferredName() const = 0; + +signals: + void signalName(const QString& name); + +public slots: + void slotSetModified(bool modified_ = true) { m_modified = modified_; } + +protected: + QWidget* optionsWidget() { return m_optionsWidget; } + void addFieldsWidget(const StringMap& customFields, const QStringList& fieldsToAdd); + void saveFieldsConfig(KConfigGroup& config) const; + +private: + bool m_modified; + bool m_accepted; + QWidget* m_optionsWidget; + QDict<QCheckBox> m_fields; +}; + + } +} + +#endif diff --git a/src/fetch/crossreffetcher.cpp b/src/fetch/crossreffetcher.cpp new file mode 100644 index 0000000..8c5d303 --- /dev/null +++ b/src/fetch/crossreffetcher.cpp @@ -0,0 +1,392 @@ +/*************************************************************************** + copyright : (C) 2007 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 "crossreffetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../core/netaccess.h" +#include "../imagefactory.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <klineedit.h> +#include <kactivelabel.h> + +#include <qlabel.h> +#include <qwhatsthis.h> +#include <qlayout.h> +#include <qfile.h> + +// #define CROSSREF_TEST + +#define CROSSREF_USE_UNIXREF + +namespace { + static const char* CROSSREF_BASE_URL = "http://www.crossref.org/openurl/?url_ver=Z39.88-2004&noredirect=true"; +} + +using Tellico::Fetch::CrossRefFetcher; + +CrossRefFetcher::CrossRefFetcher(QObject* parent_) + : Fetcher(parent_), m_xsltHandler(0), m_job(0), m_started(false) { +} + +CrossRefFetcher::~CrossRefFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString CrossRefFetcher::defaultName() { + return QString::fromLatin1("CrossRef"); +} + +QString CrossRefFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool CrossRefFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void CrossRefFetcher::readConfigHook(const KConfigGroup& config_) { + QString s = config_.readEntry("User"); + if(!s.isEmpty()) { + m_user = s; + } + s = config_.readEntry("Password"); + if(!s.isEmpty()) { + m_password = s; + } +} + +void CrossRefFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + + if(m_user.isEmpty() || m_password.isEmpty()) { + message(i18n("%1 requires a username and password.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + m_data.truncate(0); + +// myDebug() << "CrossRefFetcher::search() - value = " << value_ << endl; + + KURL u = searchURL(m_key, m_value); + if(u.isEmpty()) { + stop(); + return; + } + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void CrossRefFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "CrossRefFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void CrossRefFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void CrossRefFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "CrossRefFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "CrossRefFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from crossreffetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + + // assume result is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + + if(!coll) { + myDebug() << "CrossRefFetcher::slotComplete() - no valid result" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + + stop(); // required +} + +Tellico::Data::EntryPtr CrossRefFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + // if URL but no cover image, fetch it + if(!entry->field(QString::fromLatin1("url")).isEmpty()) { + Data::CollPtr coll = entry->collection(); + Data::FieldPtr field = coll->fieldByName(QString::fromLatin1("cover")); + if(!field && !coll->imageFields().isEmpty()) { + field = coll->imageFields().front(); + } else if(!field) { + field = new Data::Field(QString::fromLatin1("cover"), i18n("Front Cover"), Data::Field::Image); + coll->addField(field); + } + if(entry->field(field).isEmpty()) { + QPixmap pix = NetAccess::filePreview(entry->field(QString::fromLatin1("url"))); + if(!pix.isNull()) { + QString id = ImageFactory::addImage(pix, QString::fromLatin1("PNG")); + if(!id.isEmpty()) { + entry->setField(field, id); + } + } + } + } + return entry; +} + +void CrossRefFetcher::initXSLTHandler() { +#ifdef CROSSREF_USE_UNIXREF + QString xsltfile = locate("appdata", QString::fromLatin1("unixref2tellico.xsl")); +#else + QString xsltfile = locate("appdata", QString::fromLatin1("crossref2tellico.xsl")); +#endif + if(xsltfile.isEmpty()) { + kdWarning() << "CrossRefFetcher::initXSLTHandler() - can not locate xslt file." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "CrossRefFetcher::initXSLTHandler() - error in crossref2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +KURL CrossRefFetcher::searchURL(FetchKey key_, const QString& value_) const { + KURL u(QString::fromLatin1(CROSSREF_BASE_URL)); +#ifdef CROSSREF_USE_UNIXREF + u.addQueryItem(QString::fromLatin1("format"), QString::fromLatin1("unixref")); +#endif + u.addQueryItem(QString::fromLatin1("req_dat"), QString::fromLatin1("ourl_%1:%2").arg(m_user, m_password)); + + switch(key_) { + case DOI: + u.addQueryItem(QString::fromLatin1("rft_id"), QString::fromLatin1("info:doi/%1").arg(value_)); + break; + + default: + kdWarning() << "CrossRefFetcher::search() - key not recognized: " << m_key << endl; + return KURL(); + } + +#ifdef CROSSREF_TEST + u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/crossref.xml")); +#endif + myDebug() << "CrossRefFetcher::search() - url: " << u.url() << endl; + return u; +} + +void CrossRefFetcher::updateEntry(Data::EntryPtr entry_) { + QString doi = entry_->field(QString::fromLatin1("doi")); + if(!doi.isEmpty()) { + search(Fetch::DOI, doi); + return; + } + +#if 0 + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + m_limit = 10; // raise limit so more possibility of match + search(Fetch::Title, t); + return; + } +#endif + + myDebug() << "CrossRefFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +void CrossRefFetcher::updateEntrySynchronous(Data::EntryPtr entry) { + if(!entry) { + return; + } + if(m_user.isEmpty() || m_password.isEmpty()) { + myDebug() << "CrossRefFetcher::updateEntrySynchronous() - username and password is required" << endl; + return; + } + QString doi = entry->field(QString::fromLatin1("doi")); + if(doi.isEmpty()) { + return; + } + + KURL u = searchURL(DOI, doi); + QString xml = FileHandler::readTextFile(u, true, true); + if(xml.isEmpty()) { + return; + } + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + return; + } + } + + // assume result is always utf-8 + QString str = m_xsltHandler->applyStylesheet(xml); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(coll && coll->entryCount() > 0) { + myLog() << "CrossRefFetcher::updateEntrySynchronous() - found DOI result, merging" << endl; + Data::Collection::mergeEntry(entry, coll->entries().front(), false /*overwrite*/); + } +} + +Tellico::Fetch::ConfigWidget* CrossRefFetcher::configWidget(QWidget* parent_) const { + return new CrossRefFetcher::ConfigWidget(parent_, this); +} + +CrossRefFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const CrossRefFetcher* fetcher_) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 4, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = 0; + + KActiveLabel* al = new KActiveLabel(i18n("CrossRef requires an account for access. " + "Please read the terms and conditions and " + "<a href='http://www.crossref.org/requestaccount/'>" + "request an account</a>. Enter your OpenURL " + "account information below."), + optionsWidget()); + ++row; + l->addMultiCellWidget(al, row, row, 0, 1); + // richtext gets weird with size + al->setMinimumWidth(al->sizeHint().width()); + + QLabel* label = new QLabel(i18n("&Username: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_userEdit = new KLineEdit(optionsWidget()); + connect(m_userEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_userEdit, row, 1); + QString w = i18n("A username and password is required to access the CrossRef service. The password is " + "stored as plain text in the Tellico configuration file."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_userEdit, w); + label->setBuddy(m_userEdit); + + label = new QLabel(i18n("&Password: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_passEdit = new KLineEdit(optionsWidget()); + connect(m_passEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_passEdit, row, 1); + QWhatsThis::add(label, w); + QWhatsThis::add(m_passEdit, w); + label->setBuddy(m_passEdit); + + if(fetcher_) { + m_userEdit->setText(fetcher_->m_user); + m_passEdit->setText(fetcher_->m_password); + } +} + +void CrossRefFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + QString s = m_userEdit->text(); + config_.writeEntry("User", s); + + s = m_passEdit->text(); + config_.writeEntry("Password", s); + + slotSetModified(false); +} + +QString CrossRefFetcher::ConfigWidget::preferredName() const { + return CrossRefFetcher::defaultName(); +} + +#include "crossreffetcher.moc" diff --git a/src/fetch/crossreffetcher.h b/src/fetch/crossreffetcher.h new file mode 100644 index 0000000..392d46a --- /dev/null +++ b/src/fetch/crossreffetcher.h @@ -0,0 +1,97 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_CROSSREFFETCHER_H +#define TELLICO_FETCH_CROSSREFFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +class KLineEdit; + +namespace Tellico { + + class XSLTHandler; + + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class CrossRefFetcher : public Fetcher { +Q_OBJECT + +public: + CrossRefFetcher(QObject* parent); + ~CrossRefFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + + virtual bool canSearch(FetchKey k) const { return k == DOI; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return CrossRef; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + virtual void updateEntrySynchronous(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const CrossRefFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + private: + KLineEdit* m_userEdit; + KLineEdit* m_passEdit; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + KURL searchURL(FetchKey key, const QString& value) const; + + XSLTHandler* m_xsltHandler; + + QString m_user; + QString m_password; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } +} +#endif diff --git a/src/fetch/discogsfetcher.cpp b/src/fetch/discogsfetcher.cpp new file mode 100644 index 0000000..31a8bab --- /dev/null +++ b/src/fetch/discogsfetcher.cpp @@ -0,0 +1,413 @@ +/*************************************************************************** + copyright : (C) 2008 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 "discogsfetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../imagefactory.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kio/job.h> + +#include <qlabel.h> +#include <qlayout.h> +#include <qfile.h> +#include <qwhatsthis.h> + +//#define DISCOGS_TEST + +namespace { + static const int DISCOGS_MAX_RETURNS_TOTAL = 20; + static const char* DISCOGS_API_URL = "http://www.discogs.com"; + static const char* DISCOGS_API_KEY = "de6cb96534"; +} + +using Tellico::Fetch::DiscogsFetcher; + +DiscogsFetcher::DiscogsFetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_xsltHandler(0), + m_limit(DISCOGS_MAX_RETURNS_TOTAL), m_job(0), m_started(false), + m_apiKey(QString::fromLatin1(DISCOGS_API_KEY)) { +} + +DiscogsFetcher::~DiscogsFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString DiscogsFetcher::defaultName() { + return i18n("Discogs Audio Search"); +} + +QString DiscogsFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool DiscogsFetcher::canFetch(int type) const { + return type == Data::Collection::Album; +} + +void DiscogsFetcher::readConfigHook(const KConfigGroup& config_) { + QString k = config_.readEntry("API Key"); + if(!k.isEmpty()) { + m_apiKey = k; + } + m_fetchImages = config_.readBoolEntry("Fetch Images", true); + m_fields = config_.readListEntry("Custom Fields"); +} + +void DiscogsFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_; + m_started = true; + m_start = 1; + m_total = -1; + doSearch(); +} + +void DiscogsFetcher::continueSearch() { + m_started = true; + doSearch(); +} + +void DiscogsFetcher::doSearch() { + KURL u(QString::fromLatin1(DISCOGS_API_URL)); + u.addQueryItem(QString::fromLatin1("f"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("api_key"), m_apiKey); + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + switch(m_key) { + case Title: + u.setPath(QString::fromLatin1("/search")); + u.addQueryItem(QString::fromLatin1("q"), m_value); + u.addQueryItem(QString::fromLatin1("type"), QString::fromLatin1("release")); + break; + + case Person: + u.setPath(QString::fromLatin1("/artist/%1").arg(m_value)); + break; + + case Keyword: + u.setPath(QString::fromLatin1("/search")); + u.addQueryItem(QString::fromLatin1("q"), m_value); + u.addQueryItem(QString::fromLatin1("type"), QString::fromLatin1("all")); + break; + + default: + kdWarning() << "DiscogsFetcher::search() - key not recognized: " << m_key << endl; + stop(); + return; + } + +#ifdef DISCOGS_TEST + u = KURL(QString::fromLatin1("/home/robby/discogs-results.xml")); +#endif +// myDebug() << "DiscogsFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void DiscogsFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void DiscogsFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void DiscogsFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "DiscogsFetcher::slotComplete()" << endl; + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "DiscogsFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from discogsfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + + if(m_total == -1) { + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "DiscogsFetcher::slotComplete() - server did not return valid XML." << endl; + return; + } + // total is /resp/searchresults/@numResults + QDomNode n = dom.documentElement().namedItem(QString::fromLatin1("resp")) + .namedItem(QString::fromLatin1("searchresults")); + QDomElement e = n.toElement(); + if(!e.isNull()) { + m_total = e.attribute(QString::fromLatin1("numResults")).toInt(); + myDebug() << "total = " << m_total; + } + } + + // assume discogs is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(!coll) { + myDebug() << "DiscogsFetcher::slotComplete() - no collection pointer" << endl; + stop(); + return; + } + + int count = 0; + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); count < m_limit && entry != entries.end(); ++entry, ++count) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")); + + SearchResult* r = new SearchResult(this, entry->title(), desc, QString()); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + m_start = m_entries.count() + 1; + // not sure how tospecify start in the REST url + // m_hasMoreResults = m_start <= m_total; + + stop(); // required +} + +Tellico::Data::EntryPtr DiscogsFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + if(!entry) { + kdWarning() << "DiscogsFetcher::fetchEntry() - no entry in dict" << endl; + return 0; + } + // one way we tell if this entry has been fully initialized is to + // check for a cover image + if(!entry->field(QString::fromLatin1("cover")).isEmpty()) { + myLog() << "DiscogsFetcher::fetchEntry() - already downloaded " << entry->title() << endl; + return entry; + } + + QString release = entry->field(QString::fromLatin1("discogs-id")); + if(release.isEmpty()) { + myDebug() << "DiscogsFetcher::fetchEntry() - no discogs release found" << endl; + return entry; + } + +#ifdef DISCOGS_TEST + KURL u(QString::fromLatin1("/home/robby/discogs-release.xml")); +#else + KURL u(QString::fromLatin1(DISCOGS_API_URL)); + u.setPath(QString::fromLatin1("/release/%1").arg(release)); + u.addQueryItem(QString::fromLatin1("f"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("api_key"), m_apiKey); +#endif +// myDebug() << "DiscogsFetcher::fetchEntry() - url: " << u << endl; + + // quiet, utf8, allowCompressed + QString output = FileHandler::readTextFile(u, true, true, true); +#if 0 + kdWarning() << "Remove output debug from discogsfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << output; + } + f.close(); +#endif + + Import::TellicoImporter imp(m_xsltHandler->applyStylesheet(output)); + Data::CollPtr coll = imp.collection(); +// getTracks(entry); + if(!coll) { + kdWarning() << "DiscogsFetcher::fetchEntry() - no collection pointer" << endl; + return entry; + } + + if(coll->entryCount() > 1) { + myDebug() << "DiscogsFetcher::fetchEntry() - weird, more than one entry found" << endl; + } + + const StringMap customFields = this->customFields(); + for(StringMap::ConstIterator it = customFields.begin(); it != customFields.end(); ++it) { + if(!m_fields.contains(it.key())) { + coll->removeField(it.key()); + } + } + + // don't want to include id + coll->removeField(QString::fromLatin1("discogs-id")); + + entry = coll->entries().front(); + m_entries.replace(uid_, entry); + return entry; +} + +void DiscogsFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("discogs2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "DiscogsFetcher::initXSLTHandler() - can not locate discogs2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "DiscogsFetcher::initXSLTHandler() - error in discogs2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +void DiscogsFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "DiscogsFetcher::updateEntry()" << endl; + + QString value; + QString title = entry_->field(QString::fromLatin1("title")); + if(!title.isEmpty()) { + search(Title, value); + return; + } + + QString artist = entry_->field(QString::fromLatin1("artist")); + if(!artist.isEmpty()) { + search(Person, artist); + return; + } + + myDebug() << "DiscogsFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* DiscogsFetcher::configWidget(QWidget* parent_) const { + return new DiscogsFetcher::ConfigWidget(parent_, this); +} + +DiscogsFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const DiscogsFetcher* fetcher_) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 2, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + QLabel* label = new QLabel(i18n("API &key: "), optionsWidget()); + l->addWidget(label, ++row, 0); + + m_apiKeyEdit = new KLineEdit(optionsWidget()); + connect(m_apiKeyEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_apiKeyEdit, row, 1); + QString w = i18n("With your discogs.com account you receive an API key for the usage of their XML-based interface " + "(See http://www.discogs.com/help/api)."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_apiKeyEdit, w); + label->setBuddy(m_apiKeyEdit); + + m_fetchImageCheck = new QCheckBox(i18n("Download cover &image"), optionsWidget()); + connect(m_fetchImageCheck, SIGNAL(clicked()), SLOT(slotSetModified())); + ++row; + l->addMultiCellWidget(m_fetchImageCheck, row, row, 0, 1); + w = i18n("The cover image may be downloaded as well. However, too many large images in the " + "collection may degrade performance."); + QWhatsThis::add(m_fetchImageCheck, w); + + l->setRowStretch(++row, 10); + + // now add additional fields widget + addFieldsWidget(DiscogsFetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); + + if(fetcher_) { + m_apiKeyEdit->setText(fetcher_->m_apiKey); + m_fetchImageCheck->setChecked(fetcher_->m_fetchImages); + } else { + m_apiKeyEdit->setText(QString::fromLatin1(DISCOGS_API_KEY)); + m_fetchImageCheck->setChecked(true); + } +} + +void DiscogsFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + QString apiKey = m_apiKeyEdit->text().stripWhiteSpace(); + if(!apiKey.isEmpty()) { + config_.writeEntry("API Key", apiKey); + } + config_.writeEntry("Fetch Images", m_fetchImageCheck->isChecked()); + + saveFieldsConfig(config_); + slotSetModified(false); +} + +QString DiscogsFetcher::ConfigWidget::preferredName() const { + return DiscogsFetcher::defaultName(); +} + +Tellico::StringMap DiscogsFetcher::customFields() { + StringMap map; + map[QString::fromLatin1("producer")] = i18n("Producer"); + map[QString::fromLatin1("nationality")] = i18n("Nationality"); + map[QString::fromLatin1("discogs")] = i18n("Discogs Link"); + return map; +} + +#include "discogsfetcher.moc" diff --git a/src/fetch/discogsfetcher.h b/src/fetch/discogsfetcher.h new file mode 100644 index 0000000..ac8c1b8 --- /dev/null +++ b/src/fetch/discogsfetcher.h @@ -0,0 +1,117 @@ +/*************************************************************************** + copyright : (C) 2008 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; * + * * + ***************************************************************************/ + +#ifndef DISCOGSFETCHER_H +#define DISCOGSFETCHER_H + +namespace Tellico { + class XSLTHandler; +} + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" +#include <klineedit.h> + +#include <qdom.h> +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * A fetcher for discogs.com + * + * @author Robby Stephenson + */ +class DiscogsFetcher : public Fetcher { +Q_OBJECT + +public: + /** + */ + DiscogsFetcher(QObject* parent, const char* name = 0); + /** + */ + virtual ~DiscogsFetcher(); + + /** + */ + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + // amazon can search title or person + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Discogs; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + static StringMap customFields(); + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const DiscogsFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup&); + virtual QString preferredName() const; + private: + KLineEdit *m_apiKeyEdit; + QCheckBox* m_fetchImageCheck; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + void doSearch(); + + XSLTHandler* m_xsltHandler; + int m_limit; + int m_start; + int m_total; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; + + bool m_fetchImages; + QString m_apiKey; + QStringList m_fields; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/entrezfetcher.cpp b/src/fetch/entrezfetcher.cpp new file mode 100644 index 0000000..14b9e20 --- /dev/null +++ b/src/fetch/entrezfetcher.cpp @@ -0,0 +1,498 @@ +/*************************************************************************** + copyright : (C) 2005-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 "entrezfetcher.h" +#include "../tellico_kernel.h" +#include "../latin1literal.h" +#include "../collection.h" +#include "../entry.h" +#include "../filehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kstandarddirs.h> +#include <kio/job.h> + +#include <qdom.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qfile.h> + +//#define ENTREZ_TEST + +namespace { + static const int ENTREZ_MAX_RETURNS_TOTAL = 25; + static const char* ENTREZ_BASE_URL = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/"; + static const char* ENTREZ_SEARCH_CGI = "esearch.fcgi"; + static const char* ENTREZ_SUMMARY_CGI = "esummary.fcgi"; + static const char* ENTREZ_FETCH_CGI = "efetch.fcgi"; + static const char* ENTREZ_LINK_CGI = "elink.fcgi"; + static const char* ENTREZ_DEFAULT_DATABASE = "pubmed"; +} + +using Tellico::Fetch::EntrezFetcher; + +EntrezFetcher::EntrezFetcher(QObject* parent_, const char* name_) : Fetcher(parent_, name_), m_xsltHandler(0), + m_step(Begin), m_started(false) { +} + +EntrezFetcher::~EntrezFetcher() { +} + +QString EntrezFetcher::defaultName() { + return i18n("Entrez Database"); +} + +QString EntrezFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool EntrezFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void EntrezFetcher::readConfigHook(const KConfigGroup& config_) { + QString s = config_.readEntry("Database", QString::fromLatin1(ENTREZ_DEFAULT_DATABASE)); // default to pubmed + if(!s.isEmpty()) { + m_dbname = s; + } + m_fields = config_.readListEntry("Custom Fields"); +} + +void EntrezFetcher::search(FetchKey key_, const QString& value_) { + m_started = true; + m_start = 1; + m_total = -1; + +// only search if current collection is a bibliography + if(!canFetch(Kernel::self()->collectionType())) { + myDebug() << "EntrezFetcher::search() - collection type mismatch, stopping" << endl; + stop(); + return; + } + if(m_dbname.isEmpty()) { + m_dbname = QString::fromLatin1(ENTREZ_DEFAULT_DATABASE); + } + +#ifdef ENTREZ_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/esearch.xml")); +#else + KURL u(QString::fromLatin1(ENTREZ_BASE_URL)); + u.addPath(QString::fromLatin1(ENTREZ_SEARCH_CGI)); + u.addQueryItem(QString::fromLatin1("tool"), QString::fromLatin1("Tellico")); + u.addQueryItem(QString::fromLatin1("retmode"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("usehistory"), QString::fromLatin1("y")); + u.addQueryItem(QString::fromLatin1("retmax"), QString::fromLatin1("1")); // we're just getting the count + u.addQueryItem(QString::fromLatin1("db"), m_dbname); + u.addQueryItem(QString::fromLatin1("term"), value_); + switch(key_) { + case Title: + u.addQueryItem(QString::fromLatin1("field"), QString::fromLatin1("titl")); + break; + + case Person: + u.addQueryItem(QString::fromLatin1("field"), QString::fromLatin1("auth")); + break; + + case Keyword: + // for Tellico Keyword searches basically mean search for any field matching +// u.addQueryItem(QString::fromLatin1("field"), QString::fromLatin1("word")); + break; + + case PubmedID: + u.addQueryItem(QString::fromLatin1("field"), QString::fromLatin1("pmid")); + break; + + case DOI: + case Raw: + u.setQuery(u.query() + '&' + value_); + break; + + default: + kdWarning() << "EntrezFetcher::search() - FetchKey not supported" << endl; + stop(); + return; + } +#endif + + m_step = Search; +// myLog() << "EntrezFetcher::doSearch() - url: " << u.url() << endl; + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void EntrezFetcher::continueSearch() { + m_started = true; + doSummary(); +} + +void EntrezFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + m_step = Begin; + emit signalDone(this); +} + +void EntrezFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void EntrezFetcher::slotComplete(KIO::Job* job_) { + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "EntrezFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from entrezfetcher.cpp: " << __LINE__ << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + switch(m_step) { + case Search: + searchResults(); + break; + case Summary: + summaryResults(); + break; + case Begin: + case Fetch: + default: + myLog() << "EntrezFetcher::slotComplete() - wrong step = " << m_step << endl; + stop(); + break; + } +} + +void EntrezFetcher::searchResults() { + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "EntrezFetcher::searchResults() - server did not return valid XML." << endl; + stop(); + return; + } + // find Count, QueryKey, and WebEnv elements + int count = 0; + for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if(e.isNull()) { + continue; + } + if(e.tagName() == Latin1Literal("Count")) { + m_total = e.text().toInt(); + ++count; + } else if(e.tagName() == Latin1Literal("QueryKey")) { + m_queryKey = e.text(); + ++count; + } else if(e.tagName() == Latin1Literal("WebEnv")) { + m_webEnv = e.text(); + ++count; + } + if(count >= 3) { + break; // found them all + } + } + + m_data.truncate(0); + doSummary(); +} + +void EntrezFetcher::doSummary() { +#ifdef ENTREZ_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/esummary.xml")); +#else + KURL u(QString::fromLatin1(ENTREZ_BASE_URL)); + u.addPath(QString::fromLatin1(ENTREZ_SUMMARY_CGI)); + u.addQueryItem(QString::fromLatin1("tool"), QString::fromLatin1("Tellico")); + u.addQueryItem(QString::fromLatin1("retmode"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("retstart"), QString::number(m_start)); + u.addQueryItem(QString::fromLatin1("retmax"), QString::number(QMIN(m_total-m_start-1, ENTREZ_MAX_RETURNS_TOTAL))); + u.addQueryItem(QString::fromLatin1("usehistory"), QString::fromLatin1("y")); + u.addQueryItem(QString::fromLatin1("db"), m_dbname); + u.addQueryItem(QString::fromLatin1("query_key"), m_queryKey); + u.addQueryItem(QString::fromLatin1("WebEnv"), m_webEnv); +#endif + + m_step = Summary; +// myLog() << "EntrezFetcher::searchResults() - url: " << u.url() << endl; + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void EntrezFetcher::summaryResults() { + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "EntrezFetcher::summaryResults() - server did not return valid XML." << endl; + stop(); + return; + } + // top child is eSummaryResult + // all children are DocSum + for(QDomNode n = dom.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if(e.isNull() || e.tagName() != Latin1Literal("DocSum")) { + continue; + } + QDomNodeList nodes = e.elementsByTagName(QString::fromLatin1("Id")); + if(nodes.count() == 0) { + myDebug() << "EntrezFetcher::summaryResults() - no Id elements" << endl; + continue; + } + int id = nodes.item(0).toElement().text().toInt(); + QString title, pubdate, authors; + nodes = e.elementsByTagName(QString::fromLatin1("Item")); + for(uint j = 0; j < nodes.count(); ++j) { + if(nodes.item(j).toElement().attribute(QString::fromLatin1("Name")) == Latin1Literal("Title")) { + title = nodes.item(j).toElement().text(); + } else if(nodes.item(j).toElement().attribute(QString::fromLatin1("Name")) == Latin1Literal("PubDate")) { + pubdate = nodes.item(j).toElement().text(); + } else if(nodes.item(j).toElement().attribute(QString::fromLatin1("Name")) == Latin1Literal("AuthorList")) { + QStringList list; + for(QDomNode aNode = nodes.item(j).firstChild(); !aNode.isNull(); aNode = aNode.nextSibling()) { + // lazy, assume all children Items are authors + if(aNode.nodeName() == Latin1Literal("Item")) { + list << aNode.toElement().text(); + } + } + authors = list.join(QString::fromLatin1("; ")); + } + if(!title.isEmpty() && !pubdate.isEmpty() && !authors.isEmpty()) { + break; // done now + } + } + SearchResult* r = new SearchResult(this, title, pubdate + '/' + authors, QString()); + m_matches.insert(r->uid, id); + emit signalResultFound(r); + } + m_start = m_matches.count() + 1; + m_hasMoreResults = m_start <= m_total; + stop(); // done searching +} + +Tellico::Data::EntryPtr EntrezFetcher::fetchEntry(uint uid_) { + // if we already grabbed this one, then just pull it out of the dict + Data::EntryPtr entry = m_entries[uid_]; + if(entry) { + return entry; + } + + if(!m_matches.contains(uid_)) { + return 0; + } + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return 0; + } + } + + int id = m_matches[uid_]; +#ifdef ENTREZ_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/pubmed.xml")); +#else + KURL u(QString::fromLatin1(ENTREZ_BASE_URL)); + u.addPath(QString::fromLatin1(ENTREZ_FETCH_CGI)); + u.addQueryItem(QString::fromLatin1("tool"), QString::fromLatin1("Tellico")); + u.addQueryItem(QString::fromLatin1("retmode"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("rettype"), QString::fromLatin1("abstract")); + u.addQueryItem(QString::fromLatin1("db"), m_dbname); + u.addQueryItem(QString::fromLatin1("id"), QString::number(id)); +#endif + // now it's sychronous, and we know that it's utf8 + QString xmlOutput = FileHandler::readTextFile(u, false /*quiet*/, true /*utf8*/); + if(xmlOutput.isEmpty()) { + kdWarning() << "EntrezFetcher::fetchEntry() - unable to download " << u << endl; + return 0; + } +#if 0 + kdWarning() << "EntrezFetcher::fetchEntry() - turn me off!" << endl; + QFile f1(QString::fromLatin1("/tmp/test-entry.xml")); + if(f1.open(IO_WriteOnly)) { + QTextStream t(&f1); + t.setEncoding(QTextStream::UnicodeUTF8); + t << xmlOutput; + } + f1.close(); +#endif + QString str = m_xsltHandler->applyStylesheet(xmlOutput); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(!coll) { + kdWarning() << "EntrezFetcher::fetchEntry() - invalid collection" << endl; + return 0; + } + if(coll->entryCount() == 0) { + myDebug() << "EntrezFetcher::fetchEntry() - no entries in collection" << endl; + return 0; + } else if(coll->entryCount() > 1) { + myDebug() << "EntrezFetcher::fetchEntry() - collection has multiple entries, taking first one" << endl; + } + + Data::EntryPtr e = coll->entries().front(); + + // try to get a link, but only if necessary + if(m_fields.contains(QString::fromLatin1("url"))) { + KURL link(QString::fromLatin1(ENTREZ_BASE_URL)); + link.addPath(QString::fromLatin1(ENTREZ_LINK_CGI)); + link.addQueryItem(QString::fromLatin1("tool"), QString::fromLatin1("Tellico")); + link.addQueryItem(QString::fromLatin1("cmd"), QString::fromLatin1("llinks")); + link.addQueryItem(QString::fromLatin1("db"), m_dbname); + link.addQueryItem(QString::fromLatin1("dbfrom"), m_dbname); + link.addQueryItem(QString::fromLatin1("id"), QString::number(id)); + + QDomDocument linkDom = FileHandler::readXMLFile(link, false /* namespace */, true /* quiet */); + // need eLinkResult/LinkSet/IdUrlList/IdUrlSet/ObjUrl/Url + QDomNode linkNode = linkDom.namedItem(QString::fromLatin1("eLinkResult")) + .namedItem(QString::fromLatin1("LinkSet")) + .namedItem(QString::fromLatin1("IdUrlList")) + .namedItem(QString::fromLatin1("IdUrlSet")) + .namedItem(QString::fromLatin1("ObjUrl")) + .namedItem(QString::fromLatin1("Url")); + if(!linkNode.isNull()) { + QString u = linkNode.toElement().text(); +// myDebug() << u << endl; + if(!u.isEmpty()) { + if(!coll->hasField(QString::fromLatin1("url"))) { + Data::FieldPtr field = new Data::Field(QString::fromLatin1("url"), i18n("URL"), Data::Field::URL); + field->setCategory(i18n("Miscellaneous")); + coll->addField(field); + } + e->setField(QString::fromLatin1("url"), u); + } + } + } + + const StringMap customFields = EntrezFetcher::customFields(); + for(StringMap::ConstIterator it = customFields.begin(); it != customFields.end(); ++it) { + if(!m_fields.contains(it.key())) { + coll->removeField(it.key()); + } + } + + m_entries.insert(uid_, e); + return e; +} + +void EntrezFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("pubmed2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "EntrezFetcher::initXSLTHandler() - can not locate pubmed2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + if(!m_xsltHandler) { + m_xsltHandler = new XSLTHandler(u); + } + if(!m_xsltHandler->isValid()) { + kdWarning() << "EntrezFetcher::initXSLTHandler() - error in pubmed2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +void EntrezFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "EntrezFetcher::updateEntry()" << endl; + QString s = entry_->field(QString::fromLatin1("pmid")); + if(!s.isEmpty()) { + search(PubmedID, s); + return; + } + + s = entry_->field(QString::fromLatin1("doi")); + if(!s.isEmpty()) { + search(DOI, s); + return; + } + + s = entry_->field(QString::fromLatin1("title")); + if(!s.isEmpty()) { + search(Title, s); + return; + } + + myDebug() << "EntrezFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* EntrezFetcher::configWidget(QWidget* parent_) const { + return new EntrezFetcher::ConfigWidget(parent_, this); +} + +EntrezFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const EntrezFetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); + + // now add additional fields widget + addFieldsWidget(EntrezFetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); +} + +void EntrezFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + saveFieldsConfig(config_); + slotSetModified(false); +} + +QString EntrezFetcher::ConfigWidget::preferredName() const { + return EntrezFetcher::defaultName(); +} + +//static +Tellico::StringMap EntrezFetcher::customFields() { + StringMap map; + map[QString::fromLatin1("institution")] = i18n("Institution"); + map[QString::fromLatin1("abstract")] = i18n("Abstract"); + map[QString::fromLatin1("url")] = i18n("URL"); + return map; +} + +#include "entrezfetcher.moc" diff --git a/src/fetch/entrezfetcher.h b/src/fetch/entrezfetcher.h new file mode 100644 index 0000000..c8aac49 --- /dev/null +++ b/src/fetch/entrezfetcher.h @@ -0,0 +1,113 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ENTREZFETCHER_H +#define TELLICO_ENTREZFETCHER_H + +namespace Tellico { + class XSLTHandler; +} + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class EntrezFetcher : public Fetcher { +Q_OBJECT + +public: + EntrezFetcher(QObject* parent, const char* name=0); + /** + */ + virtual ~EntrezFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + // pubmed can search title, person, and keyword + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword || k == Raw || k == PubmedID || k == DOI; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Entrez; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + virtual void updateEntry(Data::EntryPtr entry); + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + static StringMap customFields(); + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const EntrezFetcher* fetcher=0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + void doSummary(); + + void searchResults(); + void summaryResults(); + + enum Step { + Begin, + Search, + Summary, + Fetch + }; + + XSLTHandler* m_xsltHandler; + QString m_dbname; + + int m_start; + int m_total; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; // map from search result id to entry + QMap<int, int> m_matches; // search result id to pubmed id + QGuardedPtr<KIO::Job> m_job; + + QString m_queryKey; + QString m_webEnv; + Step m_step; + + bool m_started; + QStringList m_fields; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/execexternalfetcher.cpp b/src/fetch/execexternalfetcher.cpp new file mode 100644 index 0000000..07b99d8 --- /dev/null +++ b/src/fetch/execexternalfetcher.cpp @@ -0,0 +1,561 @@ +/*************************************************************************** + copyright : (C) 2005-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 "execexternalfetcher.h" +#include "messagehandler.h" +#include "fetchmanager.h" +#include "../collection.h" +#include "../entry.h" +#include "../importdialog.h" +#include "../translators/tellicoimporter.h" +#include "../tellico_debug.h" +#include "../gui/combobox.h" +#include "../gui/lineedit.h" +#include "../gui/collectiontypecombo.h" +#include "../tellico_utils.h" +#include "../newstuff/manager.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kprocess.h> +#include <kurlrequester.h> +#include <kaccelmanager.h> + +#include <qlayout.h> +#include <qlabel.h> +#include <qwhatsthis.h> +#include <qregexp.h> +#include <qvgroupbox.h> +#include <qfile.h> // needed for QFile::remove + +using Tellico::Fetch::ExecExternalFetcher; + +QStringList ExecExternalFetcher::parseArguments(const QString& str_) { + // matching escaped quotes is too hard... :( +// QRegExp quotes(QString::fromLatin1("[^\\\\](['\"])(.*[^\\\\])\\1")); + QRegExp quotes(QString::fromLatin1("(['\"])(.*)\\1")); + quotes.setMinimal(true); + QRegExp spaces(QString::fromLatin1("\\s+")); + spaces.setMinimal(true); + + QStringList args; + int pos = 0; + for(int nextPos = quotes.search(str_); nextPos > -1; pos = nextPos+1, nextPos = quotes.search(str_, pos)) { + // a non-quotes arguments runs from pos to nextPos + args += QStringList::split(spaces, str_.mid(pos, nextPos-pos)); + // move nextpos marker to end of match + pos = quotes.pos(2); // skip quotation mark + nextPos += quotes.matchedLength(); + args += str_.mid(pos, nextPos-pos-1); + } + // catch the end stuff + args += QStringList::split(spaces, str_.mid(pos)); + +#if 0 + for(QStringList::ConstIterator it = args.begin(); it != args.end(); ++it) { + myDebug() << *it << endl; + } +#endif + + return args; +} + +ExecExternalFetcher::ExecExternalFetcher(QObject* parent_, const char* name_/*=0*/) : Fetcher(parent_, name_), + m_started(false), m_collType(-1), m_formatType(-1), m_canUpdate(false), m_process(0), m_deleteOnRemove(false) { +} + +ExecExternalFetcher::~ExecExternalFetcher() { + stop(); +} + +QString ExecExternalFetcher::defaultName() { + return i18n("External Application"); +} + +QString ExecExternalFetcher::source() const { + return m_name; +} + +bool ExecExternalFetcher::canFetch(int type_) const { + return m_collType == -1 ? false : m_collType == type_; +} + +void ExecExternalFetcher::readConfigHook(const KConfigGroup& config_) { + QString s = config_.readPathEntry("ExecPath"); + if(!s.isEmpty()) { + m_path = s; + } + QValueList<int> il; + if(config_.hasKey("ArgumentKeys")) { + il = config_.readIntListEntry("ArgumentKeys"); + } else { + il.append(Keyword); + } + QStringList sl = config_.readListEntry("Arguments"); + if(il.count() != sl.count()) { + kdWarning() << "ExecExternalFetcher::readConfig() - unequal number of arguments and keys" << endl; + } + int n = QMIN(il.count(), sl.count()); + for(int i = 0; i < n; ++i) { + m_args[static_cast<FetchKey>(il[i])] = sl[i]; + } + if(config_.hasKey("UpdateArgs")) { + m_canUpdate = true; + m_updateArgs = config_.readEntry("UpdateArgs"); + } else { + m_canUpdate = false; + } + m_collType = config_.readNumEntry("CollectionType", -1); + m_formatType = config_.readNumEntry("FormatType", -1); + m_deleteOnRemove = config_.readBoolEntry("DeleteOnRemove", false); + m_newStuffName = config_.readEntry("NewStuffName"); +} + +void ExecExternalFetcher::search(FetchKey key_, const QString& value_) { + m_started = true; + + if(!m_args.contains(key_)) { + stop(); + return; + } + + // should KProcess::quote() be used? + // %1 gets replaced by the search value, but since the arguments are going to be split + // the search value needs to be enclosed in quotation marks + // but first check to make sure the user didn't do that already + // AND the "%1" wasn't used in the settings + QString value = value_; + if(key_ == ISBN) { + value.remove('-'); // remove hyphens from isbn values + // shouldn't hurt and might keep from confusing stupid search sources + } + QRegExp rx1(QString::fromLatin1("['\"].*\\1")); + if(!rx1.exactMatch(value)) { + value.prepend('"').append('"'); + } + QString args = m_args[key_]; + QRegExp rx2(QString::fromLatin1("['\"]%1\\1")); + args.replace(rx2, QString::fromLatin1("%1")); + startSearch(parseArguments(args.arg(value))); // replace %1 with search value +} + +void ExecExternalFetcher::startSearch(const QStringList& args_) { + if(m_path.isEmpty()) { + stop(); + return; + } + +#if 0 + myDebug() << m_path << endl; + for(QStringList::ConstIterator it = args_.begin(); it != args_.end(); ++it) { + myDebug() << " " << *it << endl; + } +#endif + + m_process = new KProcess(); + connect(m_process, SIGNAL(receivedStdout(KProcess*, char*, int)), SLOT(slotData(KProcess*, char*, int))); + connect(m_process, SIGNAL(receivedStderr(KProcess*, char*, int)), SLOT(slotError(KProcess*, char*, int))); + connect(m_process, SIGNAL(processExited(KProcess*)), SLOT(slotProcessExited(KProcess*))); + *m_process << m_path << args_; + if(!m_process->start(KProcess::NotifyOnExit, KProcess::AllOutput)) { + myDebug() << "ExecExternalFetcher::startSearch() - process failed to start" << endl; + stop(); + } +} + +void ExecExternalFetcher::stop() { + if(!m_started) { + return; + } + if(m_process) { + m_process->kill(); + delete m_process; + m_process = 0; + } + m_data.truncate(0); + m_started = false; + m_errors.clear(); + emit signalDone(this); +} + +void ExecExternalFetcher::slotData(KProcess*, char* buffer_, int len_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(buffer_, len_); +} + +void ExecExternalFetcher::slotError(KProcess*, char* buffer_, int len_) { + GUI::CursorSaver cs(Qt::arrowCursor); + QString msg = QString::fromLocal8Bit(buffer_, len_); + msg.prepend(source() + QString::fromLatin1(": ")); + if(msg.endsWith(QChar('\n'))) { + msg.truncate(msg.length()-1); + } + myDebug() << "ExecExternalFetcher::slotError() - " << msg << endl; + m_errors << msg; +} + +void ExecExternalFetcher::slotProcessExited(KProcess*) { +// myDebug() << "ExecExternalFetcher::slotProcessExited()" << endl; + if(!m_process->normalExit() || m_process->exitStatus()) { + myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": process did not exit successfully" << endl; + if(!m_errors.isEmpty()) { + message(m_errors.join(QChar('\n')), MessageHandler::Error); + } + stop(); + return; + } + if(!m_errors.isEmpty()) { + message(m_errors.join(QChar('\n')), MessageHandler::Warning); + } + + if(m_data.isEmpty()) { + myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": no data" << endl; + stop(); + return; + } + + Import::Format format = static_cast<Import::Format>(m_formatType > -1 ? m_formatType : Import::TellicoXML); + Import::Importer* imp = ImportDialog::importer(format, KURL::List()); + if(!imp) { + stop(); + return; + } + + imp->setText(QString::fromUtf8(m_data, m_data.size())); + Data::CollPtr coll = imp->collection(); + if(!coll) { + if(!imp->statusMessage().isEmpty()) { + message(imp->statusMessage(), MessageHandler::Status); + } + myDebug() << "ExecExternalFetcher::slotProcessExited() - "<< source() << ": no collection pointer" << endl; + delete imp; + stop(); + return; + } + + delete imp; + if(coll->entryCount() == 0) { +// myDebug() << "ExecExternalFetcher::slotProcessExited() - no results" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + QString desc; + switch(coll->type()) { + case Data::Collection::Book: + case Data::Collection::Bibtex: + desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + break; + + case Data::Collection::Video: + desc = entry->field(QString::fromLatin1("studio")) + + QChar('/') + + entry->field(QString::fromLatin1("director")) + + QChar('/') + + entry->field(QString::fromLatin1("year")) + + QChar('/') + + entry->field(QString::fromLatin1("medium")); + break; + + case Data::Collection::Album: + desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + case Data::Collection::Game: + desc = entry->field(QString::fromLatin1("platform")); + break; + + case Data::Collection::ComicBook: + desc = entry->field(QString::fromLatin1("publisher")) + + QChar('/') + + entry->field(QString::fromLatin1("pub_year")); + break; + + case Data::Collection::BoardGame: + desc = entry->field(QString::fromLatin1("designer")) + + QChar('/') + + entry->field(QString::fromLatin1("publisher")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + default: + break; + } + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, entry); + emit signalResultFound(r); + } + stop(); // be sure to call this +} + +Tellico::Data::EntryPtr ExecExternalFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +void ExecExternalFetcher::updateEntry(Data::EntryPtr entry_) { + if(!m_canUpdate) { + emit signalDone(this); // must do this + } + + m_started = true; + + Data::ConstEntryPtr e(entry_.data()); + QStringList args = parseArguments(m_updateArgs); + for(QStringList::Iterator it = args.begin(); it != args.end(); ++it) { + *it = Data::Entry::dependentValue(e, *it, false); + } + startSearch(args); +} + +Tellico::Fetch::ConfigWidget* ExecExternalFetcher::configWidget(QWidget* parent_) const { + return new ExecExternalFetcher::ConfigWidget(parent_, this); +} + +ExecExternalFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ExecExternalFetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_), m_deleteOnRemove(false) { + QGridLayout* l = new QGridLayout(optionsWidget(), 5, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + + QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); + l->addWidget(label, ++row, 0); + m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); + connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + l->addWidget(m_collCombo, row, 1); + QString w = i18n("Set the collection type of the data returned from the external application."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_collCombo, w); + label->setBuddy(m_collCombo); + + label = new QLabel(i18n("&Result type: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_formatCombo = new GUI::ComboBox(optionsWidget()); + Import::FormatMap formatMap = ImportDialog::formatMap(); + for(Import::FormatMap::Iterator it = formatMap.begin(); it != formatMap.end(); ++it) { + if(ImportDialog::formatImportsText(it.key())) { + m_formatCombo->insertItem(it.data(), it.key()); + } + } + connect(m_formatCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + l->addWidget(m_formatCombo, row, 1); + w = i18n("Set the result type of the data returned from the external application."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_formatCombo, w); + label->setBuddy(m_formatCombo); + + label = new QLabel(i18n("Application &path: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_pathEdit = new KURLRequester(optionsWidget()); + connect(m_pathEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_pathEdit, row, 1); + w = i18n("Set the path of the application to run that should output a valid Tellico data file."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_pathEdit, w); + label->setBuddy(m_pathEdit); + + w = i18n("Select the search keys supported by the data source."); + QString w2 = i18n("Add any arguments that may be needed. <b>%1</b> will be replaced by the search term."); + QVGroupBox* box = new QVGroupBox(i18n("Arguments"), optionsWidget()); + ++row; + l->addMultiCellWidget(box, row, row, 0, 1); + QWidget* grid = new QWidget(box); + QGridLayout* gridLayout = new QGridLayout(grid); + gridLayout->setSpacing(2); + row = -1; + const Fetch::KeyMap keyMap = Fetch::Manager::self()->keyMap(); + for(Fetch::KeyMap::ConstIterator it = keyMap.begin(); it != keyMap.end(); ++it) { + FetchKey key = it.key(); + if(key == Raw) { + continue; + } + QCheckBox* cb = new QCheckBox(it.data(), grid); + gridLayout->addWidget(cb, ++row, 0); + m_cbDict.insert(key, cb); + GUI::LineEdit* le = new GUI::LineEdit(grid); + le->setHint(QString::fromLatin1("%1")); // for example + le->completionObject()->addItem(QString::fromLatin1("%1")); + gridLayout->addWidget(le, row, 1); + m_leDict.insert(key, le); + if(fetcher_ && fetcher_->m_args.contains(key)) { + cb->setChecked(true); + le->setEnabled(true); + le->setText(fetcher_->m_args[key]); + } else { + cb->setChecked(false); + le->setEnabled(false); + } + connect(cb, SIGNAL(toggled(bool)), le, SLOT(setEnabled(bool))); + QWhatsThis::add(cb, w); + QWhatsThis::add(le, w2); + } + m_cbUpdate = new QCheckBox(i18n("Update"), grid); + gridLayout->addWidget(m_cbUpdate, ++row, 0); + m_leUpdate = new GUI::LineEdit(grid); + m_leUpdate->setHint(QString::fromLatin1("%{title}")); // for example + m_leUpdate->completionObject()->addItem(QString::fromLatin1("%{title}")); + m_leUpdate->completionObject()->addItem(QString::fromLatin1("%{isbn}")); + gridLayout->addWidget(m_leUpdate, row, 1); + /* TRANSLATORS: Do not translate %{author}. */ + w2 = i18n("<p>Enter the arguments which should be used to search for available updates to an entry.</p><p>" + "The format is the same as for <i>Dependent</i> fields, where field values " + "are contained inside braces, such as <i>%{author}</i>. See the documentation for details.</p>"); + QWhatsThis::add(m_cbUpdate, w); + QWhatsThis::add(m_leUpdate, w2); + if(fetcher_ && fetcher_->m_canUpdate) { + m_cbUpdate->setChecked(true); + m_leUpdate->setEnabled(true); + m_leUpdate->setText(fetcher_->m_updateArgs); + } else { + m_cbUpdate->setChecked(false); + m_leUpdate->setEnabled(false); + } + connect(m_cbUpdate, SIGNAL(toggled(bool)), m_leUpdate, SLOT(setEnabled(bool))); + + l->setRowStretch(++row, 1); + + if(fetcher_) { + m_pathEdit->setURL(fetcher_->m_path); + m_newStuffName = fetcher_->m_newStuffName; + } + if(fetcher_ && fetcher_->m_collType > -1) { + m_collCombo->setCurrentType(fetcher_->m_collType); + } else { + m_collCombo->setCurrentType(Data::Collection::Book); + } + if(fetcher_ && fetcher_->m_formatType > -1) { + m_formatCombo->setCurrentItem(formatMap[static_cast<Import::Format>(fetcher_->m_formatType)]); + } else { + m_formatCombo->setCurrentItem(formatMap[Import::TellicoXML]); + } + m_deleteOnRemove = fetcher_ && fetcher_->m_deleteOnRemove; + KAcceleratorManager::manage(optionsWidget()); +} + +ExecExternalFetcher::ConfigWidget::~ConfigWidget() { +} + +void ExecExternalFetcher::ConfigWidget::readConfig(KConfig* config_) { + m_pathEdit->setURL(config_->readPathEntry("ExecPath")); + QValueList<int> argKeys = config_->readIntListEntry("ArgumentKeys"); + QStringList argValues = config_->readListEntry("Arguments"); + if(argKeys.count() != argValues.count()) { + kdWarning() << "ExecExternalFetcher::ConfigWidget::readConfig() - unequal number of arguments and keys" << endl; + } + int n = QMIN(argKeys.count(), argValues.count()); + QMap<FetchKey, QString> args; + for(int i = 0; i < n; ++i) { + args[static_cast<FetchKey>(argKeys[i])] = argValues[i]; + } + for(QValueList<int>::Iterator it = argKeys.begin(); it != argKeys.end(); ++it) { + if(*it == Raw) { + continue; + } + FetchKey key = static_cast<FetchKey>(*it); + QCheckBox* cb = m_cbDict[key]; + KLineEdit* le = m_leDict[key]; + if(cb && le) { + if(args.contains(key)) { + cb->setChecked(true); + le->setEnabled(true); + le->setText(args[key]); + } else { + cb->setChecked(false); + le->setEnabled(false); + le->clear(); + } + } + } + + if(config_->hasKey("UpdateArgs")) { + m_cbUpdate->setChecked(true); + m_leUpdate->setEnabled(true); + m_leUpdate->setText(config_->readEntry("UpdateArgs")); + } else { + m_cbUpdate->setChecked(false); + m_leUpdate->setEnabled(false); + m_leUpdate->clear(); + } + + int collType = config_->readNumEntry("CollectionType"); + m_collCombo->setCurrentType(collType); + + Import::FormatMap formatMap = ImportDialog::formatMap(); + int formatType = config_->readNumEntry("FormatType"); + m_formatCombo->setCurrentItem(formatMap[static_cast<Import::Format>(formatType)]); + m_deleteOnRemove = config_->readBoolEntry("DeleteOnRemove", false); + m_name = config_->readEntry("Name"); + m_newStuffName = config_->readEntry("NewStuffName"); +} + +void ExecExternalFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + QString s = m_pathEdit->url(); + if(!s.isEmpty()) { + config_.writePathEntry("ExecPath", s); + } + QValueList<int> keys; + QStringList args; + for(QIntDictIterator<QCheckBox> it(m_cbDict); it.current(); ++it) { + if(it.current()->isChecked()) { + keys << it.currentKey(); + args << m_leDict[it.currentKey()]->text(); + } + } + config_.writeEntry("ArgumentKeys", keys); + config_.writeEntry("Arguments", args); + + if(m_cbUpdate->isChecked()) { + config_.writeEntry("UpdateArgs", m_leUpdate->text()); + } else { + config_.deleteEntry("UpdateArgs"); + } + + config_.writeEntry("CollectionType", m_collCombo->currentType()); + config_.writeEntry("FormatType", m_formatCombo->currentData().toInt()); + config_.writeEntry("DeleteOnRemove", m_deleteOnRemove); + if(!m_newStuffName.isEmpty()) { + config_.writeEntry("NewStuffName", m_newStuffName); + } + slotSetModified(false); +} + +void ExecExternalFetcher::ConfigWidget::removed() { + if(!m_deleteOnRemove) { + return; + } + if(!m_newStuffName.isEmpty()) { + NewStuff::Manager man(this); + man.removeScript(m_newStuffName); + } +} + +QString ExecExternalFetcher::ConfigWidget::preferredName() const { + return m_name.isEmpty() ? ExecExternalFetcher::defaultName() : m_name; +} + +#include "execexternalfetcher.moc" diff --git a/src/fetch/execexternalfetcher.h b/src/fetch/execexternalfetcher.h new file mode 100644 index 0000000..bdc2a40 --- /dev/null +++ b/src/fetch/execexternalfetcher.h @@ -0,0 +1,118 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_EXECEXTERNALFETCHER_H +#define TELLICO_EXECEXTERNALFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qintdict.h> + +class KProcess; +class KURLRequester; +class KLineEdit; +class KComboBox; + +class QCheckBox; + +namespace Tellico { + namespace GUI { + class ComboBox; + class LineEdit; + class CollectionTypeCombo; + } + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class ExecExternalFetcher : public Fetcher { +Q_OBJECT + +public: + ExecExternalFetcher(QObject* parent, const char* name=0); + /** + */ + virtual ~ExecExternalFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual bool canSearch(FetchKey k) const { return m_args.contains(k); } + virtual bool canUpdate() const { return m_canUpdate; } + virtual void search(FetchKey key, const QString& value); + virtual void updateEntry(Data::EntryPtr entry); + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return ExecExternal; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + const QString& execPath() const { return m_path; } + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent = 0, const ExecExternalFetcher* fetcher = 0); + ~ConfigWidget(); + + void readConfig(KConfig* config); + virtual void saveConfig(KConfigGroup& config); + virtual void removed(); + virtual QString preferredName() const; + + private: + bool m_deleteOnRemove : 1; + QString m_name, m_newStuffName; + KURLRequester* m_pathEdit; + GUI::CollectionTypeCombo* m_collCombo; + GUI::ComboBox* m_formatCombo; + QIntDict<QCheckBox> m_cbDict; + QIntDict<GUI::LineEdit> m_leDict; + QCheckBox* m_cbUpdate; + GUI::LineEdit* m_leUpdate; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KProcess* proc, char* buffer, int len); + void slotError(KProcess* proc, char* buffer, int len); + void slotProcessExited(KProcess* proc); + +private: + static QStringList parseArguments(const QString& str); + + void startSearch(const QStringList& args); + + bool m_started; + int m_collType; + int m_formatType; + QString m_path; + QMap<FetchKey, QString> m_args; + bool m_canUpdate : 1; + QString m_updateArgs; + KProcess* m_process; + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; // map from search result id to entry + QStringList m_errors; + bool m_deleteOnRemove : 1; + QString m_newStuffName; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/fetch.h b/src/fetch/fetch.h new file mode 100644 index 0000000..0cdb726 --- /dev/null +++ b/src/fetch/fetch.h @@ -0,0 +1,64 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_H +#define TELLICO_FETCH_H + +namespace Tellico { + namespace Fetch { + +/** + * FetchFirst must be first, and the rest must follow consecutively in value. + * FetchLast must be last! + */ +enum FetchKey { + FetchFirst = 0, + Title, + Person, + ISBN, + UPC, + Keyword, + DOI, + ArxivID, + PubmedID, + LCCN, + Raw, + FetchLast +}; + +// real ones must start at 0! +enum Type { + Unknown = -1, + Amazon = 0, + IMDB, + Z3950, + SRU, + Entrez, + ExecExternal, + Yahoo, + AnimeNfo, + IBS, + ISBNdb, + GCstarPlugin, + CrossRef, + Citebase, + Arxiv, + Bibsonomy, + GoogleScholar, + Discogs +}; + + } +} + +#endif diff --git a/src/fetch/fetcher.cpp b/src/fetch/fetcher.cpp new file mode 100644 index 0000000..3bc7749 --- /dev/null +++ b/src/fetch/fetcher.cpp @@ -0,0 +1,61 @@ +/*************************************************************************** + copyright : (C) 2005-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 "fetcher.h" +#include "messagehandler.h" +#include "../entry.h" + +#include <kglobal.h> +#include <kconfig.h> + +using Tellico::Fetch::Fetcher; +using Tellico::Fetch::SearchResult; + +Fetcher::~Fetcher() { + KConfigGroup config(KGlobal::config(), m_configGroup); + saveConfigHook(config); +} + +void Fetcher::readConfig(const KConfigGroup& config_, const QString& groupName_) { + m_configGroup = groupName_; + + QString s = config_.readEntry("Name"); + if(!s.isEmpty()) { + m_name = s; + } + m_updateOverwrite = config_.readBoolEntry("UpdateOverwrite", false); + // be sure to read config for subclass + readConfigHook(config_); +} + +void Fetcher::message(const QString& message_, int type_) const { + if(m_messager) { + m_messager->send(message_, static_cast<MessageHandler::Type>(type_)); + } +} + +void Fetcher::infoList(const QString& message_, const QStringList& list_) const { + if(m_messager) { + m_messager->infoList(message_, list_); + } +} + +void Fetcher::updateEntry(Data::EntryPtr) { + emit signalDone(this); +} + +Tellico::Data::EntryPtr SearchResult::fetchEntry() { + return fetcher->fetchEntry(uid); +} + +#include "fetcher.moc" diff --git a/src/fetch/fetcher.h b/src/fetch/fetcher.h new file mode 100644 index 0000000..0d2496e --- /dev/null +++ b/src/fetch/fetcher.h @@ -0,0 +1,151 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FETCHER_H +#define FETCHER_H + +#include "fetch.h" +#include "../datavectors.h" + +#include <kapplication.h> // for KApplication::random() + +#include <qobject.h> +#include <qstring.h> + +class KConfigGroup; + +namespace Tellico { + namespace Fetch { + class ConfigWidget; + class MessageHandler; + class SearchResult; + +/** + * The top-level abstract class for fetching data. + * + * @author Robby Stephenson + */ +class Fetcher : public QObject, public KShared { +Q_OBJECT + +public: + typedef KSharedPtr<Fetcher> Ptr; + typedef KSharedPtr<const Fetcher> CPtr; + + /** + */ + Fetcher(QObject* parent, const char* name = 0) : QObject(parent, name), KShared(), + m_updateOverwrite(false), m_hasMoreResults(false), + m_messager(0) {} + /** + */ + virtual ~Fetcher(); + + /** + * Returns true if the fetcher might return entries from a certain collection type. + */ + virtual bool canFetch(int type) const = 0; + /** + * Returns true if the fetcher can search using a certain key. + */ + virtual bool canSearch(FetchKey key) const = 0; + virtual bool canUpdate() const { return true; } + + /** + * Returns the type of the data source. + */ + virtual Type type() const = 0; + /** + * Returns the name of the data source, as defined by the user. + */ + virtual QString source() const = 0; + /** + * Returns whether the fetcher will overwite existing info when updating + */ + bool updateOverwrite() const { return m_updateOverwrite; } + /** + * Starts a search, using a key and value. + */ + virtual void search(FetchKey key, const QString& value) = 0; + virtual void continueSearch() {} + virtual void updateEntry(Data::EntryPtr); + // mopst fetchers won't support this. it's particular useful for text fetchers + virtual void updateEntrySynchronous(Data::EntryPtr) {} + /** + * Returns true if the fetcher is currently searching. + */ + virtual bool isSearching() const = 0; + /** + * Returns true if the fetcher can continue and fetch more results + * The fetcher is responsible for remembering state. + */ + virtual bool hasMoreResults() const { return m_hasMoreResults; } + /** + * Stops the fetcher. + */ + virtual void stop() = 0; + /** + * Fetches an entry, given the uid of the search result. + */ + virtual Data::EntryPtr fetchEntry(uint uid) = 0; + + void setMessageHandler(MessageHandler* handler) { m_messager = handler; } + MessageHandler* messageHandler() const { return m_messager; } + /** + */ + void message(const QString& message, int type) const; + void infoList(const QString& message, const QStringList& list) const; + + /** + * Reads the config for the widget, given a config group. + */ + void readConfig(const KConfigGroup& config, const QString& groupName); + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual ConfigWidget* configWidget(QWidget* parent) const = 0; + +signals: +// void signalStatus(const QString& status); + void signalResultFound(Tellico::Fetch::SearchResult* result); + void signalDone(Tellico::Fetch::Fetcher::Ptr); + +protected: + QString m_name; + bool m_updateOverwrite : 1; + bool m_hasMoreResults : 1; + +private: + virtual void readConfigHook(const KConfigGroup&) = 0; + virtual void saveConfigHook(KConfigGroup&) {} + + MessageHandler* m_messager; + QString m_configGroup; +}; + +class SearchResult { +public: + SearchResult(Fetcher::Ptr f, const QString& t, const QString& d, const QString& i) + : uid(KApplication::random()), fetcher(f), title(t), desc(d), isbn(i) {} + Data::EntryPtr fetchEntry(); + uint uid; + Fetcher::Ptr fetcher; + QString title; + QString desc; + QString isbn; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/fetchmanager.cpp b/src/fetch/fetchmanager.cpp new file mode 100644 index 0000000..84f4f39 --- /dev/null +++ b/src/fetch/fetchmanager.cpp @@ -0,0 +1,707 @@ +/*************************************************************************** + copyright : (C) 2003-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 <config.h> + +#include "fetchmanager.h" +#include "configwidget.h" +#include "messagehandler.h" +#include "../tellico_kernel.h" +#include "../entry.h" +#include "../collection.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" + +#ifdef AMAZON_SUPPORT +#include "amazonfetcher.h" +#endif +#ifdef IMDB_SUPPORT +#include "imdbfetcher.h" +#endif +#ifdef HAVE_YAZ +#include "z3950fetcher.h" +#endif +#include "srufetcher.h" +#include "entrezfetcher.h" +#include "execexternalfetcher.h" +#include "yahoofetcher.h" +#include "animenfofetcher.h" +#include "ibsfetcher.h" +#include "isbndbfetcher.h" +#include "gcstarpluginfetcher.h" +#include "crossreffetcher.h" +#include "arxivfetcher.h" +#include "citebasefetcher.h" +#include "bibsonomyfetcher.h" +#include "googlescholarfetcher.h" +#include "discogsfetcher.h" + +#include <kglobal.h> +#include <kconfig.h> +#include <klocale.h> +#include <kiconloader.h> +#include <kmimetype.h> +#include <kstandarddirs.h> +#include <dcopref.h> +#include <ktempfile.h> +#include <kio/netaccess.h> + +#include <qfileinfo.h> +#include <qdir.h> + +#define LOAD_ICON(name, group, size) \ + KGlobal::iconLoader()->loadIcon(name, static_cast<KIcon::Group>(group), size_) + +using Tellico::Fetch::Manager; +Manager* Manager::s_self = 0; + +Manager::Manager() : QObject(), m_currentFetcherIndex(-1), m_messager(new ManagerMessage()), + m_count(0), m_loadDefaults(false) { + loadFetchers(); + +// m_keyMap.insert(FetchFirst, QString::null); + m_keyMap.insert(Title, i18n("Title")); + m_keyMap.insert(Person, i18n("Person")); + m_keyMap.insert(ISBN, i18n("ISBN")); + m_keyMap.insert(UPC, i18n("UPC/EAN")); + m_keyMap.insert(Keyword, i18n("Keyword")); + m_keyMap.insert(DOI, i18n("DOI")); + m_keyMap.insert(ArxivID, i18n("arXiv ID")); + m_keyMap.insert(PubmedID, i18n("Pubmed ID")); + // to keep from having a new i18n string, just remove octothorpe + m_keyMap.insert(LCCN, i18n("LCCN#").remove('#')); + m_keyMap.insert(Raw, i18n("Raw Query")); +// m_keyMap.insert(FetchLast, QString::null); +} + +Manager::~Manager() { + delete m_messager; +} + +void Manager::loadFetchers() { +// myDebug() << "Manager::loadFetchers()" << endl; + m_fetchers.clear(); + m_configMap.clear(); + + KConfig* config = KGlobal::config(); + if(config->hasGroup(QString::fromLatin1("Data Sources"))) { + KConfigGroup configGroup(config, QString::fromLatin1("Data Sources")); + int nSources = configGroup.readNumEntry("Sources Count", 0); + for(int i = 0; i < nSources; ++i) { + QString group = QString::fromLatin1("Data Source %1").arg(i); + Fetcher::Ptr f = createFetcher(config, group); + if(f) { + m_configMap.insert(f, group); + m_fetchers.append(f); + f->setMessageHandler(m_messager); + } + } + m_loadDefaults = false; + } else { // add default sources + m_fetchers = defaultFetchers(); + m_loadDefaults = true; + } +} + +Tellico::Fetch::FetcherVec Manager::fetchers(int type_) { + FetcherVec vec; + for(FetcherVec::Iterator it = m_fetchers.begin(); it != m_fetchers.end(); ++it) { + if(it->canFetch(type_)) { + vec.append(it.data()); + } + } + return vec; +} + +Tellico::Fetch::KeyMap Manager::keyMap(const QString& source_) const { + // an empty string means return all + if(source_.isEmpty()) { + return m_keyMap; + } + + // assume there's only one fetcher match + KSharedPtr<const Fetcher> f = 0; + for(FetcherVec::ConstIterator it = m_fetchers.constBegin(); it != m_fetchers.constEnd(); ++it) { + if(source_ == it->source()) { + f = it.data(); + break; + } + } + if(!f) { + kdWarning() << "Manager::keyMap() - no fetcher found!" << endl; + return KeyMap(); + } + + KeyMap map; + for(KeyMap::ConstIterator it = m_keyMap.begin(); it != m_keyMap.end(); ++it) { + if(f->canSearch(it.key())) { + map.insert(it.key(), it.data()); + } + } + return map; +} + +void Manager::startSearch(const QString& source_, FetchKey key_, const QString& value_) { + if(value_.isEmpty()) { + emit signalDone(); + return; + } + + // assume there's only one fetcher match + int i = 0; + m_currentFetcherIndex = -1; + for(FetcherVec::Iterator it = m_fetchers.begin(); it != m_fetchers.end(); ++it, ++i) { + if(source_ == it->source()) { + ++m_count; // Fetcher::search() might emit done(), so increment before calling search() + connect(it.data(), SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*)), + SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*))); + connect(it.data(), SIGNAL(signalDone(Tellico::Fetch::Fetcher::Ptr)), + SLOT(slotFetcherDone(Tellico::Fetch::Fetcher::Ptr))); + it->search(key_, value_); + m_currentFetcherIndex = i; + break; + } + } +} + +void Manager::continueSearch() { + if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast<int>(m_fetchers.count())) { + myDebug() << "Manager::continueSearch() - can't continue!" << endl; + emit signalDone(); + return; + } + Fetcher::Ptr f = m_fetchers[m_currentFetcherIndex]; + if(f && f->hasMoreResults()) { + ++m_count; + connect(f, SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*)), + SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*))); + connect(f, SIGNAL(signalDone(Tellico::Fetch::Fetcher::Ptr)), + SLOT(slotFetcherDone(Tellico::Fetch::Fetcher::Ptr))); + f->continueSearch(); + } else { + emit signalDone(); + } +} + +bool Manager::hasMoreResults() const { + if(m_currentFetcherIndex < 0 || m_currentFetcherIndex >= static_cast<int>(m_fetchers.count())) { + return false; + } + Fetcher::Ptr f = m_fetchers[m_currentFetcherIndex]; + return f && f->hasMoreResults(); +} + +void Manager::stop() { +// myDebug() << "Manager::stop()" << endl; + for(FetcherVec::Iterator it = m_fetchers.begin(); it != m_fetchers.end(); ++it) { + if(it->isSearching()) { + it->stop(); + } + } +#ifndef NDEBUG + if(m_count != 0) { + myDebug() << "Manager::stop() - count should be 0!" << endl; + } +#endif + m_count = 0; +} + +void Manager::slotFetcherDone(Fetcher::Ptr fetcher_) { +// myDebug() << "Manager::slotFetcherDone() - " << (fetcher_ ? fetcher_->source() : QString::null) +// << " :" << m_count << endl; + fetcher_->disconnect(); // disconnect all signals + --m_count; + if(m_count <= 0) { + emit signalDone(); + } +} + +bool Manager::canFetch() const { + for(FetcherVec::ConstIterator it = m_fetchers.constBegin(); it != m_fetchers.constEnd(); ++it) { + if(it->canFetch(Kernel::self()->collectionType())) { + return true; + } + } + return false; +} + +Tellico::Fetch::Fetcher::Ptr Manager::createFetcher(KConfig* config_, const QString& group_) { + if(!config_->hasGroup(group_)) { + myDebug() << "Manager::createFetcher() - no config group for " << group_ << endl; + return 0; + } + + KConfigGroup config(config_, group_); + + int fetchType = config.readNumEntry("Type", Fetch::Unknown); + if(fetchType == Fetch::Unknown) { + myDebug() << "Manager::createFetcher() - unknown type " << fetchType << ", skipping" << endl; + return 0; + } + + Fetcher::Ptr f = 0; + switch(fetchType) { + case Amazon: +#ifdef AMAZON_SUPPORT + { + int site = config.readNumEntry("Site", AmazonFetcher::Unknown); + if(site == AmazonFetcher::Unknown) { + myDebug() << "Manager::createFetcher() - unknown amazon site " << site << ", skipping" << endl; + } else { + f = new AmazonFetcher(static_cast<AmazonFetcher::Site>(site), this); + } + } +#endif + break; + + case IMDB: +#ifdef IMDB_SUPPORT + f = new IMDBFetcher(this); +#endif + break; + + case Z3950: +#ifdef HAVE_YAZ + f = new Z3950Fetcher(this); +#endif + break; + + case SRU: + f = new SRUFetcher(this); + break; + + case Entrez: + f = new EntrezFetcher(this); + break; + + case ExecExternal: + f = new ExecExternalFetcher(this); + break; + + case Yahoo: + f = new YahooFetcher(this); + break; + + case AnimeNfo: + f = new AnimeNfoFetcher(this); + break; + + case IBS: + f = new IBSFetcher(this); + break; + + case ISBNdb: + f = new ISBNdbFetcher(this); + break; + + case GCstarPlugin: + f = new GCstarPluginFetcher(this); + break; + + case CrossRef: + f = new CrossRefFetcher(this); + break; + + case Arxiv: + f = new ArxivFetcher(this); + break; + + case Citebase: + f = new CitebaseFetcher(this); + break; + + case Bibsonomy: + f = new BibsonomyFetcher(this); + break; + + case GoogleScholar: + f = new GoogleScholarFetcher(this); + break; + + case Discogs: + f = new DiscogsFetcher(this); + break; + + case Unknown: + default: + break; + } + if(f) { + f->readConfig(config, group_); + } + return f; +} + +// static +Tellico::Fetch::FetcherVec Manager::defaultFetchers() { + FetcherVec vec; +#ifdef AMAZON_SUPPORT + vec.append(new AmazonFetcher(AmazonFetcher::US, this)); +#endif +#ifdef IMDB_SUPPORT + vec.append(new IMDBFetcher(this)); +#endif + vec.append(SRUFetcher::libraryOfCongress(this)); + vec.append(new ISBNdbFetcher(this)); + vec.append(new YahooFetcher(this)); + vec.append(new AnimeNfoFetcher(this)); + vec.append(new ArxivFetcher(this)); + vec.append(new GoogleScholarFetcher(this)); + vec.append(new DiscogsFetcher(this)); +// only add IBS if user includes italian + if(KGlobal::locale()->languagesTwoAlpha().contains(QString::fromLatin1("it"))) { + vec.append(new IBSFetcher(this)); + } + return vec; +} + +Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_) { + if(m_loadDefaults) { + return defaultFetchers(); + } + + FetcherVec vec; + KConfigGroup config(KGlobal::config(), "Data Sources"); + int nSources = config.readNumEntry("Sources Count", 0); + for(int i = 0; i < nSources; ++i) { + QString group = QString::fromLatin1("Data Source %1").arg(i); + // needs the KConfig* + Fetcher::Ptr f = createFetcher(KGlobal::config(), group); + if(f && f->canFetch(collType_) && f->canUpdate()) { + vec.append(f); + } + } + return vec; +} + +Tellico::Fetch::FetcherVec Manager::createUpdateFetchers(int collType_, FetchKey key_) { + FetcherVec fetchers; + // creates new fetchers + FetcherVec allFetchers = createUpdateFetchers(collType_); + for(Fetch::FetcherVec::Iterator it = allFetchers.begin(); it != allFetchers.end(); ++it) { + if(it->canSearch(key_)) { + fetchers.append(it); + } + } + return fetchers; +} + +Tellico::Fetch::Fetcher::Ptr Manager::createUpdateFetcher(int collType_, const QString& source_) { + Fetcher::Ptr fetcher = 0; + // creates new fetchers + FetcherVec fetchers = createUpdateFetchers(collType_); + for(Fetch::FetcherVec::Iterator it = fetchers.begin(); it != fetchers.end(); ++it) { + if(it->source() == source_) { + fetcher = it; + break; + } + } + return fetcher; +} + +void Manager::updateStatus(const QString& message_) { + emit signalStatus(message_); +} + +Tellico::Fetch::TypePairList Manager::typeList() { + Fetch::TypePairList list; +#ifdef AMAZON_SUPPORT + list.append(TypePair(AmazonFetcher::defaultName(), Amazon)); +#endif +#ifdef IMDB_SUPPORT + list.append(TypePair(IMDBFetcher::defaultName(), IMDB)); +#endif +#ifdef HAVE_YAZ + list.append(TypePair(Z3950Fetcher::defaultName(), Z3950)); +#endif + list.append(TypePair(SRUFetcher::defaultName(), SRU)); + list.append(TypePair(EntrezFetcher::defaultName(), Entrez)); + list.append(TypePair(ExecExternalFetcher::defaultName(), ExecExternal)); + list.append(TypePair(YahooFetcher::defaultName(), Yahoo)); + list.append(TypePair(AnimeNfoFetcher::defaultName(), AnimeNfo)); + list.append(TypePair(IBSFetcher::defaultName(), IBS)); + list.append(TypePair(ISBNdbFetcher::defaultName(), ISBNdb)); + list.append(TypePair(GCstarPluginFetcher::defaultName(), GCstarPlugin)); + list.append(TypePair(CrossRefFetcher::defaultName(), CrossRef)); + list.append(TypePair(ArxivFetcher::defaultName(), Arxiv)); + list.append(TypePair(CitebaseFetcher::defaultName(), Citebase)); + list.append(TypePair(BibsonomyFetcher::defaultName(), Bibsonomy)); + list.append(TypePair(GoogleScholarFetcher::defaultName(),GoogleScholar)); + list.append(TypePair(DiscogsFetcher::defaultName(), Discogs)); + + // now find all the scripts distributed with tellico + QStringList files = KGlobal::dirs()->findAllResources("appdata", QString::fromLatin1("data-sources/*.spec"), + false, true); + for(QStringList::Iterator it = files.begin(); it != files.end(); ++it) { + KConfig spec(*it, false, false); + QString name = spec.readEntry("Name"); + if(name.isEmpty()) { + myDebug() << "Fetch::Manager::typeList() - no Name for " << *it << endl; + continue; + } + + if(!bundledScriptHasExecPath(*it, &spec)) { // no available exec + continue; + } + + list.append(TypePair(name, ExecExternal)); + m_scriptMap.insert(name, *it); + } + list.sort(); + return list; +} + + +// called when creating a new fetcher +Tellico::Fetch::ConfigWidget* Manager::configWidget(QWidget* parent_, Type type_, const QString& name_) { + ConfigWidget* w = 0; + switch(type_) { +#ifdef AMAZON_SUPPORT + case Amazon: + w = new AmazonFetcher::ConfigWidget(parent_); + break; +#endif +#ifdef IMDB_SUPPORT + case IMDB: + w = new IMDBFetcher::ConfigWidget(parent_); + break; +#endif +#ifdef HAVE_YAZ + case Z3950: + w = new Z3950Fetcher::ConfigWidget(parent_); + break; +#endif + case SRU: + w = new SRUConfigWidget(parent_); + break; + case Entrez: + w = new EntrezFetcher::ConfigWidget(parent_); + break; + case ExecExternal: + w = new ExecExternalFetcher::ConfigWidget(parent_); + if(!name_.isEmpty() && m_scriptMap.contains(name_)) { + // bundledScriptHasExecPath() actually needs to write the exec path + // back to the config so the configWidget can read it. But if the spec file + // is not readablle, that doesn't work. So work around it with a copy to a temp file + KTempFile tmpFile; + tmpFile.setAutoDelete(true); + KURL from, to; + from.setPath(m_scriptMap[name_]); + to.setPath(tmpFile.name()); + // have to overwrite since KTempFile already created it + if(!KIO::NetAccess::file_copy(from, to, -1, true /*overwrite*/)) { + myDebug() << KIO::NetAccess::lastErrorString() << endl; + } + KConfig spec(to.path(), false, false); + // pass actual location of spec file + if(name_ == spec.readEntry("Name") && bundledScriptHasExecPath(m_scriptMap[name_], &spec)) { + static_cast<ExecExternalFetcher::ConfigWidget*>(w)->readConfig(&spec); + } else { + kdWarning() << "Fetch::Manager::configWidget() - Can't read config file for " << to.path() << endl; + } + } + break; + case Yahoo: + w = new YahooFetcher::ConfigWidget(parent_); + break; + case AnimeNfo: + w = new AnimeNfoFetcher::ConfigWidget(parent_); + break; + case IBS: + w = new IBSFetcher::ConfigWidget(parent_); + break; + case ISBNdb: + w = new ISBNdbFetcher::ConfigWidget(parent_); + break; + case GCstarPlugin: + w = new GCstarPluginFetcher::ConfigWidget(parent_); + break; + case CrossRef: + w = new CrossRefFetcher::ConfigWidget(parent_); + break; + case Arxiv: + w = new ArxivFetcher::ConfigWidget(parent_); + break; + case Citebase: + w = new CitebaseFetcher::ConfigWidget(parent_); + break; + case Bibsonomy: + w = new BibsonomyFetcher::ConfigWidget(parent_); + break; + case GoogleScholar: + w = new GoogleScholarFetcher::ConfigWidget(parent_); + break; + case Discogs: + w = new DiscogsFetcher::ConfigWidget(parent_); + break; + case Unknown: + kdWarning() << "Fetch::Manager::configWidget() - no widget defined for type = " << type_ << endl; + } + return w; +} + +// static +QString Manager::typeName(Fetch::Type type_) { + switch(type_) { +#ifdef AMAZON_SUPPORT + case Amazon: return AmazonFetcher::defaultName(); +#endif +#ifdef IMDB_SUPPORT + case IMDB: return IMDBFetcher::defaultName(); +#endif +#ifdef HAVE_YAZ + case Z3950: return Z3950Fetcher::defaultName(); +#endif + case SRU: return SRUFetcher::defaultName(); + case Entrez: return EntrezFetcher::defaultName(); + case ExecExternal: return ExecExternalFetcher::defaultName(); + case Yahoo: return YahooFetcher::defaultName(); + case AnimeNfo: return AnimeNfoFetcher::defaultName(); + case IBS: return IBSFetcher::defaultName(); + case ISBNdb: return ISBNdbFetcher::defaultName(); + case GCstarPlugin: return GCstarPluginFetcher::defaultName(); + case CrossRef: return CrossRefFetcher::defaultName(); + case Arxiv: return ArxivFetcher::defaultName(); + case Citebase: return CitebaseFetcher::defaultName(); + case Bibsonomy: return BibsonomyFetcher::defaultName(); + case GoogleScholar: return GoogleScholarFetcher::defaultName(); + case Discogs: return DiscogsFetcher::defaultName(); + case Unknown: break; + } + myWarning() << "Manager::typeName() - none found for " << type_ << endl; + return QString::null; +} + +QPixmap Manager::fetcherIcon(Fetch::Fetcher::CPtr fetcher_, int group_, int size_) { +#ifdef HAVE_YAZ + if(fetcher_->type() == Fetch::Z3950) { + const Fetch::Z3950Fetcher* f = static_cast<const Fetch::Z3950Fetcher*>(fetcher_.data()); + KURL u; + u.setProtocol(QString::fromLatin1("http")); + u.setHost(f->host()); + QString icon = favIcon(u); + if(u.isValid() && !icon.isEmpty()) { + return LOAD_ICON(icon, group_, size_); + } + } else +#endif + if(fetcher_->type() == Fetch::ExecExternal) { + const Fetch::ExecExternalFetcher* f = static_cast<const Fetch::ExecExternalFetcher*>(fetcher_.data()); + const QString p = f->execPath(); + KURL u; + if(p.find(QString::fromLatin1("allocine")) > -1) { + u = QString::fromLatin1("http://www.allocine.fr"); + } else if(p.find(QString::fromLatin1("ministerio_de_cultura")) > -1) { + u = QString::fromLatin1("http://www.mcu.es"); + } else if(p.find(QString::fromLatin1("dark_horse_comics")) > -1) { + u = QString::fromLatin1("http://www.darkhorse.com"); + } else if(p.find(QString::fromLatin1("boardgamegeek")) > -1) { + u = QString::fromLatin1("http://www.boardgamegeek.com"); + } else if(f->source().find(QString::fromLatin1("amarok"), 0, false /*case-sensitive*/) > -1) { + return LOAD_ICON(QString::fromLatin1("amarok"), group_, size_); + } + if(!u.isEmpty() && u.isValid()) { + QString icon = favIcon(u); + if(!icon.isEmpty()) { + return LOAD_ICON(icon, group_, size_); + } + } + } + return fetcherIcon(fetcher_->type(), group_); +} + +QPixmap Manager::fetcherIcon(Fetch::Type type_, int group_, int size_) { + QString name; + switch(type_) { + case Amazon: + name = favIcon("http://amazon.com"); break; + case IMDB: + name = favIcon("http://imdb.com"); break; + case Z3950: + name = QString::fromLatin1("network"); break; // rather arbitrary + case SRU: + name = QString::fromLatin1("network_local"); break; // just to be different than z3950 + case Entrez: + name = favIcon("http://www.ncbi.nlm.nih.gov"); break; + case ExecExternal: + name = QString::fromLatin1("exec"); break; + case Yahoo: + name = favIcon("http://yahoo.com"); break; + case AnimeNfo: + name = favIcon("http://animenfo.com"); break; + case IBS: + name = favIcon("http://internetbookshop.it"); break; + case ISBNdb: + name = favIcon("http://isbndb.com"); break; + case GCstarPlugin: + name = QString::fromLatin1("gcstar"); break; + case CrossRef: + name = favIcon("http://crossref.org"); break; + case Arxiv: + name = favIcon("http://arxiv.org"); break; + case Citebase: + name = favIcon("http://citebase.org"); break; + case Bibsonomy: + name = favIcon("http://bibsonomy.org"); break; + case GoogleScholar: + name = favIcon("http://scholar.google.com"); break; + case Discogs: + name = favIcon("http://www.discogs.com"); break; + case Unknown: + kdWarning() << "Fetch::Manager::fetcherIcon() - no pixmap defined for type = " << type_ << endl; + } + + return name.isEmpty() ? QPixmap() : LOAD_ICON(name, group_, size_); +} + +QString Manager::favIcon(const KURL& url_) { + DCOPRef kded("kded", "favicons"); + DCOPReply reply = kded.call("iconForURL(KURL)", url_); + QString iconName = reply.isValid() ? reply : QString(); + if(!iconName.isEmpty()) { + return iconName; + } else { + // go ahead and try to download it for later + kded.call("downloadHostIcon(KURL)", url_); + } + return KMimeType::iconForURL(url_); +} + +bool Manager::bundledScriptHasExecPath(const QString& specFile_, KConfig* config_) { + // make sure ExecPath is set and executable + // for the bundled scripts, either the exec name is not set, in which case it is the + // name of the spec file, minus the .spec, or the exec is set, and is local to the dir + // if not, look for it + QString exec = config_->readPathEntry("ExecPath"); + QFileInfo specInfo(specFile_), execInfo(exec); + if(exec.isEmpty() || !execInfo.exists()) { + exec = specInfo.dirPath(true) + QDir::separator() + specInfo.baseName(true); // remove ".spec" + } else if(execInfo.isRelative()) { + exec = specInfo.dirPath(true) + exec; + } else if(!execInfo.isExecutable()) { + kdWarning() << "Fetch::Manager::execPathForBundledScript() - not executable: " << specFile_ << endl; + return false; + } + execInfo.setFile(exec); + if(!execInfo.exists() || !execInfo.isExecutable()) { + kdWarning() << "Fetch::Manager::execPathForBundledScript() - no exec file for " << specFile_ << endl; + kdWarning() << "exec = " << exec << endl; + return false; // we're not ok + } + + config_->writePathEntry("ExecPath", exec); + config_->sync(); // might be readonly, but that's ok + return true; +} + +#include "fetchmanager.moc" diff --git a/src/fetch/fetchmanager.h b/src/fetch/fetchmanager.h new file mode 100644 index 0000000..7036d71 --- /dev/null +++ b/src/fetch/fetchmanager.h @@ -0,0 +1,108 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FETCHMANAGER_H +#define FETCHMANAGER_H + +namespace Tellico { + namespace Fetch { + class SearchResult; + class ConfigWidget; + class ManagerMessage; + } +} + +#include "fetcher.h" +#include "../ptrvector.h" + +#include <ksortablevaluelist.h> + +#include <qobject.h> +#include <qmap.h> + +namespace Tellico { + namespace Fetch { + +typedef KSortableItem<Type, QString> TypePair; // fetcher info, type and name of type +typedef KSortableValueList<Type, QString> TypePairList; +typedef QMap<FetchKey, QString> KeyMap; // map key type to name of key +typedef Vector<Fetcher> FetcherVec; + +/** + * A manager for handling all the different classes of Fetcher. + * + * @author Robby Stephenson + */ +class Manager : public QObject { +Q_OBJECT + +public: + static Manager* self() { if(!s_self) s_self = new Manager(); return s_self; } + + ~Manager(); + + KeyMap keyMap(const QString& source = QString::null) const; + void startSearch(const QString& source, FetchKey key, const QString& value); + void continueSearch(); + void stop(); + bool canFetch() const; + bool hasMoreResults() const; + void loadFetchers(); + const FetcherVec& fetchers() const { return m_fetchers; } + FetcherVec fetchers(int type); + TypePairList typeList(); + ConfigWidget* configWidget(QWidget* parent, Type type, const QString& name); + + // create fetcher for updating an entry + FetcherVec createUpdateFetchers(int collType); + FetcherVec createUpdateFetchers(int collType, FetchKey key); + Fetcher::Ptr createUpdateFetcher(int collType, const QString& source); + + static QString typeName(Type type); + static QPixmap fetcherIcon(Fetch::Type type, int iconGroup=3 /*Small*/, int size=0 /* default */); + static QPixmap fetcherIcon(Fetch::Fetcher::CPtr ptr, int iconGroup=3 /*Small*/, int size=0 /* default*/); + +signals: + void signalStatus(const QString& status); + void signalResultFound(Tellico::Fetch::SearchResult* result); + void signalDone(); + +private slots: + void slotFetcherDone(Tellico::Fetch::Fetcher::Ptr); + +private: + friend class ManagerMessage; + static Manager* s_self; + + Manager(); + Fetcher::Ptr createFetcher(KConfig* config, const QString& configGroup); + FetcherVec defaultFetchers(); + void updateStatus(const QString& message); + + static QString favIcon(const KURL& url); + static bool bundledScriptHasExecPath(const QString& specFile, KConfig* config); + + FetcherVec m_fetchers; + int m_currentFetcherIndex; + KeyMap m_keyMap; + typedef QMap<Fetcher::Ptr, QString> ConfigMap; + ConfigMap m_configMap; + StringMap m_scriptMap; + ManagerMessage* m_messager; + uint m_count; + bool m_loadDefaults : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/gcstarpluginfetcher.cpp b/src/fetch/gcstarpluginfetcher.cpp new file mode 100644 index 0000000..4bffed7 --- /dev/null +++ b/src/fetch/gcstarpluginfetcher.cpp @@ -0,0 +1,486 @@ +/*************************************************************************** + copyright : (C) 2005-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 "gcstarpluginfetcher.h" +#include "messagehandler.h" +#include "fetchmanager.h" +#include "../collection.h" +#include "../entry.h" +#include "../translators/tellicoimporter.h" +#include "../gui/combobox.h" +#include "../gui/collectiontypecombo.h" +#include "../filehandler.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" + +#include <kconfig.h> +#include <kprocess.h> +#include <kprocio.h> +#include <kstandarddirs.h> +#include <kaccelmanager.h> + +#include <qdir.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qwhatsthis.h> + +using Tellico::Fetch::GCstarPluginFetcher; + +GCstarPluginFetcher::PluginMap GCstarPluginFetcher::pluginMap; +GCstarPluginFetcher::PluginParse GCstarPluginFetcher::pluginParse = NotYet; + +//static +GCstarPluginFetcher::PluginList GCstarPluginFetcher::plugins(int collType_) { + if(!pluginMap.contains(collType_)) { + GUI::CursorSaver cs; + QString gcstar = KStandardDirs::findExe(QString::fromLatin1("gcstar")); + + if(pluginParse == NotYet) { + KProcIO proc; + proc << gcstar << QString::fromLatin1("--version"); + // wait 5 seconds at most, just a sanity thing, never want to block completely + if(proc.start(KProcess::Block) && proc.wait(5)) { + QString output; + proc.readln(output); + if(!output.isEmpty()) { + // always going to be x.y[.z] ? + QRegExp versionRx(QString::fromLatin1("(\\d+)\\.(\\d+)(?:\\.(\\d+))?")); + if(versionRx.search(output) > -1) { + int x = versionRx.cap(1).toInt(); + int y = versionRx.cap(2).toInt(); + int z = versionRx.cap(3).toInt(); // ok to be empty + myDebug() << QString::fromLatin1("GCstarPluginFetcher() - found %1.%2.%3").arg(x).arg(y).arg(z) << endl; + // --list-plugins argument was added for 1.3 release + pluginParse = (x >= 1 && y >=3) ? New : Old; + } + } + } + // if still zero, then we should use old in future + if(pluginParse == NotYet) { + pluginParse = Old; + } + } + + if(pluginParse == New) { + readPluginsNew(collType_, gcstar); + } else { + readPluginsOld(collType_, gcstar); + } + } + + return pluginMap.contains(collType_) ? pluginMap[collType_] : GCstarPluginFetcher::PluginList(); +} + +void GCstarPluginFetcher::readPluginsNew(int collType_, const QString& gcstar_) { + PluginList plugins; + + QString gcstarCollection = gcstarType(collType_); + if(gcstarCollection.isEmpty()) { + pluginMap.insert(collType_, plugins); + return; + } + + KProcIO proc; + proc << gcstar_ + << QString::fromLatin1("-x") + << QString::fromLatin1("--list-plugins") + << QString::fromLatin1("--collection") << gcstarCollection; + + if(!proc.start(KProcess::Block)) { + myWarning() << "GCstarPluginFetcher::readPluginsNew() - can't start" << endl; + return; + } + + bool hasName = false; + PluginInfo info; + QString line; + for(int length = 0; length > -1; length = proc.readln(line)) { + if(line.isEmpty()) { + if(hasName) { + plugins << info; + } + hasName = false; + info.clear(); + } else { + // authors have \t at beginning + line = line.stripWhiteSpace(); + if(!hasName) { + info.insert(QString::fromLatin1("name"), line); + hasName = true; + } else { + info.insert(QString::fromLatin1("author"), line); + } +// myDebug() << line << endl; + } + } + + pluginMap.insert(collType_, plugins); +} + +void GCstarPluginFetcher::readPluginsOld(int collType_, const QString& gcstar_) { + QDir dir(gcstar_, QString::fromLatin1("GC*.pm")); + dir.cd(QString::fromLatin1("../../lib/gcstar/GCPlugins/")); + + QRegExp rx(QString::fromLatin1("get(Name|Author|Lang)\\s*\\{\\s*return\\s+['\"](.+)['\"]")); + rx.setMinimal(true); + + PluginList plugins; + + QString dirName = gcstarType(collType_); + if(dirName.isEmpty()) { + pluginMap.insert(collType_, plugins); + return; + } + + QStringList files = dir.entryList(); + for(QStringList::ConstIterator file = files.begin(); file != files.end(); ++file) { + KURL u; + u.setPath(dir.filePath(*file)); + PluginInfo info; + QString text = FileHandler::readTextFile(u); + for(int pos = rx.search(text); + pos > -1; + pos = rx.search(text, pos+rx.matchedLength())) { + info.insert(rx.cap(1).lower(), rx.cap(2)); + } + // only add if it has a name + if(info.contains(QString::fromLatin1("name"))) { + plugins << info; + } + } + // inserting empty map is ok + pluginMap.insert(collType_, plugins); +} + +QString GCstarPluginFetcher::gcstarType(int collType_) { + switch(collType_) { + case Data::Collection::Book: return QString::fromLatin1("GCbooks"); + case Data::Collection::Video: return QString::fromLatin1("GCfilms"); + case Data::Collection::Game: return QString::fromLatin1("GCgames"); + case Data::Collection::Album: return QString::fromLatin1("GCmusics"); + case Data::Collection::Coin: return QString::fromLatin1("GCcoins"); + case Data::Collection::Wine: return QString::fromLatin1("GCwines"); + case Data::Collection::BoardGame: return QString::fromLatin1("GCboardgames"); + default: break; + } + return QString(); +} + +GCstarPluginFetcher::GCstarPluginFetcher(QObject* parent_, const char* name_/*=0*/) : Fetcher(parent_, name_), + m_started(false), m_collType(-1), m_process(0) { +} + +GCstarPluginFetcher::~GCstarPluginFetcher() { + stop(); +} + +QString GCstarPluginFetcher::defaultName() { + return i18n("GCstar Plugin"); +} + +QString GCstarPluginFetcher::source() const { + return m_name; +} + +bool GCstarPluginFetcher::canFetch(int type_) const { + return m_collType == -1 ? false : m_collType == type_; +} + +void GCstarPluginFetcher::readConfigHook(const KConfigGroup& config_) { + m_collType = config_.readNumEntry("CollectionType", -1); + m_plugin = config_.readEntry("Plugin"); +} + +void GCstarPluginFetcher::search(FetchKey key_, const QString& value_) { + m_started = true; + m_data.truncate(0); + + if(key_ != Fetch::Title) { + myDebug() << "GCstarPluginFetcher::search() - only Title searches are supported" << endl; + stop(); + return; + } + + QString gcstar = KStandardDirs::findExe(QString::fromLatin1("gcstar")); + if(gcstar.isEmpty()) { + myWarning() << "GCstarPluginFetcher::search() - gcstar not found!" << endl; + stop(); + return; + } + + QString gcstarCollection = gcstarType(m_collType); + + if(m_plugin.isEmpty()) { + myWarning() << "GCstarPluginFetcher::search() - no plugin name! " << endl; + stop(); + return; + } + + m_process = new KProcess(); + connect(m_process, SIGNAL(receivedStdout(KProcess*, char*, int)), SLOT(slotData(KProcess*, char*, int))); + connect(m_process, SIGNAL(receivedStderr(KProcess*, char*, int)), SLOT(slotError(KProcess*, char*, int))); + connect(m_process, SIGNAL(processExited(KProcess*)), SLOT(slotProcessExited(KProcess*))); + QStringList args; + args << gcstar << QString::fromLatin1("-x") + << QString::fromLatin1("--collection") << gcstarCollection + << QString::fromLatin1("--export") << QString::fromLatin1("Tellico") + << QString::fromLatin1("--website") << m_plugin + << QString::fromLatin1("--download") << KProcess::quote(value_); + myLog() << "GCstarPluginFetcher::search() - " << args.join(QChar(' ')) << endl; + *m_process << args; + if(!m_process->start(KProcess::NotifyOnExit, KProcess::AllOutput)) { + myDebug() << "GCstarPluginFetcher::startSearch() - process failed to start" << endl; + stop(); + } +} + +void GCstarPluginFetcher::stop() { + if(!m_started) { + return; + } + if(m_process) { + m_process->kill(); + delete m_process; + m_process = 0; + } + m_data.truncate(0); + m_started = false; + m_errors.clear(); + emit signalDone(this); +} + +void GCstarPluginFetcher::slotData(KProcess*, char* buffer_, int len_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(buffer_, len_); +} + +void GCstarPluginFetcher::slotError(KProcess*, char* buffer_, int len_) { + QString msg = QString::fromLocal8Bit(buffer_, len_); + msg.prepend(source() + QString::fromLatin1(": ")); + myDebug() << "GCstarPluginFetcher::slotError() - " << msg << endl; + m_errors << msg; +} + +void GCstarPluginFetcher::slotProcessExited(KProcess*) { +// myDebug() << "GCstarPluginFetcher::slotProcessExited()" << endl; + if(!m_process->normalExit() || m_process->exitStatus()) { + myDebug() << "GCstarPluginFetcher::slotProcessExited() - "<< source() << ": process did not exit successfully" << endl; + if(!m_errors.isEmpty()) { + message(m_errors.join(QChar('\n')), MessageHandler::Error); + } + stop(); + return; + } + if(!m_errors.isEmpty()) { + message(m_errors.join(QChar('\n')), MessageHandler::Warning); + } + + if(m_data.isEmpty()) { + myDebug() << "GCstarPluginFetcher::slotProcessExited() - "<< source() << ": no data" << endl; + stop(); + return; + } + + Import::TellicoImporter imp(QString::fromUtf8(m_data, m_data.size())); + + Data::CollPtr coll = imp.collection(); + if(!coll) { + if(!imp.statusMessage().isEmpty()) { + message(imp.statusMessage(), MessageHandler::Status); + } + myDebug() << "GCstarPluginFetcher::slotProcessExited() - "<< source() << ": no collection pointer" << endl; + stop(); + return; + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + QString desc; + switch(coll->type()) { + case Data::Collection::Book: + case Data::Collection::Bibtex: + desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + break; + + case Data::Collection::Video: + desc = entry->field(QString::fromLatin1("studio")) + + QChar('/') + + entry->field(QString::fromLatin1("director")) + + QChar('/') + + entry->field(QString::fromLatin1("year")) + + QChar('/') + + entry->field(QString::fromLatin1("medium")); + break; + + case Data::Collection::Album: + desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + case Data::Collection::Game: + desc = entry->field(QString::fromLatin1("platform")); + break; + + case Data::Collection::ComicBook: + desc = entry->field(QString::fromLatin1("publisher")) + + QChar('/') + + entry->field(QString::fromLatin1("pub_year")); + break; + + case Data::Collection::BoardGame: + desc = entry->field(QString::fromLatin1("designer")) + + QChar('/') + + entry->field(QString::fromLatin1("publisher")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + default: + break; + } + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, entry); + emit signalResultFound(r); + } + stop(); // be sure to call this +} + +Tellico::Data::EntryPtr GCstarPluginFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +void GCstarPluginFetcher::updateEntry(Data::EntryPtr entry_) { + // ry searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "GCstarPluginFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* GCstarPluginFetcher::configWidget(QWidget* parent_) const { + return new GCstarPluginFetcher::ConfigWidget(parent_, this); +} + +GCstarPluginFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GCstarPluginFetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_), m_needPluginList(true) { + QGridLayout* l = new QGridLayout(optionsWidget(), 3, 4); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + + QLabel* label = new QLabel(i18n("Collection &type:"), optionsWidget()); + l->addWidget(label, ++row, 0); + m_collCombo = new GUI::CollectionTypeCombo(optionsWidget()); + connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_collCombo, SIGNAL(activated(int)), SLOT(slotTypeChanged())); + l->addMultiCellWidget(m_collCombo, row, row, 1, 3); + QString w = i18n("Set the collection type of the data returned from the plugin."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_collCombo, w); + label->setBuddy(m_collCombo); + + label = new QLabel(i18n("&Plugin: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_pluginCombo = new GUI::ComboBox(optionsWidget()); + connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + connect(m_pluginCombo, SIGNAL(activated(int)), SLOT(slotPluginChanged())); + l->addMultiCellWidget(m_pluginCombo, row, row, 1, 3); + w = i18n("Select the GCstar plugin used for the data source."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_pluginCombo, w); + label->setBuddy(m_pluginCombo); + + label = new QLabel(i18n("Author: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_authorLabel = new QLabel(optionsWidget()); + l->addWidget(m_authorLabel, row, 1); + +// label = new QLabel(i18n("Language: "), optionsWidget()); +// l->addWidget(label, row, 2); +// m_langLabel = new QLabel(optionsWidget()); +// l->addWidget(m_langLabel, row, 3); + + if(fetcher_ && fetcher_->m_collType > -1) { + m_collCombo->setCurrentType(fetcher_->m_collType); + } else { + m_collCombo->setCurrentType(Kernel::self()->collectionType()); + } + + if(fetcher_) { + m_originalPluginName = fetcher_->m_plugin; + } + + KAcceleratorManager::manage(optionsWidget()); +} + +GCstarPluginFetcher::ConfigWidget::~ConfigWidget() { +} + +void GCstarPluginFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + config_.writeEntry("CollectionType", m_collCombo->currentType()); + config_.writeEntry("Plugin", m_pluginCombo->currentText()); +} + +QString GCstarPluginFetcher::ConfigWidget::preferredName() const { + return QString::fromLatin1("GCstar - ") + m_pluginCombo->currentText(); +} + +void GCstarPluginFetcher::ConfigWidget::slotTypeChanged() { + int collType = m_collCombo->currentType(); + m_pluginCombo->clear(); + QStringList pluginNames; + GCstarPluginFetcher::PluginList list = GCstarPluginFetcher::plugins(collType); + for(GCstarPluginFetcher::PluginList::ConstIterator it = list.begin(); it != list.end(); ++it) { + pluginNames << (*it)[QString::fromLatin1("name")].toString(); + m_pluginCombo->insertItem(pluginNames.last(), *it); + } + slotPluginChanged(); + emit signalName(preferredName()); +} + +void GCstarPluginFetcher::ConfigWidget::slotPluginChanged() { + PluginInfo info = m_pluginCombo->currentData().toMap(); + m_authorLabel->setText(info[QString::fromLatin1("author")].toString()); +// m_langLabel->setText(info[QString::fromLatin1("lang")].toString()); + emit signalName(preferredName()); +} + +void GCstarPluginFetcher::ConfigWidget::showEvent(QShowEvent*) { + if(m_needPluginList) { + m_needPluginList = false; + slotTypeChanged(); // update plugin combo box + if(!m_originalPluginName.isEmpty()) { + m_pluginCombo->setCurrentText(m_originalPluginName); + slotPluginChanged(); + } + } +} + +#include "gcstarpluginfetcher.moc" diff --git a/src/fetch/gcstarpluginfetcher.h b/src/fetch/gcstarpluginfetcher.h new file mode 100644 index 0000000..1994b58 --- /dev/null +++ b/src/fetch/gcstarpluginfetcher.h @@ -0,0 +1,121 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GCSTARPLUGINFETCHER_H +#define TELLICO_GCSTARPLUGINFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qintdict.h> + +class QLabel; +class KProcess; + +namespace Tellico { + namespace GUI { + class ComboBox; + class CollectionTypeCombo; + } + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class GCstarPluginFetcher : public Fetcher { +Q_OBJECT + +public: + + GCstarPluginFetcher(QObject* parent, const char* name=0); + /** + */ + virtual ~GCstarPluginFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual bool canSearch(FetchKey k) const { return k == Title; } + + virtual void search(FetchKey key, const QString& value); + virtual void updateEntry(Data::EntryPtr entry); + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return GCstarPlugin; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KProcess* proc, char* buffer, int len); + void slotError(KProcess* proc, char* buffer, int len); + void slotProcessExited(KProcess* proc); + +private: + // map Author, Name, Lang, etc... + typedef QMap<QString, QVariant> PluginInfo; + typedef QValueList<PluginInfo> PluginList; + // map collection type to all available plugins + typedef QMap<int, PluginList> PluginMap; + static PluginMap pluginMap; + static PluginList plugins(int collType); + // we need to keep track if we've searched for plugins yet and by what method + enum PluginParse {NotYet, Old, New}; + static PluginParse pluginParse; + static void readPluginsNew(int collType, const QString& exe); + static void readPluginsOld(int collType, const QString& exe); + static QString gcstarType(int collType); + + bool m_started; + int m_collType; + QString m_plugin; + KProcess* m_process; + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; // map from search result id to entry + QStringList m_errors; +}; + +class GCstarPluginFetcher::ConfigWidget : public Fetch::ConfigWidget { +Q_OBJECT + +public: + ConfigWidget(QWidget* parent, const GCstarPluginFetcher* fetcher = 0); + ~ConfigWidget(); + + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + +private slots: + void slotTypeChanged(); + void slotPluginChanged(); + +private: + void showEvent(QShowEvent* event); + + bool m_needPluginList; + QString m_originalPluginName; + GUI::CollectionTypeCombo* m_collCombo; + GUI::ComboBox* m_pluginCombo; + QLabel* m_authorLabel; + QLabel* m_langLabel; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/googlescholarfetcher.cpp b/src/fetch/googlescholarfetcher.cpp new file mode 100644 index 0000000..21979c4 --- /dev/null +++ b/src/fetch/googlescholarfetcher.cpp @@ -0,0 +1,233 @@ +/*************************************************************************** + copyright : (C) 2008 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 "googlescholarfetcher.h" +#include "messagehandler.h" +#include "../filehandler.h" +#include "../translators/bibteximporter.h" +#include "../collection.h" +#include "../entry.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kio/job.h> + +#include <qlabel.h> +#include <qlayout.h> + +namespace { + static const int GOOGLE_MAX_RETURNS_TOTAL = 20; + static const char* SCHOLAR_BASE_URL = "http://scholar.google.com/scholar"; +} + +using Tellico::Fetch::GoogleScholarFetcher; + +GoogleScholarFetcher::GoogleScholarFetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), + m_limit(GOOGLE_MAX_RETURNS_TOTAL), m_start(0), m_job(0), m_started(false), + m_cookieIsSet(false) { + m_bibtexRx = QRegExp(QString::fromLatin1("<a\\s.*href\\s*=\\s*\"([^>]*scholar\\.bib[^>]*)\"")); + m_bibtexRx.setMinimal(true); +} + +GoogleScholarFetcher::~GoogleScholarFetcher() { +} + +QString GoogleScholarFetcher::defaultName() { + // no i18n + return QString::fromLatin1("Google Scholar"); +} + +QString GoogleScholarFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool GoogleScholarFetcher::canFetch(int type) const { + return type == Data::Collection::Bibtex; +} + +void GoogleScholarFetcher::readConfigHook(const KConfigGroup& config_) { + Q_UNUSED(config_); +} + +void GoogleScholarFetcher::search(FetchKey key_, const QString& value_) { + if(!m_cookieIsSet) { + // have to set preferences to have bibtex output + FileHandler::readTextFile(QString::fromLatin1("http://scholar.google.com/scholar_setprefs?num=100&scis=yes&scisf=4&submit=Save+Preferences"), true); + m_cookieIsSet = true; + } + m_key = key_; + m_value = value_; + m_started = true; + m_start = 0; + m_total = -1; + doSearch(); +} + +void GoogleScholarFetcher::continueSearch() { + m_started = true; + doSearch(); +} + +void GoogleScholarFetcher::doSearch() { +// myDebug() << "GoogleScholarFetcher::search() - value = " << value_ << endl; + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + KURL u(QString::fromLatin1(SCHOLAR_BASE_URL)); + u.addQueryItem(QString::fromLatin1("start"), QString::number(m_start)); + + switch(m_key) { + case Title: + u.addQueryItem(QString::fromLatin1("q"), QString::fromLatin1("allintitle:%1").arg(m_value)); + break; + + case Keyword: + u.addQueryItem(QString::fromLatin1("q"), m_value); + break; + + case Person: + u.addQueryItem(QString::fromLatin1("q"), QString::fromLatin1("author:%1").arg(m_value)); + break; + + default: + kdWarning() << "GoogleScholarFetcher::search() - key not recognized: " << m_key << endl; + stop(); + return; + } +// myDebug() << "GoogleScholarFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void GoogleScholarFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void GoogleScholarFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void GoogleScholarFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "GoogleScholarFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "GoogleScholarFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + + QString text = QString::fromUtf8(m_data, m_data.size()); + QString bibtex; + int count = 0; + for(int pos = text.find(m_bibtexRx); count < m_limit && pos > -1; pos = text.find(m_bibtexRx, pos+m_bibtexRx.matchedLength()), ++count) { + KURL bibtexUrl(QString::fromLatin1(SCHOLAR_BASE_URL), m_bibtexRx.cap(1)); +// myDebug() << bibtexUrl << endl; + bibtex += FileHandler::readTextFile(bibtexUrl, true); + } + + Import::BibtexImporter imp(bibtex); + Data::CollPtr coll = imp.collection(); + if(!coll) { + myDebug() << "GoogleScholarFetcher::slotComplete() - no collection pointer" << endl; + stop(); + return; + } + + count = 0; + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); count < m_limit && entry != entries.end(); ++entry, ++count) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + m_start = m_entries.count(); +// m_hasMoreResults = m_start <= m_total; + m_hasMoreResults = false; // for now, no continued searches + + stop(); // required +} + +Tellico::Data::EntryPtr GoogleScholarFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +void GoogleScholarFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "GoogleScholarFetcher::updateEntry()" << endl; + // limit to top 5 results + m_limit = 5; + + QString title = entry_->field(QString::fromLatin1("title")); + if(!title.isEmpty()) { + search(Title, title); + return; + } + + myDebug() << "GoogleScholarFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* GoogleScholarFetcher::configWidget(QWidget* parent_) const { + return new GoogleScholarFetcher::ConfigWidget(parent_, this); +} + +GoogleScholarFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const GoogleScholarFetcher*/*=0*/) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +QString GoogleScholarFetcher::ConfigWidget::preferredName() const { + return GoogleScholarFetcher::defaultName(); +} + +#include "googlescholarfetcher.moc" diff --git a/src/fetch/googlescholarfetcher.h b/src/fetch/googlescholarfetcher.h new file mode 100644 index 0000000..4e15475 --- /dev/null +++ b/src/fetch/googlescholarfetcher.h @@ -0,0 +1,103 @@ +/*************************************************************************** + copyright : (C) 2008 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; * + * * + ***************************************************************************/ + +#ifndef GOOGLESCHOLARFETCHER_H +#define GOOGLESCHOLARFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qguardedptr.h> +#include <qregexp.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * A fetcher for Google Scholar + * + * @author Robby Stephenson + */ +class GoogleScholarFetcher : public Fetcher { +Q_OBJECT + +public: + /** + */ + GoogleScholarFetcher(QObject* parent, const char* name = 0); + /** + */ + virtual ~GoogleScholarFetcher(); + + /** + */ + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + // amazon can search title or person + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return GoogleScholar; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const GoogleScholarFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup&) {} + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void doSearch(); + + int m_limit; + int m_start; + int m_total; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; + + QRegExp m_bibtexRx; + bool m_cookieIsSet; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/ibsfetcher.cpp b/src/fetch/ibsfetcher.cpp new file mode 100644 index 0000000..b11258b --- /dev/null +++ b/src/fetch/ibsfetcher.cpp @@ -0,0 +1,415 @@ +/*************************************************************************** + copyright : (C) 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 "ibsfetcher.h" +#include "messagehandler.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collections/bookcollection.h" +#include "../entry.h" +#include "../filehandler.h" +#include "../latin1literal.h" +#include "../imagefactory.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kio/job.h> + +#include <qregexp.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qfile.h> + +//#define IBS_TEST + +namespace { + static const char* IBS_BASE_URL = "http://www.internetbookshop.it/ser/serpge.asp"; +} + +using Tellico::Fetch::IBSFetcher; + +IBSFetcher::IBSFetcher(QObject* parent_, const char* name_ /*=0*/) + : Fetcher(parent_, name_), m_started(false) { +} + +QString IBSFetcher::defaultName() { + return i18n("Internet Bookshop (ibs.it)"); +} + +QString IBSFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool IBSFetcher::canFetch(int type) const { + return type == Data::Collection::Book || type == Data::Collection::Bibtex; +} + +void IBSFetcher::readConfigHook(const KConfigGroup& config_) { + Q_UNUSED(config_); +} + +void IBSFetcher::search(FetchKey key_, const QString& value_) { + m_started = true; + m_matches.clear(); + +#ifdef IBS_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/ibs.html")); +#else + KURL u(QString::fromLatin1(IBS_BASE_URL)); + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + switch(key_) { + case Title: + u.addQueryItem(QString::fromLatin1("Type"), QString::fromLatin1("keyword")); + u.addQueryItem(QString::fromLatin1("T"), value_); + break; + + case Person: + u.addQueryItem(QString::fromLatin1("Type"), QString::fromLatin1("keyword")); + u.addQueryItem(QString::fromLatin1("A"), value_); + break; + + case ISBN: + { + QString s = value_; + s.remove('-'); + // limit to first isbn + s = s.section(';', 0, 0); + u.setFileName(QString::fromLatin1("serdsp.asp")); + u.addQueryItem(QString::fromLatin1("isbn"), s); + } + break; + + case Keyword: + u.addQueryItem(QString::fromLatin1("Type"), QString::fromLatin1("keyword")); + u.addQueryItem(QString::fromLatin1("S"), value_); + break; + + default: + kdWarning() << "IBSFetcher::search() - key not recognized: " << key_ << endl; + stop(); + return; + } +#endif +// myDebug() << "IBSFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + if(key_ == ISBN) { + connect(m_job, SIGNAL(result(KIO::Job*)), SLOT(slotCompleteISBN(KIO::Job*))); + } else { + connect(m_job, SIGNAL(result(KIO::Job*)), SLOT(slotComplete(KIO::Job*))); + } +} + +void IBSFetcher::stop() { + if(!m_started) { + return; + } + + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void IBSFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void IBSFetcher::slotComplete(KIO::Job* job_) { + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "IBSFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + + QString s = Tellico::decodeHTML(QString(m_data)); + // really specific regexp + QString pat = QString::fromLatin1("http://www.internetbookshop.it/code/"); + QRegExp anchorRx(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*[\"'](") + + QRegExp::escape(pat) + + QString::fromLatin1("[^\"]*)\"[^>]*><b>([^<]+)<"), false); + anchorRx.setMinimal(true); + QRegExp tagRx(QString::fromLatin1("<.*>")); + tagRx.setMinimal(true); + + QString u, t, d; + int pos2; + for(int pos = anchorRx.search(s); m_started && pos > -1; pos = anchorRx.search(s, pos+anchorRx.matchedLength())) { + if(!u.isEmpty()) { + SearchResult* r = new SearchResult(this, t, d, QString()); + emit signalResultFound(r); + +#ifdef IBS_TEST + KURL url = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/ibs2.html")); +#else + // the url probable contains & so be careful + KURL url = u.replace(QString::fromLatin1("&"), QChar('&')); +#endif + m_matches.insert(r->uid, url); + + u.truncate(0); + t.truncate(0); + d.truncate(0); + } + u = anchorRx.cap(1); + t = anchorRx.cap(2); + pos2 = s.find(QString::fromLatin1("<br>"), pos, false); + if(pos2 > -1) { + int pos3 = s.find(QString::fromLatin1("<br>"), pos2+1, false); + if(pos3 > -1) { + d = s.mid(pos2, pos3-pos2).remove(tagRx).simplifyWhiteSpace(); + } + } + } +#ifndef IBS_TEST + if(!u.isEmpty()) { + SearchResult* r = new SearchResult(this, t, d, QString()); + emit signalResultFound(r); + m_matches.insert(r->uid, u.replace(QString::fromLatin1("&"), QChar('&'))); + } +#endif + + stop(); +} + +void IBSFetcher::slotCompleteISBN(KIO::Job* job_) { + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "IBSFetcher::slotCompleteISBN() - no data" << endl; + stop(); + return; + } + + QString str = Tellico::decodeHTML(QString(m_data)); + if(str.find(QString::fromLatin1("Libro non presente"), 0, false /* cas-sensitive */) > -1) { + stop(); + return; + } + Data::EntryPtr entry = parseEntry(str); + if(entry) { + QString desc = entry->field(QString::fromLatin1("author")) + + '/' + entry->field(QString::fromLatin1("publisher")); + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + emit signalResultFound(r); + m_matches.insert(r->uid, static_cast<KIO::TransferJob*>(job_)->url().url()); + } + + stop(); +} + +Tellico::Data::EntryPtr IBSFetcher::fetchEntry(uint uid_) { + // if we already grabbed this one, then just pull it out of the dict + Data::EntryPtr entry = m_entries[uid_]; + if(entry) { + return entry; + } + + KURL url = m_matches[uid_]; + if(url.isEmpty()) { + kdWarning() << "IBSFetcher::fetchEntry() - no url in map" << endl; + return 0; + } + + QString results = Tellico::decodeHTML(FileHandler::readTextFile(url, true)); + if(results.isEmpty()) { + myDebug() << "IBSFetcher::fetchEntry() - no text results" << endl; + return 0; + } + +// myDebug() << url.url() << endl; +#if 0 + kdWarning() << "Remove debug from ibsfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.html")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << results; + } + f.close(); +#endif + + entry = parseEntry(results); + if(!entry) { + myDebug() << "IBSFetcher::fetchEntry() - error in processing entry" << endl; + return 0; + } + m_entries.insert(uid_, entry); // keep for later + return entry; +} + +Tellico::Data::EntryPtr IBSFetcher::parseEntry(const QString& str_) { + // myDebug() << "IBSFetcher::parseEntry()" << endl; + // class might be anime_info_top + QString pat = QString::fromLatin1("%1(?:<[^>]+>)+([^<>\\s][^<>]+)"); + + QRegExp isbnRx(QString::fromLatin1("isbn=([\\dxX]{13})"), false); + QString isbn; + int pos = isbnRx.search(str_); + if(pos > -1) { + isbn = isbnRx.cap(1); + } + + Data::CollPtr coll = new Data::BookCollection(true); + + // map captions in HTML to field names + QMap<QString, QString> fieldMap; + fieldMap.insert(QString::fromLatin1("Titolo"), QString::fromLatin1("title")); + fieldMap.insert(QString::fromLatin1("Autore"), QString::fromLatin1("author")); + fieldMap.insert(QString::fromLatin1("Anno"), QString::fromLatin1("pub_year")); + fieldMap.insert(QString::fromLatin1("Categoria"), QString::fromLatin1("genre")); + fieldMap.insert(QString::fromLatin1("Rilegatura"), QString::fromLatin1("binding")); + fieldMap.insert(QString::fromLatin1("Editore"), QString::fromLatin1("publisher")); + fieldMap.insert(QString::fromLatin1("Dati"), QString::fromLatin1("edition")); + + QRegExp pagesRx(QString::fromLatin1("(\\d+) p\\.(\\s*,\\s*)?")); + Data::EntryPtr entry = new Data::Entry(coll); + + for(QMap<QString, QString>::Iterator it = fieldMap.begin(); it != fieldMap.end(); ++it) { + QRegExp infoRx(pat.arg(it.key())); + pos = infoRx.search(str_); + if(pos > -1) { + if(it.data() == Latin1Literal("edition")) { + int pos2 = pagesRx.search(infoRx.cap(1)); + if(pos2 > -1) { + entry->setField(QString::fromLatin1("pages"), pagesRx.cap(1)); + entry->setField(it.data(), infoRx.cap(1).remove(pagesRx)); + } else { + entry->setField(it.data(), infoRx.cap(1)); + } + } else { + entry->setField(it.data(), infoRx.cap(1)); + } + } + } + + // image + if(!isbn.isEmpty()) { + entry->setField(QString::fromLatin1("isbn"), isbn); +#if 1 + QString imgURL = QString::fromLatin1("http://giotto.ibs.it/cop/copt13.asp?f=%1").arg(isbn); + myLog() << "IBSFetcher() - cover = " << imgURL << endl; + QString id = ImageFactory::addImage(imgURL, true, QString::fromLatin1("http://internetbookshop.it")); + if(!id.isEmpty()) { + entry->setField(QString::fromLatin1("cover"), id); + } +#else + QRegExp imgRx(QString::fromLatin1("<img\\s+[^>]*\\s*src\\s*=\\s*\"(http://[^/]*\\.ibs\\.it/[^\"]+e=%1)").arg(isbn)); + imgRx.setMinimal(true); + pos = imgRx.search(str_); + if(pos > -1) { + myLog() << "IBSFetcher() - cover = " << imgRx.cap(1) << endl; + QString id = ImageFactory::addImage(imgRx.cap(1), true, QString::fromLatin1("http://internetbookshop.it")); + if(!id.isEmpty()) { + entry->setField(QString::fromLatin1("cover"), id); + } + } +#endif + } + + // now look for description + QRegExp descRx(QString::fromLatin1("Descrizione(?:<[^>]+>)+([^<>\\s].+)</span>"), false); + descRx.setMinimal(true); + pos = descRx.search(str_); + if(pos == -1) { + descRx.setPattern(QString::fromLatin1("In sintesi(?:<[^>]+>)+([^<>\\s].+)</span>")); + pos = descRx.search(str_); + } + if(pos > -1) { + Data::FieldPtr f = new Data::Field(QString::fromLatin1("plot"), i18n("Plot Summary"), Data::Field::Para); + coll->addField(f); + entry->setField(f, descRx.cap(1).simplifyWhiteSpace()); + } + + // IBS switches the surname and family name of the author + QStringList names = entry->fields(QString::fromLatin1("author"), false); + if(!names.isEmpty() && !names[0].isEmpty()) { + for(QStringList::Iterator it = names.begin(); it != names.end(); ++it) { + if((*it).find(',') > -1) { + continue; // skip if it has a comma + } + QStringList words = QStringList::split(' ', *it); + if(words.isEmpty()) { + continue; + } + // put first word in back + words.append(words[0]); + words.pop_front(); + *it = words.join(QChar(' ')); + } + entry->setField(QString::fromLatin1("author"), names.join(QString::fromLatin1("; "))); + } + return entry; +} + +void IBSFetcher::updateEntry(Data::EntryPtr entry_) { + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(!isbn.isEmpty()) { + search(Fetch::ISBN, isbn); + return; + } + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "IBSFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* IBSFetcher::configWidget(QWidget* parent_) const { + return new IBSFetcher::ConfigWidget(parent_); +} + +IBSFetcher::ConfigWidget::ConfigWidget(QWidget* parent_) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +QString IBSFetcher::ConfigWidget::preferredName() const { + return IBSFetcher::defaultName(); +} + +#include "ibsfetcher.moc" diff --git a/src/fetch/ibsfetcher.h b/src/fetch/ibsfetcher.h new file mode 100644 index 0000000..39326b2 --- /dev/null +++ b/src/fetch/ibsfetcher.h @@ -0,0 +1,87 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_IBSFETCHER_H +#define TELLICO_FETCH_IBSFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * A fetcher for animenfo.com + * + * @author Robby Stephenson + */ +class IBSFetcher : public Fetcher { +Q_OBJECT + +public: + IBSFetcher(QObject* parent, const char* name = 0); + virtual ~IBSFetcher() {} + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + // can search title, person, isbn, or keyword. No UPC or Raw for now. + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == Keyword; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return IBS; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_); + virtual void saveConfig(KConfigGroup&) {} + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + void slotCompleteISBN(KIO::Job* job); + +private: + Data::EntryPtr parseEntry(const QString& str); + + QByteArray m_data; + int m_total; + QMap<int, Data::EntryPtr> m_entries; + QMap<int, KURL> m_matches; + QGuardedPtr<KIO::Job> m_job; + + bool m_started; +// QStringList m_fields; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/imdbfetcher.cpp b/src/fetch/imdbfetcher.cpp new file mode 100644 index 0000000..1066177 --- /dev/null +++ b/src/fetch/imdbfetcher.cpp @@ -0,0 +1,1208 @@ +/*************************************************************************** + copyright : (C) 2004-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 "imdbfetcher.h" +#include "../tellico_kernel.h" +#include "../collections/videocollection.h" +#include "../entry.h" +#include "../field.h" +#include "../filehandler.h" +#include "../latin1literal.h" +#include "../imagefactory.h" +#include "../tellico_utils.h" +#include "../gui/listboxtext.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kdialogbase.h> +#include <kconfig.h> +#include <klineedit.h> +#include <knuminput.h> + +#include <qregexp.h> +#include <qfile.h> +#include <qmap.h> +#include <qvbox.h> +#include <qlabel.h> +#include <qlistbox.h> +#include <qwhatsthis.h> +#include <qlayout.h> +#include <qcheckbox.h> +#include <qvgroupbox.h> + +//#define IMDB_TEST + +namespace { + static const char* IMDB_SERVER = "akas.imdb.com"; + static const uint IMDB_MAX_RESULTS = 20; + static const QString sep = QString::fromLatin1("; "); +} + +using Tellico::Fetch::IMDBFetcher; + +QRegExp* IMDBFetcher::s_tagRx = 0; +QRegExp* IMDBFetcher::s_anchorRx = 0; +QRegExp* IMDBFetcher::s_anchorTitleRx = 0; +QRegExp* IMDBFetcher::s_anchorNameRx = 0; +QRegExp* IMDBFetcher::s_titleRx = 0; + +// static +void IMDBFetcher::initRegExps() { + s_tagRx = new QRegExp(QString::fromLatin1("<.*>")); + s_tagRx->setMinimal(true); + + s_anchorRx = new QRegExp(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*\"([^\"]*)\"[^<]*>([^<]*)</a>"), false); + s_anchorRx->setMinimal(true); + + s_anchorTitleRx = new QRegExp(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*\"([^\"]*/title/[^\"]*)\"[^<]*>([^<]*)</a>"), false); + s_anchorTitleRx->setMinimal(true); + + s_anchorNameRx = new QRegExp(QString::fromLatin1("<a\\s+[^>]*href\\s*=\\s*\"([^\"]*/name/[^\"]*)\"[^<]*>([^<]*)</a>"), false); + s_anchorNameRx->setMinimal(true); + + s_titleRx = new QRegExp(QString::fromLatin1("<title>(.*)</title>"), false); + s_titleRx->setMinimal(true); +} + +IMDBFetcher::IMDBFetcher(QObject* parent_, const char* name_) : Fetcher(parent_, name_), + m_job(0), m_started(false), m_fetchImages(true), m_host(QString::fromLatin1(IMDB_SERVER)), + m_limit(IMDB_MAX_RESULTS), m_countOffset(0) { + if(!s_tagRx) { + initRegExps(); + } +} + +IMDBFetcher::~IMDBFetcher() { +} + +QString IMDBFetcher::defaultName() { + return i18n("Internet Movie Database"); +} + +QString IMDBFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool IMDBFetcher::canFetch(int type) const { + return type == Data::Collection::Video; +} + +void IMDBFetcher::readConfigHook(const KConfigGroup& config_) { + QString h = config_.readEntry("Host"); + if(!h.isEmpty()) { + m_host = h; + } + m_numCast = config_.readNumEntry("Max Cast", 10); + m_fetchImages = config_.readBoolEntry("Fetch Images", true); + m_fields = config_.readListEntry("Custom Fields"); +} + +// multiple values not supported +void IMDBFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_; + m_started = true; + m_redirected = false; + m_data.truncate(0); + m_matches.clear(); + m_popularTitles.truncate(0); + m_exactTitles.truncate(0); + m_partialTitles.truncate(0); + m_currentTitleBlock = Unknown; + m_countOffset = 0; + +// only search if current collection is a video collection + if(Kernel::self()->collectionType() != Data::Collection::Video) { + myDebug() << "IMDBFetcher::search() - collection type mismatch, stopping" << endl; + stop(); + return; + } + +#ifdef IMDB_TEST + if(m_key == Title) { + m_url = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/imdb-title.html")); + m_redirected = false; + } else { + m_url = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/imdb-name.html")); + m_redirected = true; + } +#else + m_url = KURL(); + m_url.setProtocol(QString::fromLatin1("http")); + m_url.setHost(m_host.isEmpty() ? QString::fromLatin1(IMDB_SERVER) : m_host); + m_url.setPath(QString::fromLatin1("/find")); + + switch(key_) { + case Title: + m_url.addQueryItem(QString::fromLatin1("s"), QString::fromLatin1("tt")); + break; + + case Person: + m_url.addQueryItem(QString::fromLatin1("s"), QString::fromLatin1("nm")); + break; + + default: + kdWarning() << "IMDBFetcher::search() - FetchKey not supported" << endl; + stop(); + return; + } + + // as far as I can tell, the url encoding should always be iso-8859-1 + // not utf-8 + m_url.addQueryItem(QString::fromLatin1("q"), value_, 4 /* iso-8859-1 */); + +// myDebug() << "IMDBFetcher::search() url = " << m_url << endl; +#endif + + m_job = KIO::get(m_url, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); + connect(m_job, SIGNAL(redirection(KIO::Job *, const KURL&)), + SLOT(slotRedirection(KIO::Job*, const KURL&))); +} + +void IMDBFetcher::continueSearch() { + m_started = true; + m_limit += IMDB_MAX_RESULTS; + + if(m_currentTitleBlock == Popular) { + parseTitleBlock(m_popularTitles); + // if the offset is 0, then we need to be looking at the next block + m_currentTitleBlock = m_countOffset == 0 ? Exact : Popular; + } + + // current title block might have changed + if(m_currentTitleBlock == Exact) { + parseTitleBlock(m_exactTitles); + m_currentTitleBlock = m_countOffset == 0 ? Partial : Exact; + } + + if(m_currentTitleBlock == Partial) { + parseTitleBlock(m_partialTitles); + m_currentTitleBlock = m_countOffset == 0 ? Unknown : Partial; + } + + if(m_currentTitleBlock == SinglePerson) { + parseSingleNameResult(); + } + + stop(); +} + +void IMDBFetcher::stop() { + if(!m_started) { + return; + } +// myLog() << "IMDBFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + + m_started = false; + m_redirected = false; + + emit signalDone(this); +} + +void IMDBFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void IMDBFetcher::slotRedirection(KIO::Job*, const KURL& toURL_) { + m_url = toURL_; + m_redirected = true; +} + +void IMDBFetcher::slotComplete(KIO::Job* job_) { + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + stop(); + return; + } + + // a single result was found if we got redirected + if(m_key == Title) { + if(m_redirected) { + parseSingleTitleResult(); + } else { + parseMultipleTitleResults(); + } + } else { + if(m_redirected) { + parseSingleNameResult(); + } else { + parseMultipleNameResults(); + } + } +} + +void IMDBFetcher::parseSingleTitleResult() { +// myDebug() << "IMDBFetcher::parseSingleTitleResult()" << endl; + s_titleRx->search(Tellico::decodeHTML(QString(m_data))); + // split title at parenthesis + const QString cap1 = s_titleRx->cap(1); + int pPos = cap1.find('('); + // FIXME: maybe remove parentheses here? + SearchResult* r = new SearchResult(this, + pPos == -1 ? cap1 : cap1.left(pPos), + pPos == -1 ? QString::null : cap1.mid(pPos), + QString()); + m_matches.insert(r->uid, m_url); + emit signalResultFound(r); + + m_hasMoreResults = false; + stop(); +} + +void IMDBFetcher::parseMultipleTitleResults() { +// myDebug() << "IMDBFetcher::parseMultipleTitleResults()" << endl; + QString output = Tellico::decodeHTML(QString(m_data)); + + // IMDb can return three title lists, popular, exact, and partial + // the popular titles are in the first table, after the "Popular Results" text + int pos_popular = output.find(QString::fromLatin1("Popular Titles"), 0, false); + int pos_exact = output.find(QString::fromLatin1("Exact Matches"), QMAX(pos_popular, 0), false); + int pos_partial = output.find(QString::fromLatin1("Partial Matches"), QMAX(pos_exact, 0), false); + int end_popular = pos_exact; // keep track of where to end + if(end_popular == -1) { + end_popular = pos_partial == -1 ? output.length() : pos_partial; + } + int end_exact = pos_partial; // keep track of where to end + if(end_exact == -1) { + end_exact = output.length(); + } + + // if found popular matches + if(pos_popular > -1) { + m_popularTitles = output.mid(pos_popular, end_popular-pos_popular); + } + // if found exact matches + if(pos_exact > -1) { + m_exactTitles = output.mid(pos_exact, end_exact-pos_exact); + } + if(pos_partial > -1) { + m_partialTitles = output.mid(pos_partial); + } + + parseTitleBlock(m_popularTitles); + // if the offset is 0, then we need to be looking at the next block + m_currentTitleBlock = m_countOffset == 0 ? Exact : Popular; + + if(m_matches.size() < m_limit) { + parseTitleBlock(m_exactTitles); + m_currentTitleBlock = m_countOffset == 0 ? Partial : Exact; + } + + if(m_matches.size() < m_limit) { + parseTitleBlock(m_partialTitles); + m_currentTitleBlock = m_countOffset == 0 ? Unknown : Partial; + } + +#ifndef NDEBUG + if(m_matches.size() == 0) { + myDebug() << "IMDBFetcher::parseMultipleTitleResults() - no matches found." << endl; + } +#endif + + stop(); +} + +void IMDBFetcher::parseTitleBlock(const QString& str_) { + if(str_.isEmpty()) { + m_countOffset = 0; + return; + } +// myDebug() << "IMDBFetcher::parseTitleBlock() - " << m_currentTitleBlock << endl; + + QRegExp akaRx(QString::fromLatin1("aka (.*)(</li>|<br)"), false); + akaRx.setMinimal(true); + + m_hasMoreResults = false; + + int count = 0; + int start = s_anchorTitleRx->search(str_); + while(m_started && start > -1) { + // split title at parenthesis + const QString cap1 = s_anchorTitleRx->cap(1); // the anchor url + const QString cap2 = s_anchorTitleRx->cap(2).stripWhiteSpace(); // the anchor text + start += s_anchorTitleRx->matchedLength(); + int pPos = cap2.find('('); // if it has parentheses, use that for description + QString desc; + if(pPos > -1) { + int pPos2 = cap2.find(')', pPos+1); + if(pPos2 > -1) { + desc = cap2.mid(pPos+1, pPos2-pPos-1); + } + } else { + // parenthesis might be outside anchor tag + int end = s_anchorTitleRx->search(str_, start); + if(end == -1) { + end = str_.length(); + } + QString text = str_.mid(start, end-start); + pPos = text.find('('); + if(pPos > -1) { + int pNewLine = text.find(QString::fromLatin1("<br")); + if(pNewLine == -1 || pPos < pNewLine) { + int pPos2 = text.find(')', pPos); + desc = text.mid(pPos+1, pPos2-pPos-1); + } + pPos = -1; + } + } + // multiple matches might have 'aka' info + int end = s_anchorTitleRx->search(str_, start+1); + if(end == -1) { + end = str_.length(); + } + int akaPos = akaRx.search(str_, start+1); + if(akaPos > -1 && akaPos < end) { + // limit to 50 chars + desc += QChar(' ') + akaRx.cap(1).stripWhiteSpace().remove(*s_tagRx); + if(desc.length() > 50) { + desc = desc.left(50) + QString::fromLatin1("..."); + } + } + + start = s_anchorTitleRx->search(str_, start); + + if(count < m_countOffset) { + ++count; + continue; + } + + // if we got this far, then there is a valid result + if(m_matches.size() >= m_limit) { + m_hasMoreResults = true; + break; + } + + SearchResult* r = new SearchResult(this, pPos == -1 ? cap2 : cap2.left(pPos), desc, QString()); + KURL u(m_url, cap1); + u.setQuery(QString::null); + m_matches.insert(r->uid, u); + emit signalResultFound(r); + ++count; + } + if(!m_hasMoreResults && m_currentTitleBlock != Partial) { + m_hasMoreResults = true; + } + m_countOffset = m_matches.size() < m_limit ? 0 : count; +} + +void IMDBFetcher::parseSingleNameResult() { +// myDebug() << "IMDBFetcher::parseSingleNameResult()" << endl; + + m_currentTitleBlock = SinglePerson; + + QString output = Tellico::decodeHTML(QString(m_data)); + + int pos = s_anchorTitleRx->search(output); + if(pos == -1) { + stop(); + return; + } + + QRegExp tvRegExp(QString::fromLatin1("TV\\sEpisode"), false); + + int len = 0; + int count = 0; + QString desc; + for( ; m_started && pos > -1; pos = s_anchorTitleRx->search(output, pos+len)) { + desc.truncate(0); + bool isEpisode = false; + len = s_anchorTitleRx->cap(0).length(); + // split title at parenthesis + const QString cap2 = s_anchorTitleRx->cap(2).stripWhiteSpace(); + int pPos = cap2.find('('); + if(pPos > -1) { + desc = cap2.mid(pPos); + } else { + // look until the next <a + int aPos = output.find(QString::fromLatin1("<a"), pos+len, false); + if(aPos == -1) { + aPos = output.length(); + } + QString tmp = output.mid(pos+len, aPos-pos-len); + if(tmp.find(tvRegExp) > -1) { + isEpisode = true; + } + pPos = tmp.find('('); + if(pPos > -1) { + int pNewLine = tmp.find(QString::fromLatin1("<br")); + if(pNewLine == -1 || pPos < pNewLine) { + int pEnd = tmp.find(')', pPos+1); + desc = tmp.mid(pPos+1, pEnd-pPos-1).remove(*s_tagRx); + } + // but need to indicate it wasn't found initially + pPos = -1; + } + } + + ; + + if(count < m_countOffset) { + ++count; + continue; + } + + ++count; + if(isEpisode) { + continue; + } + + // if we got this far, then there is a valid result + if(m_matches.size() >= m_limit) { + m_hasMoreResults = true; + break; + } + + // FIXME: maybe remove parentheses here? + SearchResult* r = new SearchResult(this, pPos == -1 ? cap2 : cap2.left(pPos), desc, QString()); + KURL u(m_url, s_anchorTitleRx->cap(1)); // relative URL constructor + u.setQuery(QString::null); + m_matches.insert(r->uid, u); +// myDebug() << u.prettyURL() << endl; +// myDebug() << cap2 << endl; + emit signalResultFound(r); + } + if(pos == -1) { + m_hasMoreResults = false; + } + m_countOffset = count - 1; + + stop(); +} + +void IMDBFetcher::parseMultipleNameResults() { +// myDebug() << "IMDBFetcher::parseMultipleNameResults()" << endl; + + // the exact results are in the first table after the "exact results" text + QString output = Tellico::decodeHTML(QString(m_data)); + int pos = output.find(QString::fromLatin1("Popular Results"), 0, false); + if(pos == -1) { + pos = output.find(QString::fromLatin1("Exact Matches"), 0, false); + } + + // find beginning of partial matches + int end = output.find(QString::fromLatin1("Other Results"), QMAX(pos, 0), false); + if(end == -1) { + end = output.find(QString::fromLatin1("Partial Matches"), QMAX(pos, 0), false); + if(end == -1) { + end = output.find(QString::fromLatin1("Approx Matches"), QMAX(pos, 0), false); + if(end == -1) { + end = output.length(); + } + } + } + + QMap<QString, KURL> map; + QMap<QString, int> nameMap; + + QString s; + // if found exact matches + if(pos > -1) { + pos = s_anchorNameRx->search(output, pos+13); + while(pos > -1 && pos < end && m_matches.size() < m_limit) { + KURL u(m_url, s_anchorNameRx->cap(1)); + s = s_anchorNameRx->cap(2).stripWhiteSpace() + ' '; + // if more than one exact, add parentheses + if(nameMap.contains(s) && nameMap[s] > 0) { + // fix the first one that didn't have a number + if(nameMap[s] == 1) { + KURL u2 = map[s]; + map.remove(s); + map.insert(s + "(1) ", u2); + } + nameMap.insert(s, nameMap[s] + 1); + // check for duplicate names + s += QString::fromLatin1("(%1) ").arg(nameMap[s]); + } else { + nameMap.insert(s, 1); + } + map.insert(s, u); + pos = s_anchorNameRx->search(output, pos+s_anchorNameRx->cap(0).length()); + } + } + + // go ahead and search for partial matches + pos = s_anchorNameRx->search(output, end); + while(pos > -1 && m_matches.size() < m_limit) { + KURL u(m_url, s_anchorNameRx->cap(1)); // relative URL + s = s_anchorNameRx->cap(2).stripWhiteSpace(); + if(nameMap.contains(s) && nameMap[s] > 0) { + // fix the first one that didn't have a number + if(nameMap[s] == 1) { + KURL u2 = map[s]; + map.remove(s); + map.insert(s + " (1)", u2); + } + nameMap.insert(s, nameMap[s] + 1); + // check for duplicate names + s += QString::fromLatin1(" (%1)").arg(nameMap[s]); + } else { + nameMap.insert(s, 1); + } + map.insert(s, u); + pos = s_anchorNameRx->search(output, pos+s_anchorNameRx->cap(0).length()); + } + + if(map.count() == 0) { + stop(); + return; + } + + KDialogBase* dlg = new KDialogBase(Kernel::self()->widget(), "imdb dialog", + true, i18n("Select IMDB Result"), KDialogBase::Ok|KDialogBase::Cancel); + QVBox* box = new QVBox(dlg); + box->setSpacing(10); + (void) new QLabel(i18n("<qt>Your search returned multiple matches. Please select one below.</qt>"), box); + + QListBox* listBox = new QListBox(box); + listBox->setMinimumWidth(400); + listBox->setColumnMode(QListBox::FitToWidth); + const QStringList values = map.keys(); + for(QStringList::ConstIterator it = values.begin(); it != values.end(); ++it) { + if((*it).endsWith(QChar(' '))) { + GUI::ListBoxText* box = new GUI::ListBoxText(listBox, *it, 0); + box->setColored(true); + } else { + (void) new GUI::ListBoxText(listBox, *it); + } + } + listBox->setSelected(0, true); + QWhatsThis::add(listBox, i18n("<qt>Select a search result.</qt>")); + + dlg->setMainWidget(box); + if(dlg->exec() != QDialog::Accepted || listBox->currentText().isEmpty()) { + dlg->delayedDestruct(); + stop(); + return; + } + + m_url = map[listBox->currentText()]; + dlg->delayedDestruct(); + + // redirected is true since that's how I tell if an exact match has been found + m_redirected = true; + m_data.truncate(0); + m_job = KIO::get(m_url, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); + connect(m_job, SIGNAL(redirection(KIO::Job *, const KURL&)), + SLOT(slotRedirection(KIO::Job*, const KURL&))); + + // do not stop() here +} + +Tellico::Data::EntryPtr IMDBFetcher::fetchEntry(uint uid_) { + // if we already grabbed this one, then just pull it out of the dict + Data::EntryPtr entry = m_entries[uid_]; + if(entry) { + return entry; + } + + KURL url = m_matches[uid_]; + if(url.isEmpty()) { + myDebug() << "IMDBFetcher::fetchEntry() - no url found" << endl; + return 0; + } + + KURL origURL = m_url; // keep to switch back + QString results; + // if the url matches the current one, no need to redownload it + if(url == m_url) { +// myDebug() << "IMDBFetcher::fetchEntry() - matches previous URL, no downloading needed." << endl; + results = Tellico::decodeHTML(QString(m_data)); + } else { + // now it's sychronous +#ifdef IMDB_TEST + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/imdb-title-result.html")); + results = Tellico::decodeHTML(FileHandler::readTextFile(u)); +#else + // be quiet about failure + results = Tellico::decodeHTML(FileHandler::readTextFile(url, true)); + m_url = url; // needed for processing +#endif + } + if(results.isEmpty()) { + myDebug() << "IMDBFetcher::fetchEntry() - no text results" << endl; + m_url = origURL; + return 0; + } + + entry = parseEntry(results); + m_url = origURL; + if(!entry) { + myDebug() << "IMDBFetcher::fetchEntry() - error in processing entry" << endl; + return 0; + } + m_entries.insert(uid_, entry); // keep for later + return entry; +} + +Tellico::Data::EntryPtr IMDBFetcher::parseEntry(const QString& str_) { + Data::CollPtr coll = new Data::VideoCollection(true); + Data::EntryPtr entry = new Data::Entry(coll); + + doTitle(str_, entry); + doRunningTime(str_, entry); + doAspectRatio(str_, entry); + doAlsoKnownAs(str_, entry); + doPlot(str_, entry, m_url); + doLists(str_, entry); + doPerson(str_, entry, QString::fromLatin1("Director"), QString::fromLatin1("director")); + doPerson(str_, entry, QString::fromLatin1("Writer"), QString::fromLatin1("writer")); + doRating(str_, entry); + doCast(str_, entry, m_url); + if(m_fetchImages) { + // needs base URL + doCover(str_, entry, m_url); + } + + const QString imdb = QString::fromLatin1("imdb"); + if(!coll->hasField(imdb) && m_fields.findIndex(imdb) > -1) { + Data::FieldPtr field = new Data::Field(imdb, i18n("IMDB Link"), Data::Field::URL); + field->setCategory(i18n("General")); + coll->addField(field); + } + if(coll->hasField(imdb) && coll->fieldByName(imdb)->type() == Data::Field::URL) { + m_url.setQuery(QString::null); + entry->setField(imdb, m_url.url()); + } + return entry; +} + +void IMDBFetcher::doTitle(const QString& str_, Data::EntryPtr entry_) { + if(s_titleRx->search(str_) > -1) { + const QString cap1 = s_titleRx->cap(1); + // titles always have parentheses + int pPos = cap1.find('('); + QString title = cap1.left(pPos).stripWhiteSpace(); + // remove first and last quotes is there + if(title.startsWith(QChar('"')) && title.endsWith(QChar('"'))) { + title = title.mid(1, title.length()-2); + } + entry_->setField(QString::fromLatin1("title"), title); + // remove parenthesis + uint pPos2 = pPos+1; + while(pPos2 < cap1.length() && cap1[pPos2].isDigit()) { + ++pPos2; + } + QString year = cap1.mid(pPos+1, pPos2-pPos-1); + if(!year.isEmpty()) { + entry_->setField(QString::fromLatin1("year"), year); + } + } +} + +void IMDBFetcher::doRunningTime(const QString& str_, Data::EntryPtr entry_) { + // running time + QRegExp runtimeRx(QString::fromLatin1("runtime:.*(\\d+)\\s+min"), false); + runtimeRx.setMinimal(true); + + if(runtimeRx.search(str_) > -1) { +// myDebug() << "running-time = " << runtimeRx.cap(1) << endl; + entry_->setField(QString::fromLatin1("running-time"), runtimeRx.cap(1)); + } +} + +void IMDBFetcher::doAspectRatio(const QString& str_, Data::EntryPtr entry_) { + QRegExp rx(QString::fromLatin1("aspect ratio:.*([\\d\\.]+\\s*:\\s*[\\d\\.]+)"), false); + rx.setMinimal(true); + + if(rx.search(str_) > -1) { +// myDebug() << "aspect ratio = " << rx.cap(1) << endl; + entry_->setField(QString::fromLatin1("aspect-ratio"), rx.cap(1).stripWhiteSpace()); + } +} + +void IMDBFetcher::doAlsoKnownAs(const QString& str_, Data::EntryPtr entry_) { + if(m_fields.findIndex(QString::fromLatin1("alttitle")) == -1) { + return; + } + + // match until next b tag +// QRegExp akaRx(QString::fromLatin1("also known as(.*)<b(?:\\s.*)?>")); + QRegExp akaRx(QString::fromLatin1("also known as(.*)<(b[>\\s/]|div)"), false); + akaRx.setMinimal(true); + + if(akaRx.search(str_) > -1 && !akaRx.cap(1).isEmpty()) { + Data::FieldPtr f = entry_->collection()->fieldByName(QString::fromLatin1("alttitle")); + if(!f) { + f = new Data::Field(QString::fromLatin1("alttitle"), i18n("Alternative Titles"), Data::Field::Table); + f->setFormatFlag(Data::Field::FormatTitle); + entry_->collection()->addField(f); + } + + // split by <br>, remembering it could become valid xhtml! + QRegExp brRx(QString::fromLatin1("<br[\\s/]*>"), false); + brRx.setMinimal(true); + QStringList list = QStringList::split(brRx, akaRx.cap(1)); + // lang could be included with [fr] +// const QRegExp parRx(QString::fromLatin1("\\(.+\\)")); + const QRegExp brackRx(QString::fromLatin1("\\[\\w+\\]")); + QStringList values; + for(QStringList::Iterator it = list.begin(); it != list.end(); ++it) { + QString s = *it; + // sometimes, the word "more" gets linked to the releaseinfo page, check that + if(s.find(QString::fromLatin1("releaseinfo")) > -1) { + continue; + } + s.remove(*s_tagRx); + s.remove(brackRx); + s = s.stripWhiteSpace(); + // the first value ends up being or starting with the colon after "Also know as" + // I'm too lazy to figure out a better regexp + if(s.startsWith(QChar(':'))) { + s = s.mid(1); + } + if(!s.isEmpty()) { + values += s; + } + } + if(!values.isEmpty()) { + entry_->setField(QString::fromLatin1("alttitle"), values.join(sep)); + } + } +} + +void IMDBFetcher::doPlot(const QString& str_, Data::EntryPtr entry_, const KURL& baseURL_) { + // plot summaries provided by users are on a separate page + // should those be preferred? + + bool useUserSummary = false; + + QString thisPlot; + // match until next opening tag + QRegExp plotRx(QString::fromLatin1("plot (?:outline|summary):(.*)<[^/].*</"), false); + plotRx.setMinimal(true); + QRegExp plotURLRx(QString::fromLatin1("<a\\s+.*href\\s*=\\s*\".*/title/.*/plotsummary\""), false); + plotURLRx.setMinimal(true); + if(plotRx.search(str_) > -1) { + thisPlot = plotRx.cap(1); + thisPlot.remove(*s_tagRx); // remove HTML tags + entry_->setField(QString::fromLatin1("plot"), thisPlot); + // if thisPlot ends with (more) or contains + // a url that ends with plotsummary, then we'll grab it, otherwise not + if(plotRx.cap(0).endsWith(QString::fromLatin1("(more)</")) || plotURLRx.search(plotRx.cap(0)) > -1) { + useUserSummary = true; + } + } + + if(useUserSummary) { + QRegExp idRx(QString::fromLatin1("title/(tt\\d+)")); + idRx.search(baseURL_.path()); + KURL plotURL = baseURL_; + plotURL.setPath(QString::fromLatin1("/title/") + idRx.cap(1) + QString::fromLatin1("/plotsummary")); + // be quiet about failure + QString plotPage = FileHandler::readTextFile(plotURL, true); + + if(!plotPage.isEmpty()) { + QRegExp plotRx(QString::fromLatin1("<p\\s+class\\s*=\\s*\"plotpar\">(.*)</p")); + plotRx.setMinimal(true); + if(plotRx.search(plotPage) > -1) { + QString userPlot = plotRx.cap(1); + userPlot.remove(*s_tagRx); // remove HTML tags + entry_->setField(QString::fromLatin1("plot"), Tellico::decodeHTML(userPlot)); + } + } + } +} + +void IMDBFetcher::doPerson(const QString& str_, Data::EntryPtr entry_, + const QString& imdbHeader_, const QString& fieldName_) { + QRegExp br2Rx(QString::fromLatin1("<br[\\s/]*>\\s*<br[\\s/]*>"), false); + br2Rx.setMinimal(true); + QRegExp divRx(QString::fromLatin1("<[/]*div"), false); + divRx.setMinimal(true); + QString name = QString::fromLatin1("/name/"); + + StringSet people; + for(int pos = str_.find(imdbHeader_); pos > 0; pos = str_.find(imdbHeader_, pos)) { + // loop until repeated <br> tags or </div> tag + const int endPos1 = str_.find(br2Rx, pos); + const int endPos2 = str_.find(divRx, pos); + const int endPos = QMIN(endPos1, endPos2); // ok to be -1 + pos = s_anchorRx->search(str_, pos+1); + while(pos > -1 && pos < endPos) { + if(s_anchorRx->cap(1).find(name) > -1) { + people.add(s_anchorRx->cap(2).stripWhiteSpace()); + } + pos = s_anchorRx->search(str_, pos+1); + } + } + if(!people.isEmpty()) { + entry_->setField(fieldName_, people.toList().join(sep)); + } +} + +void IMDBFetcher::doCast(const QString& str_, Data::EntryPtr entry_, const KURL& baseURL_) { + // the extended cast list is on a separate page + // that's usually a lot of people + // but since it can be in billing order, the main actors might not + // be in the short list + QRegExp idRx(QString::fromLatin1("title/(tt\\d+)")); + idRx.search(baseURL_.path()); +#ifdef IMDB_TEST + KURL castURL = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/imdb-title-fullcredits.html")); +#else + KURL castURL = baseURL_; + castURL.setPath(QString::fromLatin1("/title/") + idRx.cap(1) + QString::fromLatin1("/fullcredits")); +#endif + // be quiet about failure and be sure to translate entities + QString castPage = Tellico::decodeHTML(FileHandler::readTextFile(castURL, true)); + + int pos = -1; + // the text to search, depends on which page is being read + QString castText = castPage; + if(castText.isEmpty()) { + // fall back to short list + castText = str_; + pos = castText.find(QString::fromLatin1("cast overview"), 0, false); + if(pos == -1) { + pos = castText.find(QString::fromLatin1("credited cast"), 0, false); + } + } else { + // first look for anchor + QRegExp castAnchorRx(QString::fromLatin1("<a\\s+name\\s*=\\s*\"cast\""), false); + pos = castText.find(castAnchorRx); + if(pos < 0) { + QRegExp tableClassRx(QString::fromLatin1("<table\\s+class\\s*=\\s*\"cast\""), false); + pos = castText.find(tableClassRx); + if(pos < 0) { + // fragile, the word "cast" appears in the title, but need to find + // the one right above the actual cast table + // for TV shows, there's a link on the sidebar for "episodes case" + // so need to not match that one + pos = castText.find(QString::fromLatin1("cast</"), 0, false); + if(pos > 9) { + // back up 9 places + if(castText.mid(pos-9, 9).startsWith(QString::fromLatin1("episodes"))) { + // find next cast list + pos = castText.find(QString::fromLatin1("cast</"), pos+6, false); + } + } + } + } + } + if(pos == -1) { // no cast list found + myDebug() << "IMDBFetcher::doCast() - no cast list found" << endl; + return; + } + + const QString name = QString::fromLatin1("/name/"); + QRegExp tdRx(QString::fromLatin1("<td[^>]*>(.*)</td>"), false); + tdRx.setMinimal(true); + + QStringList cast; + // loop until closing table tag + const int endPos = castText.find(QString::fromLatin1("</table"), pos, false); + pos = s_anchorRx->search(castText, pos+1); + while(pos > -1 && pos < endPos && static_cast<int>(cast.count()) < m_numCast) { + if(s_anchorRx->cap(1).find(name) > -1) { + // now search for <td> item with character name + // there's a column with ellipses then the character + const int pos2 = tdRx.search(castText, pos); + if(pos2 > -1 && tdRx.search(castText, pos2+1) > -1) { + cast += s_anchorRx->cap(2).stripWhiteSpace() + + QString::fromLatin1("::") + tdRx.cap(1).simplifyWhiteSpace().remove(*s_tagRx); + } else { + cast += s_anchorRx->cap(2).stripWhiteSpace(); + } + } + pos = s_anchorRx->search(castText, pos+1); + } + + if(!cast.isEmpty()) { + entry_->setField(QString::fromLatin1("cast"), cast.join(sep)); + } +} + +void IMDBFetcher::doRating(const QString& str_, Data::EntryPtr entry_) { + if(m_fields.findIndex(QString::fromLatin1("imdb-rating")) == -1) { + return; + } + + // don't add a colon, since there's a <br> at the end + // some of the imdb images use /10.gif in their path, so check for space or bracket + QRegExp rx(QString::fromLatin1("[>\\s](\\d+.?\\d*)/10[<//s]"), false); + rx.setMinimal(true); + + if(rx.search(str_) > -1 && !rx.cap(1).isEmpty()) { + Data::FieldPtr f = entry_->collection()->fieldByName(QString::fromLatin1("imdb-rating")); + if(!f) { + f = new Data::Field(QString::fromLatin1("imdb-rating"), i18n("IMDB Rating"), Data::Field::Rating); + f->setCategory(i18n("General")); + f->setProperty(QString::fromLatin1("maximum"), QString::fromLatin1("10")); + entry_->collection()->addField(f); + } + + bool ok; + float value = rx.cap(1).toFloat(&ok); + if(ok) { + entry_->setField(QString::fromLatin1("imdb-rating"), QString::number(value)); + } + } +} + +void IMDBFetcher::doCover(const QString& str_, Data::EntryPtr entry_, const KURL& baseURL_) { + // cover is the img with the "cover" alt text + QRegExp imgRx(QString::fromLatin1("<img\\s+[^>]*src\\s*=\\s*\"([^\"]*)\"[^>]*>"), false); + imgRx.setMinimal(true); + + QRegExp posterRx(QString::fromLatin1("<a\\s+[^>]*name\\s*=\\s*\"poster\"[^>]*>(.*)</a>"), false); + posterRx.setMinimal(true); + + const QString cover = QString::fromLatin1("cover"); + + int pos = posterRx.search(str_); + while(pos > -1) { + if(imgRx.search(posterRx.cap(1)) > -1) { + KURL u(baseURL_, imgRx.cap(1)); + QString id = ImageFactory::addImage(u, true); + if(!id.isEmpty()) { + entry_->setField(cover, id); + } + return; + } + pos = posterRx.search(str_, pos+1); + } + + // didn't find the cover, IMDb also used to put "cover" inside the url + pos = imgRx.search(str_); + while(pos > -1) { + if(imgRx.cap(0).find(cover, 0, false) > -1) { + KURL u(baseURL_, imgRx.cap(1)); + QString id = ImageFactory::addImage(u, true); + if(!id.isEmpty()) { + entry_->setField(cover, id); + } + return; + } + pos = imgRx.search(str_, pos+1); + } +} + +// end up reparsing whole string, but it's not really that slow +// loook at every anchor tag in the string +void IMDBFetcher::doLists(const QString& str_, Data::EntryPtr entry_) { + const QString genre = QString::fromLatin1("/Genres/"); + const QString country = QString::fromLatin1("/Countries/"); + const QString lang = QString::fromLatin1("/Languages/"); + const QString colorInfo = QString::fromLatin1("color-info"); + const QString cert = QString::fromLatin1("certificates="); + const QString soundMix = QString::fromLatin1("sound-mix="); + const QString year = QString::fromLatin1("/Years/"); + const QString company = QString::fromLatin1("/company/"); + + // IIMdb also has links with the word "sections" in them, remove that + // for genres and nationalities + + QStringList genres, countries, langs, certs, tracks, studios; + for(int pos = s_anchorRx->search(str_); pos > -1; pos = s_anchorRx->search(str_, pos+1)) { + const QString cap1 = s_anchorRx->cap(1); + if(cap1.find(genre) > -1) { + if(s_anchorRx->cap(2).find(QString::fromLatin1(" section"), 0, false) == -1) { + genres += s_anchorRx->cap(2).stripWhiteSpace(); + } + } else if(cap1.find(country) > -1) { + if(s_anchorRx->cap(2).find(QString::fromLatin1(" section"), 0, false) == -1) { + countries += s_anchorRx->cap(2).stripWhiteSpace(); + } + } else if(cap1.find(lang) > -1) { + langs += s_anchorRx->cap(2).stripWhiteSpace(); + } else if(cap1.find(colorInfo) > -1) { + // change "black and white" to "black & white" + entry_->setField(QString::fromLatin1("color"), + s_anchorRx->cap(2).replace(QString::fromLatin1("and"), QChar('&')).stripWhiteSpace()); + } else if(cap1.find(cert) > -1) { + certs += s_anchorRx->cap(2).stripWhiteSpace(); + } else if(cap1.find(soundMix) > -1) { + tracks += s_anchorRx->cap(2).stripWhiteSpace(); + } else if(cap1.find(company) > -1) { + studios += s_anchorRx->cap(2).stripWhiteSpace(); + // if year field wasn't set before, do it now + } else if(entry_->field(QString::fromLatin1("year")).isEmpty() && cap1.find(year) > -1) { + entry_->setField(QString::fromLatin1("year"), s_anchorRx->cap(2).stripWhiteSpace()); + } + } + + entry_->setField(QString::fromLatin1("genre"), genres.join(sep)); + entry_->setField(QString::fromLatin1("nationality"), countries.join(sep)); + entry_->setField(QString::fromLatin1("language"), langs.join(sep)); + entry_->setField(QString::fromLatin1("audio-track"), tracks.join(sep)); + entry_->setField(QString::fromLatin1("studio"), studios.join(sep)); + if(!certs.isEmpty()) { + // first try to set default certification + const QStringList& certsAllowed = entry_->collection()->fieldByName(QString::fromLatin1("certification"))->allowed(); + for(QStringList::ConstIterator it = certs.begin(); it != certs.end(); ++it) { + QString country = (*it).section(':', 0, 0); + QString cert = (*it).section(':', 1, 1); + if(cert == Latin1Literal("Unrated")) { + cert = QChar('U'); + } + cert += QString::fromLatin1(" (") + country + ')'; + if(certsAllowed.findIndex(cert) > -1) { + entry_->setField(QString::fromLatin1("certification"), cert); + break; + } + } + + // now add new field for all certifications + const QString allc = QString::fromLatin1("allcertification"); + if(m_fields.findIndex(allc) > -1) { + Data::FieldPtr f = entry_->collection()->fieldByName(allc); + if(!f) { + f = new Data::Field(allc, i18n("Certifications"), Data::Field::Table); + f->setFlags(Data::Field::AllowGrouped); + entry_->collection()->addField(f); + } + entry_->setField(QString::fromLatin1("allcertification"), certs.join(sep)); + } + } +} + +void IMDBFetcher::updateEntry(Data::EntryPtr entry_) { +// myLog() << "IMDBFetcher::updateEntry() - " << entry_->title() << endl; + // only take first 5 + m_limit = 5; + QString t = entry_->field(QString::fromLatin1("title")); + KURL link = entry_->field(QString::fromLatin1("imdb")); + if(!link.isEmpty() && link.isValid()) { + // check if we want a different host + if(link.host() != m_host) { +// myLog() << "IMDBFetcher::updateEntry() - switching hosts to " << m_host << endl; + link.setHost(m_host); + } + m_key = Fetch::Title; + m_value = t; + m_started = true; + m_data.truncate(0); + m_matches.clear(); + m_url = link; + m_redirected = true; // m_redirected is used as a flag later to tell if we get a single result + m_job = KIO::get(m_url, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); + connect(m_job, SIGNAL(redirection(KIO::Job *, const KURL&)), + SLOT(slotRedirection(KIO::Job*, const KURL&))); + return; + } + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* IMDBFetcher::configWidget(QWidget* parent_) const { + return new IMDBFetcher::ConfigWidget(parent_, this); +} + +IMDBFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const IMDBFetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 4, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + QLabel* label = new QLabel(i18n("Hos&t: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_hostEdit = new KLineEdit(optionsWidget()); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_hostEdit, row, 1); + QString w = i18n("The Internet Movie Database uses several different servers. Choose the one " + "you wish to use."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_hostEdit, w); + label->setBuddy(m_hostEdit); + + label = new QLabel(i18n("&Maximum cast: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_numCast = new KIntSpinBox(0, 99, 1, 10, 10, optionsWidget()); + connect(m_numCast, SIGNAL(valueChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_numCast, row, 1); + w = i18n("The list of cast members may include many people. Set the maximum number returned from the search."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_numCast, w); + label->setBuddy(m_numCast); + + m_fetchImageCheck = new QCheckBox(i18n("Download cover &image"), optionsWidget()); + connect(m_fetchImageCheck, SIGNAL(clicked()), SLOT(slotSetModified())); + ++row; + l->addMultiCellWidget(m_fetchImageCheck, row, row, 0, 1); + w = i18n("The cover image may be downloaded as well. However, too many large images in the " + "collection may degrade performance."); + QWhatsThis::add(m_fetchImageCheck, w); + + l->setRowStretch(++row, 10); + + // now add additional fields widget + addFieldsWidget(IMDBFetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); + + if(fetcher_) { + m_hostEdit->setText(fetcher_->m_host); + m_numCast->setValue(fetcher_->m_numCast); + m_fetchImageCheck->setChecked(fetcher_->m_fetchImages); + } else { //defaults + m_hostEdit->setText(QString::fromLatin1(IMDB_SERVER)); + m_numCast->setValue(10); + m_fetchImageCheck->setChecked(true); + } +} + +void IMDBFetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + QString host = m_hostEdit->text().stripWhiteSpace(); + if(!host.isEmpty()) { + config_.writeEntry("Host", host); + } + config_.writeEntry("Max Cast", m_numCast->value()); + config_.writeEntry("Fetch Images", m_fetchImageCheck->isChecked()); + + saveFieldsConfig(config_); + slotSetModified(false); +} + +QString IMDBFetcher::ConfigWidget::preferredName() const { + return IMDBFetcher::defaultName(); +} + +//static +Tellico::StringMap IMDBFetcher::customFields() { + StringMap map; + map[QString::fromLatin1("imdb")] = i18n("IMDB Link"); + map[QString::fromLatin1("imdb-rating")] = i18n("IMDB Rating"); + map[QString::fromLatin1("alttitle")] = i18n("Alternative Titles"); + map[QString::fromLatin1("allcertification")] = i18n("Certifications"); + return map; +} + +#include "imdbfetcher.moc" diff --git a/src/fetch/imdbfetcher.h b/src/fetch/imdbfetcher.h new file mode 100644 index 0000000..3dc19f2 --- /dev/null +++ b/src/fetch/imdbfetcher.h @@ -0,0 +1,141 @@ +/*************************************************************************** + copyright : (C) 2004-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; * + * * + ***************************************************************************/ + +#ifndef IMDBFETCHER_H +#define IMDBFETCHER_H + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kurl.h> +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qmap.h> +#include <qguardedptr.h> + +class KLineEdit; +class KIntSpinBox; +class QCheckBox; +class QRegExpr; + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class IMDBFetcher : public Fetcher { +Q_OBJECT + +public: + IMDBFetcher(QObject* parent, const char* name=0); + /** + */ + virtual ~IMDBFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + // imdb can search title, person + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return IMDB; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + static StringMap customFields(); + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const IMDBFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + + private: + KLineEdit* m_hostEdit; + QCheckBox* m_fetchImageCheck; + KIntSpinBox* m_numCast; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + void slotRedirection(KIO::Job* job, const KURL& toURL); + +private: + static void initRegExps(); + static QRegExp* s_tagRx; + static QRegExp* s_anchorRx; + static QRegExp* s_anchorTitleRx; + static QRegExp* s_anchorNameRx; + static QRegExp* s_titleRx; + + void doTitle(const QString& s, Data::EntryPtr e); + void doRunningTime(const QString& s, Data::EntryPtr e); + void doAspectRatio(const QString& s, Data::EntryPtr e); + void doAlsoKnownAs(const QString& s, Data::EntryPtr e); + void doPlot(const QString& s, Data::EntryPtr e, const KURL& baseURL_); + void doPerson(const QString& s, Data::EntryPtr e, + const QString& imdbHeader, const QString& fieldName); + void doCast(const QString& s, Data::EntryPtr e, const KURL& baseURL_); + void doLists(const QString& s, Data::EntryPtr e); + void doRating(const QString& s, Data::EntryPtr e); + void doCover(const QString& s, Data::EntryPtr e, const KURL& baseURL); + + void parseSingleTitleResult(); + void parseSingleNameResult(); + void parseMultipleTitleResults(); + void parseTitleBlock(const QString& str); + void parseMultipleNameResults(); + Data::EntryPtr parseEntry(const QString& str); + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QMap<int, KURL> m_matches; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; + bool m_fetchImages; + + QString m_host; + int m_numCast; + KURL m_url; + bool m_redirected; + uint m_limit; + QStringList m_fields; + + QString m_popularTitles; + QString m_exactTitles; + QString m_partialTitles; + enum TitleBlock { Unknown = 0, Popular = 1, Exact = 2, Partial = 3, SinglePerson = 4}; + TitleBlock m_currentTitleBlock; + int m_countOffset; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/isbndbfetcher.cpp b/src/fetch/isbndbfetcher.cpp new file mode 100644 index 0000000..5ffc379 --- /dev/null +++ b/src/fetch/isbndbfetcher.cpp @@ -0,0 +1,350 @@ +/*************************************************************************** + copyright : (C) 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 "isbndbfetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> + +#include <qdom.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qfile.h> + +namespace { + static const int ISBNDB_RETURNS_PER_REQUEST = 10; + static const int ISBNDB_MAX_RETURNS_TOTAL = 25; + static const char* ISBNDB_BASE_URL = "http://isbndb.com/api/books.xml"; + static const char* ISBNDB_APP_ID = "3B9S3BQS"; +} + +using Tellico::Fetch::ISBNdbFetcher; + +ISBNdbFetcher::ISBNdbFetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_xsltHandler(0), + m_limit(ISBNDB_MAX_RETURNS_TOTAL), m_page(1), m_total(-1), m_countOffset(0), + m_job(0), m_started(false) { +} + +ISBNdbFetcher::~ISBNdbFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString ISBNdbFetcher::defaultName() { + return i18n("ISBNdb.com"); +} + +QString ISBNdbFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool ISBNdbFetcher::canFetch(int type) const { + return type == Data::Collection::Book || type == Data::Collection::ComicBook || type == Data::Collection::Bibtex; +} + +void ISBNdbFetcher::readConfigHook(const KConfigGroup& config_) { + Q_UNUSED(config_); +} + +void ISBNdbFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_.stripWhiteSpace(); + m_started = true; + m_page = 1; + m_total = -1; + m_numResults = 0; + m_countOffset = 0; + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + doSearch(); +} + +void ISBNdbFetcher::continueSearch() { + m_started = true; + m_limit += ISBNDB_MAX_RETURNS_TOTAL; + doSearch(); +} + +void ISBNdbFetcher::doSearch() { + m_data.truncate(0); + +// myDebug() << "ISBNdbFetcher::search() - value = " << value_ << endl; + + KURL u(QString::fromLatin1(ISBNDB_BASE_URL)); + u.addQueryItem(QString::fromLatin1("access_key"), QString::fromLatin1(ISBNDB_APP_ID)); + u.addQueryItem(QString::fromLatin1("results"), QString::fromLatin1("details,authors,subjects,texts")); + u.addQueryItem(QString::fromLatin1("page_number"), QString::number(m_page)); + + switch(m_key) { + case Title: + u.addQueryItem(QString::fromLatin1("index1"), QString::fromLatin1("title")); + u.addQueryItem(QString::fromLatin1("value1"), m_value); + break; + + case Person: + // yes, this also queries titles, too, it's a limitation of the isbndb api service + u.addQueryItem(QString::fromLatin1("index1"), QString::fromLatin1("combined")); + u.addQueryItem(QString::fromLatin1("value1"), m_value); + break; + + case Keyword: + u.addQueryItem(QString::fromLatin1("index1"), QString::fromLatin1("full")); + u.addQueryItem(QString::fromLatin1("value1"), m_value); + break; + + case ISBN: + u.addQueryItem(QString::fromLatin1("index1"), QString::fromLatin1("isbn")); + { + // only grab first value + QString v = m_value.section(QChar(';'), 0); + v.remove('-'); + u.addQueryItem(QString::fromLatin1("value1"), v); + } + break; + + default: + kdWarning() << "ISBNdbFetcher::search() - key not recognized: " << m_key << endl; + stop(); + return; + } +// myDebug() << "ISBNdbFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void ISBNdbFetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "ISBNdbFetcher::stop()" << endl; + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void ISBNdbFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void ISBNdbFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "ISBNdbFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "ISBNdbFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from isbndbfetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "ISBNdbFetcher::slotComplete() - server did not return valid XML." << endl; + return; + } + + if(m_total == -1) { + QDomNode n = dom.documentElement().namedItem(QString::fromLatin1("BookList")); + QDomElement e = n.toElement(); + if(!e.isNull()) { + m_total = e.attribute(QString::fromLatin1("total_results"), QString::number(-1)).toInt(); + } + } + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + + // assume result is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + + int count = 0; + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); m_numResults < m_limit && entry != entries.end(); ++entry, ++count) { + if(count < m_countOffset) { + continue; + } + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + ++m_numResults; + } + + // are there any additional results to get? + m_hasMoreResults = m_page * ISBNDB_RETURNS_PER_REQUEST < m_total; + + const int currentTotal = QMIN(m_total, m_limit); + if(m_page * ISBNDB_RETURNS_PER_REQUEST < currentTotal) { + int foundCount = (m_page-1) * ISBNDB_RETURNS_PER_REQUEST + coll->entryCount(); + message(i18n("Results from %1: %2/%3").arg(source()).arg(foundCount).arg(m_total), MessageHandler::Status); + ++m_page; + m_countOffset = 0; + doSearch(); + } else { + m_countOffset = m_entries.count() % ISBNDB_RETURNS_PER_REQUEST; + if(m_countOffset == 0) { + ++m_page; // need to go to next page + } + stop(); // required + } +} + +Tellico::Data::EntryPtr ISBNdbFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + if(!entry) { + kdWarning() << "ISBNdbFetcher::fetchEntry() - no entry in dict" << endl; + return 0; + } + + // if the publisher id is set, then we need to grab the real publisher name + const QString id = entry->field(QString::fromLatin1("pub_id")); + if(!id.isEmpty()) { + KURL u(QString::fromLatin1(ISBNDB_BASE_URL)); + u.setFileName(QString::fromLatin1("publishers.xml")); + u.addQueryItem(QString::fromLatin1("access_key"), QString::fromLatin1(ISBNDB_APP_ID)); + u.addQueryItem(QString::fromLatin1("index1"), QString::fromLatin1("publisher_id")); + u.addQueryItem(QString::fromLatin1("value1"), id); + + QDomDocument dom = FileHandler::readXMLFile(u, true); + if(!dom.isNull()) { + QString pub = dom.documentElement().namedItem(QString::fromLatin1("PublisherList")) + .namedItem(QString::fromLatin1("PublisherData")) + .namedItem(QString::fromLatin1("Name")) + .toElement().text(); + if(!pub.isEmpty()) { + entry->setField(QString::fromLatin1("publisher"), pub); + } + } + entry->setField(QString::fromLatin1("pub_id"), QString()); + } + + return entry; +} + +void ISBNdbFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("isbndb2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "ISBNdbFetcher::initXSLTHandler() - can not locate isbndb2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "ISBNdbFetcher::initXSLTHandler() - error in isbndb2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +void ISBNdbFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "ISBNdbFetcher::updateEntry()" << endl; + // limit to top 5 results + m_limit = 5; + + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(!isbn.isEmpty()) { + search(Fetch::ISBN, isbn); + return; + } + + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + m_limit = 10; // raise limit so more possibility of match + search(Fetch::Title, t); + return; + } + + myDebug() << "ISBNdbFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* ISBNdbFetcher::configWidget(QWidget* parent_) const { + return new ISBNdbFetcher::ConfigWidget(parent_, this); +} + +ISBNdbFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const ISBNdbFetcher*/*=0*/) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +QString ISBNdbFetcher::ConfigWidget::preferredName() const { + return ISBNdbFetcher::defaultName(); +} + +#include "isbndbfetcher.moc" diff --git a/src/fetch/isbndbfetcher.h b/src/fetch/isbndbfetcher.h new file mode 100644 index 0000000..e49246a --- /dev/null +++ b/src/fetch/isbndbfetcher.h @@ -0,0 +1,94 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_ISBNDBFETCHER_H +#define TELLICO_FETCH_ISBNDBFETCHER_H + +namespace Tellico { + class XSLTHandler; +} + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <kio/job.h> + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class ISBNdbFetcher : public Fetcher { +Q_OBJECT + +public: + ISBNdbFetcher(QObject* parent = 0, const char* name = 0); + ~ISBNdbFetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == Keyword || k == ISBN; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return ISBNdb; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const ISBNdbFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup&) {} + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + void doSearch(); + + XSLTHandler* m_xsltHandler; + int m_limit; + int m_page; + int m_total; + int m_numResults; + int m_countOffset; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } +} +#endif diff --git a/src/fetch/messagehandler.cpp b/src/fetch/messagehandler.cpp new file mode 100644 index 0000000..f3c36a1 --- /dev/null +++ b/src/fetch/messagehandler.cpp @@ -0,0 +1,35 @@ +/*************************************************************************** + copyright : (C) 2005-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 "messagehandler.h" +#include "fetchmanager.h" +#include "../tellico_kernel.h" + +#include <kmessagebox.h> + +using Tellico::Fetch::ManagerMessage; + +// all messages go to manager +void ManagerMessage::send(const QString& message_, Type type_) { + Fetch::Manager::self()->updateStatus(message_); + // plus errors get a message box + if(type_ == Error) { + KMessageBox::sorry(Kernel::self()->widget(), message_); + } else if(type_ == Warning) { + KMessageBox::information(Kernel::self()->widget(), message_); + } +} + +void ManagerMessage::infoList(const QString& message_, const QStringList& list_) { + KMessageBox::informationList(Kernel::self()->widget(), message_, list_); +} diff --git a/src/fetch/messagehandler.h b/src/fetch/messagehandler.h new file mode 100644 index 0000000..0ec9269 --- /dev/null +++ b/src/fetch/messagehandler.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_MESSAGEHANDLER_H +#define TELLICO_FETCH_MESSAGEHANDLER_H + +class QString; +class QStringList; + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class MessageHandler { +public: + enum Type { Status, Warning, Error, ListError }; + + MessageHandler() {} + virtual ~MessageHandler() {} + + virtual void send(const QString& message, Type type) = 0; + virtual void infoList(const QString& message, const QStringList& list) = 0; +}; + +class ManagerMessage : public MessageHandler { +public: + ManagerMessage() : MessageHandler() {} + virtual ~ManagerMessage() {} + + virtual void send(const QString& message, Type type); + virtual void infoList(const QString& message, const QStringList& list); +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/scripts/Makefile.am b/src/fetch/scripts/Makefile.am new file mode 100644 index 0000000..050c460 --- /dev/null +++ b/src/fetch/scripts/Makefile.am @@ -0,0 +1,30 @@ +####### kdevelop will overwrite this part!!! (begin)########## + +EXTRA_DIST = \ +fr.allocine.py fr.allocine.py.spec \ +ministerio_de_cultura.py ministerio_de_cultura.py.spec \ +dark_horse_comics.py dark_horse_comics.py.spec \ +boardgamegeek.rb boardgamegeek.rb.spec + +####### kdevelop will overwrite this part!!! (end)############ + +scriptdir = $(kde_datadir)/tellico/data-sources +script_SCRIPTS = \ +fr.allocine.py \ +ministerio_de_cultura.py \ +dark_horse_comics.py \ +boardgamegeek.rb + +script_DATA = \ +fr.allocine.py.spec \ +ministerio_de_cultura.py.spec \ +dark_horse_comics.py.spec \ +boardgamegeek.rb.spec + +KDE_OPTIONS = noautodist + +CLEANFILES = *~ + +# probably a better way to do this +uninstall-hook: + -if [ -d $(scriptdir) ]; then rmdir $(scriptdir); fi diff --git a/src/fetch/scripts/boardgamegeek.rb b/src/fetch/scripts/boardgamegeek.rb new file mode 100644 index 0000000..b3cf4f3 --- /dev/null +++ b/src/fetch/scripts/boardgamegeek.rb @@ -0,0 +1,235 @@ +#!/usr/bin/env ruby +# +# *************************************************************************** +# copyright : (C) 2006 by Steve Beattie +# : (C) 2008 by Sven Werlen +# email : sbeattie@suse.de +# : sven@boisdechet.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; * +# * * +# *************************************************************************** + +# $Id: boardgamegeek.rb 313 2006-10-02 22:17:11Z steve $ + +# This program is expected to be invoked from tellico +# (http://periapsis.org/tellico) as an external data source. It provides +# searches for boardgames from the boardgamegeek.com website, via +# boardgamegeek's xmlapi interface +# (http://www.boardgamegeek.com/xmlapi/) +# +# It only allows searches via name; the boardgamegeek xmlapi is not yet +# rich enough to support queries by designer, publisher, category, or +# mechanism. I'd like to add support for querying by boardgamegeek id, +# but that needs additional support in tellico. +# +# Sven Werlen: 03 Feb 2008: script has been extended to retrieve cover +# images (/thumbnail from xmlapi). Images are retrieved from the website +# and base64 is generated on-the-fly. +# +require 'rexml/document' +require 'net/http' +require 'cgi' +require "base64" +include REXML + +$my_version = '$Rev: 313 $' + +class Game + attr_writer :year + attr_writer :description + attr_writer :cover + attr_writer :image + + def initialize(name, id) + @name = name + @id = id + @publishers = [] + @designers = [] + @players = [] + end + + def add_publisher(publisher) + @publishers << publisher + end + + def add_designer(designer) + @designers << designer + end + + def add_players(players) + @players << players + end + + def to_s() + "@name (#@id #@publishers #@year)" + end + + def toXML() + element = Element.new 'entry' + element.add_element Element.new('title').add_text(@name) + element.add_element Element.new('description').add_text(@description) if @description + element.add_element Element.new('year').add_text(@year) if @year + element.add_element Element.new('boardgamegeek-link').add_text("http://www.boardgamegeek/game/#{@id}") if @id + element.add_element Element.new('bggid').add_text(@id) if @id + element.add_element Element.new('cover').add_text(@cover) if @cover + + if @publishers.length > 0 + pub_elements = Element.new('publishers') + @publishers.each {|p| pub_elements.add_element Element.new('publisher').add_text(p)} + element.add_element pub_elements + end + if @designers.length > 0 + des_elements = Element.new('designers') + @designers.each {|d| des_elements.add_element Element.new('designer').add_text(d)} + element.add_element des_elements + end + if @players.length > 0 + players_elements = Element.new('num-players') + @players.each {|n| players_elements.add_element Element.new('num-player').add_text(n.to_s)} + element.add_element players_elements + end + return element + end + + def image() + image = Element.new 'image' + image.add_attribute('format', 'JPEG') + image.add_attribute('id', @id + ".jpg") + image.add_text(@image) + return image + end +end + +def getGameList(query) + #puts("Query is #{query}") + + search_result = nil + Net::HTTP.start('www.boardgamegeek.com', 80) do + |http| search_result = (http.get("/xmlapi/search?search=#{CGI.escape(query)}", + {"User-Agent" => "BoardGameGeek plugin for Tellico #{$my_version}"}).body) + http.finish + end + doc = REXML::Document.new(search_result) + + games = XPath.match(doc, "//game") + #games.each {|g| puts g.elements['name'].text+g.attributes['gameid']} + ids = [] + games.each {|g| ids << g.attributes['gameid']} + return ids +end + +def getGameDetails(ids) + #ids.each {|id| puts id} + + query = "/xmlapi/game/#{ids.join(',')}" + #puts query + search_result = nil + Net::HTTP.start('www.boardgamegeek.com', 80) do |http| + search_result = http.get(query, {"User-Agent" => "BoardGameGeek plugin for Tellico #{$my_version}"}) + http.finish + end + games = [] + case search_result + when Net::HTTPOK then + doc = REXML::Document.new(search_result.body) + + games_xml = XPath.match(doc, "//game") + games_xml.each do |g| + if( g.elements['name'] != nil ) + game = Game.new(g.elements['name'].text, g.attributes['gameid']) + game.year = g.elements['yearpublished'].text + game.description = g.elements['description'].text + g.elements.each('publisher'){|p| game.add_publisher p.elements['name'].text} + g.elements.each('designer'){|d| game.add_designer d.elements['name'].text} + minp = Integer(g.elements['minplayers'].text) + maxp = Integer(g.elements['maxplayers'].text) + minp.upto(maxp) {|n| game.add_players(n)} + + # retrieve cover + coverurl = g.elements['thumbnail'] != nil ? g.elements['thumbnail'].text : nil + if( coverurl =~ /files.boardgamegeek.com(.*)$/ ) + # puts "downloading... " + $1 + cover = nil + Net::HTTP.start('files.boardgamegeek.com', 80) do |http| + cover = (http.get($1, {"User-Agent" => "BoardGameGeek plugin for Tellico #{$my_version}"})) + end + case cover + when Net::HTTPOK then + game.cover = g.attributes['gameid'] + ".jpg"; + game.image = Base64.encode64(cover.body); + end + else + # puts "invalid cover: " + coverurl + end + games << game + end + end + end + return games +end + +def listToXML(gameList) + doc = REXML::Document.new + doc << REXML::DocType.new('tellico PUBLIC', '"-//Robby Stephenson/DTD Tellico V10.0//EN" "http://periapsis.org/tellico/dtd/v10/tellico.dtd"') + doc << XMLDecl.new + tellico = Element.new 'tellico' + tellico.add_attribute('xmlns', 'http://periapsis.org/tellico/') + tellico.add_attribute('syntaxVersion', '10') + collection = Element.new 'collection' + collection.add_attribute('title', 'My Collection') + collection.add_attribute('type', '13') + + fields = Element.new 'fields' + field = Element.new 'field' + field.add_attribute('name', '_default') + fields.add_element(field) + field = Element.new 'field' + field.add_attribute('name', 'bggid') + field.add_attribute('title', 'BoardGameGeek ID') + field.add_attribute('category', 'General') + field.add_attribute('flags', '0') + field.add_attribute('format', '4') + field.add_attribute('type', '6') + field.add_attribute('i18n', 'true') + fields.add_element(field) + collection.add_element(fields) + + images = Element.new 'images' + + id = 0 + gameList.each do + |g| element = g.toXML() + element.add_attribute('id', id) + id = id + 1 + collection.add_element(element) + images.add_element(g.image()); + end + collection.add_element(images); + tellico.add_element(collection) + doc.add_element(tellico) + doc.write($stdout, 0) + puts "" +end + +if __FILE__ == $0 + + def showUsage + warn "usage: #{__FILE__} game_query" + exit 1 + end + + showUsage unless ARGV.length == 1 + + idList = getGameList(ARGV.shift) + if idList + gameList = getGameDetails(idList) + end + + listToXML(gameList) +end diff --git a/src/fetch/scripts/boardgamegeek.rb.spec b/src/fetch/scripts/boardgamegeek.rb.spec new file mode 100644 index 0000000..6e0aab0 --- /dev/null +++ b/src/fetch/scripts/boardgamegeek.rb.spec @@ -0,0 +1,7 @@ +Name=BoardGameGeek +Type=data-source +ArgumentKeys=1 +Arguments=%1 +CollectionType=13 +FormatType=0 +UpdateArgs=%{title} diff --git a/src/fetch/scripts/dark_horse_comics.py b/src/fetch/scripts/dark_horse_comics.py new file mode 100644 index 0000000..4f3b651 --- /dev/null +++ b/src/fetch/scripts/dark_horse_comics.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# *************************************************************************** +# copyright : (C) 2006 by Mathias Monnerville +# email : tellico_dev@yahoo.fr +# *************************************************************************** +# +# *************************************************************************** +# * * +# * 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; * +# * * +# *************************************************************************** + +# $Id: comics_darkhorsecomics.py 123 2006-03-24 08:47:48Z mathias $ + +""" +This script has to be used with tellico (http://periapsis.org/tellico) as an external data source program. +It allows searching through the Dark Horse Comics web database. + +Related info and cover are fetched automatically. It takes only one argument (comic title). + +Tellico data source setup: +- source name: Dark Horse Comics (US) (or whatever you want :) +- Collection type: comics collection +- Result type: tellico +- Path: /path/to/script/comics_darkhorsecomics.py +- Arguments: +Title (checked) = %1 +Update (checked) = %{title} +""" + +import sys, os, re, md5, random, string +import urllib, urllib2, time, base64 +import xml.dom.minidom + +XML_HEADER = """<?xml version="1.0" encoding="UTF-8"?>""" +DOCTYPE = """<!DOCTYPE tellico PUBLIC "-//Robby Stephenson/DTD Tellico V9.0//EN" "http://periapsis.org/tellico/dtd/v9/tellico.dtd">""" +NULLSTRING = '' + +VERSION = "0.2" + + +def genMD5(): + """ + Generates and returns a random md5 string. Its main purpose is to allow random + image file name generation. + """ + obj = md5.new() + float = random.random() + obj.update(str(float)) + return obj.hexdigest() + +class BasicTellicoDOM: + """ + This class manages tellico's XML data model (DOM) + """ + def __init__(self): + self.__doc = xml.dom.minidom.Document() + self.__root = self.__doc.createElement('tellico') + self.__root.setAttribute('xmlns', 'http://periapsis.org/tellico/') + self.__root.setAttribute('syntaxVersion', '9') + + self.__collection = self.__doc.createElement('collection') + self.__collection.setAttribute('title', 'My Comics') + self.__collection.setAttribute('type', '6') + + self.__images = self.__doc.createElement('images') + + self.__root.appendChild(self.__collection) + self.__doc.appendChild(self.__root) + + # Current movie id. See entry's id attribute in self.addEntry() + self.__currentId = 0 + + + def addEntry(self, movieData): + """ + Add a comic entry. + Returns an entry node instance + """ + d = movieData + entryNode = self.__doc.createElement('entry') + entryNode.setAttribute('id', str(self.__currentId)) + + titleNode = self.__doc.createElement('title') + titleNode.appendChild(self.__doc.createTextNode(unicode(d['title'], 'latin-1').encode('utf-8'))) + + yearNode = self.__doc.createElement('pub_year') + yearNode.appendChild(self.__doc.createTextNode(d['pub_year'])) + + countryNode = self.__doc.createElement('country') + countryNode.appendChild(self.__doc.createTextNode(d['country'])) + pubNode = self.__doc.createElement('publisher') + pubNode.appendChild(self.__doc.createTextNode(d['publisher'])) + langNode = self.__doc.createElement('language') + langNode.appendChild(self.__doc.createTextNode(d['language'])) + + writersNode = self.__doc.createElement('writers') + for g in d['writer']: + writerNode = self.__doc.createElement('writer') + writerNode.appendChild(self.__doc.createTextNode(unicode(g, 'latin-1').encode('utf-8'))) + writersNode.appendChild(writerNode) + + genresNode = self.__doc.createElement('genres') + for g in d['genre']: + genreNode = self.__doc.createElement('genre') + genreNode.appendChild(self.__doc.createTextNode(unicode(g, 'latin-1').encode('utf-8'))) + genresNode.appendChild(genreNode) + + commentsNode = self.__doc.createElement('comments') + #for g in d['comments']: + # commentsNode.appendChild(self.__doc.createTextNode(unicode("%s\n\n" % g, 'latin-1').encode('utf-8'))) + commentsData = string.join(d['comments'], '\n\n') + commentsNode.appendChild(self.__doc.createTextNode(unicode(commentsData, 'latin-1').encode('utf-8'))) + + artistsNode = self.__doc.createElement('artists') + for k, v in d['artist'].iteritems(): + artistNode = self.__doc.createElement('artist') + artistNode.appendChild(self.__doc.createTextNode(unicode(v, 'latin-1').encode('utf-8'))) + artistsNode.appendChild(artistNode) + + pagesNode = self.__doc.createElement('pages') + pagesNode.appendChild(self.__doc.createTextNode(d['pages'])) + + issueNode = self.__doc.createElement('issue') + issueNode.appendChild(self.__doc.createTextNode(d['issue'])) + + if d['image']: + imageNode = self.__doc.createElement('image') + imageNode.setAttribute('format', 'JPEG') + imageNode.setAttribute('id', d['image'][0]) + imageNode.appendChild(self.__doc.createTextNode(unicode(d['image'][1], 'latin-1').encode('utf-8'))) + + coverNode = self.__doc.createElement('cover') + coverNode.appendChild(self.__doc.createTextNode(d['image'][0])) + + for name in ( 'writersNode', 'genresNode', 'artistsNode', 'pagesNode', 'yearNode', + 'titleNode', 'issueNode', 'commentsNode', 'pubNode', 'langNode', + 'countryNode' ): + entryNode.appendChild(eval(name)) + + if d['image']: + entryNode.appendChild(coverNode) + self.__images.appendChild(imageNode) + + self.__collection.appendChild(entryNode) + + self.__currentId += 1 + return entryNode + + def printEntry(self, nEntry): + """ + Prints entry's XML content to stdout + """ + try: + print nEntry.toxml() + except: + print sys.stderr, "Error while outputing XML content from entry to Tellico" + + def printXMLTree(self): + """ + Outputs XML content to stdout + """ + self.__collection.appendChild(self.__images) + print XML_HEADER; print DOCTYPE + print self.__root.toxml() + + +class DarkHorseParser: + def __init__(self): + self.__baseURL = 'http://www.darkhorse.com' + self.__basePath = '/profile/profile.php?sku=' + self.__searchURL = '/search/search.php?frompage=userinput&sstring=%s&x=0&y=0' + self.__coverPath = 'http://images.darkhorse.com/covers/' + self.__movieURL = self.__baseURL + self.__basePath + + # Define some regexps + self.__regExps = { 'title' : '<font size="\+2"><b>(?P<title>.*?)</b></font>', + 'pub_date' : '<b>Pub.* Date:</b> *<a.*>(?P<pub_date>.*)</a>', + 'desc' : '<p>(?P<desc>.*?)<br>', + 'writer' : '<b>Writer: *</b> *<a.*?>(?P<writer>.*)</a>', + 'cover_artist' : '<b>Cover Artist: *</b> *<a.*>(?P<cover_artist>.*)</a>', + 'penciller' : '<b>Penciller: *</b> *<a.*>(?P<penciller>.*)</a>', + 'inker' : '<b>Inker: *</b> *<a.*>(?P<inker>.*)</a>', + 'letterer' : '<b>Letterer: *</b> *<a.*>(?P<letterer>.*)</a>', + 'colorist' : '<b>Colorist: *</b> *<a.*>(?P<colorist>.*)</a>', + 'genre' : '<b>Genre: *</b> *<a.*?>(?P<genre>.*?)</a><br>', + 'format' : '<b>Format: *</b> *(?P<format>.*?)<br>', + } + + # Compile patterns objects + self.__regExpsPO = {} + for k, pattern in self.__regExps.iteritems(): + self.__regExpsPO[k] = re.compile(pattern) + + self.__domTree = BasicTellicoDOM() + + def run(self, title): + """ + Runs the allocine.fr parser: fetch movie related links, then fills and prints the DOM tree + to stdout (in tellico format) so that tellico can use it. + """ + self.__getMovie(title) + # Print results to stdout + self.__domTree.printXMLTree() + + def __getHTMLContent(self, url): + """ + Fetch HTML data from url + """ + u = urllib2.urlopen(url) + self.__data = u.read() + u.close() + + def __fetchMovieLinks(self): + """ + Retrieve all links related to the search. self.__data contains HTML content fetched by self.__getHTMLContent() + that need to be parsed. + """ + matchList = re.findall("""<a *href="%s(?P<page>.*?)">(?P<title>.*?)</a>""" % self.__basePath.replace('?', '\?'), self.__data) + if not matchList: return None + + return matchList + + def __fetchCover(self, path, delete = True): + """ + Fetch cover to /tmp. Returns base64 encoding of data. + The image is deleted if delete is True + """ + md5 = genMD5() + imObj = urllib2.urlopen(path.strip()) + img = imObj.read() + imObj.close() + imgPath = "/tmp/%s.jpeg" % md5 + try: + f = open(imgPath, 'w') + f.write(img) + f.close() + except: + print sys.stderr, "Error: could not write image into /tmp" + + b64data = (md5 + '.jpeg', base64.encodestring(img)) + + # Delete temporary image + if delete: + try: + os.remove(imgPath) + except: + print sys.stderr, "Error: could not delete temporary image /tmp/%s.jpeg" % md5 + + return b64data + + def __fetchMovieInfo(self, url): + """ + Looks for movie information + """ + self.__getHTMLContent(url) + + # First grab picture data + imgMatch = re.search("""<img src="%s(?P<imgpath>.*?)".*>""" % self.__coverPath, self.__data) + if imgMatch: + imgPath = self.__coverPath + imgMatch.group('imgpath') + # Fetch cover and gets its base64 encoded data + b64img = self.__fetchCover(imgPath) + else: + b64img = None + + # Now isolate data between <div class="bodytext">...</div> elements + # re.S sets DOTALL; it makes the "." special character match any character at all, including a newline + m = re.search("""<div class="bodytext">(?P<part>.*)</div>""", self.__data, re.S) + self.__data = m.group('part') + + matches = {} + data = {} + data['comments'] = [] + data['artist'] = {} + + # Default values + data['publisher'] = 'Dark Horse Comics' + data['language'] = 'English' + data['country'] = 'USA' + + data['image'] = b64img + data['pub_year'] = NULLSTRING + + for name, po in self.__regExpsPO.iteritems(): + data[name] = NULLSTRING + if name == 'desc': + matches[name] = re.findall(self.__regExps[name], self.__data, re.S | re.I) + else: + matches[name] = po.search(self.__data) + + if matches[name]: + if name == 'title': + title = matches[name].group('title').strip() + data[name] = title + # Look for issue information + m = re.search("#(?P<issue>[0-9]+)", title) + if m: + data['issue'] = m.group('issue') + else: + data['issue'] = '' + + elif name == 'pub_date': + pub_date = matches[name].group('pub_date').strip() + data['pub_year'] = pub_date[-4:] + # Add this to comments field + data['comments'].insert(0, "Pub. Date: %s" % pub_date) + + elif name == 'desc': + # Find biggest size + max = 0 + for i in range(len(matches[name])): + if len(matches[name][i]) > len(matches[name][max]): + max = i + data['comments'].append(matches[name][max].strip()) + + elif name == 'writer': + # We may find several writers + data[name] = [] + writersList = re.sub('</?a.*?>', '', matches[name].group('writer')).split(',') + for d in writersList: + data[name].append(d.strip()) + + elif name == 'cover_artist': + data['artist']['Cover Artist'] = matches[name].group('cover_artist').strip() + + elif name == 'penciller': + data['artist']['Penciller'] = matches[name].group('penciller').strip() + + elif name == 'inker': + data['artist']['Inker'] = matches[name].group('inker').strip() + + elif name == 'colorist': + data['artist']['Colorist'] = matches[name].group('colorist').strip() + + elif name == 'letterer': + data['artist']['Letterer'] = matches[name].group('letterer').strip() + + elif name == 'genre': + # We may find several genres + data[name] = [] + genresList = re.sub('</?a.*?>', '', matches[name].group('genre')).split(',') + for d in genresList: + data[name].append(d.strip()) + + elif name == 'format': + format = matches[name].group('format').strip() + data['comments'].insert(1, format) + m = re.search("(?P<pages>[0-9]+)", format) + if m: + data['pages'] = m.group('pages') + else: + data['pages'] = '' + + return data + + + def __getMovie(self, title): + if not len(title): return + + self.__title = title + self.__getHTMLContent("%s%s" % (self.__baseURL, self.__searchURL % urllib.quote(self.__title))) + + # Get all links + links = self.__fetchMovieLinks() + + # Now retrieve infos + if links: + for entry in links: + data = self.__fetchMovieInfo( url = self.__movieURL + entry[0] ) + # Add DC link (custom field) + data['darkhorse'] = "%s%s" % (self.__movieURL, entry[0]) + node = self.__domTree.addEntry(data) + # Print entries on-the-fly + #self.__domTree.printEntry(node) + else: + return None + +def halt(): + print "HALT." + sys.exit(0) + +def showUsage(): + print "Usage: %s comic" % sys.argv[0] + sys.exit(1) + +def main(): + if len(sys.argv) < 2: + showUsage() + + parser = DarkHorseParser() + parser.run(sys.argv[1]) + +if __name__ == '__main__': + main() diff --git a/src/fetch/scripts/dark_horse_comics.py.spec b/src/fetch/scripts/dark_horse_comics.py.spec new file mode 100644 index 0000000..9481dc8 --- /dev/null +++ b/src/fetch/scripts/dark_horse_comics.py.spec @@ -0,0 +1,7 @@ +Name=Dark Horse Comics +Type=data-source +ArgumentKeys=1 +Arguments=%1 +CollectionType=6 +FormatType=0 +UpdateArgs=%{title} diff --git a/src/fetch/scripts/fr.allocine.py b/src/fetch/scripts/fr.allocine.py new file mode 100755 index 0000000..97a2247 --- /dev/null +++ b/src/fetch/scripts/fr.allocine.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# *************************************************************************** +# copyright : (C) 2006 by Mathias Monnerville +# email : tellico@monnerville.com +# *************************************************************************** +# +# *************************************************************************** +# * * +# * 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; * +# * * +# *************************************************************************** + +# Version 0.4: 2007-08-27 +# * Fixed parsing errors: some fields in allocine's HTML pages have changed recently. Multiple actors and genres +# could not be retrieved. Fixed bad http request error due to some changes in HTML code. +# +# Version 0.3: +# * Fixed parsing: some fields in allocine's HTML pages have changed. Movie's image could not be fetched anymore. Fixed. +# +# Version 0.2: +# * Fixed parsing: allocine's HTML pages have changed. Movie's image could not be fetched anymore. +# +# Version 0.1: +# * Initial release. + +import sys, os, re, md5, random +import urllib, urllib2, time, base64 +import xml.dom.minidom + +XML_HEADER = """<?xml version="1.0" encoding="UTF-8"?>""" +DOCTYPE = """<!DOCTYPE tellico PUBLIC "-//Robby Stephenson/DTD Tellico V9.0//EN" "http://periapsis.org/tellico/dtd/v9/tellico.dtd">""" + +VERSION = "0.4" + +def genMD5(): + obj = md5.new() + float = random.random() + obj.update(str(float)) + return obj.hexdigest() + +class BasicTellicoDOM: + def __init__(self): + self.__doc = xml.dom.minidom.Document() + self.__root = self.__doc.createElement('tellico') + self.__root.setAttribute('xmlns', 'http://periapsis.org/tellico/') + self.__root.setAttribute('syntaxVersion', '9') + + self.__collection = self.__doc.createElement('collection') + self.__collection.setAttribute('title', 'My Movies') + self.__collection.setAttribute('type', '3') + + self.__fields = self.__doc.createElement('fields') + # Add all default (standard) fields + self.__dfltField = self.__doc.createElement('field') + self.__dfltField.setAttribute('name', '_default') + + # Add a custom 'Collection' field + self.__customField = self.__doc.createElement('field') + self.__customField.setAttribute('name', 'titre-original') + self.__customField.setAttribute('title', 'Original Title') + self.__customField.setAttribute('flags', '8') + self.__customField.setAttribute('category', 'General') + self.__customField.setAttribute('format', '1') + self.__customField.setAttribute('type', '1') + self.__customField.setAttribute('i18n', 'yes') + + self.__fields.appendChild(self.__dfltField) + self.__fields.appendChild(self.__customField) + self.__collection.appendChild(self.__fields) + + self.__images = self.__doc.createElement('images') + + self.__root.appendChild(self.__collection) + self.__doc.appendChild(self.__root) + + # Current movie id + self.__currentId = 0 + + + def addEntry(self, movieData): + """ + Add a movie entry + """ + d = movieData + entryNode = self.__doc.createElement('entry') + entryNode.setAttribute('id', str(self.__currentId)) + + titleNode = self.__doc.createElement('title') + titleNode.appendChild(self.__doc.createTextNode(unicode(d['title'], 'latin-1').encode('utf-8'))) + + otitleNode = self.__doc.createElement('titre-original') + otitleNode.appendChild(self.__doc.createTextNode(unicode(d['otitle'], 'latin-1').encode('utf-8'))) + + yearNode = self.__doc.createElement('year') + yearNode.appendChild(self.__doc.createTextNode(unicode(d['year'], 'latin-1').encode('utf-8'))) + + genresNode = self.__doc.createElement('genres') + for g in d['genres']: + genreNode = self.__doc.createElement('genre') + genreNode.appendChild(self.__doc.createTextNode(unicode(g, 'latin-1').encode('utf-8'))) + genresNode.appendChild(genreNode) + + natsNode = self.__doc.createElement('nationalitys') + natNode = self.__doc.createElement('nat') + natNode.appendChild(self.__doc.createTextNode(unicode(d['nat'], 'latin-1').encode('utf-8'))) + natsNode.appendChild(natNode) + + castsNode = self.__doc.createElement('casts') + for g in d['actors']: + castNode = self.__doc.createElement('cast') + col1Node = self.__doc.createElement('column') + col2Node = self.__doc.createElement('column') + col1Node.appendChild(self.__doc.createTextNode(unicode(g, 'latin-1').encode('utf-8'))) + castNode.appendChild(col1Node) + castNode.appendChild(col2Node) + castsNode.appendChild(castNode) + + dirsNode = self.__doc.createElement('directors') + for g in d['dirs']: + dirNode = self.__doc.createElement('director') + dirNode.appendChild(self.__doc.createTextNode(unicode(g, 'latin-1').encode('utf-8'))) + dirsNode.appendChild(dirNode) + + timeNode = self.__doc.createElement('running-time') + timeNode.appendChild(self.__doc.createTextNode(unicode(d['time'], 'latin-1').encode('utf-8'))) + + allocineNode = self.__doc.createElement(unicode('allocin-link', 'latin-1').encode('utf-8')) + allocineNode.appendChild(self.__doc.createTextNode(unicode(d['allocine'], 'latin-1').encode('utf-8'))) + + plotNode = self.__doc.createElement('plot') + plotNode.appendChild(self.__doc.createTextNode(unicode(d['plot'], 'latin-1').encode('utf-8'))) + + if d['image']: + imageNode = self.__doc.createElement('image') + imageNode.setAttribute('format', 'JPEG') + imageNode.setAttribute('id', d['image'][0]) + imageNode.setAttribute('width', '120') + imageNode.setAttribute('height', '160') + imageNode.appendChild(self.__doc.createTextNode(unicode(d['image'][1], 'latin-1').encode('utf-8'))) + + coverNode = self.__doc.createElement('cover') + coverNode.appendChild(self.__doc.createTextNode(d['image'][0])) + + for name in ( 'titleNode', 'otitleNode', 'yearNode', 'genresNode', 'natsNode', + 'castsNode', 'dirsNode', 'timeNode', 'allocineNode', 'plotNode' ): + entryNode.appendChild(eval(name)) + + if d['image']: + entryNode.appendChild(coverNode) + self.__images.appendChild(imageNode) + + self.__collection.appendChild(entryNode) + + self.__currentId += 1 + + def printXML(self): + """ + Outputs XML content to stdout + """ + self.__collection.appendChild(self.__images) + print XML_HEADER; print DOCTYPE + print self.__root.toxml() + + +class AlloCineParser: + def __init__(self): + self.__baseURL = 'http://www.allocine.fr' + self.__basePath = '/film/fichefilm_gen_cfilm' + self.__searchURL= 'http://www.allocine.fr/recherche/?motcle=%s&f=3&rub=1' + self.__movieURL = self.__baseURL + self.__basePath + + # Define some regexps + self.__regExps = { 'title' : '<title>(?P<title>.+?)</title>', + 'dirs' : 'Ralis par <a.*?>(?P<step1>.+?)</a>.*?</h4>', + 'actors' : '<h4>Avec *<a.*?>(?P<step1>.+)</a> ', + 'nat' : '<h4>Film *(?P<nat>.+?)[,\.]', + 'genres' : '<h4>Genre *: *<a.*?>(?P<step1>.+?)</a></h4>', + 'time' : '<h4>Dure *: *(?P<hours>[0-9])?h *(?P<mins>[0-9]{1,2})min', + 'year' : 'Anne de production *: *(?P<year>[0-9]{4})', + # Original movie title + 'otitle' : 'Titre original *: *<i>(?P<otitle>.+?)</i>', + 'plot' : """(?s)<td valign="top" style="padding:10 0 0 0"><div align="justify"><h4> *(?P<plot>.+?) *</h4>""", + 'image' : """<td valign="top" width="120".*?<img src="(?P<image>.+?)" border"""} + + + self.__domTree = BasicTellicoDOM() + + def run(self, title): + """ + Runs the allocine.fr parser: fetch movie related links, then fills and prints the DOM tree + to stdout (in tellico format) so that tellico can use it. + """ + self.__getMovie(title) + # Print results to stdout + self.__domTree.printXML() + + def __getHTMLContent(self, url): + """ + Fetch HTML data from url + """ + + u = urllib2.urlopen(url) + self.__data = u.read() + u.close() + + def __fetchMovieLinks(self): + """ + Retrieve all links related to movie + """ + matchList = re.findall("""<h4><a *href="%s=(?P<page>.*?\.html?)" *class="link1">(?P<title>.*?)</a>""" % self.__basePath, self.__data) + if not matchList: return None + + return matchList + + def __fetchMovieInfo(self, url): + """ + Looks for movie information + """ + self.__getHTMLContent(url) + + matches = data = {} + + for name, regexp in self.__regExps.iteritems(): + if name == 'image': + matches[name] = re.findall(self.__regExps[name], self.__data, re.S | re.I) + else: + matches[name] = re.search(regexp, self.__data) + + if matches[name]: + if name == 'title': + data[name] = matches[name].group('title').strip() + elif name == 'dirs': + dirsList = re.sub('</?a.*?>', '', matches[name].group('step1')).split(',') + data[name] = [] + for d in dirsList: + data[name].append(d.strip()) + + elif name == 'actors': + actorsList = re.sub('</?a.*?>', '', matches[name].group('step1')).split(',') + data[name] = [] + for d in actorsList: + data[name].append(d.strip()) + + elif name == 'nat': + data[name] = matches[name].group('nat').strip() + + elif name == 'genres': + genresList = re.sub('</?a.*?>', '', matches[name].group('step1')).split(',') + data[name] = [] + for d in genresList: + data[name].append(d.strip()) + + elif name == 'time': + h, m = matches[name].group('hours'), matches[name].group('mins') + totmin = int(h)*60+int(m) + data[name] = str(totmin) + + elif name == 'year': + data[name] = matches[name].group('year').strip() + + elif name == 'otitle': + data[name] = matches[name].group('otitle').strip() + + elif name == 'plot': + data[name] = matches[name].group('plot').strip() + + # Image path + elif name == 'image': + # Save image to a temporary folder + md5 = genMD5() + imObj = urllib2.urlopen(matches[name][0].strip()) + img = imObj.read() + imObj.close() + imgPath = "/tmp/%s.jpeg" % md5 + try: + f = open(imgPath, 'w') + f.write(img) + f.close() + except: + # Could be great if we can pass exit code and some message + # to tellico in case of failure... + pass + + data[name] = (md5 + '.jpeg', base64.encodestring(img)) + # Delete temporary image + try: + os.remove(imgPath) + except: + # Could be great if we can pass exit code and some msg + # to tellico in case of failure... + pass + else: + matches[name] = '' + + return data + + + def __getMovie(self, title): + if not len(title): return + + self.__title = title + self.__getHTMLContent(self.__searchURL % urllib.quote(self.__title)) + + # Get all links + links = self.__fetchMovieLinks() + + # Now retrieve infos + if links: + for entry in links: + data = self.__fetchMovieInfo( url = "%s=%s" % (self.__movieURL, entry[0]) ) + # Add allocine link (custom field) + data['allocine'] = "%s=%s" % (self.__movieURL, entry[0]) + self.__domTree.addEntry(data) + else: + return None + + + +def showUsage(): + print "Usage: %s movietitle" % sys.argv[0] + sys.exit(1) + +def main(): + if len(sys.argv) < 2: + showUsage() + + parser = AlloCineParser() + parser.run(sys.argv[1]) + +if __name__ == '__main__': + main() diff --git a/src/fetch/scripts/fr.allocine.py.spec b/src/fetch/scripts/fr.allocine.py.spec new file mode 100644 index 0000000..773b951 --- /dev/null +++ b/src/fetch/scripts/fr.allocine.py.spec @@ -0,0 +1,7 @@ +Name=Allocine.fr +Type=data-source +ArgumentKeys=1 +Arguments=%1 +CollectionType=3 +FormatType=0 +UpdateArgs=%{title} diff --git a/src/fetch/scripts/ministerio_de_cultura.py b/src/fetch/scripts/ministerio_de_cultura.py new file mode 100644 index 0000000..8a768f9 --- /dev/null +++ b/src/fetch/scripts/ministerio_de_cultura.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# *************************************************************************** +# copyright : (C) 2006-2008 by Mathias Monnerville +# email : tellico@monnerville.com +# *************************************************************************** +# +# *************************************************************************** +# * * +# * 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; * +# * * +# *************************************************************************** + +# $Id: books_ministerio_de_cultura.py 428 2007-03-07 13:17:17Z mathias $ + +""" +This script has to be used with tellico (http://periapsis.org/tellico) as an external data source program. +It allows searching for books in Spanish Ministry of Culture's database (at http://www.mcu.es/bases/spa/isbn/ISBN.html). + +Multiple ISBN/UPC searching is supported through the -m option: + ./books_ministerio_de_cultura.py -m filename +where filename holds one ISBN or UPC per line. + +Tellico data source setup: +- Source type: External Application +- Source name: Ministerio de Cultura (ES) (or whatever you want :) +- Collection type: Book Collection +- Result type: Tellico +- Path: /path/to/script/books_ministerio_de_cultura.py +- Arguments: +Title (checked) = -t %1 +Person (checked) = -a %1 +ISBN (checked) = -i %1 +UPC (checked) = -i %1 +Update (checked) = %{title} + +** Please note that this script is also part of the Tellico's distribution. +** You will always find the latest version in the SVN trunk of Tellico + +SVN Version: + * Removes translators for Authors List + * Adds translators to translator field + * Change from "Collection" to "Series" + * Process "Series Number" + * Adds in comments "ed.lit." authors + * If there isn't connection to Spanish Ministry of Culture + shows a nice error message (timeout: 5 seconds) + * Removed "translated from/to" from Comments field as already + exists in "Publishing" field + * Removed "Collection" field as I moved to Series/Series Number + +Version 0.3.2: + * Now find 'notas' field related information + * search URL modified to fetch information of exhausted books too + +Version 0.3.1: +Bug Fixes: + * The 'tr.' string does not appear among authors anymore + * Fixed an AttributeError exception related to a regexp matching the number of pages + +Version 0.3: +Bug Fixes: + * URL of the search engine has changed: + http://www.mcu.es/bases/spa/isbn/ISBN.html is now http://www.mcu.es/comun/bases/isbn/ISBN.html + * All the regexps have been rewritten to match the new site's content + +Version 0.2: +New features: + * Support for multiple ISBN/UPC searching (support from command line with -m option) + * Default books collection enhanced with a new custom field 'Collection' + * Search extended for both available and exhausted books + * Hyphens are stripped out in the ISBN (or UPC) search + +Bug Fixes: + * Publication year now holds only the year + * ISBN regexp fix + * Fix for publisher field (values were inverted) + * -i parameter works for both ISBN and UPC based search + +Version 0.1: + * Initial Release +""" + +import sys, os, re, md5, random, string +import urllib, urllib2, time, base64 +import xml.dom.minidom, types +import socket + +XML_HEADER = """<?xml version="1.0" encoding="UTF-8"?>""" +DOCTYPE = """<!DOCTYPE tellico PUBLIC "-//Robby Stephenson/DTD Tellico V9.0//EN" "http://periapsis.org/tellico/dtd/v9/tellico.dtd">""" +NULLSTRING = '' + +VERSION = "0.3.2" + +ISBN, AUTHOR, TITLE = range(3) + +TRANSLATOR_STR = "tr." +EDLIT_STR = "ed. lit." + +class EngineError(Exception): pass + +class BasicTellicoDOM: + """ + This class manages tellico's XML data model (DOM) + """ + def __init__(self): + self.__doc = xml.dom.minidom.Document() + self.__root = self.__doc.createElement('tellico') + self.__root.setAttribute('xmlns', 'http://periapsis.org/tellico/') + self.__root.setAttribute('syntaxVersion', '9') + + self.__collection = self.__doc.createElement('collection') + self.__collection.setAttribute('title', 'My Books') + self.__collection.setAttribute('type', '2') + + self.__fields = self.__doc.createElement('fields') + # Add all default (standard) fields + self.__dfltField = self.__doc.createElement('field') + self.__dfltField.setAttribute('name', '_default') + + # Add a custom 'Collection' field (Left by reference for + # the future) + #self.__customCollectionField = self.__doc.createElement('field') + #self.__customCollectionField.setAttribute('name', 'book_collection') + #self.__customCollectionField.setAttribute('title', 'Collection') + #self.__customCollectionField.setAttribute('flags', '7') + #self.__customCollectionField.setAttribute('category', 'Classification') + #self.__customCollectionField.setAttribute('format', '0') + #self.__customCollectionField.setAttribute('type', '1') + #self.__customCollectionField.setAttribute('i18n', 'yes') + + + self.__fields.appendChild(self.__dfltField) + #self.__fields.appendChild(self.__customCollectionField) + self.__collection.appendChild(self.__fields) + + self.__root.appendChild(self.__collection) + self.__doc.appendChild(self.__root) + + # Current movie id. See entry's id attribute in self.addEntry() + self.__currentId = 0 + + + def addEntry(self, movieData): + """ + Add a comic entry. + Returns an entry node instance + """ + + d = movieData + + # Convert all strings to UTF-8 + for i in d.keys(): + if type(d[i]) == types.ListType: + d[i] = [unicode(d[i][j], 'latin-1').encode('utf-8') for j in range(len(d[i]))] + elif type(d[i]) == types.StringType: + d[i] = unicode(d[i], 'latin-1').encode('utf-8') + + entryNode = self.__doc.createElement('entry') + entryNode.setAttribute('id', str(self.__currentId)) + + titleNode = self.__doc.createElement('title') + titleNode.appendChild(self.__doc.createTextNode(d['title'])) + + yearNode = self.__doc.createElement('pub_year') + yearNode.appendChild(self.__doc.createTextNode(d['pub_year'])) + + pubNode = self.__doc.createElement('publisher') + pubNode.appendChild(self.__doc.createTextNode(d['publisher'])) + + langsNode = self.__doc.createElement('languages') + for l in d['language']: + langNode = self.__doc.createElement('language') + langNode.appendChild(self.__doc.createTextNode(l)) + langsNode.appendChild(langNode) + + keywordsNode = self.__doc.createElement('keywords') + keywordNode = self.__doc.createElement('keyword') + keywordNode.appendChild(self.__doc.createTextNode(d['keyword'])) + keywordsNode.appendChild(keywordNode) + + edNode = self.__doc.createElement('edition') + edNode.appendChild(self.__doc.createTextNode(d['edition'])) + + writersNode = self.__doc.createElement('authors') + for g in d['author']: + writerNode = self.__doc.createElement('author') + writerNode.appendChild(self.__doc.createTextNode(g)) + writersNode.appendChild(writerNode) + + commentsNode = self.__doc.createElement('comments') + commentsData = string.join(d['comments'], '<br/>') + commentsNode.appendChild(self.__doc.createTextNode(commentsData)) + + pagesNode = self.__doc.createElement('pages') + pagesNode.appendChild(self.__doc.createTextNode(d['pages'])) + + isbnNode = self.__doc.createElement('isbn') + isbnNode.appendChild(self.__doc.createTextNode(d['isbn'])) + + priceNode = self.__doc.createElement('pur_price') + priceNode.appendChild(self.__doc.createTextNode(d['pur_price'])) + + seriesNode = self.__doc.createElement('series') + seriesNode.appendChild(self.__doc.createTextNode(d['series'])) + + seriesNumNode = self.__doc.createElement('series_num') + seriesNumNode.appendChild(self.__doc.createTextNode(d['series_num'])) + + translatorNode = self.__doc.createElement('translator') + translatorNode.appendChild(self.__doc.createTextNode(d['translator'])) + + for name in ( 'title', 'year', 'pub', 'langs', 'keyword', 'ed', 'writers', + 'comments', 'pages', 'isbn', 'price', 'series', 'seriesNum', 'translator' ): + entryNode.appendChild(eval(name + 'Node')) + + self.__collection.appendChild(entryNode) + self.__currentId += 1 + + return entryNode + + def printEntry(self, nEntry): + """ + Prints entry's XML content to stdout + """ + + try: + print nEntry.toxml() + except: + print sys.stderr, "Error while outputing XML content from entry to Tellico" + + def printXMLTree(self): + """ + Outputs XML content to stdout + """ + + print XML_HEADER; print DOCTYPE + print self.__root.toxml() + + +class MinisterioCulturaParser: + def __init__(self): + # Search form is at http://www.mcu.es/comun/bases/isbn/ISBN.html + self.__baseURL = 'http://www.mcu.es' + self.__searchURL = '/cgi-brs/BasesHTML/isbn/BRSCGI?CMD=VERLST&BASE=ISBN&DOCS=1-15&CONF=AEISPA.cnf&OPDEF=AND&SEPARADOR=' + \ + '&WDIS-C=DISPONIBLE+or+AGOTADO&WGEN-C=&WISB-C=%s&WAUT-C=%s&WTIT-C=%s&WMAT-C=&WEDI-C=&' + + self.__suffixURL = 'WFEP-C=&%40T353-GE=&%40T353-LE=&WSER-C=&WLUG-C=&WLEN-C=&WCLA-C=&WSOP-C=' + + # Define some regexps + self.__regExps = { 'author' : '<th scope="row">Autor:.*?<td>(?P<author>.*?)</td>', + 'isbn' : '<span class="cabTitulo">ISBN.*?<strong>(?P<isbn>.*?)</strong>', # Matches ISBN 13 + 'title' : '<th scope="row">Título:.*?<td>(?P<title>.*?)</td>', + 'language' : '<th scope="row">Lengua:.*?<td>(?P<language>.*?)</td>', + 'edition' : '<th scope="row">Edición:.*?<td>.*?<span>(?P<edition>.*?)</span>', + 'pur_price' : '<th scope="row">Precio:.*?<td>.*?<span>(?P<pur_price>.*?)€</span>', + 'desc' : '<th scope="row">Descripción:.*?<td>.*?<span>(?P<desc>.*?)</span>', + 'publication' : '<th scope="row">Publicación:.*?<td>.*?<span>(?P<publication>.*?)</span>', + 'keyword' : '<th scope="row">Materias:.*?<td>.*?<span>(?P<keywords>.*?)</span>', + 'notas' : '<th scope="row">Notas:.*?<td>.*?<span>(?P<notas>.*?)</span>', + 'cdu' : '<th scope="row">CDU:.*?<td><span>(?P<cdu>.*?)</span></td>', + 'encuadernacion': '<th scope="row">Encuadernación:.*?<td>.*?<span>(?P<encuadernacion>.*?)</span>', + 'series' : '<th scope="row">Colección:.*?<td>.*?<span>(?P<series>.*?)</span>' + } + + # Compile patterns objects + self.__regExpsPO = {} + for k, pattern in self.__regExps.iteritems(): + self.__regExpsPO[k] = re.compile(pattern) + + self.__domTree = BasicTellicoDOM() + + def run(self, criteria, kind): + """ + Runs the parser: fetch book related links, then fills and prints the DOM tree + to stdout (in tellico format) so that tellico can use it. + """ + + # Strip out hyphens if kind is ISBN + if kind == ISBN: + criteria = criteria.replace('-', NULLSTRING) + # Support for multiple search + isbnList = criteria.split(';') + for n in isbnList: + self.__getBook(n, kind) + else: + self.__getBook(criteria, kind) + + # Print results to stdout + self.__domTree.printXMLTree() + + def __getHTMLContent(self, url): + """ + Fetch HTML data from url + """ + + try: + u = urllib2.urlopen(url) + except Exception, e: + u.close() + sys.exit(""" +Network error while getting HTML content. +Tellico cannot connect to: http://www.mcu.es/comun/bases/isbn/ISBN.htm webpage: +'%s'""" % e) + + + self.__data = u.read() + u.close() + + def __fetchBookLinks(self): + """ + Retrieve all links related to the search. self.__data contains HTML content fetched by self.__getHTMLContent() + that need to be parsed. + """ + + matchList = re.findall("""<div class="isbnResDescripcion">.*?<p>.*?<A target="_top" HREF="(?P<url>.*?)">""", self.__data, re.S) + + if not matchList: return None + return matchList + + def __fetchBookInfo(self, url): + """ + Looks for book information + """ + + self.__getHTMLContent(url) + + matches = {} + data = {} + + data['comments'] = [] + # Empty string if series not available + data['series_num'] = NULLSTRING + data['translator'] = NULLSTRING + + for name, po in self.__regExpsPO.iteritems(): + data[name] = NULLSTRING + matches[name] = re.search(self.__regExps[name], self.__data, re.S | re.I) + + + if matches[name]: + if name == 'title': + d = matches[name].group('title').strip() + d = re.sub('<.?strong>', NULLSTRING, d) + d = re.sub('\n', NULLSTRING, d) + data['title'] = d + + elif name == 'isbn': + data['isbn'] = matches[name].group('isbn').strip() + + elif name == 'edition': + data['edition'] = matches[name].group('edition').strip() + + elif name == 'pur_price': + d = matches[name].group('pur_price') + data['pur_price'] = d.strip() + ' EUR' + + elif name == 'publication': + d = matches[name].group('publication') + for p in ('</?[Aa].*?>', ' ', ':', ','): + d = re.sub(p, NULLSTRING, d) + + d = d.split('\n') + # d[1] is an empty string + data['publisher'] = "%s (%s)" % (d[2], d[0]) + data['pub_year'] = re.sub('\d{2}\/', NULLSTRING, d[3]) + del data['publication'] + + elif name == 'desc': + d = matches[name].group('desc') + m = re.search('\d+ ', d) + # When not available + data['pages'] = NULLSTRING + if m: + data['pages'] = m.group(0).strip() + m = re.search('; (?P<format>.*cm)', d) + if m: + data['comments'].append('Format: ' + m.group('format').strip()) + del data['desc'] + + elif name == 'encuadernacion': + data['comments'].append(matches[name].group('encuadernacion').strip()) + + elif name == 'keyword': + d = matches[name].group('keywords') + d = re.sub('</?[Aa].*?>', NULLSTRING, d) + data['keyword'] = d.strip() + + elif name == 'cdu': + data['comments'].append('CDU: ' + matches[name].group('cdu').strip()) + + elif name == 'notas': + data['comments'].append(matches[name].group('notas').strip()) + + elif name == 'series': + d = matches[name].group('series').strip() + d = re.sub(' ', ' ', d) + data[name] = d + # data[name] can contain something like 'Byblos, 162/24' + + # Maybe better to add the reg exp to get seriesNum in self.__regExps + p = re.compile('[0-9]+$') + s = re.search(p, data[name]) + + if s: + # if series ends with a number, it seems that is a + # number of the book inside the series. We save in seriesNum + data['series_num'] = s.group() + + # it removes lasts digits (plus one because is space or /) from + # data['series'] + l = len(data['series_num']) + 1 + data[name] = data[name][0:-l] + data[name] = data[name].rstrip(",") # remove the , between series and series_num + + elif name == 'author': + # We may find several authors + data[name] = [] + authorsList = re.findall('<a.*?>(?P<author>.*?)</a>', matches[name].group('author'), re.S | re.I) + if not authorsList: + # No href links + authors = re.search('<li>(?P<author>.*?)</li>', matches[name].group('author'), re.S | re.I) + try: + results = authors.group('author').strip().split(',') + except AttributeError: + results = [] + results = [r.strip() for r in results] + data[name] = results + else: + for d in authorsList: + # Sometimes, the search engine outputs some image between a elements + if d.strip()[:4] != '<img': + data[name].append(d.strip()) + + # Move tr authors (translators) to translators list + translator = self.__getSpecialRol(data[name], TRANSLATOR_STR) + edlit = self.__getSpecialRol(data[name], EDLIT_STR) + data[name] = self.__removeSpecialsFromAuthors(data[name], translator, TRANSLATOR_STR) + data[name] = self.__removeSpecialsFromAuthors(data[name], edlit, EDLIT_STR) + + if len(translator) > 0: + data['translator'] = self.__formatSpecials(translator, NULLSTRING) + + if len(edlit) > 0: + data['comments'].append(self.__formatSpecials(edlit, "Editor Literario: ")) + + elif name == 'language': + # We may find several languages + d = matches[name].group('language') + d = re.sub('\n', NULLSTRING, d) + d = d.split('<span>') + a = [] + for lg in d: + if len(lg): + lg = re.sub('</span>', NULLSTRING, lg) + # Because HTML is not interpreted in the 'language' field of Tellico + lg = re.sub('ó', 'o', lg) + a.append(lg.strip()) + # Removes that word so that only the language name remains. + a[0] = re.sub('publicacion: ', NULLSTRING, a[0]) + data['language'] = a + # Add other language related info to the 'comments' field too + #for lg in a[1:]: + #data['comments'].append(lg) + + return data + + + def __getBook(self, data, kind = ISBN): + if not len(data): + raise EngineError, "No data given. Unable to proceed." + + if kind == ISBN: + self.__getHTMLContent("%s%s%s" % (self.__baseURL, self.__searchURL % \ + (urllib.quote(data), # ISBN + NULLSTRING, # AUTHOR + NULLSTRING), # TITLE + self.__suffixURL) + ) + elif kind == AUTHOR: + self.__getHTMLContent("%s%s%s" % (self.__baseURL, self.__searchURL % \ + (NULLSTRING, # ISBN + urllib.quote(data), # AUTHOR + NULLSTRING), # TITLE + self.__suffixURL) + ) + + elif kind == TITLE: + self.__getHTMLContent("%s%s%s" % (self.__baseURL, self.__searchURL % \ + (NULLSTRING, # ISBN + NULLSTRING, # AUTHOR + urllib.quote(data)), # TITLE + self.__suffixURL) + ) + + # Get all links + links = self.__fetchBookLinks() + + # Now retrieve infos + if links: + for entry in links: + data = self.__fetchBookInfo( url = self.__baseURL + entry.replace(' ', '%20') ) + node = self.__domTree.addEntry(data) + else: + return None + + def __getSpecialRol(self, authors, special): + """ + Receives a list like ['Stephen King','Lorenzo Cortina','tr.', + 'Rosala Vzquez','tr.'] and returns a list with special names + """ + + j = 0; max = len(authors) + special_rol = [] + while j < max: + if authors[j] == special: + special_rol.append(authors[j-1]) + j += 1 + + return special_rol + + def __removeSpecialsFromAuthors(self, authors, specials, string): + """ + Receives a list with authors+translators and removes 'tr.' and + authors from there. Example: + authors: ['Stephen King','Lorenzo Cortina','tr.','Rosala Vzquez','tr.'] + translators: ['Lorenzo Cortina','Rosala Vzquez'] + returns: ['Stephen King'] + + (We could also guess string value because is the next position + in authors list) + """ + + newauthors = authors[:] + + for t in specials: + newauthors.remove(t) + newauthors.remove(string) + + return newauthors + + def __formatSpecials(self, translators, prefix): + """ + Receives a list with translators and returns a string + (authors are handled different: each author in a different node) + """ + + return prefix + string.join(translators, '; ') + +def halt(): + print "HALT." + sys.exit(0) + +def showUsage(): + print """Usage: %s options +Where options are: + -t title + -i (ISBN|UPC) + -a author + -m filename (support for multiple ISBN/UPC search)""" % sys.argv[0] + sys.exit(1) + +def main(): + if len(sys.argv) < 3: + showUsage() + + socket.setdefaulttimeout(5) + + # ;-separated ISBNs string + isbnStringList = NULLSTRING + + opts = {'-t' : TITLE, '-i' : ISBN, '-a' : AUTHOR, '-m' : isbnStringList} + if sys.argv[1] not in opts.keys(): + showUsage() + + if sys.argv[1] == '-m': + try: + f = open(sys.argv[2], 'r') + data = f.readlines() + # remove trailing \n + sys.argv[2] = string.join([d[:-1] for d in data], ';') + sys.argv[1] = '-i' + f.close() + except IOError, e: + print "Error: %s" % e + sys.exit(1) + + parser = MinisterioCulturaParser() + parser.run(sys.argv[2], opts[sys.argv[1]]) + +if __name__ == '__main__': + main() diff --git a/src/fetch/scripts/ministerio_de_cultura.py.spec b/src/fetch/scripts/ministerio_de_cultura.py.spec new file mode 100644 index 0000000..ef24ac5 --- /dev/null +++ b/src/fetch/scripts/ministerio_de_cultura.py.spec @@ -0,0 +1,7 @@ +Name=Spanish Ministry of Culture +Type=data-source +ArgumentKeys=1,2,3,4 +Arguments=-t %1,-a %1,-i %1,-i %1 +CollectionType=2 +FormatType=0 +UpdateArgs=-t %{title} diff --git a/src/fetch/srufetcher.cpp b/src/fetch/srufetcher.cpp new file mode 100644 index 0000000..1d7289b --- /dev/null +++ b/src/fetch/srufetcher.cpp @@ -0,0 +1,541 @@ +/*************************************************************************** + copyright : (C) 2003-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 "srufetcher.h" +#include "messagehandler.h" +#include "../field.h" +#include "../collection.h" +#include "../translators/tellico_xml.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../translators/dcimporter.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" +#include "../gui/lineedit.h" +#include "../gui/combobox.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../lccnvalidator.h" + +#include <klocale.h> +#include <kio/job.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kcombobox.h> +#include <kaccelmanager.h> +#include <knuminput.h> + +#include <qlabel.h> +#include <qlayout.h> +#include <qwhatsthis.h> + +//#define SRU_DEBUG + +namespace { + // 7090 was the old default port, but that wa sjust because LoC used it + // let's use default HTTP port of 80 now + static const int SRU_DEFAULT_PORT = 80; + static const int SRU_MAX_RECORDS = 25; +} + +using Tellico::Fetch::SRUFetcher; +using Tellico::Fetch::SRUConfigWidget; + +SRUFetcher::SRUFetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_job(0), m_MARCXMLHandler(0), m_MODSHandler(0), m_started(false) { +} + +SRUFetcher::SRUFetcher(const QString& name_, const QString& host_, uint port_, const QString& path_, + QObject* parent_) : Fetcher(parent_), + m_host(host_), m_port(port_), m_path(path_), + m_job(0), m_MARCXMLHandler(0), m_MODSHandler(0), m_started(false) { + m_name = name_; // m_name is protected in super class +} + +SRUFetcher::~SRUFetcher() { + delete m_MARCXMLHandler; + m_MARCXMLHandler = 0; + delete m_MODSHandler; + m_MODSHandler = 0; +} + +QString SRUFetcher::defaultName() { + return i18n("SRU Server"); +} + +QString SRUFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool SRUFetcher::canFetch(int type) const { + return type == Data::Collection::Book || type == Data::Collection::Bibtex; +} + +void SRUFetcher::readConfigHook(const KConfigGroup& config_) { + m_host = config_.readEntry("Host"); + int p = config_.readNumEntry("Port", SRU_DEFAULT_PORT); + if(p > 0) { + m_port = p; + } + m_path = config_.readEntry("Path"); + // used to be called Database + if(m_path.isEmpty()) { + m_path = config_.readEntry("Database"); + } + if(!m_path.startsWith(QChar('/'))) { + m_path.prepend('/'); + } + m_format = config_.readEntry("Format", QString::fromLatin1("mods")); + m_fields = config_.readListEntry("Custom Fields"); +} + +void SRUFetcher::search(FetchKey key_, const QString& value_) { + if(m_host.isEmpty() || m_path.isEmpty()) { + myDebug() << "SRUFetcher::search() - settings are not set!" << endl; + stop(); + return; + } + + m_started = true; + +#ifdef SRU_DEBUG + KURL u = KURL::fromPathOrURL(QString::fromLatin1("/home/robby/sru.xml")); +#else + KURL u; + u.setProtocol(QString::fromLatin1("http")); + u.setHost(m_host); + u.setPort(m_port); + u.setPath(m_path); + + u.addQueryItem(QString::fromLatin1("operation"), QString::fromLatin1("searchRetrieve")); + u.addQueryItem(QString::fromLatin1("version"), QString::fromLatin1("1.1")); + u.addQueryItem(QString::fromLatin1("maximumRecords"), QString::number(SRU_MAX_RECORDS)); + u.addQueryItem(QString::fromLatin1("recordSchema"), m_format); + + const int type = Kernel::self()->collectionType(); + QString str = QChar('"') + value_ + QChar('"'); + switch(key_) { + case Title: + u.addQueryItem(QString::fromLatin1("query"), QString::fromLatin1("dc.title=") + str); + break; + + case Person: + { + QString s; + if(type == Data::Collection::Book || type == Data::Collection::Bibtex) { + s = QString::fromLatin1("author=") + str + QString::fromLatin1(" or dc.author=") + str; + } else { + s = QString::fromLatin1("dc.creator=") + str + QString::fromLatin1(" or dc.editor=") + str; + } + u.addQueryItem(QString::fromLatin1("query"), s); + } + break; + + case ISBN: + // no validation here + str.remove('-'); + // limit to first isbn + str = str.section(';', 0, 0); + u.addQueryItem(QString::fromLatin1("query"), QString::fromLatin1("bath.isbn=") + str); + break; + + case LCCN: + { + // limit to first lccn + str.remove('-'); + str = str.section(';', 0, 0); + // also try formalized lccn + QString lccn = LCCNValidator::formalize(str); + u.addQueryItem(QString::fromLatin1("query"), + QString::fromLatin1("bath.lccn=") + str + + QString::fromLatin1(" or bath.lccn=") + lccn + ); + } + break; + + case Keyword: + u.addQueryItem(QString::fromLatin1("query"), str); + break; + + case Raw: + { + QString key = value_.section('=', 0, 0).stripWhiteSpace(); + QString str = value_.section('=', 1).stripWhiteSpace(); + u.addQueryItem(key, str); + } + break; + + default: + kdWarning() << "SRUFetcher::search() - key not recognized: " << key_ << endl; + stop(); + break; + } +#endif +// myDebug() << u.prettyURL() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void SRUFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void SRUFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void SRUFetcher::slotComplete(KIO::Job* job_) { + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + stop(); + return; + } + + Data::CollPtr coll; + QString msg; + + const QString result = QString::fromUtf8(m_data, m_data.size()); + + // first check for SRU errors + const QString& diag = XML::nsZingDiag; + Import::XMLImporter xmlImporter(result); + QDomDocument dom = xmlImporter.domDocument(); + + QDomNodeList diagList = dom.elementsByTagNameNS(diag, QString::fromLatin1("diagnostic")); + for(uint i = 0; i < diagList.count(); ++i) { + QDomElement elem = diagList.item(i).toElement(); + QDomNodeList nodeList1 = elem.elementsByTagNameNS(diag, QString::fromLatin1("message")); + QDomNodeList nodeList2 = elem.elementsByTagNameNS(diag, QString::fromLatin1("details")); + for(uint j = 0; j < nodeList1.count(); ++j) { + QString d = nodeList1.item(j).toElement().text(); + if(!d.isEmpty()) { + QString d2 = nodeList2.item(j).toElement().text(); + if(!d2.isEmpty()) { + d += " (" + d2 + ')'; + } + myDebug() << "SRUFetcher::slotComplete() - " << d << endl; + if(!msg.isEmpty()) msg += '\n'; + msg += d; + } + } + } + + QString modsResult; + if(m_format == Latin1Literal("mods")) { + modsResult = result; + } else if(m_format == Latin1Literal("marcxml") && initMARCXMLHandler()) { + modsResult = m_MARCXMLHandler->applyStylesheet(result); + } + if(!modsResult.isEmpty() && initMODSHandler()) { + Import::TellicoImporter imp(m_MODSHandler->applyStylesheet(modsResult)); + coll = imp.collection(); + if(!msg.isEmpty()) msg += '\n'; + msg += imp.statusMessage(); + } else if(m_format == Latin1Literal("dc")) { + Import::DCImporter imp(dom); + coll = imp.collection(); + if(!msg.isEmpty()) msg += '\n'; + msg += imp.statusMessage(); + } else { + myDebug() << "SRUFetcher::slotComplete() - unrecognized format: " << m_format << endl; + stop(); + return; + } + + if(coll && !msg.isEmpty()) { + message(msg, coll->entryCount() == 0 ? MessageHandler::Warning : MessageHandler::Status); + } + + if(!coll) { + myDebug() << "SRUFetcher::slotComplete() - no collection pointer" << endl; + if(!msg.isEmpty()) { + message(msg, MessageHandler::Error); + } + stop(); + return; + } + + const StringMap customFields = SRUFetcher::customFields(); + for(StringMap::ConstIterator it = customFields.begin(); it != customFields.end(); ++it) { + if(!m_fields.contains(it.key())) { + coll->removeField(it.key()); + } + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + QString desc; + switch(coll->type()) { + case Data::Collection::Book: + desc = entry->field(QString::fromLatin1("author")) + + QChar('/') + + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + break; + + case Data::Collection::Video: + desc = entry->field(QString::fromLatin1("studio")) + + QChar('/') + + entry->field(QString::fromLatin1("director")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + case Data::Collection::Album: + desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + break; + + default: + break; + } + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, entry); + emit signalResultFound(r); + } + stop(); +} + +Tellico::Data::EntryPtr SRUFetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +void SRUFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "SRUFetcher::updateEntry() - " << source() << ": " << entry_->title() << endl; + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(!isbn.isEmpty()) { + search(Fetch::ISBN, isbn); + return; + } + + QString lccn = entry_->field(QString::fromLatin1("lccn")); + if(!lccn.isEmpty()) { + search(Fetch::LCCN, lccn); + return; + } + + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "SRUFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +bool SRUFetcher::initMARCXMLHandler() { + if(m_MARCXMLHandler) { + return true; + } + + QString xsltfile = locate("appdata", QString::fromLatin1("MARC21slim2MODS3.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "SRUFetcher::initHandlers() - can not locate MARC21slim2MODS3.xsl." << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + + m_MARCXMLHandler = new XSLTHandler(u); + if(!m_MARCXMLHandler->isValid()) { + kdWarning() << "SRUFetcher::initHandlers() - error in MARC21slim2MODS3.xsl." << endl; + delete m_MARCXMLHandler; + m_MARCXMLHandler = 0; + return false; + } + return true; +} + +bool SRUFetcher::initMODSHandler() { + if(m_MODSHandler) { + return true; + } + + QString xsltfile = locate("appdata", QString::fromLatin1("mods2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "SRUFetcher::initHandlers() - can not locate mods2tellico.xsl." << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + + m_MODSHandler = new XSLTHandler(u); + if(!m_MODSHandler->isValid()) { + kdWarning() << "SRUFetcher::initHandlers() - error in mods2tellico.xsl." << endl; + delete m_MODSHandler; + m_MODSHandler = 0; + return false; + } + return true; +} + +Tellico::Fetch::Fetcher::Ptr SRUFetcher::libraryOfCongress(QObject* parent_) { + return new SRUFetcher(i18n("Library of Congress (US)"), QString::fromLatin1("z3950.loc.gov"), 7090, + QString::fromLatin1("voyager"), parent_); +} + +// static +Tellico::StringMap SRUFetcher::customFields() { + StringMap map; + map[QString::fromLatin1("address")] = i18n("Address"); + map[QString::fromLatin1("abstract")] = i18n("Abstract"); + return map; +} + +Tellico::Fetch::ConfigWidget* SRUFetcher::configWidget(QWidget* parent_) const { + return new SRUConfigWidget(parent_, this); +} + +SRUConfigWidget::SRUConfigWidget(QWidget* parent_, const SRUFetcher* fetcher_ /*=0*/) + : ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 4, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + QLabel* label = new QLabel(i18n("Hos&t: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_hostEdit = new GUI::LineEdit(optionsWidget()); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SIGNAL(signalName(const QString&))); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SLOT(slotCheckHost())); + l->addWidget(m_hostEdit, row, 1); + QString w = i18n("Enter the host name of the server."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_hostEdit, w); + label->setBuddy(m_hostEdit); + + label = new QLabel(i18n("&Port: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_portSpinBox = new KIntSpinBox(0, 999999, 1, SRU_DEFAULT_PORT, 10, optionsWidget()); + connect(m_portSpinBox, SIGNAL(valueChanged(int)), SLOT(slotSetModified())); + l->addWidget(m_portSpinBox, row, 1); + w = i18n("Enter the port number of the server. The default is %1.").arg(SRU_DEFAULT_PORT); + QWhatsThis::add(label, w); + QWhatsThis::add(m_portSpinBox, w); + label->setBuddy(m_portSpinBox); + + label = new QLabel(i18n("Path: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_pathEdit = new GUI::LineEdit(optionsWidget()); + connect(m_pathEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_pathEdit, row, 1); + w = i18n("Enter the path to the database used by the server."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_pathEdit, w); + label->setBuddy(m_pathEdit); + + label = new QLabel(i18n("Format: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_formatCombo = new GUI::ComboBox(optionsWidget()); + m_formatCombo->insertItem(QString::fromLatin1("MODS"), QString::fromLatin1("mods")); + m_formatCombo->insertItem(QString::fromLatin1("MARCXML"), QString::fromLatin1("marcxml")); + m_formatCombo->insertItem(QString::fromLatin1("Dublin Core"), QString::fromLatin1("dc")); + connect(m_formatCombo, SIGNAL(activated(int)), SLOT(slotSetModified())); + l->addWidget(m_formatCombo, row, 1); + w = i18n("Enter the result format used by the server."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_formatCombo, w); + label->setBuddy(m_formatCombo); + + l->setRowStretch(++row, 1); + + // now add additional fields widget + addFieldsWidget(SRUFetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); + + if(fetcher_) { + m_hostEdit->setText(fetcher_->m_host); + m_portSpinBox->setValue(fetcher_->m_port); + m_pathEdit->setText(fetcher_->m_path); + m_formatCombo->setCurrentData(fetcher_->m_format); + } + KAcceleratorManager::manage(optionsWidget()); +} + +void SRUConfigWidget::saveConfig(KConfigGroup& config_) { + QString s = m_hostEdit->text().stripWhiteSpace(); + if(!s.isEmpty()) { + config_.writeEntry("Host", s); + } + int port = m_portSpinBox->value(); + if(port > 0) { + config_.writeEntry("Port", port); + } + s = m_pathEdit->text().stripWhiteSpace(); + if(!s.isEmpty()) { + config_.writeEntry("Path", s); + } + s = m_formatCombo->currentData().toString(); + if(!s.isEmpty()) { + config_.writeEntry("Format", s); + } + saveFieldsConfig(config_); + slotSetModified(false); +} + +QString SRUConfigWidget::preferredName() const { + QString s = m_hostEdit->text(); + return s.isEmpty() ? SRUFetcher::defaultName() : s; +} + +void SRUConfigWidget::slotCheckHost() { + QString s = m_hostEdit->text(); + // someone might be pasting a full URL, check that + if(s.find(':') > -1 || s.find('/') > -1) { + KURL u(s); + if(u.isValid()) { + m_hostEdit->setText(u.host()); + if(u.port() > 0) { + m_portSpinBox->setValue(u.port()); + } + if(!u.path().isEmpty()) { + m_pathEdit->setText(u.path()); + } + } + } +} + +#include "srufetcher.moc" diff --git a/src/fetch/srufetcher.h b/src/fetch/srufetcher.h new file mode 100644 index 0000000..fd07323 --- /dev/null +++ b/src/fetch/srufetcher.h @@ -0,0 +1,131 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_SRUFETCHER_H +#define TELLICO_SRUFETCHER_H + +namespace Tellico { + class XSLTHandler; + namespace GUI { + class LineEdit; + class ComboBox; + } +} + +class KIntSpinBox; +class KComboBox; +namespace KIO { + class Job; +} + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace Tellico { + namespace Fetch { + +class SRUConfigWidget; + +/** + * A fetcher for SRU servers. + * Right now, only MODS is supported. + * + * @author Robby Stephenson + */ +class SRUFetcher : public Fetcher { +Q_OBJECT + +friend class SRUConfigWidget; + +public: + /** + */ + SRUFetcher(QObject* parent, const char* name = 0); + SRUFetcher(const QString& name, const QString& host, uint port, const QString& dbname, + QObject* parent); + /** + */ + virtual ~SRUFetcher(); + + /** + */ + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + // only search title, person, isbn, or keyword. No Raw for now. + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == Keyword || k == LCCN; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return SRU; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + static StringMap customFields(); + + virtual ConfigWidget* configWidget(QWidget* parent) const; + + static QString defaultName(); + + static Fetcher::Ptr libraryOfCongress(QObject* parent); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + bool initMARCXMLHandler(); + bool initMODSHandler(); + + QString m_host; + uint m_port; + QString m_path; + QString m_format; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; + QGuardedPtr<KIO::Job> m_job; + XSLTHandler* m_MARCXMLHandler; + XSLTHandler* m_MODSHandler; + bool m_started; + QStringList m_fields; +}; + +class SRUConfigWidget : public ConfigWidget { +Q_OBJECT + +friend class SRUFetcher; + +public: + SRUConfigWidget(QWidget* parent_, const SRUFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup& config); + virtual QString preferredName() const; + +private slots: + void slotCheckHost(); + +private: + GUI::LineEdit* m_hostEdit; + KIntSpinBox* m_portSpinBox; + GUI::LineEdit* m_pathEdit; + GUI::ComboBox* m_formatCombo; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/yahoofetcher.cpp b/src/fetch/yahoofetcher.cpp new file mode 100644 index 0000000..002b63b --- /dev/null +++ b/src/fetch/yahoofetcher.cpp @@ -0,0 +1,400 @@ +/*************************************************************************** + copyright : (C) 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 "yahoofetcher.h" +#include "messagehandler.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../imagefactory.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../collection.h" +#include "../entry.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kio/job.h> + +#include <qdom.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qfile.h> + +namespace { + static const int YAHOO_MAX_RETURNS_TOTAL = 20; + static const char* YAHOO_BASE_URL = "http://search.yahooapis.com/AudioSearchService/V1/albumSearch"; + static const char* YAHOO_APP_ID = "tellico-robby"; +} + +using Tellico::Fetch::YahooFetcher; + +YahooFetcher::YahooFetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_xsltHandler(0), + m_limit(YAHOO_MAX_RETURNS_TOTAL), m_job(0), m_started(false) { +} + +YahooFetcher::~YahooFetcher() { + delete m_xsltHandler; + m_xsltHandler = 0; +} + +QString YahooFetcher::defaultName() { + return i18n("Yahoo! Audio Search"); +} + +QString YahooFetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool YahooFetcher::canFetch(int type) const { + return type == Data::Collection::Album; +} + +void YahooFetcher::readConfigHook(const KConfigGroup& config_) { + Q_UNUSED(config_); +} + +void YahooFetcher::search(FetchKey key_, const QString& value_) { + m_key = key_; + m_value = value_; + m_started = true; + m_start = 1; + m_total = -1; + doSearch(); +} + +void YahooFetcher::continueSearch() { + m_started = true; + doSearch(); +} + +void YahooFetcher::doSearch() { +// myDebug() << "YahooFetcher::search() - value = " << value_ << endl; + + KURL u(QString::fromLatin1(YAHOO_BASE_URL)); + u.addQueryItem(QString::fromLatin1("appid"), QString::fromLatin1(YAHOO_APP_ID)); + u.addQueryItem(QString::fromLatin1("type"), QString::fromLatin1("all")); + u.addQueryItem(QString::fromLatin1("output"), QString::fromLatin1("xml")); + u.addQueryItem(QString::fromLatin1("start"), QString::number(m_start)); + u.addQueryItem(QString::fromLatin1("results"), QString::number(YAHOO_MAX_RETURNS_TOTAL)); + + if(!canFetch(Kernel::self()->collectionType())) { + message(i18n("%1 does not allow searching for this collection type.").arg(source()), MessageHandler::Warning); + stop(); + return; + } + + switch(m_key) { + case Title: + u.addQueryItem(QString::fromLatin1("album"), m_value); + break; + + case Person: + u.addQueryItem(QString::fromLatin1("artist"), m_value); + break; + + // raw is used for the entry updates + case Raw: +// u.removeQueryItem(QString::fromLatin1("type")); +// u.addQueryItem(QString::fromLatin1("type"), QString::fromLatin1("phrase")); + u.setQuery(u.query() + '&' + m_value); + break; + + default: + kdWarning() << "YahooFetcher::search() - key not recognized: " << m_key << endl; + stop(); + return; + } +// myDebug() << "YahooFetcher::search() - url: " << u.url() << endl; + + m_job = KIO::get(u, false, false); + connect(m_job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(m_job, SIGNAL(result(KIO::Job*)), + SLOT(slotComplete(KIO::Job*))); +} + +void YahooFetcher::stop() { + if(!m_started) { + return; + } + if(m_job) { + m_job->kill(); + m_job = 0; + } + m_data.truncate(0); + m_started = false; + emit signalDone(this); +} + +void YahooFetcher::slotData(KIO::Job*, const QByteArray& data_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void YahooFetcher::slotComplete(KIO::Job* job_) { +// myDebug() << "YahooFetcher::slotComplete()" << endl; + // since the fetch is done, don't worry about holding the job pointer + m_job = 0; + + if(job_->error()) { + job_->showErrorDialog(Kernel::self()->widget()); + stop(); + return; + } + + if(m_data.isEmpty()) { + myDebug() << "YahooFetcher::slotComplete() - no data" << endl; + stop(); + return; + } + +#if 0 + kdWarning() << "Remove debug from yahoofetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << QCString(m_data, m_data.size()+1); + } + f.close(); +#endif + + if(!m_xsltHandler) { + initXSLTHandler(); + if(!m_xsltHandler) { // probably an error somewhere in the stylesheet loading + stop(); + return; + } + } + + if(m_total == -1) { + QDomDocument dom; + if(!dom.setContent(m_data, false)) { + kdWarning() << "YahooFetcher::slotComplete() - server did not return valid XML." << endl; + return; + } + // total is top level element, with attribute totalResultsAvailable + QDomElement e = dom.documentElement(); + if(!e.isNull()) { + m_total = e.attribute(QString::fromLatin1("totalResultsAvailable")).toInt(); + } + } + + // assume yahoo is always utf-8 + QString str = m_xsltHandler->applyStylesheet(QString::fromUtf8(m_data, m_data.size())); + Import::TellicoImporter imp(str); + Data::CollPtr coll = imp.collection(); + if(!coll) { + myDebug() << "YahooFetcher::slotComplete() - no collection pointer" << endl; + stop(); + return; + } + + int count = 0; + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); count < m_limit && entry != entries.end(); ++entry, ++count) { + if(!m_started) { + // might get aborted + break; + } + QString desc = entry->field(QString::fromLatin1("artist")) + + QChar('/') + + entry->field(QString::fromLatin1("label")) + + QChar('/') + + entry->field(QString::fromLatin1("year")); + + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, Data::EntryPtr(entry)); + emit signalResultFound(r); + } + m_start = m_entries.count() + 1; + m_hasMoreResults = m_start <= m_total; + stop(); // required +} + +Tellico::Data::EntryPtr YahooFetcher::fetchEntry(uint uid_) { + Data::EntryPtr entry = m_entries[uid_]; + if(!entry) { + kdWarning() << "YahooFetcher::fetchEntry() - no entry in dict" << endl; + return 0; + } + + KURL imageURL = entry->field(QString::fromLatin1("image")); + if(!imageURL.isEmpty()) { + QString id = ImageFactory::addImage(imageURL, true); + if(id.isEmpty()) { + // rich text causes layout issues +// emit signalStatus(i18n("<qt>The cover image for <i>%1</i> could not be loaded.</qt>").arg( +// entry->field(QString::fromLatin1("title")))); + message(i18n("The cover image could not be loaded."), MessageHandler::Warning); + } else { + entry->setField(QString::fromLatin1("cover"), id); + } + } + + getTracks(entry); + + // don't want to show image urls in the fetch dialog + entry->setField(QString::fromLatin1("image"), QString::null); + // no need for album id now ? + entry->setField(QString::fromLatin1("yahoo"), QString::null); + return entry; +} + +void YahooFetcher::initXSLTHandler() { + QString xsltfile = locate("appdata", QString::fromLatin1("yahoo2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "YahooFetcher::initXSLTHandler() - can not locate yahoo2tellico.xsl." << endl; + return; + } + + KURL u; + u.setPath(xsltfile); + + delete m_xsltHandler; + m_xsltHandler = new XSLTHandler(u); + if(!m_xsltHandler->isValid()) { + kdWarning() << "YahooFetcher::initXSLTHandler() - error in yahoo2tellico.xsl." << endl; + delete m_xsltHandler; + m_xsltHandler = 0; + return; + } +} + +void YahooFetcher::getTracks(Data::EntryPtr entry_) { + // get album id + if(!entry_ || entry_->field(QString::fromLatin1("yahoo")).isEmpty()) { + return; + } + + const QString albumid = entry_->field(QString::fromLatin1("yahoo")); + + KURL u(QString::fromLatin1(YAHOO_BASE_URL)); + u.setFileName(QString::fromLatin1("songSearch")); + u.addQueryItem(QString::fromLatin1("appid"), QString::fromLatin1(YAHOO_APP_ID)); + u.addQueryItem(QString::fromLatin1("type"), QString::fromLatin1("all")); + u.addQueryItem(QString::fromLatin1("output"), QString::fromLatin1("xml")); + // go ahesad and ask for all results, since there might well be more than 10 songs on the CD + u.addQueryItem(QString::fromLatin1("results"), QString::number(50)); + u.addQueryItem(QString::fromLatin1("albumid"), albumid); + +// myDebug() << "YahooFetcher::getTracks() - url: " << u.url() << endl; + QDomDocument dom = FileHandler::readXMLFile(u, false /*no namespace*/, true /*quiet*/); + if(dom.isNull()) { + myDebug() << "YahooFetcher::getTracks() - null dom returned" << endl; + return; + } + +#if 0 + kdWarning() << "Remove debug from yahoofetcher.cpp" << endl; + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t.setEncoding(QTextStream::UnicodeUTF8); + t << dom.toString(); + } + f.close(); +#endif + + const QString track = QString::fromLatin1("track"); + + QDomNodeList nodes = dom.documentElement().childNodes(); + for(uint i = 0; i < nodes.count(); ++i) { + QDomElement e = nodes.item(i).toElement(); + if(e.isNull()) { + continue; + } + QString t = e.namedItem(QString::fromLatin1("Title")).toElement().text(); + QString n = e.namedItem(QString::fromLatin1("Track")).toElement().text(); + bool ok; + int trackNum = Tellico::toUInt(n, &ok); + // trackNum might be 0 + if(t.isEmpty() || !ok || trackNum < 1) { + continue; + } + QString a = e.namedItem(QString::fromLatin1("Artist")).toElement().text(); + QString l = e.namedItem(QString::fromLatin1("Length")).toElement().text(); + + int len = Tellico::toUInt(l, &ok); + QString value = t + "::" + a; + if(ok && len > 0) { + value += + "::" + Tellico::minutes(len); + } + entry_->setField(track, insertValue(entry_->field(track), value, trackNum)); + } +} + +// not zero-based +QString YahooFetcher::insertValue(const QString& str_, const QString& value_, uint pos_) { + QStringList list = Data::Field::split(str_, true); + for(uint i = list.count(); i < pos_; ++i) { + list += QString::null; + } + bool write = true; + if(!list[pos_-1].isNull()) { + // for some reason, some songs are repeated from yahoo, with 0 length, don't overwrite that + if(value_.contains(QString::fromLatin1("::")) < 2) { // means no length value + write = false; + } + } + if(!value_.isEmpty() && write) { + list[pos_-1] = value_; + } + return list.join(QString::fromLatin1("; ")); +} + +void YahooFetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "YahooFetcher::updateEntry()" << endl; + // limit to top 5 results + m_limit = 5; + + QString value; + QString title = entry_->field(QString::fromLatin1("title")); + if(!title.isEmpty()) { + value += QString::fromLatin1("album=") + title; + } + QString artist = entry_->field(QString::fromLatin1("artist")); + if(!artist.isEmpty()) { + if(!value.isEmpty()) { + value += '&'; + } + value += QString::fromLatin1("artist=") + artist; + } + if(!value.isEmpty()) { + search(Fetch::Raw, value); + return; + } + + myDebug() << "YahooFetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* YahooFetcher::configWidget(QWidget* parent_) const { + return new YahooFetcher::ConfigWidget(parent_, this); +} + +YahooFetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const YahooFetcher*/*=0*/) + : Fetch::ConfigWidget(parent_) { + QVBoxLayout* l = new QVBoxLayout(optionsWidget()); + l->addWidget(new QLabel(i18n("This source has no options."), optionsWidget())); + l->addStretch(); +} + +QString YahooFetcher::ConfigWidget::preferredName() const { + return YahooFetcher::defaultName(); +} + +#include "yahoofetcher.moc" diff --git a/src/fetch/yahoofetcher.h b/src/fetch/yahoofetcher.h new file mode 100644 index 0000000..7ff5733 --- /dev/null +++ b/src/fetch/yahoofetcher.h @@ -0,0 +1,105 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef YAHOOFETCHER_H +#define YAHOOFETCHER_H + +namespace Tellico { + class XSLTHandler; +} + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qcstring.h> // for QByteArray +#include <qguardedptr.h> + +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Fetch { + +/** + * @author Robby Stephenson + */ +class YahooFetcher : public Fetcher { +Q_OBJECT + +public: + /** + */ + YahooFetcher(QObject* parent, const char* name = 0); + /** + */ + virtual ~YahooFetcher(); + + /** + */ + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Yahoo; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + + /** + * Returns a widget for modifying the fetcher's config. + */ + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget : public Fetch::ConfigWidget { + public: + ConfigWidget(QWidget* parent_, const YahooFetcher* fetcher = 0); + virtual void saveConfig(KConfigGroup&) {} + virtual QString preferredName() const; + }; + friend class ConfigWidget; + + static QString defaultName(); + +private slots: + void slotData(KIO::Job* job, const QByteArray& data); + void slotComplete(KIO::Job* job); + +private: + void initXSLTHandler(); + void doSearch(); + void getTracks(Data::EntryPtr entry); + QString insertValue(const QString& str, const QString& value, uint pos); + + XSLTHandler* m_xsltHandler; + int m_limit; + int m_start; + int m_total; + + QByteArray m_data; + QMap<int, Data::EntryPtr> m_entries; // they get modified after collection is created, so can't be const + QGuardedPtr<KIO::Job> m_job; + + FetchKey m_key; + QString m_value; + bool m_started; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetch/z3950-servers.cfg b/src/fetch/z3950-servers.cfg new file mode 100644 index 0000000..f4f6157 --- /dev/null +++ b/src/fetch/z3950-servers.cfg @@ -0,0 +1,106 @@ +[loc] +Charset=marc8 +Database=Voyager +Host=z3950.loc.gov +Locale=en +Name=Library of Congress (US) +Port=7090 +Syntax=mods + +[blzcat] +Host=3950cat.bl.uk +Port=9909 +Database=BLAC +Name=The British Library +Charset=marc-8 +Locale=en_GB + +[sudoc] +Host=carmin.sudoc.abes.fr +Port=210 +Database=ABES-Z39-PUBLIC +Name=Sudoc (France) +Charset=iso-5426 +Locale=fr +Syntax=usmarc + +[bibsys] +Host=z3950.bibsys.no +Port=2100 +Database=BIBSYS +Name=BIBSYS (Norway) +Charset=iso-8859-1 +Locale=no +Syntax=usmarc + +[sbn] +Host=opac.sbn.it +Port=3950 +Database=nopac +Name=Italian National Library +Charset=iso-8859-1 +Locale=it +Syntax=unimarc + +[porbase] +Host=z3950.bn.pt +Port=210 +Database=bnd +Name=Portuguese National Library +Charset=iso-8859-1 +Locale=pt +Syntax=unimarc + +[nlp] +Host=alpha.bn.org.pl +Port=210 +Database=INNOPAC +Name=National Library of Poland +Charset=iso6937 +Locale=pl +Syntax=usmarc + +[amicus] +Host=amicus.collectionscanada.ca +Port=210 +Database=NL +Name=National Library of Canada +Charset=iso-8859-1 +Locale=ca +Syntax=marc21 + +[iul] +Host=libnet.ac.il +Port=9991 +Database=ULI02 +Name=Israel Union List +Charset=utf-8 +Locale=il +Syntax=marc21 + +[naul] +Host=catalogue.nla.gov.au +Port=7090 +Database=Voyager +Name=National Library of Australia +Charset=utf-8 +Locale=au +Syntax=marc21 + +[libis] +Host=z3950.libis.lt +Port=210 +Database=knygos +Name=National Library of Lithuania +Charset=utf-8 +Syntax=unimarc +Locale=lt + +[copac] +Host=z3950.copac.ac.uk +Port=210 +Database=COPAC +Name=Copac (UK and Ireland) +Charset=utf-8 +Locale=uk,ie,en +Syntax=mods diff --git a/src/fetch/z3950connection.cpp b/src/fetch/z3950connection.cpp new file mode 100644 index 0000000..27efe51 --- /dev/null +++ b/src/fetch/z3950connection.cpp @@ -0,0 +1,503 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson + email : $EMAIL + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "z3950connection.h" +#include "z3950fetcher.h" +#include "messagehandler.h" +#include "../latin1literal.h" +#include "../tellico_debug.h" +#include "../iso5426converter.h" +#include "../iso6937converter.h" + +#include <config.h> + +#ifdef HAVE_YAZ +extern "C" { +#include <yaz/zoom.h> +#include <yaz/marcdisp.h> +#include <yaz/yaz-version.h> +} +#endif + +#include <klocale.h> + +#include <qfile.h> + +namespace { + static const size_t Z3950_DEFAULT_MAX_RECORDS = 20; +} + +using Tellico::Fetch::Z3950ResultFound; +using Tellico::Fetch::Z3950Connection; + +Z3950ResultFound::Z3950ResultFound(const QString& s) : QCustomEvent(uid()) + , m_result(QDeepCopy<QString>(s)) { + ++Z3950Connection::resultsLeft; +} + +Z3950ResultFound::~Z3950ResultFound() { + --Z3950Connection::resultsLeft; +} + +class Z3950Connection::Private { +public: + Private() {} +#ifdef HAVE_YAZ + ~Private() { + ZOOM_options_destroy(conn_opt); + ZOOM_connection_destroy(conn); + }; + + ZOOM_options conn_opt; + ZOOM_connection conn; +#endif +}; + +int Z3950Connection::resultsLeft = 0; + +// since the character set goes into a yaz api call +// I'm paranoid about user insertions, so just grab 64 +// characters at most +Z3950Connection::Z3950Connection(Z3950Fetcher* fetcher, + const QString& host, + uint port, + const QString& dbname, + const QString& sourceCharSet, + const QString& syntax, + const QString& esn) + : QThread() + , d(new Private()) + , m_connected(false) + , m_aborted(false) + , m_fetcher(fetcher) + , m_host(QDeepCopy<QString>(host)) + , m_port(port) + , m_dbname(QDeepCopy<QString>(dbname)) + , m_sourceCharSet(QDeepCopy<QString>(sourceCharSet.left(64))) + , m_syntax(QDeepCopy<QString>(syntax)) + , m_esn(QDeepCopy<QString>(esn)) + , m_start(0) + , m_limit(Z3950_DEFAULT_MAX_RECORDS) + , m_hasMore(false) { +} + +Z3950Connection::~Z3950Connection() { + m_connected = false; + delete d; + d = 0; +} + +void Z3950Connection::reset() { + m_start = 0; + m_limit = Z3950_DEFAULT_MAX_RECORDS; +} + +void Z3950Connection::setQuery(const QString& query_) { + m_pqn = QDeepCopy<QString>(query_); +} + +void Z3950Connection::setUserPassword(const QString& user_, const QString& pword_) { + m_user = QDeepCopy<QString>(user_); + m_password = QDeepCopy<QString>(pword_); +} + +void Z3950Connection::run() { +// myDebug() << "Z3950Connection::run() - " << m_fetcher->source() << endl; + m_aborted = false; + m_hasMore = false; + resultsLeft = 0; +#ifdef HAVE_YAZ + + if(!makeConnection()) { + done(); + return; + } + + ZOOM_query query = ZOOM_query_create(); + myLog() << "Z3950Connection::run() - pqn = " << toCString(m_pqn) << endl; + int errcode = ZOOM_query_prefix(query, toCString(m_pqn)); + if(errcode != 0) { + myDebug() << "Z3950Connection::run() - query error: " << m_pqn << endl; + ZOOM_query_destroy(query); + QString s = i18n("Query error!"); + s += ' ' + m_pqn; + done(s, MessageHandler::Error); + return; + } + + ZOOM_resultset resultSet = ZOOM_connection_search(d->conn, query); + + // check abort status + if(m_aborted) { + done(); + return; + } + + // I know the LOC wants the syntax = "xml" and esn = "mods" + // to get MODS data, that seems a bit odd... + // esn only makes sense for marc and grs-1 + // if syntax is mods, set esn to mods too + QCString type = "raw"; + if(m_syntax == Latin1Literal("mods")) { + m_syntax = QString::fromLatin1("xml"); + ZOOM_resultset_option_set(resultSet, "elementSetName", "mods"); + type = "xml"; + } else { + ZOOM_resultset_option_set(resultSet, "elementSetName", m_esn.latin1()); + } + ZOOM_resultset_option_set(resultSet, "start", QCString().setNum(m_start)); + ZOOM_resultset_option_set(resultSet, "count", QCString().setNum(m_limit-m_start)); + // search in default syntax, unless syntax is already set + if(!m_syntax.isEmpty()) { + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", m_syntax.latin1()); + } + + const char* errmsg; + const char* addinfo; + errcode = ZOOM_connection_error(d->conn, &errmsg, &addinfo); + if(errcode != 0) { + ZOOM_resultset_destroy(resultSet); + ZOOM_query_destroy(query); + m_connected = false; + + QString s = i18n("Connection search error %1: %2").arg(errcode).arg(toString(errmsg)); + if(!QCString(addinfo).isEmpty()) { + s += " (" + toString(addinfo) + ")"; + } + myDebug() << "Z3950Connection::run() - " << s << endl; + done(s, MessageHandler::Error); + return; + } + + const size_t numResults = ZOOM_resultset_size(resultSet); + + QString newSyntax = m_syntax; + if(numResults > 0) { + myLog() << "Z3950Connection::run() - current syntax is " << m_syntax << " (" << numResults << " results)" << endl; + // so now we know that results exist, might have to check syntax + int len; + ZOOM_record rec = ZOOM_resultset_record(resultSet, 0); + // want raw unless it's mods + ZOOM_record_get(rec, type, &len); + if(len > 0 && m_syntax.isEmpty()) { + newSyntax = QString::fromLatin1(ZOOM_record_get(rec, "syntax", &len)).lower(); + myLog() << "Z3950Connection::run() - syntax guess is " << newSyntax << endl; + if(newSyntax == Latin1Literal("mods") || newSyntax == Latin1Literal("xml")) { + m_syntax = QString::fromLatin1("xml"); + ZOOM_resultset_option_set(resultSet, "elementSetName", "mods"); + } else if(newSyntax == Latin1Literal("grs-1")) { + // if it's defaulting to grs-1, go ahead and change it to try to get a marc + // record since grs-1 is a last resort for us + newSyntax.truncate(0); + } + } + // right now, we just understand mods, unimarc, marc21/usmarc, and grs-1 + if(newSyntax != Latin1Literal("xml") && + newSyntax != Latin1Literal("usmarc") && + newSyntax != Latin1Literal("marc21") && + newSyntax != Latin1Literal("unimarc") && + newSyntax != Latin1Literal("grs-1")) { + myLog() << "Z3950Connection::run() - changing z39.50 syntax to MODS" << endl; + newSyntax = QString::fromLatin1("xml"); + ZOOM_resultset_option_set(resultSet, "elementSetName", "mods"); + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", newSyntax.latin1()); + rec = ZOOM_resultset_record(resultSet, 0); + ZOOM_record_get(rec, "xml", &len); + if(len == 0) { + // change set name back + ZOOM_resultset_option_set(resultSet, "elementSetName", m_esn.latin1()); + newSyntax = QString::fromLatin1("usmarc"); // try usmarc + myLog() << "Z3950Connection::run() - changing z39.50 syntax to USMARC" << endl; + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", newSyntax.latin1()); + rec = ZOOM_resultset_record(resultSet, 0); + ZOOM_record_get(rec, "raw", &len); + } + if(len == 0) { + newSyntax = QString::fromLatin1("marc21"); // try marc21 + myLog() << "Z3950Connection::run() - changing z39.50 syntax to MARC21" << endl; + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", newSyntax.latin1()); + rec = ZOOM_resultset_record(resultSet, 0); + ZOOM_record_get(rec, "raw", &len); + } + if(len == 0) { + newSyntax = QString::fromLatin1("unimarc"); // try unimarc + myLog() << "Z3950Connection::run() - changing z39.50 syntax to UNIMARC" << endl; + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", newSyntax.latin1()); + rec = ZOOM_resultset_record(resultSet, 0); + ZOOM_record_get(rec, "raw", &len); + } + if(len == 0) { + newSyntax = QString::fromLatin1("grs-1"); // try grs-1 + myLog() << "Z3950Connection::run() - changing z39.50 syntax to GRS-1" << endl; + ZOOM_resultset_option_set(resultSet, "preferredRecordSyntax", newSyntax.latin1()); + rec = ZOOM_resultset_record(resultSet, 0); + ZOOM_record_get(rec, "raw", &len); + } + if(len == 0) { + myLog() << "Z3950Connection::run() - giving up" << endl; + ZOOM_resultset_destroy(resultSet); + ZOOM_query_destroy(query); + done(i18n("Record syntax error"), MessageHandler::Error); + return; + } + myLog() << "Z3950Connection::run() - final syntax is " << newSyntax << endl; + } + } + + // go back to fooling ourselves and calling it mods + if(m_syntax == Latin1Literal("xml")) { + m_syntax = QString::fromLatin1("mods"); + } + if(newSyntax == Latin1Literal("xml")) { + newSyntax = QString::fromLatin1("mods"); + } + // save syntax change for next time + if(m_syntax != newSyntax) { + kapp->postEvent(m_fetcher, new Z3950SyntaxChange(newSyntax)); + m_syntax = newSyntax; + } + + if(m_sourceCharSet.isEmpty()) { + m_sourceCharSet = QString::fromLatin1("marc-8"); + } + + const size_t realLimit = QMIN(numResults, m_limit); + + for(size_t i = m_start; i < realLimit && !m_aborted; ++i) { + myLog() << "Z3950Connection::run() - grabbing index " << i << endl; + ZOOM_record rec = ZOOM_resultset_record(resultSet, i); + if(!rec) { + myDebug() << "Z3950Connection::run() - no record returned for index " << i << endl; + continue; + } + int len; + QString data; + if(m_syntax == Latin1Literal("mods")) { + data = toString(ZOOM_record_get(rec, "xml", &len)); + } else if(m_syntax == Latin1Literal("grs-1")) { // grs-1 + // we're going to parse the rendered data, very ugly... + data = toString(ZOOM_record_get(rec, "render", &len)); + } else { +#if 0 + kdWarning() << "Remove debug from z3950connection.cpp" << endl; + { + QFile f1(QString::fromLatin1("/tmp/z3950.raw")); + if(f1.open(IO_WriteOnly)) { + QDataStream t(&f1); + t << ZOOM_record_get(rec, "raw", &len); + } + f1.close(); + } +#endif + data = toXML(ZOOM_record_get(rec, "raw", &len), m_sourceCharSet); + } + Z3950ResultFound* ev = new Z3950ResultFound(data); + QApplication::postEvent(m_fetcher, ev); + } + + ZOOM_resultset_destroy(resultSet); + ZOOM_query_destroy(query); + + m_hasMore = m_limit < numResults; + if(m_hasMore) { + m_start = m_limit; + m_limit += Z3950_DEFAULT_MAX_RECORDS; + } +#endif + done(); +} + +bool Z3950Connection::makeConnection() { + if(m_connected) { + return true; + } +// myDebug() << "Z3950Connection::makeConnection() - " << m_fetcher->source() << endl; +// I don't know what to do except assume database, user, and password are in locale encoding +#ifdef HAVE_YAZ + d->conn_opt = ZOOM_options_create(); + ZOOM_options_set(d->conn_opt, "implementationName", "Tellico"); + ZOOM_options_set(d->conn_opt, "databaseName", toCString(m_dbname)); + ZOOM_options_set(d->conn_opt, "user", toCString(m_user)); + ZOOM_options_set(d->conn_opt, "password", toCString(m_password)); + + d->conn = ZOOM_connection_create(d->conn_opt); + ZOOM_connection_connect(d->conn, m_host.latin1(), m_port); + + int errcode; + const char* errmsg; // unused: carries same info as 'errcode' + const char* addinfo; + errcode = ZOOM_connection_error(d->conn, &errmsg, &addinfo); + if(errcode != 0) { + ZOOM_options_destroy(d->conn_opt); + ZOOM_connection_destroy(d->conn); + m_connected = false; + + QString s = i18n("Connection error %1: %2").arg(errcode).arg(toString(errmsg)); + if(!QCString(addinfo).isEmpty()) { + s += " (" + toString(addinfo) + ")"; + } + myDebug() << "Z3950Connection::makeConnection() - " << s << endl; + done(s, MessageHandler::Error); + return false; + } +#endif + m_connected = true; + return true; +} + +void Z3950Connection::done() { + checkPendingEvents(); + kapp->postEvent(m_fetcher, new Z3950ConnectionDone(m_hasMore)); +} + +void Z3950Connection::done(const QString& msg_, int type_) { + checkPendingEvents(); + if(m_aborted) { + kapp->postEvent(m_fetcher, new Z3950ConnectionDone(m_hasMore)); + } else { + kapp->postEvent(m_fetcher, new Z3950ConnectionDone(m_hasMore, msg_, type_)); + } +} + +void Z3950Connection::checkPendingEvents() { + // if there's still some pending result events, go ahead and just wait 1 second + if(resultsLeft > 0) { + sleep(1); + } +} + +inline +QCString Z3950Connection::toCString(const QString& text_) { + return iconvRun(text_.utf8(), QString::fromLatin1("utf-8"), m_sourceCharSet); +} + +inline +QString Z3950Connection::toString(const QCString& text_) { + return QString::fromUtf8(iconvRun(text_, m_sourceCharSet, QString::fromLatin1("utf-8"))); +} + +// static +QCString Z3950Connection::iconvRun(const QCString& text_, const QString& fromCharSet_, const QString& toCharSet_) { +#ifdef HAVE_YAZ + if(text_.isEmpty()) { + return text_; + } + + if(fromCharSet_ == toCharSet_) { + return text_; + } + + yaz_iconv_t cd = yaz_iconv_open(toCharSet_.latin1(), fromCharSet_.latin1()); + if(!cd) { + // maybe it's iso 5426, which we sorta support + QString charSetLower = fromCharSet_.lower(); + charSetLower.remove('-').remove(' '); + if(charSetLower == Latin1Literal("iso5426")) { + return iconvRun(Iso5426Converter::toUtf8(text_).utf8(), QString::fromLatin1("utf-8"), toCharSet_); + } else if(charSetLower == Latin1Literal("iso6937")) { + return iconvRun(Iso6937Converter::toUtf8(text_).utf8(), QString::fromLatin1("utf-8"), toCharSet_); + } + kdWarning() << "Z3950Connection::iconvRun() - conversion from " << fromCharSet_ + << " to " << toCharSet_ << " is unsupported" << endl; + return text_; + } + + const char* input = text_; + size_t inlen = text_.length(); + + size_t outlen = 2 * inlen; // this is enough, right? + QMemArray<char> result0(outlen); + char* result = result0.data(); + + int r = yaz_iconv(cd, const_cast<char**>(&input), &inlen, &result, &outlen); + if(r <= 0) { + myDebug() << "Z3950Connection::iconvRun() - can't decode buffer" << endl; + return text_; + } + // bug in yaz, need to flush buffer to catch last character + yaz_iconv(cd, 0, 0, &result, &outlen); + + // length is pointer difference + size_t len = result - result0; + + QCString output = QCString(result0, len+1); +// myDebug() << "-------------------------------------------" << endl; +// myDebug() << output << endl; +// myDebug() << "-------------------------------------------" << endl; + yaz_iconv_close(cd); + return output; +#endif + return text_; +} + +QString Z3950Connection::toXML(const QCString& marc_, const QString& charSet_) { +#ifdef HAVE_YAZ + if(marc_.isEmpty()) { + myDebug() << "Z3950Connection::toXML() - empty string" << endl; + return QString::null; + } + + yaz_iconv_t cd = yaz_iconv_open("utf-8", charSet_.latin1()); + if(!cd) { + // maybe it's iso 5426, which we sorta support + QString charSetLower = charSet_.lower(); + charSetLower.remove('-').remove(' '); + if(charSetLower == Latin1Literal("iso5426")) { + return toXML(Iso5426Converter::toUtf8(marc_).utf8(), QString::fromLatin1("utf-8")); + } else if(charSetLower == Latin1Literal("iso6937")) { + return toXML(Iso6937Converter::toUtf8(marc_).utf8(), QString::fromLatin1("utf-8")); + } + kdWarning() << "Z3950Connection::toXML() - conversion from " << charSet_ << " is unsupported" << endl; + return QString::null; + } + + yaz_marc_t mt = yaz_marc_create(); + yaz_marc_iconv(mt, cd); + yaz_marc_xml(mt, YAZ_MARC_MARCXML); + + // first 5 bytes are length + bool ok; +#if YAZ_VERSIONL < 0x030000 + int len = marc_.left(5).toInt(&ok); +#else + size_t len = marc_.left(5).toInt(&ok); +#endif + if(ok && (len < 25 || len > 100000)) { + myDebug() << "Z3950Connection::toXML() - bad length: " << (ok ? len : -1) << endl; + return QString::null; + } + +#if YAZ_VERSIONL < 0x030000 + char* result; +#else + const char* result; +#endif + int r = yaz_marc_decode_buf(mt, marc_, -1, &result, &len); + if(r <= 0) { + myDebug() << "Z3950Connection::toXML() - can't decode buffer" << endl; + return QString::null; + } + + QString output = QString::fromLatin1("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + output += QString::fromUtf8(QCString(result, len+1), len+1); +// myDebug() << QCString(result) << endl; +// myDebug() << "-------------------------------------------" << endl; +// myDebug() << output << endl; + yaz_iconv_close(cd); + yaz_marc_destroy(mt); + + return output; +#else // no yaz + return QString::null; +#endif +} diff --git a/src/fetch/z3950connection.h b/src/fetch/z3950connection.h new file mode 100644 index 0000000..0929cb7 --- /dev/null +++ b/src/fetch/z3950connection.h @@ -0,0 +1,126 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCH_Z3950CONNECTION_H +#define TELLICO_FETCH_Z3950CONNECTION_H + +#include <qthread.h> +#include <qevent.h> +#include <qdeepcopy.h> + +#include <ksharedptr.h> + +namespace Tellico { + namespace Fetch { + class Z3950Fetcher; + +class Z3950ResultFound : public QCustomEvent { +public: + Z3950ResultFound(const QString& s); + ~Z3950ResultFound(); + const QString& result() const { return m_result; } + + static int uid() { return User + 11111; } + +private: + QString m_result; +}; + +class Z3950ConnectionDone : public QCustomEvent { +public: + Z3950ConnectionDone(bool more) : QCustomEvent(uid()), m_type(-1), m_hasMore(more) {} + Z3950ConnectionDone(bool more, const QString& s, int t) : QCustomEvent(uid()), m_msg(QDeepCopy<QString>(s)), m_type(t), m_hasMore(more) {} + + const QString& message() const { return m_msg; } + int messageType() const { return m_type; } + bool hasMoreResults() const { return m_hasMore; } + + static int uid() { return User + 22222; } + +private: + QString m_msg; + int m_type; + bool m_hasMore; +}; + +class Z3950SyntaxChange : public QCustomEvent { +public: + Z3950SyntaxChange(const QString& s) : QCustomEvent(uid()), m_syntax(QDeepCopy<QString>(s)) {} + const QString& syntax() const { return m_syntax; } + + static int uid() { return User + 33333; } + +private: + QString m_syntax; +}; + +/** + * @author Robby Stephenson + */ +class Z3950Connection : public QThread { +public: + Z3950Connection(Z3950Fetcher* fetcher, + const QString& host, + uint port, + const QString& dbname, + const QString& sourceCharSet, + const QString& syntax, + const QString& esn); + ~Z3950Connection(); + + void reset(); + void setQuery(const QString& query); + void setUserPassword(const QString& user, const QString& pword); + void run(); + + void abort() { m_aborted = true; } + +private: + static QCString iconvRun(const QCString& text, const QString& fromCharSet, const QString& toCharSet); + static QString toXML(const QCString& marc, const QString& fromCharSet); + + bool makeConnection(); + void done(); + void done(const QString& message, int type); + QCString toCString(const QString& text); + QString toString(const QCString& text); + void checkPendingEvents(); + + class Private; + Private* d; + + bool m_connected; + bool m_aborted; + + KSharedPtr<Z3950Fetcher> m_fetcher; + QString m_host; + uint m_port; + QString m_dbname; + QString m_user; + QString m_password; + QString m_sourceCharSet; + QString m_syntax; + QString m_pqn; + QString m_esn; + size_t m_start; + size_t m_limit; + bool m_hasMore; + + friend class Z3950ResultFound; + static int resultsLeft; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/fetch/z3950fetcher.cpp b/src/fetch/z3950fetcher.cpp new file mode 100644 index 0000000..5e045cf --- /dev/null +++ b/src/fetch/z3950fetcher.cpp @@ -0,0 +1,782 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + * In addition, as a special exception, the author gives permission to * + * link the code of this program with the OpenSSL library released by * + * the OpenSSL Project (or with modified versions of OpenSSL that use * + * the same license as OpenSSL), and distribute linked combinations * + * including the two. You must obey the GNU General Public License in * + * all respects for all of the code used other than OpenSSL. If you * + * modify this file, you may extend this exception to your version of * + * the file, but you are not obligated to do so. If you do not wish to * + * do so, delete this exception statement from your version. * + * * + ***************************************************************************/ + +#include <config.h> + +#include "z3950fetcher.h" +#include "z3950connection.h" +#include "messagehandler.h" +#include "../collection.h" +#include "../latin1literal.h" +#include "../translators/xslthandler.h" +#include "../translators/tellicoimporter.h" +#include "../translators/grs1importer.h" +#include "../tellico_debug.h" +#include "../gui/lineedit.h" +#include "../gui/combobox.h" +#include "../isbnvalidator.h" +#include "../lccnvalidator.h" + +#include <klocale.h> +#include <kstandarddirs.h> +#include <kapplication.h> +#include <knuminput.h> +#include <kconfig.h> +#include <kcombobox.h> +#include <kaccelmanager.h> +#include <kseparator.h> + +#include <qfile.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qwhatsthis.h> +#include <qdom.h> + +namespace { + static const int Z3950_DEFAULT_PORT = 210; + static const QString Z3950_DEFAULT_ESN = QString::fromLatin1("F"); +} + +using Tellico::Fetch::Z3950Fetcher; + +Z3950Fetcher::Z3950Fetcher(QObject* parent_, const char* name_) + : Fetcher(parent_, name_), m_conn(0), m_port(Z3950_DEFAULT_PORT), m_esn(Z3950_DEFAULT_ESN), + m_started(false), m_done(true), m_MARC21XMLHandler(0), + m_UNIMARCXMLHandler(0), m_MODSHandler(0) { +} + +Z3950Fetcher::~Z3950Fetcher() { + delete m_MARC21XMLHandler; + m_MARC21XMLHandler = 0; + delete m_UNIMARCXMLHandler; + m_UNIMARCXMLHandler = 0; + delete m_MODSHandler; + m_MODSHandler = 0; + delete m_conn; + m_conn = 0; +} + +QString Z3950Fetcher::defaultName() { + return i18n("z39.50 Server"); +} + +QString Z3950Fetcher::source() const { + return m_name.isEmpty() ? defaultName() : m_name; +} + +bool Z3950Fetcher::canFetch(int type) const { + return type == Data::Collection::Book || type == Data::Collection::Bibtex; +} + +void Z3950Fetcher::readConfigHook(const KConfigGroup& config_) { + QString preset = config_.readEntry("Preset"); + if(preset.isEmpty()) { + m_host = config_.readEntry("Host"); + int p = config_.readNumEntry("Port", Z3950_DEFAULT_PORT); + if(p > 0) { + m_port = p; + } + m_dbname = config_.readEntry("Database"); + m_sourceCharSet = config_.readEntry("Charset"); + m_syntax = config_.readEntry("Syntax"); + m_user = config_.readEntry("User"); + m_password = config_.readEntry("Password"); + } else { + m_preset = preset; + QString serverFile = locate("appdata", QString::fromLatin1("z3950-servers.cfg")); + if(!serverFile.isEmpty()) { + KConfig cfg(serverFile, true /* read-only */, false /* read KDE */); + const QStringList servers = cfg.groupList(); + for(QStringList::ConstIterator server = servers.begin(); server != servers.end(); ++server) { + cfg.setGroup(*server); + + const QString id = *server; + if(id == preset) { + const QString name = cfg.readEntry("Name"); + m_host = cfg.readEntry("Host"); + m_port = cfg.readNumEntry("Port", Z3950_DEFAULT_PORT); + m_dbname = cfg.readEntry("Database"); + m_sourceCharSet = cfg.readEntry("Charset"); + m_syntax = cfg.readEntry("Syntax"); + m_user = cfg.readEntry("User"); + m_password = cfg.readEntry("Password"); + } + } + } + } + + m_fields = config_.readListEntry("Custom Fields"); +} + +void Z3950Fetcher::saveConfigHook(KConfigGroup& config_) { + config_.writeEntry("Syntax", m_syntax); + config_.sync(); +} + +void Z3950Fetcher::search(FetchKey key_, const QString& value_) { +#ifdef HAVE_YAZ + m_started = true; + m_done = false; + if(m_host.isEmpty() || m_dbname.isEmpty()) { + myDebug() << "Z3950Fetcher::search() - settings are not set!" << endl; + stop(); + return; + } + m_key = key_; + m_value = value_; + m_started = true; + + QString svalue = m_value; + QRegExp rx1(QString::fromLatin1("['\"].*\\1")); + if(!rx1.exactMatch(svalue)) { + svalue.prepend('"').append('"'); + } + + switch(key_) { + case Title: + m_pqn = QString::fromLatin1("@attr 1=4 ") + svalue; + break; + case Person: +// m_pqn = QString::fromLatin1("@or "); +// m_pqn += QString::fromLatin1("@attr 1=1 \"") + m_value + '"'; + m_pqn = QString::fromLatin1(" @attr 1=1003 ") + svalue; + break; + case ISBN: + { + m_pqn.truncate(0); + QString s = m_value; + s.remove('-'); + QStringList isbnList = QStringList::split(QString::fromLatin1("; "), s); + // also going to search for isbn10 values + for(QStringList::Iterator it = isbnList.begin(); it != isbnList.end(); ++it) { + if((*it).startsWith(QString::fromLatin1("978"))) { + QString isbn10 = ISBNValidator::isbn10(*it); + isbn10.remove('-'); + isbnList.insert(it, isbn10); + } + } + const int count = isbnList.count(); + if(count > 1) { + m_pqn = QString::fromLatin1("@or "); + } + for(int i = 0; i < count; ++i) { + m_pqn += QString::fromLatin1(" @attr 1=7 ") + isbnList[i]; + if(i < count-2) { + m_pqn += QString::fromLatin1(" @or"); + } + } + } + break; + case LCCN: + { + m_pqn.truncate(0); + QString s = m_value; + s.remove('-'); + QStringList lccnList = QStringList::split(QString::fromLatin1("; "), s); + while(!lccnList.isEmpty()) { + m_pqn += QString::fromLatin1(" @or @attr 1=9 ") + lccnList.front(); + if(lccnList.count() > 1) { + m_pqn += QString::fromLatin1(" @or"); + } + m_pqn += QString::fromLatin1(" @attr 1=9 ") + LCCNValidator::formalize(lccnList.front()); + lccnList.pop_front(); + } + } + break; + case Keyword: + m_pqn = QString::fromLatin1("@attr 1=1016 ") + svalue; + break; + case Raw: + m_pqn = m_value; + break; + default: + kdWarning() << "Z3950Fetcher::search() - key not recognized: " << key_ << endl; + stop(); + return; + } +// m_pqn = QString::fromLatin1("@attr 1=7 0253333490"); + myLog() << "Z3950Fetcher::search() - PQN query = " << m_pqn << endl; + + if(m_conn) { + m_conn->reset(); // reset counts + } + + process(); +#else // HAVE_YAZ + Q_UNUSED(key_); + Q_UNUSED(value_); + stop(); + return; +#endif +} + +void Z3950Fetcher::continueSearch() { +#ifdef HAVE_YAZ + m_started = true; + process(); +#endif +} + +void Z3950Fetcher::stop() { + if(!m_started) { + return; + } +// myDebug() << "Z3950Fetcher::stop()" << endl; + m_started = false; + if(m_conn) { + // give it a second to cleanup + m_conn->abort(); + m_conn->wait(1000); + } + emit signalDone(this); +} + +bool Z3950Fetcher::initMARC21Handler() { + if(m_MARC21XMLHandler) { + return true; + } + + QString xsltfile = locate("appdata", QString::fromLatin1("MARC21slim2MODS3.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "Z3950Fetcher::initHandlers() - can not locate MARC21slim2MODS3.xsl." << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + + m_MARC21XMLHandler = new XSLTHandler(u); + if(!m_MARC21XMLHandler->isValid()) { + kdWarning() << "Z3950Fetcher::initHandlers() - error in MARC21slim2MODS3.xsl." << endl; + delete m_MARC21XMLHandler; + m_MARC21XMLHandler = 0; + return false; + } + return true; +} + +bool Z3950Fetcher::initUNIMARCHandler() { + if(m_UNIMARCXMLHandler) { + return true; + } + + QString xsltfile = locate("appdata", QString::fromLatin1("UNIMARC2MODS3.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "Z3950Fetcher::initHandlers() - can not locate UNIMARC2MODS3.xsl." << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + + m_UNIMARCXMLHandler = new XSLTHandler(u); + if(!m_UNIMARCXMLHandler->isValid()) { + kdWarning() << "Z3950Fetcher::initHandlers() - error in UNIMARC2MODS3.xsl." << endl; + delete m_UNIMARCXMLHandler; + m_UNIMARCXMLHandler = 0; + return false; + } + return true; +} + +bool Z3950Fetcher::initMODSHandler() { + if(m_MODSHandler) { + return true; + } + + QString xsltfile = locate("appdata", QString::fromLatin1("mods2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "Z3950Fetcher::initHandlers() - can not locate mods2tellico.xsl." << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + + m_MODSHandler = new XSLTHandler(u); + if(!m_MODSHandler->isValid()) { + kdWarning() << "Z3950Fetcher::initHandlers() - error in mods2tellico.xsl." << endl; + delete m_MODSHandler; + m_MODSHandler = 0; + // no use in keeping the MARC handlers now + delete m_MARC21XMLHandler; + m_MARC21XMLHandler = 0; + delete m_UNIMARCXMLHandler; + m_UNIMARCXMLHandler = 0; + return false; + } + return true; +} + +void Z3950Fetcher::process() { + if(m_conn) { + m_conn->wait(); + } else { + m_conn = new Z3950Connection(this, m_host, m_port, m_dbname, m_sourceCharSet, m_syntax, m_esn); + if(!m_user.isEmpty()) { + m_conn->setUserPassword(m_user, m_password); + } + } + + m_conn->setQuery(m_pqn); + m_conn->start(); +} + +void Z3950Fetcher::handleResult(const QString& result_) { + if(result_.isEmpty()) { + myDebug() << "Z3950Fetcher::handleResult() - empty record found, maybe the character encoding or record format is wrong?" << endl; + return; + } + +#if 0 + kdWarning() << "Remove debug from z3950fetcher.cpp" << endl; + { + QFile f1(QString::fromLatin1("/tmp/marc.xml")); + if(f1.open(IO_WriteOnly)) { +// if(f1.open(IO_WriteOnly | IO_Append)) { + QTextStream t(&f1); + t.setEncoding(QTextStream::UnicodeUTF8); + t << result_; + } + f1.close(); + } +#endif + // assume always utf-8 + QString str, msg; + Data::CollPtr coll = 0; + // not marc, has to be grs-1 + if(m_syntax == Latin1Literal("grs-1")) { + Import::GRS1Importer imp(result_); + coll = imp.collection(); + msg = imp.statusMessage(); + } else { // now the MODS stuff + if(m_syntax == Latin1Literal("mods")) { + str = result_; + } else if(m_syntax == Latin1Literal("unimarc") && initUNIMARCHandler()) { + str = m_UNIMARCXMLHandler->applyStylesheet(result_); + } else if(initMARC21Handler()) { // got to be usmarc/marc21 + str = m_MARC21XMLHandler->applyStylesheet(result_); + } + if(str.isEmpty() || !initMODSHandler()) { + myDebug() << "Z3950Fetcher::handleResult() - empty string or can't init" << endl; + stop(); + return; + } +#if 0 + kdWarning() << "Remove debug from z3950fetcher.cpp" << endl; + { + QFile f2(QString::fromLatin1("/tmp/mods.xml")); +// if(f2.open(IO_WriteOnly)) { + if(f2.open(IO_WriteOnly | IO_Append)) { + QTextStream t(&f2); + t.setEncoding(QTextStream::UnicodeUTF8); + t << str; + } + f2.close(); + } +#endif + Import::TellicoImporter imp(m_MODSHandler->applyStylesheet(str)); + imp.setOptions(imp.options() & ~Import::ImportProgress); // no progress needed + coll = imp.collection(); + msg = imp.statusMessage(); + } + + if(!coll) { + if(!msg.isEmpty()) { + message(msg, MessageHandler::Warning); + } + myDebug() << "Z3950Fetcher::handleResult() - no collection pointer: " << msg << endl; + return; + } + + if(coll->entryCount() == 0) { +// myDebug() << "Z3950Fetcher::handleResult() - no Tellico entry in result" << endl; + return; + } + + const StringMap customFields = Z3950Fetcher::customFields(); + for(StringMap::ConstIterator it = customFields.begin(); it != customFields.end(); ++it) { + if(!m_fields.contains(it.key())) { + coll->removeField(it.key()); + } + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVec::Iterator entry = entries.begin(); entry != entries.end(); ++entry) { + QString desc = entry->field(QString::fromLatin1("author")) + '/' + + entry->field(QString::fromLatin1("publisher")); + if(!entry->field(QString::fromLatin1("cr_year")).isEmpty()) { + desc += QChar('/') + entry->field(QString::fromLatin1("cr_year")); + } else if(!entry->field(QString::fromLatin1("pub_year")).isEmpty()){ + desc += QChar('/') + entry->field(QString::fromLatin1("pub_year")); + } + SearchResult* r = new SearchResult(this, entry->title(), desc, entry->field(QString::fromLatin1("isbn"))); + m_entries.insert(r->uid, entry); + emit signalResultFound(r); + } +} + +void Z3950Fetcher::done() { + m_done = true; + stop(); +} + +Tellico::Data::EntryPtr Z3950Fetcher::fetchEntry(uint uid_) { + return m_entries[uid_]; +} + +void Z3950Fetcher::customEvent(QCustomEvent* event_) { + if(!m_conn) { + return; + } + + if(event_->type() == Z3950ResultFound::uid()) { + if(m_done) { + kdWarning() << "Z3950Fetcher::customEvent() - result returned after done signal!" << endl; + } + Z3950ResultFound* e = static_cast<Z3950ResultFound*>(event_); + handleResult(e->result()); + } else if(event_->type() == Z3950ConnectionDone::uid()) { + Z3950ConnectionDone* e = static_cast<Z3950ConnectionDone*>(event_); + if(e->messageType() > -1) { + message(e->message(), e->messageType()); + } + m_hasMoreResults = e->hasMoreResults(); + m_conn->wait(); + done(); + } else if(event_->type() == Z3950SyntaxChange::uid()) { + if(m_done) { + kdWarning() << "Z3950Fetcher::customEvent() - syntax changed after done signal!" << endl; + } + Z3950SyntaxChange* e = static_cast<Z3950SyntaxChange*>(event_); + if(m_syntax != e->syntax()) { + m_syntax = e->syntax(); + // it gets saved when saveConfigHook() get's called from the Fetcher() d'tor + } + } else { + kdWarning() << "Z3950Fetcher::customEvent() - weird type: " << event_->type() << endl; + } +} + +void Z3950Fetcher::updateEntry(Data::EntryPtr entry_) { +// myDebug() << "Z3950Fetcher::updateEntry() - " << source() << ": " << entry_->title() << endl; + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(!isbn.isEmpty()) { + search(Fetch::ISBN, isbn); + return; + } + + QString lccn = entry_->field(QString::fromLatin1("lccn")); + if(!lccn.isEmpty()) { + search(Fetch::LCCN, lccn); + return; + } + + // optimistically try searching for title and rely on Collection::sameEntry() to figure things out + QString t = entry_->field(QString::fromLatin1("title")); + if(!t.isEmpty()) { + search(Fetch::Title, t); + return; + } + + myDebug() << "Z3950Fetcher::updateEntry() - insufficient info to search" << endl; + emit signalDone(this); // always need to emit this if not continuing with the search +} + +Tellico::Fetch::ConfigWidget* Z3950Fetcher::configWidget(QWidget* parent_) const { + return new Z3950Fetcher::ConfigWidget(parent_, this); +} + +Z3950Fetcher::ConfigWidget::ConfigWidget(QWidget* parent_, const Z3950Fetcher* fetcher_/*=0*/) + : Fetch::ConfigWidget(parent_) { + QGridLayout* l = new QGridLayout(optionsWidget(), 7, 2); + l->setSpacing(4); + l->setColStretch(1, 10); + + int row = -1; + + m_usePreset = new QCheckBox(i18n("Use preset &server:"), optionsWidget()); + l->addWidget(m_usePreset, ++row, 0); + connect(m_usePreset, SIGNAL(toggled(bool)), SLOT(slotTogglePreset(bool))); + m_serverCombo = new GUI::ComboBox(optionsWidget()); + connect(m_serverCombo, SIGNAL(activated(int)), SLOT(slotPresetChanged())); + l->addWidget(m_serverCombo, row, 1); + ++row; + l->addMultiCellWidget(new KSeparator(optionsWidget()), row, row, 0, 1); + l->setRowSpacing(row, 10); + + QLabel* label = new QLabel(i18n("Hos&t: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_hostEdit = new GUI::LineEdit(optionsWidget()); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + connect(m_hostEdit, SIGNAL(textChanged(const QString&)), SIGNAL(signalName(const QString&))); + l->addWidget(m_hostEdit, row, 1); + QString w = i18n("Enter the host name of the server."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_hostEdit, w); + label->setBuddy(m_hostEdit); + + label = new QLabel(i18n("&Port: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_portSpinBox = new KIntSpinBox(0, 999999, 1, Z3950_DEFAULT_PORT, 10, optionsWidget()); + connect(m_portSpinBox, SIGNAL(valueChanged(int)), SLOT(slotSetModified())); + l->addWidget(m_portSpinBox, row, 1); + w = i18n("Enter the port number of the server. The default is %1.").arg(Z3950_DEFAULT_PORT); + QWhatsThis::add(label, w); + QWhatsThis::add(m_portSpinBox, w); + label->setBuddy(m_portSpinBox); + + label = new QLabel(i18n("&Database: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_databaseEdit = new GUI::LineEdit(optionsWidget()); + connect(m_databaseEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_databaseEdit, row, 1); + w = i18n("Enter the database name used by the server."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_databaseEdit, w); + label->setBuddy(m_databaseEdit); + + label = new QLabel(i18n("Ch&aracter set: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_charSetCombo = new KComboBox(true, optionsWidget()); + m_charSetCombo->insertItem(QString::null); + m_charSetCombo->insertItem(QString::fromLatin1("marc8")); + m_charSetCombo->insertItem(QString::fromLatin1("iso-8859-1")); + m_charSetCombo->insertItem(QString::fromLatin1("utf-8")); + connect(m_charSetCombo, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_charSetCombo, row, 1); + w = i18n("Enter the character set encoding used by the z39.50 server. The most likely choice " + "is MARC-8, although ISO-8859-1 is common as well."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_charSetCombo, w); + label->setBuddy(m_charSetCombo); + + label = new QLabel(i18n("&Format: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_syntaxCombo = new GUI::ComboBox(optionsWidget()); + m_syntaxCombo->insertItem(i18n("Auto-detect"), QString()); + m_syntaxCombo->insertItem(QString::fromLatin1("MODS"), QString::fromLatin1("mods")); + m_syntaxCombo->insertItem(QString::fromLatin1("MARC21"), QString::fromLatin1("marc21")); + m_syntaxCombo->insertItem(QString::fromLatin1("UNIMARC"), QString::fromLatin1("unimarc")); + m_syntaxCombo->insertItem(QString::fromLatin1("USMARC"), QString::fromLatin1("usmarc")); + m_syntaxCombo->insertItem(QString::fromLatin1("GRS-1"), QString::fromLatin1("grs-1")); + connect(m_syntaxCombo, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_syntaxCombo, row, 1); + w = i18n("Enter the data format used by the z39.50 server. Tellico will attempt to " + "automatically detect the best setting if <i>auto-detect</i> is selected."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_syntaxCombo, w); + label->setBuddy(m_syntaxCombo); + + label = new QLabel(i18n("&User: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_userEdit = new GUI::LineEdit(optionsWidget()); + m_userEdit->setHint(i18n("Optional")); + connect(m_userEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_userEdit, row, 1); + w = i18n("Enter the authentication user name used by the z39.50 database. Most servers " + "do not need one."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_userEdit, w); + label->setBuddy(m_userEdit); + + label = new QLabel(i18n("Pass&word: "), optionsWidget()); + l->addWidget(label, ++row, 0); + m_passwordEdit = new GUI::LineEdit(optionsWidget()); + m_passwordEdit->setHint(i18n("Optional")); + m_passwordEdit->setEchoMode(QLineEdit::Password); + connect(m_passwordEdit, SIGNAL(textChanged(const QString&)), SLOT(slotSetModified())); + l->addWidget(m_passwordEdit, row, 1); + w = i18n("Enter the authentication password used by the z39.50 database. Most servers " + "do not need one. The password will be saved in plain text in the Tellico " + "configuration file."); + QWhatsThis::add(label, w); + QWhatsThis::add(m_passwordEdit, w); + label->setBuddy(m_passwordEdit); + + l->setRowStretch(++row, 1); + + // now add additional fields widget + addFieldsWidget(Z3950Fetcher::customFields(), fetcher_ ? fetcher_->m_fields : QStringList()); + + loadPresets(fetcher_ ? fetcher_->m_preset : QString::null); + if(fetcher_) { + m_hostEdit->setText(fetcher_->m_host); + m_portSpinBox->setValue(fetcher_->m_port); + m_databaseEdit->setText(fetcher_->m_dbname); + m_userEdit->setText(fetcher_->m_user); + m_passwordEdit->setText(fetcher_->m_password); + m_charSetCombo->setCurrentText(fetcher_->m_sourceCharSet); + // the syntax is detected automatically by the fetcher + // since the config group gets deleted in the config file, + // the value needs to be retained here + m_syntax = fetcher_->m_syntax; + m_syntaxCombo->setCurrentData(m_syntax); + } + KAcceleratorManager::manage(optionsWidget()); + + // start with presets turned off + m_usePreset->setChecked(fetcher_ && !fetcher_->m_preset.isEmpty()); + + slotTogglePreset(m_usePreset->isChecked()); +} + +Z3950Fetcher::ConfigWidget::~ConfigWidget() { +} + +void Z3950Fetcher::ConfigWidget::saveConfig(KConfigGroup& config_) { + if(m_usePreset->isChecked()) { + QString presetID = m_serverCombo->currentData().toString(); + config_.writeEntry("Preset", presetID); + return; + } + config_.deleteEntry("Preset"); + + QString s = m_hostEdit->text().stripWhiteSpace(); + if(!s.isEmpty()) { + config_.writeEntry("Host", s); + } + int port = m_portSpinBox->value(); + if(port > 0) { + config_.writeEntry("Port", port); + } + s = m_databaseEdit->text().stripWhiteSpace(); + if(!s.isEmpty()) { + config_.writeEntry("Database", s); + } + s = m_charSetCombo->currentText(); + if(!s.isEmpty()) { + config_.writeEntry("Charset", s); + } + s = m_userEdit->text(); + if(!s.isEmpty()) { + config_.writeEntry("User", s); + } + s = m_passwordEdit->text(); + if(!s.isEmpty()) { + config_.writeEntry("Password", s); + } + s = m_syntaxCombo->currentData().toString(); + if(!s.isEmpty()) { + m_syntax = s; + } + config_.writeEntry("Syntax", m_syntax); + + saveFieldsConfig(config_); + slotSetModified(false); +} + +// static +Tellico::StringMap Z3950Fetcher::customFields() { + StringMap map; + map[QString::fromLatin1("address")] = i18n("Address"); + map[QString::fromLatin1("abstract")] = i18n("Abstract"); + map[QString::fromLatin1("illustrator")] = i18n("Illustrator"); + return map; +} + +void Z3950Fetcher::ConfigWidget::slotTogglePreset(bool on) { + m_serverCombo->setEnabled(on); + if(on) { + emit signalName(m_serverCombo->currentText()); + } + m_hostEdit->setEnabled(!on); + if(!on && !m_hostEdit->text().isEmpty()) { + emit signalName(m_hostEdit->text()); + } + m_portSpinBox->setEnabled(!on); + m_databaseEdit->setEnabled(!on); + m_userEdit->setEnabled(!on); + m_passwordEdit->setEnabled(!on); + m_charSetCombo->setEnabled(!on); + m_syntaxCombo->setEnabled(!on); + if(on) { + emit signalName(m_serverCombo->currentText()); + } +} + +void Z3950Fetcher::ConfigWidget::slotPresetChanged() { + emit signalName(m_serverCombo->currentText()); +} + +void Z3950Fetcher::ConfigWidget::loadPresets(const QString& current_) { + QString lang = KGlobal::locale()->languageList().first(); + QString lang2A; + { + QString dummy; + KGlobal::locale()->splitLocale(lang, lang2A, dummy, dummy); + } + + QString serverFile = locate("appdata", QString::fromLatin1("z3950-servers.cfg")); + if(serverFile.isEmpty()) { + kdWarning() << "Z3950Fetcher::loadPresets() - no z3950 servers file found" << endl; + return; + } + + int idx = -1; + + KConfig cfg(serverFile, true /* read-only */, false /* read KDE */); + const QStringList servers = cfg.groupList(); + // I want the list of servers sorted by name + QMap<QString, QString> serverNameMap; + for(QStringList::ConstIterator server = servers.constBegin(); server != servers.constEnd(); ++server) { + if((*server).isEmpty()) { + myDebug() << "Z3950Fetcher::ConfigWidget::loadPresets() - empty id" << endl; + continue; + } + cfg.setGroup(*server); + const QString name = cfg.readEntry("Name"); + if(!name.isEmpty()) { + serverNameMap.insert(name, *server); + } + } + for(QMap<QString, QString>::ConstIterator it = serverNameMap.constBegin(); it != serverNameMap.constEnd(); ++it) { + const QString name = it.key(); + const QString id = it.data(); + cfg.setGroup(id); + + m_serverCombo->insertItem(i18n(name.utf8()), id); + if(current_.isEmpty() && idx == -1) { + // set the initial selection to something depending on the language + const QStringList locales = cfg.readListEntry("Locale"); + if(locales.findIndex(lang) > -1 || locales.findIndex(lang2A) > -1) { + idx = m_serverCombo->count() - 1; + } + } else if(id == current_) { + idx = m_serverCombo->count() - 1; + } + } + if(idx > -1) { + m_serverCombo->setCurrentItem(idx); + } +} + +QString Z3950Fetcher::ConfigWidget::preferredName() const { + if(m_usePreset->isChecked()) { + return m_serverCombo->currentText(); + } + QString s = m_hostEdit->text(); + return s.isEmpty() ? i18n("z39.50 Server") : s; +} + +#include "z3950fetcher.moc" diff --git a/src/fetch/z3950fetcher.h b/src/fetch/z3950fetcher.h new file mode 100644 index 0000000..ec6dca0 --- /dev/null +++ b/src/fetch/z3950fetcher.h @@ -0,0 +1,153 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + * In addition, as a special exception, the author gives permission to * + * link the code of this program with the OpenSSL library released by * + * the OpenSSL Project (or with modified versions of OpenSSL that use * + * the same license as OpenSSL), and distribute linked combinations * + * including the two. You must obey the GNU General Public License in * + * all respects for all of the code used other than OpenSSL. If you * + * modify this file, you may extend this exception to your version of * + * the file, but you are not obligated to do so. If you do not wish to * + * do so, delete this exception statement from your version. * + * * + ***************************************************************************/ + +#ifndef TELLICO_Z3950FETCHER_H +#define TELLICO_Z3950FETCHER_H + +namespace Tellico { + class XSLTHandler; + namespace GUI { + class LineEdit; + class ComboBox; + } +} + +class KIntSpinBox; +class KComboBox; + +#include "fetcher.h" +#include "configwidget.h" +#include "../datavectors.h" + +#include <qguardedptr.h> + +namespace Tellico { + namespace Fetch { + class Z3950Connection; + +/** + * @author Robby Stephenson + */ +class Z3950Fetcher : public Fetcher { +Q_OBJECT + +public: + Z3950Fetcher(QObject* parent, const char* name = 0); + + virtual ~Z3950Fetcher(); + + virtual QString source() const; + virtual bool isSearching() const { return m_started; } + virtual void search(FetchKey key, const QString& value); + virtual void continueSearch(); + // can search title, person, isbn, or keyword. No UPC or Raw for now. + virtual bool canSearch(FetchKey k) const { return k == Title || k == Person || k == ISBN || k == Keyword || k == LCCN; } + virtual void stop(); + virtual Data::EntryPtr fetchEntry(uint uid); + virtual Type type() const { return Z3950; } + virtual bool canFetch(int type) const; + virtual void readConfigHook(const KConfigGroup& config); + virtual void saveConfigHook(KConfigGroup& config); + + virtual void updateEntry(Data::EntryPtr entry); + const QString& host() const { return m_host; } + + static StringMap customFields(); + + virtual Fetch::ConfigWidget* configWidget(QWidget* parent) const; + + class ConfigWidget; + friend class ConfigWidget; + + static QString defaultName(); + +protected: + virtual void customEvent(QCustomEvent* event); + +private: + bool initMARC21Handler(); + bool initUNIMARCHandler(); + bool initMODSHandler(); + void process(); + void handleResult(const QString& result); + void done(); + + Z3950Connection* m_conn; + + QString m_host; + uint m_port; + QString m_dbname; + QString m_user; + QString m_password; + QString m_sourceCharSet; + QString m_syntax; + QString m_pqn; // prefix query notation + QString m_esn; // element set name + + FetchKey m_key; + QString m_value; + QMap<int, Data::EntryPtr> m_entries; + bool m_started; + bool m_done; + QString m_preset; + + XSLTHandler* m_MARC21XMLHandler; + XSLTHandler* m_UNIMARCXMLHandler; + XSLTHandler* m_MODSHandler; + QStringList m_fields; + + friend class Z3950Connection; +}; + +class Z3950Fetcher::ConfigWidget : public Fetch::ConfigWidget { +Q_OBJECT + +public: + ConfigWidget(QWidget* parent, const Z3950Fetcher* fetcher = 0); + virtual ~ConfigWidget(); + virtual void saveConfig(KConfigGroup& config_); + virtual QString preferredName() const; + +private slots: + void slotTogglePreset(bool on); + void slotPresetChanged(); + +private: + void loadPresets(const QString& current); + + QCheckBox* m_usePreset; + GUI::ComboBox* m_serverCombo; + GUI::LineEdit* m_hostEdit; + KIntSpinBox* m_portSpinBox; + GUI::LineEdit* m_databaseEdit; + GUI::LineEdit* m_userEdit; + GUI::LineEdit* m_passwordEdit; + KComboBox* m_charSetCombo; + GUI::ComboBox* m_syntaxCombo; + // have to remember syntax + QString m_syntax; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fetchdialog.cpp b/src/fetchdialog.cpp new file mode 100644 index 0000000..453b1b3 --- /dev/null +++ b/src/fetchdialog.cpp @@ -0,0 +1,753 @@ +/*************************************************************************** + copyright : (C) 2003-2008 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 "fetchdialog.h" +#include "fetch/fetchmanager.h" +#include "fetch/fetcher.h" +#include "entryview.h" +#include "isbnvalidator.h" +#include "upcvalidator.h" +#include "tellico_kernel.h" +#include "filehandler.h" +#include "collection.h" +#include "entry.h" +#include "document.h" +#include "tellico_debug.h" +#include "gui/combobox.h" +#include "gui/listview.h" +#include "tellico_utils.h" +#include "stringset.h" + +#include <klocale.h> +#include <klineedit.h> +#include <kpushbutton.h> +#include <kstatusbar.h> +#include <khtmlview.h> +#include <kprogress.h> +#include <kconfig.h> +#include <kdialogbase.h> +#include <kfiledialog.h> +#include <kiconloader.h> +#include <kaccelmanager.h> +#include <ktextedit.h> + +#include <qlayout.h> +#include <qhbox.h> +#include <qvgroupbox.h> +#include <qsplitter.h> +#include <qtimer.h> +#include <qwhatsthis.h> +#include <qcheckbox.h> +#include <qvbox.h> +#include <qtimer.h> +#include <qimage.h> + +#include <config.h> +#ifdef ENABLE_WEBCAM +#include "barcode/barcode.h" +#endif + +namespace { + static const int FETCH_STATUS_ID = 0; + static const int FETCH_PROGRESS_ID = 0; + static const int FETCH_MIN_WIDTH = 600; + + static const char* FETCH_STRING_SEARCH = I18N_NOOP("&Search"); + static const char* FETCH_STRING_STOP = I18N_NOOP("&Stop"); +} + +using Tellico::FetchDialog; +using barcodeRecognition::barcodeRecognitionThread; + +class FetchDialog::SearchResultItem : public Tellico::GUI::ListViewItem { + friend class FetchDialog; + // always add to end + SearchResultItem(GUI::ListView* lv, Fetch::SearchResult* r) + : GUI::ListViewItem(lv, lv->lastItem()), m_result(r) { + setText(1, r->title); + setText(2, r->desc); + setPixmap(3, Fetch::Manager::self()->fetcherIcon(r->fetcher.data())); + setText(3, r->fetcher->source()); + } + Fetch::SearchResult* m_result; +}; + +FetchDialog::FetchDialog(QWidget* parent_, const char* name_) + : KDialogBase(parent_, name_, false, i18n("Internet Search"), 0), + m_timer(new QTimer(this)), m_started(false) { + m_collType = Kernel::self()->collectionType(); + + QWidget* mainWidget = new QWidget(this, "FetchDialog mainWidget"); + setMainWidget(mainWidget); + QVBoxLayout* topLayout = new QVBoxLayout(mainWidget, 0, KDialog::spacingHint()); + + QVGroupBox* queryBox = new QVGroupBox(i18n("Search Query"), mainWidget, "FetchDialog queryBox"); + topLayout->addWidget(queryBox); + + QHBox* box1 = new QHBox(queryBox, "FetchDialog box1"); + box1->setSpacing(KDialog::spacingHint()); + + QLabel* label = new QLabel(i18n("Start the search", "S&earch:"), box1); + + m_valueLineEdit = new KLineEdit(box1); + label->setBuddy(m_valueLineEdit); + QWhatsThis::add(m_valueLineEdit, i18n("Enter a search value. An ISBN search must include the full ISBN.")); + m_keyCombo = new GUI::ComboBox(box1); + Fetch::KeyMap map = Fetch::Manager::self()->keyMap(); + for(Fetch::KeyMap::ConstIterator it = map.begin(); it != map.end(); ++it) { + m_keyCombo->insertItem(it.data(), it.key()); + } + connect(m_keyCombo, SIGNAL(activated(int)), SLOT(slotKeyChanged(int))); + QWhatsThis::add(m_keyCombo, i18n("Choose the type of search")); + + m_searchButton = new KPushButton(box1); + m_searchButton->setGuiItem(KGuiItem(i18n(FETCH_STRING_STOP), + SmallIconSet(QString::fromLatin1("cancel")))); + connect(m_searchButton, SIGNAL(clicked()), SLOT(slotSearchClicked())); + QWhatsThis::add(m_searchButton, i18n("Click to start or stop the search")); + + // the search button's text changes from search to stop + // I don't want it resizing, so figure out the maximum size and set that + m_searchButton->polish(); + int maxWidth = m_searchButton->sizeHint().width(); + int maxHeight = m_searchButton->sizeHint().height(); + m_searchButton->setGuiItem(KGuiItem(i18n(FETCH_STRING_SEARCH), + SmallIconSet(QString::fromLatin1("find")))); + maxWidth = QMAX(maxWidth, m_searchButton->sizeHint().width()); + maxHeight = QMAX(maxHeight, m_searchButton->sizeHint().height()); + m_searchButton->setMinimumWidth(maxWidth); + m_searchButton->setMinimumHeight(maxHeight); + + QHBox* box2 = new QHBox(queryBox); + box2->setSpacing(KDialog::spacingHint()); + + m_multipleISBN = new QCheckBox(i18n("&Multiple ISBN/UPC search"), box2); + QWhatsThis::add(m_multipleISBN, i18n("Check this box to search for multiple ISBN or UPC values.")); + connect(m_multipleISBN, SIGNAL(toggled(bool)), SLOT(slotMultipleISBN(bool))); + + m_editISBN = new KPushButton(KGuiItem(i18n("Edit List..."), QString::fromLatin1("text_block")), box2); + m_editISBN->setEnabled(false); + QWhatsThis::add(m_editISBN, i18n("Click to open a text edit box for entering or editing multiple ISBN values.")); + connect(m_editISBN, SIGNAL(clicked()), SLOT(slotEditMultipleISBN())); + + // add for spacing + box2->setStretchFactor(new QWidget(box2), 10); + + label = new QLabel(i18n("Search s&ource:"), box2); + m_sourceCombo = new KComboBox(box2); + label->setBuddy(m_sourceCombo); + Fetch::FetcherVec sources = Fetch::Manager::self()->fetchers(m_collType); + for(Fetch::FetcherVec::Iterator it = sources.begin(); it != sources.end(); ++it) { + m_sourceCombo->insertItem(Fetch::Manager::self()->fetcherIcon(it.data()), (*it).source()); + } + connect(m_sourceCombo, SIGNAL(activated(const QString&)), SLOT(slotSourceChanged(const QString&))); + QWhatsThis::add(m_sourceCombo, i18n("Select the database to search")); + + QSplitter* split = new QSplitter(QSplitter::Vertical, mainWidget); + topLayout->addWidget(split); + + m_listView = new GUI::ListView(split); +// topLayout->addWidget(m_listView); +// topLayout->setStretchFactor(m_listView, 1); + m_listView->setSorting(10); // greater than number of columns, so not sorting until user clicks column header + m_listView->setShowSortIndicator(true); + m_listView->setAllColumnsShowFocus(true); + m_listView->setSelectionMode(QListView::Extended); + m_listView->addColumn(QString::null, 20); // will show a check mark when added + m_listView->setColumnAlignment(0, Qt::AlignHCenter); // align checkmark in middle +// m_listView->setColumnWidthMode(0, QListView::Manual); + m_listView->addColumn(i18n("Title")); + m_listView->addColumn(i18n("Description")); + m_listView->addColumn(i18n("Source")); + m_listView->viewport()->installEventFilter(this); + + connect(m_listView, SIGNAL(selectionChanged()), SLOT(slotShowEntry())); + // double clicking should add the entry + connect(m_listView, SIGNAL(doubleClicked(QListViewItem*)), SLOT(slotAddEntry())); + QWhatsThis::add(m_listView, i18n("As results are found, they are added to this list. Selecting one " + "will fetch the complete entry and show it in the view below.")); + + m_entryView = new EntryView(split, "entry_view"); + // don't bother creating funky gradient images for compact view + m_entryView->setUseGradientImages(false); + // set the xslt file AFTER setting the gradient image option + m_entryView->setXSLTFile(QString::fromLatin1("Compact.xsl")); + QWhatsThis::add(m_entryView->view(), i18n("An entry may be shown here before adding it to the " + "current collection by selecting it in the list above")); + + QHBox* box3 = new QHBox(mainWidget); + topLayout->addWidget(box3); + box3->setSpacing(KDialog::spacingHint()); + + m_addButton = new KPushButton(i18n("&Add Entry"), box3); + m_addButton->setEnabled(false); + m_addButton->setIconSet(UserIconSet(Kernel::self()->collectionTypeName())); + connect(m_addButton, SIGNAL(clicked()), SLOT(slotAddEntry())); + QWhatsThis::add(m_addButton, i18n("Add the selected entry to the current collection")); + + m_moreButton = new KPushButton(KGuiItem(i18n("Get More Results"), SmallIconSet(QString::fromLatin1("find"))), box3); + m_moreButton->setEnabled(false); + connect(m_moreButton, SIGNAL(clicked()), SLOT(slotMoreClicked())); + QWhatsThis::add(m_moreButton, i18n("Fetch more results from the current data source")); + + KPushButton* clearButton = new KPushButton(KStdGuiItem::clear(), box3); + connect(clearButton, SIGNAL(clicked()), SLOT(slotClearClicked())); + QWhatsThis::add(clearButton, i18n("Clear all search fields and results")); + + QHBox* bottombox = new QHBox(mainWidget, "box"); + topLayout->addWidget(bottombox); + bottombox->setSpacing(KDialog::spacingHint()); + + m_statusBar = new KStatusBar(bottombox, "statusbar"); + m_statusBar->insertItem(QString::null, FETCH_STATUS_ID, 1, false); + m_statusBar->setItemAlignment(FETCH_STATUS_ID, AlignLeft | AlignVCenter); + m_progress = new QProgressBar(m_statusBar, "progress"); + m_progress->setTotalSteps(0); + m_progress->setFixedHeight(fontMetrics().height()+2); + m_progress->hide(); + m_statusBar->addWidget(m_progress, 0, true); + + KPushButton* closeButton = new KPushButton(KStdGuiItem::close(), bottombox); + connect(closeButton, SIGNAL(clicked()), SLOT(slotClose())); + + connect(m_timer, SIGNAL(timeout()), SLOT(slotMoveProgress())); + + setMinimumWidth(QMAX(minimumWidth(), FETCH_MIN_WIDTH)); + setStatus(i18n("Ready.")); + + resize(configDialogSize(QString::fromLatin1("Fetch Dialog Options"))); + + KConfigGroup config(kapp->config(), "Fetch Dialog Options"); + QValueList<int> splitList = config.readIntListEntry("Splitter Sizes"); + if(!splitList.empty()) { + split->setSizes(splitList); + } + + connect(Fetch::Manager::self(), SIGNAL(signalResultFound(Tellico::Fetch::SearchResult*)), + SLOT(slotResultFound(Tellico::Fetch::SearchResult*))); + connect(Fetch::Manager::self(), SIGNAL(signalStatus(const QString&)), + SLOT(slotStatus(const QString&))); + connect(Fetch::Manager::self(), SIGNAL(signalDone()), + SLOT(slotFetchDone())); + + // make sure to delete results afterwards + m_results.setAutoDelete(true); + + KAcceleratorManager::manage(this); + // initialize combos + QTimer::singleShot(0, this, SLOT(slotInit())); + +#ifdef ENABLE_WEBCAM + // barcode recognition + m_barcodeRecognitionThread = new barcodeRecognitionThread(); + if (m_barcodeRecognitionThread->isWebcamAvailable()) { + m_barcodePreview = new QLabel(0); + m_barcodePreview->move( QApplication::desktop()->width() - 350, 30 ); + m_barcodePreview->setAutoResize( true ); + m_barcodePreview->show(); + } else { + m_barcodePreview = 0; + } + connect( m_barcodeRecognitionThread, SIGNAL(recognized(QString)), this, SLOT(slotBarcodeRecognized(QString)) ); + connect( m_barcodeRecognitionThread, SIGNAL(gotImage(QImage&)), this, SLOT(slotBarcodeGotImage(QImage&)) ); + m_barcodeRecognitionThread->start(); +/* //DEBUG + QImage img( "/home/sebastian/white.png", "PNG" ); + m_barcodeRecognitionThread->recognizeBarcode( img );*/ +#endif +} + +FetchDialog::~FetchDialog() { +#ifdef ENABLE_WEBCAM + m_barcodeRecognitionThread->stop(); + if (!m_barcodeRecognitionThread->wait( 1000 )) + m_barcodeRecognitionThread->terminate(); + delete m_barcodeRecognitionThread; + if (m_barcodePreview) + delete m_barcodePreview; +#endif + + // we might have downloaded a lot of images we don't need to keep + Data::EntryVec entriesToCheck; + for(QMap<int, Data::EntryPtr>::Iterator it = m_entries.begin(); it != m_entries.end(); ++it) { + entriesToCheck.append(it.data()); + } + // no additional entries to check images to keep though + Data::Document::self()->removeImagesNotInCollection(entriesToCheck, Data::EntryVec()); + + saveDialogSize(QString::fromLatin1("Fetch Dialog Options")); + + KConfigGroup config(kapp->config(), "Fetch Dialog Options"); + config.writeEntry("Splitter Sizes", static_cast<QSplitter*>(m_listView->parentWidget())->sizes()); + config.writeEntry("Search Key", m_keyCombo->currentData().toInt()); + config.writeEntry("Search Source", m_sourceCombo->currentText()); +} + +void FetchDialog::slotSearchClicked() { + m_valueLineEdit->selectAll(); + if(m_started) { + setStatus(i18n("Cancelling the search...")); + Fetch::Manager::self()->stop(); + slotFetchDone(); + } else { + QString value = m_valueLineEdit->text().simplifyWhiteSpace(); + if(value != m_oldSearch) { + m_listView->clear(); + m_entryView->clear(); + } + m_resultCount = 0; + m_oldSearch = value; + m_started = true; + m_searchButton->setGuiItem(KGuiItem(i18n(FETCH_STRING_STOP), + SmallIconSet(QString::fromLatin1("cancel")))); + startProgress(); + setStatus(i18n("Searching...")); + kapp->processEvents(); + Fetch::Manager::self()->startSearch(m_sourceCombo->currentText(), + static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt()), + value); + } +} + +void FetchDialog::slotClearClicked() { + slotFetchDone(false); + m_listView->clear(); + m_entryView->clear(); + Fetch::Manager::self()->stop(); + m_multipleISBN->setChecked(false); + m_valueLineEdit->clear(); + m_valueLineEdit->setFocus(); + m_addButton->setEnabled(false); + m_moreButton->setEnabled(false); + m_isbnList.clear(); + m_statusMessages.clear(); + setStatus(i18n("Ready.")); // because slotFetchDone() writes text +} + +void FetchDialog::slotStatus(const QString& status_) { + m_statusMessages.push_back(status_); + // if the queue was empty, start the timer + if(m_statusMessages.count() == 1) { + // wait 2 seconds + QTimer::singleShot(2000, this, SLOT(slotUpdateStatus())); + } +} + +void FetchDialog::slotUpdateStatus() { + if(m_statusMessages.isEmpty()) { + return; + } + setStatus(m_statusMessages.front()); + m_statusMessages.pop_front(); + if(!m_statusMessages.isEmpty()) { + // wait 2 seconds + QTimer::singleShot(2000, this, SLOT(slotUpdateStatus())); + } +} + +void FetchDialog::setStatus(const QString& text_) { + m_statusBar->changeItem(QChar(' ') + text_, FETCH_STATUS_ID); +} + +void FetchDialog::slotFetchDone(bool checkISBN /* = true */) { +// myDebug() << "FetchDialog::slotFetchDone()" << endl; + m_started = false; + m_searchButton->setGuiItem(KGuiItem(i18n(FETCH_STRING_SEARCH), + SmallIconSet(QString::fromLatin1("find")))); + stopProgress(); + if(m_resultCount == 0) { + slotStatus(i18n("The search returned no items.")); + } else { + /* TRANSLATORS: This is a plural form, you need to translate both lines (except "_n: ") */ + slotStatus(i18n("The search returned 1 item.", + "The search returned %n items.", + m_resultCount)); + } + m_moreButton->setEnabled(Fetch::Manager::self()->hasMoreResults()); + + // if we're not checking isbn values, then, ok to return + if(!checkISBN) { + return; + } + + const Fetch::FetchKey key = static_cast<Fetch::FetchKey>(m_keyCombo->currentData().toInt()); + // no way to currently check EAN/UPC values for non-book items + if(m_collType & (Data::Collection::Book | Data::Collection::Bibtex) && + m_multipleISBN->isChecked() && + (key == Fetch::ISBN || key == Fetch::UPC)) { + QStringList values = QStringList::split(QString::fromLatin1("; "), + m_oldSearch.simplifyWhiteSpace()); + for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { + *it = ISBNValidator::cleanValue(*it); + } + for(QListViewItemIterator it(m_listView); it.current(); ++it) { + QString i = ISBNValidator::cleanValue(static_cast<SearchResultItem*>(it.current())->m_result->isbn); + values.remove(i); + if(i.length() > 10 && i.startsWith(QString::fromLatin1("978"))) { + values.remove(ISBNValidator::cleanValue(ISBNValidator::isbn10(i))); + } + } + if(!values.isEmpty()) { + for(QStringList::Iterator it = values.begin(); it != values.end(); ++it) { + ISBNValidator::staticFixup(*it); + } + // TODO dialog caption + KDialogBase* dlg = new KDialogBase(this, "isbn not found dialog", false, QString::null, KDialogBase::Ok); + QWidget* box = new QWidget(dlg); + QVBoxLayout* lay = new QVBoxLayout(box, KDialog::marginHint(), KDialog::spacingHint()*2); + QHBoxLayout* lay2 = new QHBoxLayout(lay); + QLabel* lab = new QLabel(box); + lab->setPixmap(KGlobal::iconLoader()->loadIcon(QString::fromLatin1("messagebox_info"), + KIcon::NoGroup, + KIcon::SizeMedium)); + lay2->addWidget(lab); + QString s = i18n("No results were found for the following ISBN values:"); + lay2->addWidget(new QLabel(s, box), 10); + KTextEdit* edit = new KTextEdit(box, "isbn list edit"); + lay->addWidget(edit); + edit->setText(values.join(QChar('\n'))); + QWhatsThis::add(edit, s); + connect(dlg, SIGNAL(okClicked()), dlg, SLOT(deleteLater())); + dlg->setMainWidget(box); + dlg->setMinimumWidth(QMAX(dlg->minimumWidth(), FETCH_MIN_WIDTH*2/3)); + dlg->show(); + } + } +} + +void FetchDialog::slotResultFound(Tellico::Fetch::SearchResult* result_) { + m_results.append(result_); + (void) new SearchResultItem(m_listView, result_); + ++m_resultCount; + adjustColumnWidth(); + kapp->processEvents(); +} + +void FetchDialog::slotAddEntry() { + GUI::CursorSaver cs; + Data::EntryVec vec; + for(QListViewItemIterator it(m_listView, QListViewItemIterator::Selected); it.current(); ++it) { + SearchResultItem* item = static_cast<SearchResultItem*>(it.current()); + + Fetch::SearchResult* r = item->m_result; + Data::EntryPtr entry = m_entries[r->uid]; + if(!entry) { + setStatus(i18n("Fetching %1...").arg(r->title)); + startProgress(); + entry = r->fetchEntry(); + if(!entry) { + continue; + } + m_entries.insert(r->uid, entry); + stopProgress(); + setStatus(i18n("Ready.")); + } + // add a copy, intentionally allowing multiple copies to be added + vec.append(new Data::Entry(*entry)); + item->setPixmap(0, UserIcon(QString::fromLatin1("checkmark"))); + } + if(!vec.isEmpty()) { + Kernel::self()->addEntries(vec, true); + } +} + +void FetchDialog::slotMoreClicked() { + if(m_started) { + myDebug() << "FetchDialog::slotMoreClicked() - can't continue while running" << endl; + return; + } + + m_started = true; + m_searchButton->setGuiItem(KGuiItem(i18n(FETCH_STRING_STOP), + SmallIconSet(QString::fromLatin1("cancel")))); + startProgress(); + setStatus(i18n("Searching...")); + kapp->processEvents(); + Fetch::Manager::self()->continueSearch(); +} + +void FetchDialog::slotShowEntry() { + // just in case + m_statusMessages.clear(); + + const GUI::ListViewItemList& items = m_listView->selectedItems(); + if(items.isEmpty()) { + m_addButton->setEnabled(false); + return; + } + + m_addButton->setEnabled(true); + if(items.count() > 1) { + m_entryView->clear(); + return; + } + + SearchResultItem* item = static_cast<SearchResultItem*>(items.getFirst()); + Fetch::SearchResult* r = item->m_result; + setStatus(i18n("Fetching %1...").arg(r->title)); + Data::EntryPtr entry = m_entries[r->uid]; + if(!entry) { + GUI::CursorSaver cs; + startProgress(); + entry = r->fetchEntry(); + if(entry) { // might conceivably be null + m_entries.insert(r->uid, entry); + } + stopProgress(); + } + setStatus(i18n("Ready.")); + + m_entryView->showEntry(entry); +} + +void FetchDialog::startProgress() { + m_progress->show(); + m_timer->start(100); +} + +void FetchDialog::slotMoveProgress() { + m_progress->setProgress(m_progress->progress()+5); +} + +void FetchDialog::stopProgress() { + m_timer->stop(); + m_progress->hide(); +} + +void FetchDialog::slotInit() { + if(!Fetch::Manager::self()->canFetch()) { + m_searchButton->setEnabled(false); + Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this); + } + + KConfigGroup config(kapp->config(), "Fetch Dialog Options"); + int key = config.readNumEntry("Search Key", Fetch::FetchFirst); + // only change key if valid + if(key > Fetch::FetchFirst) { + m_keyCombo->setCurrentData(key); + } + slotKeyChanged(m_keyCombo->currentItem()); + + QString source = config.readEntry("Search Source"); + if(!source.isEmpty()) { + m_sourceCombo->setCurrentItem(source); + } + slotSourceChanged(m_sourceCombo->currentText()); + + m_valueLineEdit->setFocus(); + m_searchButton->setDefault(true); +} + +void FetchDialog::slotKeyChanged(int idx_) { + int key = m_keyCombo->data(idx_).toInt(); + if(key == Fetch::ISBN || key == Fetch::UPC || key == Fetch::LCCN) { + m_multipleISBN->setEnabled(true); + if(key == Fetch::ISBN) { + m_valueLineEdit->setValidator(new ISBNValidator(this)); + } else { + UPCValidator* upc = new UPCValidator(this); + connect(upc, SIGNAL(signalISBN()), SLOT(slotUPC2ISBN())); + m_valueLineEdit->setValidator(upc); + // only want to convert to ISBN if ISBN is accepted by the fetcher + Fetch::KeyMap map = Fetch::Manager::self()->keyMap(m_sourceCombo->currentText()); + upc->setCheckISBN(map.contains(Fetch::ISBN)); + } + } else { + m_multipleISBN->setChecked(false); + m_multipleISBN->setEnabled(false); +// slotMultipleISBN(false); + m_valueLineEdit->setValidator(0); + } +} + +void FetchDialog::slotSourceChanged(const QString& source_) { + int curr = m_keyCombo->currentData().toInt(); + m_keyCombo->clear(); + Fetch::KeyMap map = Fetch::Manager::self()->keyMap(source_); + for(Fetch::KeyMap::ConstIterator it = map.begin(); it != map.end(); ++it) { + m_keyCombo->insertItem(it.data(), it.key()); + } + m_keyCombo->setCurrentData(curr); + slotKeyChanged(m_keyCombo->currentItem()); +} + +void FetchDialog::slotMultipleISBN(bool toggle_) { + bool wasEnabled = m_valueLineEdit->isEnabled(); + m_valueLineEdit->setEnabled(!toggle_); + if(!wasEnabled && m_valueLineEdit->isEnabled()) { + // if we enable it, it probably had multiple isbn values + // the validator doesn't like that, so only keep the first value + QString val = m_valueLineEdit->text().section(';', 0, 0); + m_valueLineEdit->setText(val); + } + m_editISBN->setEnabled(toggle_); +} + +void FetchDialog::slotEditMultipleISBN() { + KDialogBase dlg(this, "isbn edit dialog", true, i18n("Edit ISBN/UPC Values"), + KDialogBase::Ok|KDialogBase::Cancel); + QVBox* box = new QVBox(&dlg); + box->setSpacing(10); + QString s = i18n("<qt>Enter the ISBN or UPC values, one per line.</qt>"); + (void) new QLabel(s, box); + m_isbnTextEdit = new KTextEdit(box, "isbn text edit"); + m_isbnTextEdit->setText(m_isbnList.join(QChar('\n'))); + QWhatsThis::add(m_isbnTextEdit, s); + KPushButton* fromFileBtn = new KPushButton(SmallIconSet(QString::fromLatin1("fileopen")), + i18n("&Load From File..."), box); + QWhatsThis::add(fromFileBtn, i18n("<qt>Load the list from a text file.</qt>")); + connect(fromFileBtn, SIGNAL(clicked()), SLOT(slotLoadISBNList())); + dlg.setMainWidget(box); + dlg.setMinimumWidth(QMAX(dlg.minimumWidth(), FETCH_MIN_WIDTH*2/3)); + + if(dlg.exec() == QDialog::Accepted) { + m_isbnList = QStringList::split('\n', m_isbnTextEdit->text()); + const QValidator* val = m_valueLineEdit->validator(); + if(val) { + for(QStringList::Iterator it = m_isbnList.begin(); it != m_isbnList.end(); ++it) { + val->fixup(*it); + if((*it).isEmpty()) { + it = m_isbnList.remove(it); + // this is next item, shift backward + --it; + } + } + } + if(m_isbnList.count() > 100) { + Kernel::self()->sorry(i18n("<qt>An ISBN search can contain a maximum of 100 ISBN values. Only the " + "first 100 values in your list will be used.</qt>"), this); + } + while(m_isbnList.count() > 100) { + m_isbnList.pop_back(); + } + m_valueLineEdit->setText(m_isbnList.join(QString::fromLatin1("; "))); + } + m_isbnTextEdit = 0; // gets auto-deleted +} + +void FetchDialog::slotLoadISBNList() { + if(!m_isbnTextEdit) { + return; + } + KURL u = KFileDialog::getOpenURL(QString::null, QString::null, this); + if(u.isValid()) { + m_isbnTextEdit->setText(m_isbnTextEdit->text() + FileHandler::readTextFile(u)); + m_isbnTextEdit->moveCursor(QTextEdit::MoveEnd, false); + m_isbnTextEdit->scrollToBottom(); + } +} + +void FetchDialog::slotUPC2ISBN() { + int key = m_keyCombo->currentData().toInt(); + if(key == Fetch::UPC) { + m_keyCombo->setCurrentData(Fetch::ISBN); + slotKeyChanged(m_keyCombo->currentItem()); + } +} + +bool FetchDialog::eventFilter(QObject* obj_, QEvent* ev_) { + if(obj_ == m_listView->viewport() && ev_->type() == QEvent::Resize) { + adjustColumnWidth(); + } + return false; +} + +void FetchDialog::adjustColumnWidth() { + if(!m_listView || m_listView->childCount() == 0) { + return; + } + + int sum1 = 0; + int sum2 = 0; + int w3 = 0; + for(QListViewItemIterator it(m_listView); it.current(); ++it) { + sum1 += it.current()->width(m_listView->fontMetrics(), m_listView, 1); + sum2 += it.current()->width(m_listView->fontMetrics(), m_listView, 2); + w3 = QMAX(w3, it.current()->width(m_listView->fontMetrics(), m_listView, 3)); + } + // try to be smart about column width + // column 0 is fixed, the checkmark icon, give it 20 + const int w0 = 20; + // column 3 is the source, say max is 25% of viewport + const int vw = m_listView->visibleWidth(); + w3 = static_cast<int>(QMIN(1.1 * w3, 0.25 * vw)); + // scale averages of col 1 and col 2 + const int avg1 = sum1 / m_listView->childCount(); + const int avg2 = sum2 / m_listView->childCount(); + const int w2 = (vw - w0 - w3) * avg2 / (avg1 + avg2); + const int w1 = vw - w0 - w3 - w2; + m_listView->setColumnWidth(1, w1); + m_listView->setColumnWidth(2, w2); + m_listView->setColumnWidth(3, w3); +} + +void FetchDialog::slotResetCollection() { + if(m_collType == Kernel::self()->collectionType()) { + return; + } + m_collType = Kernel::self()->collectionType(); + m_sourceCombo->clear(); + Fetch::FetcherVec sources = Fetch::Manager::self()->fetchers(m_collType); + for(Fetch::FetcherVec::Iterator it = sources.begin(); it != sources.end(); ++it) { + m_sourceCombo->insertItem(Fetch::Manager::self()->fetcherIcon(it.data()), (*it).source()); + } + + m_addButton->setIconSet(UserIconSet(Kernel::self()->collectionTypeName())); + + if(Fetch::Manager::self()->canFetch()) { + m_searchButton->setEnabled(true); + } else { + m_searchButton->setEnabled(false); + Kernel::self()->sorry(i18n("No Internet sources are available for your current collection type."), this); + } +} + +void FetchDialog::slotBarcodeRecognized( QString string ) +{ + // attention: this slot is called in the context of another thread => do not use GUI-functions! + QCustomEvent *e = new QCustomEvent( QEvent::User ); + QString *data = new QString( string ); + e->setData( data ); + qApp->postEvent( this, e ); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread +} + +void FetchDialog::slotBarcodeGotImage( QImage &img ) +{ + // attention: this slot is called in the context of another thread => do not use GUI-functions! + QCustomEvent *e = new QCustomEvent( QEvent::User+1 ); + QImage *data = new QImage( img.copy() ); + e->setData( data ); + qApp->postEvent( this, e ); // the event loop will call FetchDialog::customEvent() in the context of the GUI thread +} + +void FetchDialog::customEvent( QCustomEvent *e ) +{ + if (!e) + return; + if ((e->type() == QEvent::User) && e->data()) { + // slotBarcodeRecognized() queued call + QString temp = *(QString*)(e->data()); + delete (QString*)(e->data()); + qApp->beep(); + m_valueLineEdit->setText( temp ); + m_searchButton->animateClick(); + } + if ((e->type() == QEvent::User+1) && e->data()) { + // slotBarcodegotImage() queued call + QImage temp = *(QImage*)(e->data()); + delete (QImage*)(e->data()); + m_barcodePreview->setPixmap( temp ); + } +} + +#include "fetchdialog.moc" diff --git a/src/fetchdialog.h b/src/fetchdialog.h new file mode 100644 index 0000000..eef4bdc --- /dev/null +++ b/src/fetchdialog.h @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FETCHDIALOG_H +#define FETCHDIALOG_H + +#include "datavectors.h" + +#include <kdialogbase.h> + +#include <qguardedptr.h> + +namespace Tellico { + class EntryView; + namespace Fetch { + class Fetcher; + class SearchResult; + } + namespace GUI { + class ComboBox; + class ListView; + } +} + +namespace barcodeRecognition { + class barcodeRecognitionThread; +} + +class KComboBox; +class KLineEdit; +class KPushButton; +class KStatusBar; +class KTextEdit; +class QProgressBar; +class QTimer; +class QCheckBox; + + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class FetchDialog : public KDialogBase { +Q_OBJECT + +public: + /** + * Constructor + */ + FetchDialog(QWidget* parent, const char* name = 0); + ~FetchDialog(); + +public slots: + void slotResetCollection(); + +protected: + bool eventFilter(QObject* obj, QEvent* ev); + +private slots: + void slotSearchClicked(); + void slotClearClicked(); + void slotAddEntry(); + void slotMoreClicked(); + void slotShowEntry(); + void slotMoveProgress(); + + void slotStatus(const QString& status); + void slotUpdateStatus(); + + void slotFetchDone(bool checkISBN = true); + void slotResultFound(Tellico::Fetch::SearchResult* result); + void slotKeyChanged(int); + void slotSourceChanged(const QString& source); + void slotMultipleISBN(bool toggle); + void slotEditMultipleISBN(); + void slotInit(); + void slotLoadISBNList(); + void slotUPC2ISBN(); + + void slotBarcodeRecognized(QString); + void slotBarcodeGotImage(QImage&); +private: + void startProgress(); + void stopProgress(); + void setStatus(const QString& text); + void adjustColumnWidth(); + + void customEvent( QCustomEvent *e ); + + class SearchResultItem; + + KComboBox* m_sourceCombo; + GUI::ComboBox* m_keyCombo; + KLineEdit* m_valueLineEdit; + KPushButton* m_searchButton; + QCheckBox* m_multipleISBN; + KPushButton* m_editISBN; + GUI::ListView* m_listView; + EntryView* m_entryView; + KPushButton* m_addButton; + KPushButton* m_moreButton; + KStatusBar* m_statusBar; + QProgressBar* m_progress; + QTimer* m_timer; + QGuardedPtr<KTextEdit> m_isbnTextEdit; + QLabel *m_barcodePreview; + + bool m_started; + int m_resultCount; + QString m_oldSearch; + QStringList m_isbnList; + QStringList m_statusMessages; + QMap<int, Data::EntryPtr> m_entries; + QPtrList<Fetch::SearchResult> m_results; + int m_collType; + + barcodeRecognition::barcodeRecognitionThread *m_barcodeRecognitionThread; +}; + +} //end namespace + +#endif diff --git a/src/fetcherconfigdialog.cpp b/src/fetcherconfigdialog.cpp new file mode 100644 index 0000000..99d4bbe --- /dev/null +++ b/src/fetcherconfigdialog.cpp @@ -0,0 +1,225 @@ +/*************************************************************************** + copyright : (C) 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 "fetcherconfigdialog.h" +#include "fetch/fetchmanager.h" +#include "gui/combobox.h" + +#include <klocale.h> +#include <klineedit.h> +#include <kcombobox.h> +#include <kiconloader.h> + +#include <qlabel.h> +#include <qlayout.h> +#include <qhgroupbox.h> +#include <qwidgetstack.h> +#include <qwhatsthis.h> +#include <qhbox.h> +#include <qvgroupbox.h> +#include <qcheckbox.h> + +namespace { + static const int FETCHER_CONFIG_MIN_WIDTH = 600; +} + +using Tellico::FetcherConfigDialog; + +FetcherConfigDialog::FetcherConfigDialog(QWidget* parent_) + : KDialogBase(parent_, "fetcher dialog", true, i18n("Data Source Properties"), + KDialogBase::Ok | KDialogBase::Cancel | KDialogBase::Help) + , m_newSource(true) + , m_useDefaultName(true) + , m_configWidget(0) { + init(Fetch::Unknown); +} + +FetcherConfigDialog::FetcherConfigDialog(const QString& sourceName_, Fetch::Type type_, bool updateOverwrite_, + Fetch::ConfigWidget* configWidget_, QWidget* parent_) + : KDialogBase(parent_, "fetcher dialog", true, i18n("Data Source Properties"), + KDialogBase::Ok | KDialogBase::Cancel | KDialogBase::Help) + , m_newSource(false) + , m_useDefaultName(false) + , m_configWidget(configWidget_) { + init(type_); + m_nameEdit->setText(sourceName_); + m_cbOverwrite->setChecked(updateOverwrite_); +} + +void FetcherConfigDialog::init(Fetch::Type type_) { + setMinimumWidth(FETCHER_CONFIG_MIN_WIDTH); + setHelp(QString::fromLatin1("data-sources-options")); + + QWidget* widget = new QWidget(this); + QBoxLayout* topLayout = new QHBoxLayout(widget, KDialog::spacingHint()); + + QBoxLayout* vlay1 = new QVBoxLayout(topLayout, KDialog::spacingHint()); + m_iconLabel = new QLabel(widget); + if(type_ == Fetch::Unknown) { + m_iconLabel->setPixmap(KGlobal::iconLoader()->loadIcon(QString::fromLatin1("network"), KIcon::Panel, 64)); + } else { + m_iconLabel->setPixmap(Fetch::Manager::self()->fetcherIcon(type_, KIcon::Panel, 64)); + } + vlay1->addWidget(m_iconLabel); + vlay1->addStretch(1); + + QBoxLayout* vlay2 = new QVBoxLayout(topLayout, KDialog::spacingHint()); + + QGridLayout* gl = new QGridLayout(vlay2, 2, 2, KDialog::spacingHint()); + int row = -1; + + QLabel* label = new QLabel(i18n("&Source name: "), widget); + gl->addWidget(label, ++row, 0); + QString w = i18n("The name identifies the data source and should be unique and informative."); + QWhatsThis::add(label, w); + + m_nameEdit = new KLineEdit(widget); + gl->addWidget(m_nameEdit, row, 1); + m_nameEdit->setFocus(); + QWhatsThis::add(m_nameEdit, w); + label->setBuddy(m_nameEdit); + connect(m_nameEdit, SIGNAL(textChanged(const QString&)), SLOT(slotNameChanged(const QString&))); + + if(m_newSource) { + label = new QLabel(i18n("Source &type: "), widget); + } else { + // since the label doesn't have a buddy, we don't want an accel, + // but also want to reuse string we already have + label = new QLabel(i18n("Source &type: ").remove('&'), widget); + } + gl->addWidget(label, ++row, 0); + w = i18n("Tellico supports several different data sources."); + QWhatsThis::add(label, w); + + if(m_newSource) { + m_typeCombo = new GUI::ComboBox(widget); + gl->addWidget(m_typeCombo, row, 1); + QWhatsThis::add(m_typeCombo, w); + label->setBuddy(m_typeCombo); + } else { + m_typeCombo = 0; + QLabel* lab = new QLabel(Fetch::Manager::typeName(type_), widget); + gl->addWidget(lab, row, 1); + QWhatsThis::add(lab, w); + } + m_cbOverwrite = new QCheckBox(i18n("Updating from source should overwrite user data"), widget); + ++row; + gl->addMultiCellWidget(m_cbOverwrite, row, row, 0, 1); + w = i18n("If checked, updating entries will overwrite any existing information."); + QWhatsThis::add(m_cbOverwrite, w); + + if(m_newSource) { + m_stack = new QWidgetStack(widget); + vlay2->addWidget(m_stack); + connect(m_typeCombo, SIGNAL(activated(int)), SLOT(slotNewSourceSelected(int))); + + int z3950_idx = 0; + const Fetch::TypePairList typeList = Fetch::Manager::self()->typeList(); + for(Fetch::TypePairList::ConstIterator it = typeList.begin(); it != typeList.end(); ++it) { + const Fetch::TypePair& type = *it; + m_typeCombo->insertItem(type.index(), type.value()); + if(type.value() == Fetch::Z3950) { + z3950_idx = m_typeCombo->count()-1; + } + } + // make sure first widget gets initialized + // I'd like it to be the z39.50 widget + m_typeCombo->setCurrentItem(z3950_idx); + slotNewSourceSelected(z3950_idx); + } else { + m_stack = 0; + // just add config widget and reparent + m_configWidget->reparent(widget, QPoint()); + vlay2->addWidget(m_configWidget); + connect(m_configWidget, SIGNAL(signalName(const QString&)), SLOT(slotPossibleNewName(const QString&))); + } + + setMainWidget(widget); +} + +QString FetcherConfigDialog::sourceName() const { + return m_nameEdit->text(); +} + +bool FetcherConfigDialog::updateOverwrite() const { + return m_cbOverwrite->isChecked(); +} + +Tellico::Fetch::ConfigWidget* FetcherConfigDialog::configWidget() const { + if(m_newSource) { + return dynamic_cast<Fetch::ConfigWidget*>(m_stack->visibleWidget()); + } + kdWarning() << "FetcherConfigDialog::configWidget() called for modifying existing fetcher!" << endl; + return m_configWidget; +} + +Tellico::Fetch::Type FetcherConfigDialog::sourceType() const { + if(!m_newSource || m_typeCombo->count() == 0) { + kdWarning() << "FetcherConfigDialog::sourceType() called for modifying existing fetcher!" << endl; + return Fetch::Unknown; + } + return static_cast<Fetch::Type>(m_typeCombo->currentData().toInt()); +} + +void FetcherConfigDialog::slotNewSourceSelected(int idx_) { + if(!m_newSource) { + return; + } + + // always change to default name + m_useDefaultName = true; + + Fetch::ConfigWidget* cw = m_configWidgets[idx_]; + if(cw) { + m_stack->raiseWidget(cw); + slotPossibleNewName(cw->preferredName()); + return; + } + + Fetch::Type type = sourceType(); + if(type == Fetch::Unknown) { + kdWarning() << "FetcherConfigDialog::slotNewSourceSelected() - unknown source type" << endl; + return; + } + m_iconLabel->setPixmap(Fetch::Manager::self()->fetcherIcon(type, KIcon::Panel, 64)); + cw = Fetch::Manager::self()->configWidget(m_stack, type, m_typeCombo->currentText()); + if(!cw) { + // bad bad bad! + kdWarning() << "FetcherConfigDialog::slotNewSourceSelected() - no config widget found for type " << type << endl; + m_typeCombo->setCurrentItem(0); + slotNewSourceSelected(0); + return; + } + connect(cw, SIGNAL(signalName(const QString&)), SLOT(slotPossibleNewName(const QString&))); + m_configWidgets.insert(idx_, cw); + m_stack->addWidget(cw); + m_stack->raiseWidget(cw); + slotPossibleNewName(cw->preferredName()); +} + +void FetcherConfigDialog::slotNameChanged(const QString&) { + m_useDefaultName = false; +} + +void FetcherConfigDialog::slotPossibleNewName(const QString& name_) { + if(name_.isEmpty()) { + return; + } + Fetch::ConfigWidget* cw = m_stack ? static_cast<Fetch::ConfigWidget*>(m_stack->visibleWidget()) : m_configWidget; + if(m_useDefaultName || (cw && m_nameEdit->text() == cw->preferredName())) { + m_nameEdit->setText(name_); + m_useDefaultName = true; // it gets reset in slotNameChanged() + } +} + +#include "fetcherconfigdialog.moc" diff --git a/src/fetcherconfigdialog.h b/src/fetcherconfigdialog.h new file mode 100644 index 0000000..6384102 --- /dev/null +++ b/src/fetcherconfigdialog.h @@ -0,0 +1,69 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FETCHERCONFIGDIALOG_H +#define TELLICO_FETCHERCONFIGDIALOG_H + +#include "fetch/fetch.h" +#include "fetch/configwidget.h" + +#include <kdialogbase.h> + +#include <qintdict.h> + +class KLineEdit; +class QCheckBox; +class QWidgetStack; + +namespace Tellico { + namespace GUI { + class ComboBox; + } + +/** + * @author Robby Stephenson + */ +class FetcherConfigDialog : public KDialogBase { +Q_OBJECT + +public: + FetcherConfigDialog(QWidget* parent); + FetcherConfigDialog(const QString& sourceName, Fetch::Type type, bool updateOverwrite, + Fetch::ConfigWidget* configWidget, QWidget* parent); + + QString sourceName() const; + Fetch::Type sourceType() const; + bool updateOverwrite() const; + Fetch::ConfigWidget* configWidget() const; + +private slots: + void slotNewSourceSelected(int idx); + void slotNameChanged(const QString& name); + void slotPossibleNewName(const QString& name); + +private: + void init(Fetch::Type type); + + bool m_newSource : 1; + bool m_useDefaultName : 1; + Fetch::ConfigWidget* m_configWidget; + QLabel* m_iconLabel; + KLineEdit* m_nameEdit; + GUI::ComboBox* m_typeCombo; + QCheckBox* m_cbOverwrite; + QWidgetStack* m_stack; + QIntDict<Fetch::ConfigWidget> m_configWidgets; +}; + +} // end namespace +#endif diff --git a/src/field.cpp b/src/field.cpp new file mode 100644 index 0000000..206062a --- /dev/null +++ b/src/field.cpp @@ -0,0 +1,594 @@ +/*************************************************************************** + 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 "field.h" +#include "tellico_utils.h" +#include "latin1literal.h" +#include "tellico_debug.h" +#include "core/tellico_config.h" +#include "collection.h" + +#include <klocale.h> +#include <kglobal.h> + +#include <qstringlist.h> +#include <qregexp.h> +#include <qdatetime.h> + +namespace { + static const QRegExp comma_split = QRegExp(QString::fromLatin1("\\s*,\\s*")); +} + +using Tellico::Data::Field; + +//these get overwritten, but are here since they're static +QStringList Field::s_articlesApos; +QRegExp Field::s_delimiter = QRegExp(QString::fromLatin1("\\s*;\\s*")); + +// this constructor is for anything but Choice type +Field::Field(const QString& name_, const QString& title_, Type type_/*=Line*/) + : KShared(), m_name(name_), m_title(title_), m_category(i18n("General")), m_desc(title_), + m_type(type_), m_flags(0), m_formatFlag(FormatNone) { + +#ifndef NDEBUG + if(m_type == Choice) { + kdWarning() << "Field() - A different constructor should be called for multiple choice attributes." << endl; + kdWarning() << "Constructing a Field with name = " << name_ << endl; + } +#endif + // a paragraph's category is always its title, along with tables + if(isSingleCategory()) { + m_category = m_title; + } + if(m_type == Table || m_type == Table2) { + m_flags = AllowMultiple; + if(m_type == Table2) { + m_type = Table; + setProperty(QString::fromLatin1("columns"), QChar('2')); + } else { + setProperty(QString::fromLatin1("columns"), QChar('1')); + } + } else if(m_type == Date) { // hidden from user + m_formatFlag = FormatDate; + } else if(m_type == Rating) { + setProperty(QString::fromLatin1("minimum"), QChar('1')); + setProperty(QString::fromLatin1("maximum"), QChar('5')); + } + m_id = getID(); +} + +// if this constructor is called, the type is necessarily Choice +Field::Field(const QString& name_, const QString& title_, const QStringList& allowed_) + : KShared(), m_name(name_), m_title(title_), m_category(i18n("General")), m_desc(title_), + m_type(Field::Choice), m_allowed(allowed_), m_flags(0), m_formatFlag(FormatNone) { + m_id = getID(); +} + +Field::Field(const Field& field_) + : KShared(field_), m_name(field_.name()), m_title(field_.title()), m_category(field_.category()), + m_desc(field_.description()), m_type(field_.type()), + m_flags(field_.flags()), m_formatFlag(field_.formatFlag()), + m_properties(field_.propertyList()) { + if(m_type == Choice) { + m_allowed = field_.allowed(); + } else if(m_type == Table2) { + m_type = Table; + setProperty(QString::fromLatin1("columns"), QChar('2')); + } + m_id = getID(); +} + +Field& Field::operator=(const Field& field_) { + if(this == &field_) return *this; + + static_cast<KShared&>(*this) = static_cast<const KShared&>(field_); + m_name = field_.name(); + m_title = field_.title(); + m_category = field_.category(); + m_desc = field_.description(); + m_type = field_.type(); + if(m_type == Choice) { + m_allowed = field_.allowed(); + } else if(m_type == Table2) { + m_type = Table; + setProperty(QString::fromLatin1("columns"), QChar('2')); + } + m_flags = field_.flags(); + m_formatFlag = field_.formatFlag(); + m_properties = field_.propertyList(); + m_id = getID(); + return *this; +} + +Field::~Field() { +} + +void Field::setTitle(const QString& title_) { + m_title = title_; + if(isSingleCategory()) { + m_category = title_; + } +} + +void Field::setType(Field::Type type_) { + m_type = type_; + if(m_type != Field::Choice) { + m_allowed = QStringList(); + } + if(m_type == Table || m_type == Table2) { + m_flags |= AllowMultiple; + if(m_type == Table2) { + m_type = Table; + setProperty(QString::fromLatin1("columns"), QChar('2')); + } + if(property(QString::fromLatin1("columns")).isEmpty()) { + setProperty(QString::fromLatin1("columns"), QChar('1')); + } + } + if(isSingleCategory()) { + m_category = m_title; + } + // hidden from user + if(type_ == Date) { + m_formatFlag = FormatDate; + } +} + +void Field::setCategory(const QString& category_) { + if(!isSingleCategory()) { + m_category = category_; + } +} + +void Field::setFlags(int flags_) { + // tables always have multiple allowed + if(m_type == Table || m_type == Table2) { + m_flags = AllowMultiple | flags_; + } else { + m_flags = flags_; + } +} + +void Field::setFormatFlag(FormatFlag flag_) { + // Choice and Data fields are not allowed a format + if(m_type != Choice && m_type != Date) { + m_formatFlag = flag_; + } +} + +const QString& Field::defaultValue() const { + return property(QString::fromLatin1("default")); +} + +void Field::setDefaultValue(const QString& value_) { + if(m_type != Choice || m_allowed.findIndex(value_) > -1) { + setProperty(QString::fromLatin1("default"), value_); + } +} + +bool Field::isSingleCategory() const { + return (m_type == Para || m_type == Table || m_type == Table2 || m_type == Image); +} + +// format is something like "%{year} %{author}" +Tellico::Data::FieldVec Field::dependsOn(CollPtr coll_) const { + FieldVec vec; + if(m_type != Dependent || !coll_) { + return vec; + } + + const QStringList fieldNames = dependsOn(); + // do NOT call recursively! + for(QStringList::ConstIterator it = fieldNames.begin(); it != fieldNames.end(); ++it) { + FieldPtr field = coll_->fieldByName(*it); + if(!field) { + // allow the user to also use field titles + field = coll_->fieldByTitle(*it); + } + if(field) { + vec.append(field); + } + } + return vec; +} + +QStringList Field::dependsOn() const { + QStringList list; + if(m_type != Dependent) { + return list; + } + + QRegExp rx(QString::fromLatin1("%\\{(.+)\\}")); + rx.setMinimal(true); + // do NOT call recursively! + for(int pos = m_desc.find(rx); pos > -1; pos = m_desc.find(rx, pos+3)) { + list << rx.cap(1); + } + return list; +} + +QString Field::format(const QString& value_, FormatFlag flag_) { + if(value_.isEmpty()) { + return value_; + } + + QString text; + switch(flag_) { + case FormatTitle: + text = formatTitle(value_); + break; + case FormatName: + text = formatName(value_); + break; + case FormatDate: + text = formatDate(value_); + break; + case FormatPlain: + text = Config::autoCapitalization() ? capitalize(value_) : value_; + break; + default: + text = value_; + break; + } + return text; +} + +QString Field::formatTitle(const QString& title_) { + QString newTitle = title_; + // special case for multi-column tables, assume user never has '::' in a value + const QString colonColon = QString::fromLatin1("::"); + QString tail; + if(newTitle.find(colonColon) > -1) { + tail = colonColon + newTitle.section(colonColon, 1); + newTitle = newTitle.section(colonColon, 0, 0); + } + + if(Config::autoCapitalization()) { + newTitle = capitalize(newTitle); + } + + if(Config::autoFormat()) { + const QString lower = newTitle.lower(); + // TODO if the title has ",the" at the end, put it at the front + const QStringList& articles = Config::articleList(); + for(QStringList::ConstIterator it = articles.begin(); it != articles.end(); ++it) { + // assume white space is already stripped + // the articles are already in lower-case + if(lower.startsWith(*it + QChar(' '))) { + QRegExp regexp(QChar('^') + QRegExp::escape(*it) + QString::fromLatin1("\\s*"), false); + // can't just use *it since it's in lower-case + QString article = newTitle.left((*it).length()); + newTitle = newTitle.replace(regexp, QString::null) + .append(QString::fromLatin1(", ")) + .append(article); + break; + } + } + } + + // also, arbitrarily impose rule that a space must follow every comma + newTitle.replace(comma_split, QString::fromLatin1(", ")); + return newTitle + tail; +} + +QString Field::formatName(const QString& name_, bool multiple_/*=true*/) { + static const QRegExp spaceComma(QString::fromLatin1("[\\s,]")); + static const QString colonColon = QString::fromLatin1("::"); + // the ending look-ahead is so that a space is not added at the end + static const QRegExp periodSpace(QString::fromLatin1("\\.\\s*(?=.)")); + + QStringList entries; + if(multiple_) { + // split by semi-colon, optionally preceded or followed by white spacee + entries = QStringList::split(s_delimiter, name_, false); + } else { + entries << name_; + } + + QRegExp lastWord; + lastWord.setCaseSensitive(false); + + QStringList names; + for(QStringList::ConstIterator it = entries.begin(); it != entries.end(); ++it) { + QString name = *it; + // special case for 2-column tables, assume user never has '::' in a value + QString tail; + if(name.find(colonColon) > -1) { + tail = colonColon + name.section(colonColon, 1); + name = name.section(colonColon, 0, 0); + } + name.replace(periodSpace, QString::fromLatin1(". ")); + if(Config::autoCapitalization()) { + name = capitalize(name); + } + + // split the name by white space and commas + QStringList words = QStringList::split(spaceComma, name, false); + lastWord.setPattern(QChar('^') + QRegExp::escape(words.last()) + QChar('$')); + + // if it contains a comma already and the last word is not a suffix, don't format it + if(!Config::autoFormat() || (name.find(',') > -1 && Config::nameSuffixList().grep(lastWord).isEmpty())) { + // arbitrarily impose rule that no spaces before a comma and + // a single space after every comma + name.replace(comma_split, QString::fromLatin1(", ")); + names << name + tail; + continue; + } + // otherwise split it by white space, move the last word to the front + // but only if there is more than one word + if(words.count() > 1) { + // if the last word is a suffix, it has to be kept with last name + if(Config::nameSuffixList().grep(lastWord).count() > 0) { + words.prepend(words.last().append(QChar(','))); + words.remove(words.fromLast()); + } + + // now move the word + // adding comma here when there had been a suffix is because it was originally split with space or comma + words.prepend(words.last().append(QChar(','))); + words.remove(words.fromLast()); + + // update last word regexp + lastWord.setPattern(QChar('^') + QRegExp::escape(words.last()) + QChar('$')); + + // this is probably just something for me, limited to english + while(Config::surnamePrefixList().grep(lastWord).count() > 0) { + words.prepend(words.last()); + words.remove(words.fromLast()); + lastWord.setPattern(QChar('^') + QRegExp::escape(words.last()) + QChar('$')); + } + + names << words.join(QChar(' ')) + tail; + } else { + names << name + tail; + } + } + + return names.join(QString::fromLatin1("; ")); +} + +QString Field::formatDate(const QString& date_) { + // internally, this is "year-month-day" + // any of the three may be empty + // if they're not digits, return the original string + bool empty = true; + // for empty year, use current + // for empty month or date, use 1 + QStringList s = QStringList::split('-', date_, true); + bool ok = true; + int y = s.count() > 0 ? s[0].toInt(&ok) : QDate::currentDate().year(); + if(ok) { + empty = false; + } else { + y = QDate::currentDate().year(); + } + int m = s.count() > 1 ? s[1].toInt(&ok) : 1; + if(ok) { + empty = false; + } else { + m = 1; + } + int d = s.count() > 2 ? s[2].toInt(&ok) : 1; + if(ok) { + empty = false; + } else { + d = 1; + } + // rather use ISO date formatting than locale formatting for now. Primarily, it makes sorting just work. + return empty ? date_ : QDate(y, m, d).toString(Qt::ISODate); + // use short form +// return KGlobal::locale()->formatDate(date, true); +} + +QString Field::capitalize(QString str_) { + // regexp to split words + static const QRegExp rx(QString::fromLatin1("[-\\s,.;]")); + + if(str_.isEmpty()) { + return str_; + } + // first letter is always capitalized + str_.replace(0, 1, str_.at(0).upper()); + + // special case for french words like l'espace + + int pos = str_.find(rx, 1); + int nextPos; + + QRegExp wordRx; + wordRx.setCaseSensitive(false); + + QStringList notCap = Config::noCapitalizationList(); + // don't capitalize the surname prefixes + // does this hold true everywhere other than english? + notCap += Config::surnamePrefixList(); + + QString word = str_.mid(0, pos); + // now check to see if words starts with apostrophe list + for(QStringList::ConstIterator it = s_articlesApos.begin(); it != s_articlesApos.end(); ++it) { + if(word.lower().startsWith(*it)) { + uint l = (*it).length(); + str_.replace(l, 1, str_.at(l).upper()); + break; + } + } + + while(pos > -1) { + // also need to compare against list of non-capitalized words + nextPos = str_.find(rx, pos+1); + if(nextPos == -1) { + nextPos = str_.length(); + } + word = str_.mid(pos+1, nextPos-pos-1); + bool aposMatch = false; + // now check to see if words starts with apostrophe list + for(QStringList::ConstIterator it = s_articlesApos.begin(); it != s_articlesApos.end(); ++it) { + if(word.lower().startsWith(*it)) { + uint l = (*it).length(); + str_.replace(pos+l+1, 1, str_.at(pos+l+1).upper()); + aposMatch = true; + break; + } + } + + if(!aposMatch) { + wordRx.setPattern(QChar('^') + QRegExp::escape(word) + QChar('$')); + if(notCap.grep(wordRx).isEmpty() && nextPos-pos > 1) { + str_.replace(pos+1, 1, str_.at(pos+1).upper()); + } + } + + pos = str_.find(rx, pos+1); + } + return str_; +} + +QString Field::sortKeyTitle(const QString& title_) { + const QString lower = title_.lower(); + const QStringList& articles = Config::articleList(); + for(QStringList::ConstIterator it = articles.begin(); it != articles.end(); ++it) { + // assume white space is already stripped + // the articles are already in lower-case + if(lower.startsWith(*it + QChar(' '))) { + return title_.mid((*it).length() + 1); + } + } + // check apostrophes, too + for(QStringList::ConstIterator it = s_articlesApos.begin(); it != s_articlesApos.end(); ++it) { + if(lower.startsWith(*it)) { + return title_.mid((*it).length()); + } + } + return title_; +} + +// articles should all be in lower-case +void Field::articlesUpdated() { + const QStringList articles = Config::articleList(); + s_articlesApos.clear(); + for(QStringList::ConstIterator it = articles.begin(); it != articles.end(); ++it) { + if((*it).endsWith(QChar('\''))) { + s_articlesApos += (*it); + } + } +} + +// if these are changed, then CollectionFieldsDialog should be checked since it +// checks for equality against some of these strings +Field::FieldMap Field::typeMap() { + FieldMap map; + map[Field::Line] = i18n("Simple Text"); + map[Field::Para] = i18n("Paragraph"); + map[Field::Choice] = i18n("Choice"); + map[Field::Bool] = i18n("Checkbox"); + map[Field::Number] = i18n("Number"); + map[Field::URL] = i18n("URL"); + map[Field::Table] = i18n("Table"); + map[Field::Image] = i18n("Image"); + map[Field::Dependent] = i18n("Dependent"); +// map[Field::ReadOnly] = i18n("Read Only"); + map[Field::Date] = i18n("Date"); + map[Field::Rating] = i18n("Rating"); + return map; +} + +// just for formatting's sake +QStringList Field::typeTitles() { + const FieldMap& map = typeMap(); + QStringList list; + list.append(map[Field::Line]); + list.append(map[Field::Para]); + list.append(map[Field::Choice]); + list.append(map[Field::Bool]); + list.append(map[Field::Number]); + list.append(map[Field::URL]); + list.append(map[Field::Date]); + list.append(map[Field::Table]); + list.append(map[Field::Image]); + list.append(map[Field::Rating]); + list.append(map[Field::Dependent]); + return list; +} + +QStringList Field::split(const QString& string_, bool allowEmpty_) { + return string_.isEmpty() ? QStringList() : QStringList::split(s_delimiter, string_, allowEmpty_); +} + +void Field::addAllowed(const QString& value_) { + if(m_type != Choice) { + return; + } + if(m_allowed.findIndex(value_) == -1) { + m_allowed += value_; + } +} + +void Field::setProperty(const QString& key_, const QString& value_) { + m_properties.insert(key_, value_); +} + +void Field::setPropertyList(const StringMap& props_) { + m_properties = props_; +} + +void Field::convertOldRating(Data::FieldPtr field_) { + if(field_->type() != Data::Field::Choice) { + return; // nothing to do + } + + if(field_->name() != Latin1Literal("rating") + && field_->property(QString::fromLatin1("rating")) != Latin1Literal("true")) { + return; // nothing to do + } + + int min = 10; + int max = 1; + bool ok; + const QStringList& allow = field_->allowed(); + for(QStringList::ConstIterator it = allow.begin(); it != allow.end(); ++it) { + int n = Tellico::toUInt(*it, &ok); + if(!ok) { + return; // no need to convert + } + min = QMIN(min, n); + max = QMAX(max, n); + } + max = QMIN(max, 10); + if(min >= max) { + min = 1; + max = 5; + } + field_->setProperty(QString::fromLatin1("minimum"), QString::number(min)); + field_->setProperty(QString::fromLatin1("maximum"), QString::number(max)); + field_->setProperty(QString::fromLatin1("rating"), QString::null); + field_->setType(Rating); +} + +// static +long Field::getID() { + static long id = 0; + return ++id; +} + +void Field::stripArticles(QString& value) { + const QStringList articles = Config::articleList(); + if(articles.isEmpty()) { + return; + } + for(QStringList::ConstIterator it = articles.begin(); it != articles.end(); ++it) { + QRegExp rx(QString::fromLatin1("\\b") + *it + QString::fromLatin1("\\b")); + value.remove(rx); + } + value = value.stripWhiteSpace(); + value.remove(QRegExp(QString::fromLatin1(",$"))); +} diff --git a/src/field.h b/src/field.h new file mode 100644 index 0000000..c9f00da --- /dev/null +++ b/src/field.h @@ -0,0 +1,395 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FIELD_H +#define TELLICO_FIELD_H + +#include "datavectors.h" + +#include <qstringlist.h> +#include <qstring.h> +#include <qregexp.h> + +namespace Tellico { + namespace Data { + +/** + * The Field class encapsulates all the possible properties of a entry. + * + * A field can be one of eleven types. It has a name, a title, and a category, + * along with some flags characterizing certain properties + * + * @author Robby Stephenson + */ +class Field : public KShared { +public: + /** + * The possible field types. A Line is represented by a KLineEdit, + * a Para is a QMultiLineEdit encompassing multiple lines, a Choice is + * limited to set values shown in a KComboBox, and a Bool is either true + * or not and is thus a QCheckBox. A Number type is an integer, though it used + * to be a Year. A ReadOnly is a hidden value. A URL is obvious, too. + * A Table looks like a small spreadsheet with one column, and a Table2 + * type has two columns. An Image points to a QImage. A Dependent field + * depends on the values of other attributes. A Date contains a date. + * + * Don't forget to change Field::typeMap(). + * + * @see KLineEdit + * @see QTextEdit + * @see KComboBox + * @see QCheckBox + * @see QTable + **/ + enum Type { + Undef = 0, + Line = 1, + Para = 2, + Choice = 3, + Bool = 4, + ReadOnly = 5, + Number = 6, + URL = 7, + Table = 8, + Table2 = 9, // deprecated in favor of property("columns") + Image = 10, + Dependent = 11, + Date = 12, + // Michael Zimmermann used 13 for his Keyword field, so go ahead and skip it + Rating = 14 // similar to a Choice field, but allowed values are numbers only + // if you add your own field type, best to start at a high number + // say 100, to ensure uniqueness + }; + typedef QMap<Field::Type, QString> FieldMap; + + /** + * The field flags. The properties should be bit-wise OR'd together. + * + * @li AllowCompletion - Include a completion object in the lineedit. + * @li AllowMultiple - Multiple values are allowed in one field and are + * separated by a semi-colon (";"). + * @li AllowGrouped - Entries may be grouped by this field. + * @li NoDelete - The user may not delete this field. + */ + enum FieldFlags { + AllowMultiple = 1 << 0, // allow multiple values, separated by a semi-colon + AllowGrouped = 1 << 1, // this field can be used to group entries + AllowCompletion = 1 << 2, // allow auto-completion + NoDelete = 1 << 3 // don't allow user to delete this field + }; + + /** + * The field formatting flags. + * + * @li FormatTitle - The field should be formatted as a title + * @li FormatName - The field should be formatted as a personal name + * @li FormatDate - The field should be formatted as a date. + * @li FormatPlain - The field only be formatted with capitalization. + * @li FormatNone - The field should not be formatted. + */ + enum FormatFlag { + FormatPlain = 0, // format plain, allows capitalization + FormatTitle = 1, // format as a title, i.e. shift articles to end + FormatName = 2, // format as a personal full name + FormatDate = 3, // format as a date + FormatNone = 4 // no format, i.e. no capitalization allowed + }; + + /** + * The constructor for all types except Choice. The default type is Line. + * By default, the field category is set to "General", and should be modified + * using the @ref setCategory() method. + * + * @param name The field name + * @param title The field title + * @param type The field type + */ + Field(const QString& name, const QString& title, Type type = Line); + /** + * The constructor for Choice types attributes. + * By default, the field category is set to "General", and should be modified + * using the @ref setCategory() method. + * + * @param name The field name + * @param title The field title + * @param allowed The allowed values of the field + */ + Field(const QString& name, const QString& title, const QStringList& allowed); + /** + * The copy constructor + */ + Field(const Field& field); + /** + * The assignment operator + */ + Field& operator=(const Field& field); + /** + * Destructor + */ + ~Field(); + + /** + * Returns the id of the field. + * + * @return The id + */ + long id() const { return m_id; } + /** + * Returns the name of the field. + * + * @return The field name + */ + const QString& name() const { return m_name; } + /** + * Sets the name of the field. This should only be changed before the field is added + * to a collection, i.e. before any entries use it, etc. + * + * @param name The field name + */ + void setName(const QString& name) { m_name = name; } + /** + * Returns the title of the field. + * + * @return The field title + */ + const QString& title() const { return m_title; } + /** + * Sets the title of the field. + * + * @param title The field title + */ + void setTitle(const QString& title); + /** + * Returns the category of the field. + * + * @return The field category + */ + const QString& category() const { return m_category; } + /** + * Sets the category of the field. + * + * @param category The field category + */ + void setCategory(const QString& category); + /** + * Returns the name of the field. + * + * @return The field name + */ + const QStringList& allowed() const { return m_allowed; } + /** + * Sets the allowed values of the field. + * + * @param allowed The allowed values + */ + void setAllowed(const QStringList& allowed) { m_allowed = allowed; } + /** + * Add a value to the allowed list + * + * @param value The value to allow + */ + void addAllowed(const QString& value); + /** + * Returns the type of the field. + * + * @return The field type + */ + Type type() const { return m_type; } + /** + * Sets the type of the field. Be careful with this! + * + * @param type The field type + */ + void setType(Type type); + /** + * Returns the flags for the field. + * + * @return The field flags + */ + int flags() const { return m_flags; } + /** + * Sets the flags of the field. The value is + * set to the argument, so old flags are effectively removed. + * + * @param flags The field flags + */ + void setFlags(int flags); + /** + * Returns the formatting flag for the field. + * + * @return The format flag + */ + FormatFlag formatFlag() const { return m_formatFlag; } + /** + * Sets the formatting flag of the field. + * + * @param flag The field flag + */ + void setFormatFlag(FormatFlag flag); + /** + * Returns the description for the field. + * + * @return The field description + */ + const QString& description() const { return m_desc; } + /** + * Sets the description of the field. + * + * @param desc The field description + */ + void setDescription(const QString& desc) { m_desc = desc; } + /** + * Returns the default value for the field. + * + * @return The field default value + */ + const QString& defaultValue() const; + /** + * Sets the default value of the field. + * + * @param value The field default value + */ + void setDefaultValue(const QString& value); + /** + * Some attributes are always a category by themselves. + * + * @return Whether the field is th eonly member of its category + */ + bool isSingleCategory() const; + /** + * Extends a field with an additional key and value property pair. + * + * @param key The property key + * @param value The property value + */ + void setProperty(const QString& key, const QString& value); + /** + * Sets all the extended properties. Any existing ones get erased. + * + * @param properties The property list + */ + void setPropertyList(const StringMap& properties); + /** + * Return a property value. + * + * @param key The property key + * @returnThe property value + */ + const QString& property(const QString& key) const { return m_properties[key]; } + /** + * Return the list of properties. + * + * @return The property list + */ + const StringMap& propertyList() const { return m_properties; } + /** + * Return a vector of all the fields on which the value of this field depends. + * Returns an empty vector for non-Dpendent fields + */ + FieldVec dependsOn(CollPtr coll) const; + QStringList dependsOn() const; + + /*************************** STATIC **********************************/ + + /** + * A wrapper method around all the format functions. The flags + * determine which is called on the string. + */ + static QString format(const QString& value, FormatFlag flag); + /** + * A convenience function to format a string as a title. + * At the moment, this means that some articles such as "the" are placed + * at the end of the title. If autoCapitalize() is true, the title is capitalized. + * + * @param title The string to be formatted + */ + static QString formatTitle(const QString& title); + /** + * A convenience function to format a string as a personal name. + * At the moment, this means that the name is split at the last white space, + * and the last name is moved in front. If multiple=true, then the string + * is split using a semi-colon (";"), and each string is formatted and then + * joined back together. If autoCapitalize() is true, the names are capitalized. + * + * @param name The string to be formatted + * @param multiple A boolean indicating if the string can contain multiple values + */ + static QString formatName(const QString& name, bool multiple=true); + /** + * A convenience function to format a string as a date. + * + * @param date The string to be formatted + */ + static QString formatDate(const QString& date); + /** + * Helper method to fix capitalization. + * + * @param str String to fix + */ + static QString capitalize(QString str); + /** + * Return the key to be used for sorting titles + */ + static QString sortKeyTitle(const QString& title); + /** + * Returns a mapping of the FieldType enum to translated titles for the types. + */ + static FieldMap typeMap(); + /** + * Returns a list of the titles of the field types. + */ + static QStringList typeTitles(); + /** + * Splits a string into multiple values; + * + * @param string The string to be split + */ + static QStringList split(const QString& string, bool allowEmpty); + /** + * Returns the delimiter used to split field values + * + * @return The delimeter regexp + */ + static const QRegExp& delimiter() { return s_delimiter; } + /** + * reset if the field is a rating field used for syntax version 7 and earlier */ + static void convertOldRating(Data::FieldPtr field); + static void stripArticles(QString& value); + static void articlesUpdated(); + +private: + /* + * Gets the preferred ID of the collection. Currently, it just gets incremented as + * new collections are created. + */ + static long getID(); + + long m_id; + QString m_name; + QString m_title; + QString m_category; + QString m_desc; + Type m_type; + QStringList m_allowed; + int m_flags; + FormatFlag m_formatFlag; + StringMap m_properties; + + // need to remember articles with apostrophes for capitalization + static QStringList s_articlesApos; + static QRegExp s_delimiter; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/fieldcompletion.cpp b/src/fieldcompletion.cpp new file mode 100644 index 0000000..cfaf894 --- /dev/null +++ b/src/fieldcompletion.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + copyright : (C) 2003-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 "fieldcompletion.h" +#include "field.h" + +using Tellico::FieldCompletion; + +FieldCompletion::FieldCompletion(bool multiple_) : KCompletion(), m_multiple(multiple_) { +} + +QString FieldCompletion::makeCompletion(const QString& string_) { + if(completionMode() == KGlobalSettings::CompletionNone) { + m_beginText.truncate(0); + return QString::null; + } + + if(!m_multiple) { + return KCompletion::makeCompletion(string_); + } + + static QRegExp rx = Data::Field::delimiter(); + int pos = rx.searchRev(string_); + if(pos == -1) { + m_beginText.truncate(0); + return KCompletion::makeCompletion(string_); + } + + pos += rx.matchedLength(); + QString final = string_.mid(pos); + m_beginText = string_.mid(0, pos); + return m_beginText + KCompletion::makeCompletion(final); +} + +void FieldCompletion::clear() { + m_beginText.truncate(0); + KCompletion::clear(); +} + +void FieldCompletion::postProcessMatch(QString* match_) const { + if(m_multiple) { + match_->prepend(m_beginText); + } +} + +void FieldCompletion::postProcessMatches(QStringList* matches_) const { + if(m_multiple) { + for(QStringList::Iterator it = matches_->begin(); it != matches_->end(); ++it) { + (*it).prepend(m_beginText); + } + } +} + +void FieldCompletion::postProcessMatches(KCompletionMatches* matches_) const { + if(m_multiple) { + for(KCompletionMatches::Iterator it = matches_->begin(); it != matches_->end(); ++it) { + (*it).value().prepend(m_beginText); + } + } +} + + +#include "fieldcompletion.moc" diff --git a/src/fieldcompletion.h b/src/fieldcompletion.h new file mode 100644 index 0000000..14296f3 --- /dev/null +++ b/src/fieldcompletion.h @@ -0,0 +1,45 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOFIELDCOMPLETION_H +#define TELLICOFIELDCOMPLETION_H + +#include <kcompletion.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class FieldCompletion : public KCompletion { +Q_OBJECT + +public: + FieldCompletion(bool multiple); + + void setMultiple(bool m) { m_multiple = m; } + virtual QString makeCompletion(const QString& string); + virtual void clear(); + +protected: + virtual void postProcessMatch(QString* match) const; + virtual void postProcessMatches(QStringList* matches) const; + virtual void postProcessMatches(KCompletionMatches* matches) const; + +private: + bool m_multiple; + QString m_beginText; +}; + +} // end namespace +#endif diff --git a/src/filehandler.cpp b/src/filehandler.cpp new file mode 100644 index 0000000..ce5501a --- /dev/null +++ b/src/filehandler.cpp @@ -0,0 +1,418 @@ +/*************************************************************************** + copyright : (C) 2003-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 "filehandler.h" +#include "tellico_kernel.h" +#include "image.h" +#include "tellico_strings.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "core/netaccess.h" + +#include <kurl.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kio/netaccess.h> +#include <ktempfile.h> +#include <ksavefile.h> +#include <kapplication.h> +#include <kfileitem.h> +#include <kio/chmodjob.h> +#include <kfilterdev.h> + +#include <qdom.h> +#include <qfile.h> + +using Tellico::FileHandler; + +class FileHandler::ItemDeleter : public QObject { +public: + ItemDeleter(KIO::Job* job, KFileItem* item) : QObject(), m_job(job), m_item(item) { + FileHandler::s_deleterList.append(this); + connect(job, SIGNAL(result(KIO::Job*)), SLOT(deleteLater())); + } + ~ItemDeleter() { + FileHandler::s_deleterList.removeRef(this); + if(m_job) m_job->kill(); + delete m_item; m_item = 0; + } +private: + QGuardedPtr<KIO::Job> m_job; + KFileItem* m_item; +}; + +QPtrList<FileHandler::ItemDeleter> FileHandler::s_deleterList; + +FileHandler::FileRef::FileRef(const KURL& url_, bool quiet_, bool allowCompressed_) : m_device(0), m_isValid(false) { + if(url_.isEmpty()) { + return; + } + + if(!Tellico::NetAccess::download(url_, m_filename, Kernel::self()->widget())) { + myDebug() << "FileRef::FileRef() - can't download " << url_ << endl; + QString s = KIO::NetAccess::lastErrorString(); + if(!s.isEmpty()) { + myDebug() << s << endl; + } + if(!quiet_) { + Kernel::self()->sorry(i18n(errorLoad).arg(url_.fileName())); + } + return; + } + +/* + QFile* file = new QFile(m_filename); + if(file->exists()) { + m_isValid = true; + m_device = file; + } else { + delete file; + } +*/ + if(allowCompressed_) { + m_device = KFilterDev::deviceForFile(m_filename); + } else { + m_device = new QFile(m_filename); + } + m_isValid = true; +} + +FileHandler::FileRef::~FileRef() { + if(!m_filename.isEmpty()) { + Tellico::NetAccess::removeTempFile(m_filename); + } + if(m_device) { + m_device->close(); + } + delete m_device; + m_device = 0; + m_isValid = false; +} + +bool FileHandler::FileRef::open(bool quiet_) { + if(!isValid()) { + return false; + } + if(!m_device || !m_device->open(IO_ReadOnly)) { + if(!quiet_) { + KURL u; + u.setPath(fileName()); + Kernel::self()->sorry(i18n(errorLoad).arg(u.fileName())); + } + delete m_device; + m_device = 0; + m_isValid = false; + return false; + } + return true; +} + +FileHandler::FileRef* FileHandler::fileRef(const KURL& url_, bool quiet_) { + return new FileRef(url_, quiet_); +} + +QString FileHandler::readTextFile(const KURL& url_, bool quiet_/*=false*/, bool useUTF8_ /*false*/, bool compress_/*=false*/) { + FileRef f(url_, quiet_, compress_); + if(!f.isValid()) { + return QString::null; + } + + if(f.open(quiet_)) { + QTextStream stream(f.file()); + if(useUTF8_) { + stream.setEncoding(QTextStream::UnicodeUTF8); + } + return stream.read(); + } + return QString(); +} + +QDomDocument FileHandler::readXMLFile(const KURL& url_, bool processNamespace_, bool quiet_) { + FileRef f(url_, quiet_); + if(!f.isValid()) { + return QDomDocument(); + } + + QDomDocument doc; + QString errorMsg; + int errorLine, errorColumn; + if(!f.open(quiet_)) { + return QDomDocument(); + } + if(!doc.setContent(f.file(), processNamespace_, &errorMsg, &errorLine, &errorColumn)) { + if(!quiet_) { + QString details = i18n("There is an XML parsing error in line %1, column %2.").arg(errorLine).arg(errorColumn); + details += QString::fromLatin1("\n"); + details += i18n("The error message from Qt is:"); + details += QString::fromLatin1("\n\t") + errorMsg; + GUI::CursorSaver cs(Qt::arrowCursor); + KMessageBox::detailedSorry(Kernel::self()->widget(), i18n(errorLoad).arg(url_.fileName()), details); + } + return QDomDocument(); + } + return doc; +} + +QByteArray FileHandler::readDataFile(const KURL& url_, bool quiet_) { + FileRef f(url_, quiet_); + if(!f.isValid()) { + return QByteArray(); + } + + f.open(quiet_); + return f.file()->readAll(); +} + +Tellico::Data::Image* FileHandler::readImageFile(const KURL& url_, bool quiet_, const KURL& referrer_) { + if(url_.isLocalFile()) { + return readImageFile(url_, quiet_); + } + + KTempFile tmpFile; + KURL tmpURL; + tmpURL.setPath(tmpFile.name()); + tmpFile.setAutoDelete(true); + + KIO::Job* job = KIO::file_copy(url_, tmpURL, -1, true /* overwrite */); + job->addMetaData(QString::fromLatin1("referrer"), referrer_.url()); + + if(!KIO::NetAccess::synchronousRun(job, Kernel::self()->widget())) { + if(!quiet_) { + Kernel::self()->sorry(i18n(errorLoad).arg(url_.fileName())); + } + return 0; + } + return readImageFile(tmpURL, quiet_); +} + +Tellico::Data::Image* FileHandler::readImageFile(const KURL& url_, bool quiet_) { + FileRef f(url_, quiet_); + if(!f.isValid()) { + return 0; + } + + Data::Image* img = new Data::Image(f.fileName()); + if(img->isNull() && !quiet_) { + QString str = i18n("Tellico is unable to load the image - %1.").arg(url_.fileName()); + Kernel::self()->sorry(str); + } + return img; +} + +bool FileHandler::queryExists(const KURL& url_) { + if(url_.isEmpty() || !KIO::NetAccess::exists(url_, false, Kernel::self()->widget())) { + return true; + } + + // we always overwrite the current URL without asking + if(url_ != Kernel::self()->URL()) { + GUI::CursorSaver cs(Qt::arrowCursor); + QString str = i18n("A file named \"%1\" already exists. " + "Are you sure you want to overwrite it?").arg(url_.fileName()); + int want_continue = KMessageBox::warningContinueCancel(Kernel::self()->widget(), str, + i18n("Overwrite File?"), + i18n("Overwrite")); + + if(want_continue == KMessageBox::Cancel) { + return false; + } + } + + KURL backup(url_); + backup.setPath(backup.path() + QString::fromLatin1("~")); + + bool success = true; + if(url_.isLocalFile()) { + // KSaveFile messes up users and groups + // the user is always reset to the current user + KFileItemList list; + int perm = -1; + QString grp; + if(KIO::NetAccess::exists(url_, false, Kernel::self()->widget())) { + KFileItem item(KFileItem::Unknown, KFileItem::Unknown, url_, true); + perm = item.permissions(); + grp = item.group(); + + KFileItem* backupItem = new KFileItem(KFileItem::Unknown, KFileItem::Unknown, backup, true); + list.append(backupItem); + } + + success = KSaveFile::backupFile(url_.path()); + if(!list.isEmpty()) { + // have to leave the user alone + KIO::Job* job = KIO::chmod(list, perm, 0, QString(), grp, true, false); + new ItemDeleter(job, list.first()); + } + } else { + KIO::NetAccess::del(backup, Kernel::self()->widget()); // might fail if backup doesn't exist, that's ok + success = KIO::NetAccess::file_copy(url_, backup, -1 /* permissions */, true /* overwrite */, + false /* resume */, Kernel::self()->widget()); + } + if(!success) { + Kernel::self()->sorry(i18n(errorWrite).arg(url_.fileName() + '~')); + } + return success; +} + +bool FileHandler::writeTextURL(const KURL& url_, const QString& text_, bool encodeUTF8_, bool force_, bool quiet_) { + if((!force_ && !queryExists(url_)) || text_.isNull()) { + if(text_.isNull()) { + myDebug() << "FileHandler::writeTextURL() - null string for " << url_ << endl; + } + return false; + } + + if(url_.isLocalFile()) { + // KSaveFile messes up users and groups + // the user is always reset to the current user + KFileItemList list; + int perm = -1; + QString grp; + if(KIO::NetAccess::exists(url_, false, Kernel::self()->widget())) { + KFileItem* item = new KFileItem(KFileItem::Unknown, KFileItem::Unknown, url_, true); + list.append(item); + perm = item->permissions(); + grp = item->group(); + } + + KSaveFile f(url_.path()); + if(f.status() != 0) { + if(!quiet_) { + Kernel::self()->sorry(i18n(errorWrite).arg(url_.fileName())); + } + return false; + } + bool success = FileHandler::writeTextFile(f, text_, encodeUTF8_); + if(!list.isEmpty()) { + // have to leave the user alone + KIO::Job* job = KIO::chmod(list, perm, 0, QString(), grp, true, false); + new ItemDeleter(job, list.first()); + } + return success; + } + + // save to remote file + KTempFile tempfile; + KSaveFile f(tempfile.name()); + if(f.status() != 0) { + tempfile.unlink(); + if(!quiet_) { + Kernel::self()->sorry(i18n(errorWrite).arg(url_.fileName())); + } + return false; + } + + bool success = FileHandler::writeTextFile(f, text_, encodeUTF8_); + if(success) { + bool uploaded = KIO::NetAccess::upload(tempfile.name(), url_, Kernel::self()->widget()); + if(!uploaded) { + tempfile.unlink(); + if(!quiet_) { + Kernel::self()->sorry(i18n(errorUpload).arg(url_.fileName())); + } + success = false; + } + } + tempfile.unlink(); + + return success; +} + +bool FileHandler::writeTextFile(KSaveFile& f_, const QString& text_, bool encodeUTF8_) { + QTextStream* t = f_.textStream(); + if(encodeUTF8_) { + t->setEncoding(QTextStream::UnicodeUTF8); + } else { + t->setEncoding(QTextStream::Locale); + } +// kdDebug() << "-----------------------------" << endl +// << text_ << endl +// << "-----------------------------" << endl; + (*t) << text_; + bool success = f_.close(); +#ifndef NDEBUG + if(!success) { + myDebug() << "FileHandler::writeTextFile() - status = " << f_.status(); + } +#endif + return success; +} + +bool FileHandler::writeDataURL(const KURL& url_, const QByteArray& data_, bool force_, bool quiet_) { + if(!force_ && !queryExists(url_)) { + return false; + } + + if(url_.isLocalFile()) { + // KSaveFile messes up users and groups + // the user is always reset to the current user + KFileItemList list; + int perm = -1; + QString grp; + if(KIO::NetAccess::exists(url_, false, Kernel::self()->widget())) { + KFileItem* item = new KFileItem(KFileItem::Unknown, KFileItem::Unknown, url_, true); + list.append(item); + perm = item->permissions(); + grp = item->group(); + } + + KSaveFile f(url_.path()); + if(f.status() != 0) { + if(!quiet_) { + Kernel::self()->sorry(i18n(errorWrite).arg(url_.fileName())); + } + return false; + } + bool success = FileHandler::writeDataFile(f, data_); + if(!list.isEmpty()) { + // have to leave the user alone + KIO::Job* job = KIO::chmod(list, perm, 0, QString(), grp, true, false); + new ItemDeleter(job, list.first()); + } + return success; + } + + // save to remote file + KTempFile tempfile; + KSaveFile f(tempfile.name()); + if(f.status() != 0) { + if(!quiet_) { + Kernel::self()->sorry(i18n(errorWrite).arg(url_.fileName())); + } + return false; + } + + bool success = FileHandler::writeDataFile(f, data_); + if(success) { + success = KIO::NetAccess::upload(tempfile.name(), url_, Kernel::self()->widget()); + if(!success && !quiet_) { + Kernel::self()->sorry(i18n(errorUpload).arg(url_.fileName())); + } + } + tempfile.unlink(); + + return success; +} + +bool FileHandler::writeDataFile(KSaveFile& f_, const QByteArray& data_) { +// myDebug() << "FileHandler::writeDataFile()" << endl; + QDataStream* s = f_.dataStream(); + s->writeRawBytes(data_.data(), data_.size()); + return f_.close(); +} + +void FileHandler::clean() { + s_deleterList.setAutoDelete(true); + s_deleterList.clear(); + s_deleterList.setAutoDelete(false); +} diff --git a/src/filehandler.h b/src/filehandler.h new file mode 100644 index 0000000..9000478 --- /dev/null +++ b/src/filehandler.h @@ -0,0 +1,171 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FILEHANDLER_H +#define FILEHANDLER_H + +#include <qstring.h> +#include <qcstring.h> // needed for QByteArray +#include <qptrlist.h> + +class KURL; +class KSaveFile; +class KFileItem; +namespace KIO { + class Job; +} + +class QDomDocument; +class QIODevice; + +namespace Tellico { + class ImageFactory; + namespace Data { + class Image; + } + +/** + * The FileHandler class contains some utility functions for reading files. + * + * @author Robby Stephenson + */ +class FileHandler { + +friend class ImageFactory; + +public: + /** + * An internal class to handle KIO stuff. Exposed so a FileRef pointer + * can be returned from FileHandler. + */ + class FileRef { + public: + bool open(bool quiet=false); + QIODevice* file() const { return m_device; } + const QString& fileName() const { return m_filename; } + bool isValid() const { return m_isValid; } + ~FileRef(); + + private: + friend class FileHandler; + FileRef(const KURL& url, bool quiet=false, bool allowCompressed=false); + QIODevice* m_device; + QString m_filename; + bool m_isValid; + }; + friend class FileRef; + + /** + * Creates a FileRef for a given url. It's not meant to be used by methods in the class, + * Rather by a class wanting direct access to a file. The caller takes ownership of the pointer. + * + * @param url The url + * @param quiet Whether error messages should be shown + * @return The fileref + */ + static FileRef* fileRef(const KURL& url, bool quiet=false); + /** + * Read contents of a file into a string. + * + * @param url The URL of the file + * @param quiet whether the importer should report errors or not + * @param useUTF8 Whether the file should be read as UTF8 or use user locale + * @param allowCompressed Whether to check if the file is compressed or not + * @return A string containing the contents of a file + */ + static QString readTextFile(const KURL& url, bool quiet=false, bool useUTF8=false, bool allowCompressed=false); + /** + * Read contents of an XML file into a QDomDocument. + * + * @param url The URL of the file + * @param processNamespace Whether to process the namespace of the XML file + * @param quiet Whether error messages should be shown + * @return A QDomDocument containing the contents of a file + */ + static QDomDocument readXMLFile(const KURL& url, bool processNamespace, bool quiet=false); + /** + * Read contents of a data file into a QByteArray. + * + * @param url The URL of the file + * @param quiet Whether error messages should be shown + * @return A QByteArray of the file's contents + */ + static QByteArray readDataFile(const KURL& url, bool quiet=false); + /** + * Writes the contents of a string to a url. If the file already exists, a "~" is appended + * and the existing file is moved. If the file is remote, a temporary file is written and + * then uploaded. + * + * @param url The url + * @param text The text + * @param encodeUTF8 Whether to use UTF-8 encoding, or Locale + * @param force Whether to force the write + * @return A boolean indicating success + */ + static bool writeTextURL(const KURL& url, const QString& text, bool encodeUTF8, bool force=false, bool quiet=false); + /** + * Writes data to a url. If the file already exists, a "~" is appended + * and the existing file is moved. If the file is remote, a temporary file is written and + * then uploaded. + * + * @param url The url + * @param data The data + * @param force Whether to force the write + * @return A boolean indicating success + */ + static bool writeDataURL(const KURL& url, const QByteArray& data, bool force=false, bool quiet=false); + /** + * Checks to see if a URL exists already, and if so, queries the user. + * + * @param url The target URL + * @return True if it is ok to continue, false otherwise. + */ + static bool queryExists(const KURL& url); + static void clean(); + +private: + class ItemDeleter; + friend class ItemDeleter; + static QPtrList<ItemDeleter> s_deleterList; + + /** + * Read contents of a file into an image. It's private since everything should use the + * ImageFactory methods. + * + * @param url The URL of the file + * @param quiet If errors should be quiet + * @return The image + */ + static Data::Image* readImageFile(const KURL& url, bool quiet=false); + static Data::Image* readImageFile(const KURL& url, bool quiet, const KURL& referrer); + /** + * Writes the contents of a string to a file. + * + * @param file The file object + * @param text The string + * @param encodeUTF8 Whether to use UTF-8 encoding, or Locale + * @return A boolean indicating success + */ + static bool writeTextFile(KSaveFile& file, const QString& text, bool encodeUTF8); + /** + * Writes data to a file. + * + * @param file The file object + * @param data The data + * @return A boolean indicating success + */ + static bool writeDataFile(KSaveFile& file, const QByteArray& data); +}; + +} // end namespace +#endif diff --git a/src/filter.cpp b/src/filter.cpp new file mode 100644 index 0000000..9672937 --- /dev/null +++ b/src/filter.cpp @@ -0,0 +1,137 @@ +/*************************************************************************** + copyright : (C) 2003-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 "filter.h" +#include "entry.h" + +#include "tellico_debug.h" + +#include <qregexp.h> + +using Tellico::Filter; +using Tellico::FilterRule; + +FilterRule::FilterRule() : m_function(FuncEquals) { +} + +FilterRule::FilterRule(const QString& fieldName_, const QString& pattern_, Function func_) + : m_fieldName(fieldName_), m_function(func_), m_pattern(pattern_) { +} + +bool FilterRule::matches(Data::EntryPtr entry_) const { + switch (m_function) { + case FuncEquals: + return equals(entry_); + case FuncNotEquals: + return !equals(entry_); + case FuncContains: + return contains(entry_); + case FuncNotContains: + return !contains(entry_); + case FuncRegExp: + return matchesRegExp(entry_); + case FuncNotRegExp: + return !matchesRegExp(entry_); + default: + kdWarning() << "FilterRule::matches() - invalid function!" << endl; + break; + } + return true; +} + +bool FilterRule::equals(Data::EntryPtr entry_) const { + // empty field name means search all + if(m_fieldName.isEmpty()) { + QStringList list = entry_->fieldValues() + entry_->formattedFieldValues(); + for(QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { + if(QString::compare((*it).lower(), m_pattern.lower()) == 0) { + return true; + } + } + } else { + return QString::compare(entry_->field(m_fieldName).lower(), m_pattern.lower()) == 0 + || QString::compare(entry_->formattedField(m_fieldName).lower(), m_pattern.lower()) == 0; + } + + return false; +} + +bool FilterRule::contains(Data::EntryPtr entry_) const { + // empty field name means search all + if(m_fieldName.isEmpty()) { + QStringList list = entry_->fieldValues() + entry_->formattedFieldValues(); + // match is true if any strings match + for(QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { + if((*it).find(m_pattern, 0, false) >= 0) { + return true; + } + } + } else { + return entry_->field(m_fieldName).find(m_pattern, 0, false) >= 0 + || entry_->formattedField(m_fieldName).find(m_pattern, 0, false) >= 0; + } + + return false; +} + +bool FilterRule::matchesRegExp(Data::EntryPtr entry_) const { + QRegExp rx(m_pattern, false); + // empty field name means search all + if(m_fieldName.isEmpty()) { + QStringList list = entry_->fieldValues() + entry_->formattedFieldValues(); + for(QStringList::ConstIterator it = list.begin(); it != list.end(); ++it) { + if((*it).find(rx) >= 0) { + return true; + break; + } + } + } else { + return entry_->field(m_fieldName).find(rx) >= 0 + || entry_->formattedField(m_fieldName).find(rx) >= 0; + } + + return false; +} + + +/*******************************************************/ + +Filter::Filter(const Filter& other_) : QPtrList<FilterRule>(), KShared(), m_op(other_.op()), m_name(other_.name()) { + for(QPtrListIterator<FilterRule> it(other_); it.current(); ++it) { + append(new FilterRule(*it.current())); + } + setAutoDelete(true); +} + +bool Filter::matches(Data::EntryPtr entry_) const { + if(isEmpty()) { + return true; + } + + bool match = false; + for(QPtrListIterator<FilterRule> it(*this); it.current(); ++it) { + if(it.current()->matches(entry_)) { + if(m_op == Filter::MatchAny) { + return true; + } else { + match = true; + } + } else { + if(m_op == Filter::MatchAll) { + return false; + } + } + } + + return match; +} diff --git a/src/filter.h b/src/filter.h new file mode 100644 index 0000000..57997bf --- /dev/null +++ b/src/filter.h @@ -0,0 +1,130 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FILTER_H +#define TELLICO_FILTER_H + +#include "datavectors.h" + +#include <ksharedptr.h> + +#include <qptrlist.h> +#include <qstring.h> + +namespace Tellico { + namespace Data { + class Entry; + } + +/** + * @author Robby Stephenson + */ +class FilterRule { + +public: + /** + * Operators for comparison of field and contents. + * If you change the order or contents of the enum: do not forget + * to change matches() and @ref FilterRuleWidget::initLists(), too. + */ + enum Function { + FuncContains=0, FuncNotContains, + FuncEquals, FuncNotEquals, + FuncRegExp, FuncNotRegExp + }; + + FilterRule(); + FilterRule(const QString& fieldName, const QString& text, Function func); + + /** + * A rule is empty if the pattern text is empty + */ + bool isEmpty() const { return m_pattern.isEmpty(); } + /** + * This is the primary function of the rule. + * + * @return Returns true if the entry is matched by the rule. + */ + bool matches(Data::EntryPtr entry) const; + + /** + * Return filter function. This can be any of the operators + * defined in @ref Function. + */ + Function function() const { return m_function; } + /** + * Set filter function. + */ + void setFunction(Function func) { m_function = func; } + /** + * Return field name + */ + const QString& fieldName() const { return m_fieldName; } + /** + * Set field name + */ + void setFieldName(const QString& fieldName) { m_fieldName = fieldName; } + /** + * Return pattern + */ + const QString& pattern() const { return m_pattern; } + /** + * Set pattern + */ +// void setPattern(const QString& pattern) { m_pattern = pattern; } + +private: + bool equals(Data::EntryPtr entry) const; + bool contains(Data::EntryPtr entry) const; + bool matchesRegExp(Data::EntryPtr entry) const; + + QString m_fieldName; + Function m_function; + QString m_pattern; +}; + +/** + * Borrows from KMSearchPattern by Marc Mutz + * + * @author Robby Stephenson + */ +class Filter : public QPtrList<FilterRule>, public KShared { + +public: + enum FilterOp { + MatchAny, + MatchAll + }; + typedef KSharedPtr<Filter> Ptr; + + Filter(FilterOp op) : QPtrList<FilterRule>(), m_op(op) { setAutoDelete(true); } + Filter(const Filter& other); + + void setMatch(FilterOp op) { m_op = op; } + FilterOp op() const { return m_op; } + bool matches(Data::EntryPtr entry) const; + + void setName(const QString& name) { m_name = name; } + const QString& name() const { return m_name; } + + uint count() const { return QPtrList<FilterRule>::count(); } // disambiguate + +private: + Filter& operator=(const Filter& other); + + FilterOp m_op; + QString m_name; +}; + +} // end namespace +#endif diff --git a/src/filterdialog.cpp b/src/filterdialog.cpp new file mode 100644 index 0000000..ab65490 --- /dev/null +++ b/src/filterdialog.cpp @@ -0,0 +1,428 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +// The layout borrows heavily from kmsearchpatternedit.cpp in kmail +// which is authored by Marc Mutz <Marc@Mutz.com> under the GPL + +#include "filterdialog.h" +#include "tellico_kernel.h" +#include "document.h" +#include "collection.h" +#include "fieldcompletion.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kcombobox.h> +#include <klineedit.h> +#include <kpushbutton.h> +#include <kparts/componentfactory.h> +#include <kregexpeditorinterface.h> +#include <kiconloader.h> + +#include <qlayout.h> +#include <qgroupbox.h> +#include <qradiobutton.h> +#include <qvbuttongroup.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qlabel.h> +#include <qapplication.h> + +using Tellico::FilterRuleWidget; +using Tellico::FilterRuleWidgetLister; +using Tellico::FilterDialog; + +FilterRuleWidget::FilterRuleWidget(FilterRule* rule_, QWidget* parent_, const char* name_/*=0*/) + : QHBox(parent_, name_), m_editRegExp(0), m_editRegExpDialog(0) { + initLists(); + initWidget(); + + if(rule_) { + setRule(rule_); + } else { + reset(); + } +} + +void FilterRuleWidget::initLists() { + //---------- initialize list of filter fields + if(m_ruleFieldList.isEmpty()) { + m_ruleFieldList.append('<' + i18n("Any Field") + '>'); + m_ruleFieldList += Kernel::self()->fieldTitles(); + } + + //---------- initialize list of filter operators + if(m_ruleFuncList.isEmpty()) { + // also see FilterRule::matches() and FilterRule::Function + // if you change the following strings! + m_ruleFuncList.append(i18n("contains")); + m_ruleFuncList.append(i18n("does not contain")); + m_ruleFuncList.append(i18n("equals")); + m_ruleFuncList.append(i18n("does not equal")); + m_ruleFuncList.append(i18n("matches regexp")); + m_ruleFuncList.append(i18n("does not match regexp")); + } +} + +void FilterRuleWidget::initWidget() { + setSpacing(4); + + m_ruleField = new KComboBox(this); + connect(m_ruleField, SIGNAL(activated(int)), SIGNAL(signalModified())); + connect(m_ruleField, SIGNAL(activated(int)), SLOT(slotRuleFieldChanged(int))); + + m_ruleFunc = new KComboBox(this); + connect(m_ruleFunc, SIGNAL(activated(int)), SIGNAL(signalModified())); + m_ruleValue = new KLineEdit(this); + connect(m_ruleValue, SIGNAL(textChanged(const QString&)), SIGNAL(signalModified())); + + if(!KTrader::self()->query(QString::fromLatin1("KRegExpEditor/KRegExpEditor")).isEmpty()) { + m_editRegExp = new KPushButton(i18n("Edit..."), this); + connect(m_editRegExp, SIGNAL(clicked()), this, SLOT(slotEditRegExp())); + connect(m_ruleFunc, SIGNAL(activated(int)), this, SLOT(slotRuleFunctionChanged(int))); + slotRuleFunctionChanged(m_ruleFunc->currentItem()); + } + + m_ruleField->insertStringList(m_ruleFieldList); + // don't show sliders when popping up this menu +// m_ruleField->setSizeLimit(m_ruleField->count()); +// m_ruleField->adjustSize(); + + m_ruleFunc->insertStringList(m_ruleFuncList); +// m_ruleFunc->adjustSize(); + +// connect(m_ruleField, SIGNAL(textChanged(const QString &)), +// this, SIGNAL(fieldChanged(const QString &))); +// connect(m_ruleValue, SIGNAL(textChanged(const QString &)), +// this, SIGNAL(contentsChanged(const QString &))); +} + +void FilterRuleWidget::slotEditRegExp() { + if(m_editRegExpDialog == 0) { + m_editRegExpDialog = KParts::ComponentFactory::createInstanceFromQuery<QDialog>(QString::fromLatin1("KRegExpEditor/KRegExpEditor"), + QString::null, this); + } + + KRegExpEditorInterface* iface = static_cast<KRegExpEditorInterface *>(m_editRegExpDialog->qt_cast(QString::fromLatin1("KRegExpEditorInterface"))); + if(iface) { + iface->setRegExp(m_ruleValue->text()); + if(m_editRegExpDialog->exec() == QDialog::Accepted) { + m_ruleValue->setText(iface->regExp()); + } + } +} + +void FilterRuleWidget::slotRuleFieldChanged(int which_) { + Q_UNUSED(which_); + QString fieldTitle = m_ruleField->currentText(); + if(fieldTitle.isEmpty() || fieldTitle[0] == '<') { + m_ruleValue->setCompletionObject(0); + return; + } + Data::FieldPtr field = Data::Document::self()->collection()->fieldByTitle(fieldTitle); + if(field && (field->flags() & Data::Field::AllowCompletion)) { + FieldCompletion* completion = new FieldCompletion(field->flags() & Data::Field::AllowMultiple); + completion->setItems(Kernel::self()->valuesByFieldName(field->name())); + completion->setIgnoreCase(true); + m_ruleValue->setCompletionObject(completion); + m_ruleValue->setAutoDeleteCompletionObject(true); + } else { + m_ruleValue->setCompletionObject(0); + } +} + +void FilterRuleWidget::slotRuleFunctionChanged(int which_) { + // The 5th and 6th functions are for regexps + m_editRegExp->setEnabled(which_ == 4 || which_ == 5); +} + +void FilterRuleWidget::setRule(const FilterRule* rule_) { + if(!rule_) { + reset(); + return; + } + + blockSignals(true); + + if(rule_->fieldName().isEmpty()) { + m_ruleField->setCurrentItem(0); // "All Fields" + } else { + m_ruleField->setCurrentText(Kernel::self()->fieldTitleByName(rule_->fieldName())); + } + + //--------------set function and contents + m_ruleFunc->setCurrentItem(static_cast<int>(rule_->function())); + m_ruleValue->setText(rule_->pattern()); + + if(m_editRegExp) { + slotRuleFunctionChanged(static_cast<int>(rule_->function())); + } + + blockSignals(false); +} + +Tellico::FilterRule* FilterRuleWidget::rule() const { + QString field; // empty string + if(m_ruleField->currentItem() > 0) { // 0 is "All Fields", field is empty + field = Kernel::self()->fieldNameByTitle(m_ruleField->currentText()); + } + + return new FilterRule(field, m_ruleValue->text().stripWhiteSpace(), + static_cast<FilterRule::Function>(m_ruleFunc->currentItem())); +} + +void FilterRuleWidget::reset() { +// kdDebug() << "FilterRuleWidget::reset()" << endl; + blockSignals(true); + + m_ruleField->setCurrentItem(0); + m_ruleFunc->setCurrentItem(0); + m_ruleValue->clear(); + + if(m_editRegExp) { + m_editRegExp->setEnabled(false); + } + + blockSignals(false); +} + +void FilterRuleWidget::setFocus() { + m_ruleValue->setFocus(); +} + +/***************************************************************/ + +namespace { + static const int FILTER_MIN_RULE_WIDGETS = 1; + static const int FILTER_MAX_RULES = 8; +} + +FilterRuleWidgetLister::FilterRuleWidgetLister(QWidget* parent_, const char* name_) + : KWidgetLister(FILTER_MIN_RULE_WIDGETS, FILTER_MAX_RULES, parent_, name_) { +// slotClear(); +} + +void FilterRuleWidgetLister::setFilter(Filter::Ptr filter_) { +// if(mWidgetList.first()) { // move this below next 'if'? +// mWidgetList.first()->blockSignals(true); +// } + + if(filter_->isEmpty()) { + slotClear(); +// mWidgetList.first()->blockSignals(false); + return; + } + + const int count = static_cast<int>(filter_->count()); + if(count > mMaxWidgets) { + myDebug() << "FilterRuleWidgetLister::setFilter() - more rules than allowed!" << endl; + } + + // set the right number of widgets + setNumberOfShownWidgetsTo(QMAX(count, mMinWidgets)); + + // load the actions into the widgets + QPtrListIterator<QWidget> wIt(mWidgetList); + for(QPtrListIterator<FilterRule> rIt(*filter_); rIt.current() && wIt.current(); ++rIt, ++wIt) { + static_cast<FilterRuleWidget*>(*wIt)->setRule(*rIt); + } + for( ; wIt.current(); ++wIt) { // clear any remaining + static_cast<FilterRuleWidget*>(*wIt)->reset(); + } + +// mWidgetList.first()->blockSignals(false); +} + +void FilterRuleWidgetLister::reset() { + slotClear(); +} + +void FilterRuleWidgetLister::setFocus() { + if(!mWidgetList.isEmpty()) { + mWidgetList.getFirst()->setFocus(); + } +} + +QWidget* FilterRuleWidgetLister::createWidget(QWidget* parent_) { + QWidget* w = new FilterRuleWidget(static_cast<Tellico::FilterRule*>(0), parent_); + connect(w, SIGNAL(signalModified()), SIGNAL(signalModified())); + return w; +} + +void FilterRuleWidgetLister::clearWidget(QWidget* widget_) { + if(widget_) { + static_cast<FilterRuleWidget*>(widget_)->reset(); + } +} + +const QPtrList<QWidget>& FilterRuleWidgetLister::widgetList() const { + return mWidgetList; +} + +/***************************************************************/ + +namespace { + static const int FILTER_MIN_WIDTH = 600; +} + +// modal dialog so I don't have to worry about updating stuff +// don't show apply button if not saving, i.e. just modifying existing filter +FilterDialog::FilterDialog(Mode mode_, QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, true, + (mode_ == CreateFilter ? i18n("Advanced Filter") : i18n("Modify Filter")), + (mode_ == CreateFilter ? Help|Ok|Apply|Cancel : Help|Ok|Cancel), + Ok, false), + m_filter(0), m_mode(mode_), m_saveFilter(0) { + init(); +} + +void FilterDialog::init() { + QWidget* page = new QWidget(this); + setMainWidget(page); + QVBoxLayout* topLayout = new QVBoxLayout(page, 0, KDialog::spacingHint()); + + QGroupBox* m_matchGroup = new QGroupBox(1, Qt::Horizontal, i18n("Filter Criteria"), page); + topLayout->addWidget(m_matchGroup); + + QVButtonGroup* bg = new QVButtonGroup(m_matchGroup); + bg->setFrameShape(QFrame::NoFrame); + bg->setInsideMargin(0); + m_matchAll = new QRadioButton(i18n("Match a&ll of the following"), bg); + m_matchAny = new QRadioButton(i18n("Match an&y of the following"), bg); + m_matchAll->setChecked(true); + connect(bg, SIGNAL(clicked(int)), SLOT(slotFilterChanged())); + + m_ruleLister = new FilterRuleWidgetLister(m_matchGroup); + connect(m_ruleLister, SIGNAL(widgetRemoved()), SLOT(slotShrink())); + connect(m_ruleLister, SIGNAL(signalModified()), SLOT(slotFilterChanged())); + m_ruleLister->setFocus(); + + QHBoxLayout* blay = new QHBoxLayout(topLayout); + blay->addWidget(new QLabel(i18n("Filter name:"), page)); + + m_filterName = new KLineEdit(page); + blay->addWidget(m_filterName); + connect(m_filterName, SIGNAL(textChanged(const QString&)), SLOT(slotFilterChanged())); + + // only when creating a new filter can it be saved + if(m_mode == CreateFilter) { + m_saveFilter = new KPushButton(SmallIconSet(QString::fromLatin1("filter")), i18n("&Save Filter"), page); + blay->addWidget(m_saveFilter); + m_saveFilter->setEnabled(false); + connect(m_saveFilter, SIGNAL(clicked()), SLOT(slotSaveFilter())); + enableButtonApply(false); + } + enableButtonOK(false); // disable at start + actionButton(Help)->setDefault(false); // Help automatically becomes default when OK is disabled + actionButton(Cancel)->setDefault(true); // Help automatically becomes default when OK is disabled + setMinimumWidth(QMAX(minimumWidth(), FILTER_MIN_WIDTH)); + setHelp(QString::fromLatin1("filter-dialog")); +} + +Tellico::FilterPtr FilterDialog::currentFilter() { + if(!m_filter) { + m_filter = new Filter(Filter::MatchAny); + } + + if(m_matchAll->isChecked()) { + m_filter->setMatch(Filter::MatchAll); + } else { + m_filter->setMatch(Filter::MatchAny); + } + + m_filter->clear(); // deletes all old rules + for(QPtrListIterator<QWidget> it(m_ruleLister->widgetList()); it.current(); ++it) { + FilterRuleWidget* rw = static_cast<FilterRuleWidget*>(it.current()); + FilterRule* rule = rw->rule(); + if(rule && !rule->isEmpty()) { + m_filter->append(rule); + } + } + // only set name if it has rules + if(!m_filter->isEmpty()) { + m_filter->setName(m_filterName->text()); + } + return m_filter; +} + +void FilterDialog::setFilter(FilterPtr filter_) { + if(!filter_) { + slotClear(); + return; + } + + if(filter_->op() == Filter::MatchAll) { + m_matchAll->setChecked(true); + } else { + m_matchAny->setChecked(true); + } + + m_ruleLister->setFilter(filter_); + m_filterName->setText(filter_->name()); + m_filter = filter_; +} + +void FilterDialog::slotOk() { + slotApply(); + accept(); +} + +void FilterDialog::slotApply() { + emit signalUpdateFilter(currentFilter()); +} + +void FilterDialog::slotClear() { +// kdDebug() << "FilterDialog::slotClear()" << endl; + m_matchAll->setChecked(true); + m_ruleLister->reset(); + m_filterName->clear(); +} + +void FilterDialog::slotShrink() { + updateGeometry(); + QApplication::sendPostedEvents(); + resize(width(), sizeHint().height()); +} + +void FilterDialog::slotFilterChanged() { + bool emptyFilter = currentFilter()->isEmpty(); + if(m_saveFilter) { + m_saveFilter->setEnabled(!m_filterName->text().isEmpty() && !emptyFilter); + enableButtonApply(!emptyFilter); + } + enableButtonOK(!emptyFilter); + actionButton(Ok)->setDefault(!emptyFilter); +} + +void FilterDialog::slotSaveFilter() { + // non-op if editing an existing filter + if(m_mode != CreateFilter) { + return; + } + + // in this case, currentFilter() either creates a new filter or + // updates the current one. If creating a new one, then I want to copy it + bool wasEmpty = (m_filter == 0); + FilterPtr filter = new Filter(*currentFilter()); + if(wasEmpty) { + m_filter = filter; + } + // this keeps the saving completely decoupled from the filter setting in the detailed view + if(filter->isEmpty()) { + m_filter = 0; + return; + } + Kernel::self()->addFilter(filter); +} + +#include "filterdialog.moc" diff --git a/src/filterdialog.h b/src/filterdialog.h new file mode 100644 index 0000000..f2d21a6 --- /dev/null +++ b/src/filterdialog.h @@ -0,0 +1,170 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FILTERDIALOG_H +#define FILTERDIALOG_H + +// kwidgetlister is copied from kdepim/libkdenetwork cvs +#include "gui/kwidgetlister.h" +#include "filter.h" +#include "datavectors.h" + +#include <kdialogbase.h> + +#include <qhbox.h> +#include <qstring.h> +#include <qstringlist.h> + +class KComboBox; +class KLineEdit; +class KPushButton; + +class QRadioButton; +class QDialog; + +namespace Tellico { + class FilterDialog; + +/** + * A widget to edit a single FilterRule. + * It consists of a read-only @ref KComboBox for the field, + * a read-only @ref KComboBox for the function and + * a @ref KLineEdit for the content or the pattern (in case of regexps). + * + * This class borrows heavily from KMSearchRule in kmail by Marc Mutz + * + * @author Robby Stephenson + */ +class FilterRuleWidget : public QHBox { +Q_OBJECT + +public: + /** + * Constructor. You give a @ref FilterRule as parameter, which will + * be used to initialize the widget. + */ + FilterRuleWidget(FilterRule* rule, QWidget* parent, const char* name=0); + + /** + * Set the rule. The rule is accepted regardless of the return + * value of @ref FilterRule::isEmpty. This widget makes a shallow + * copy of @p rule and operates directly on it. If @p rule is + * 0, the widget resets itself, takes user input, but does essentially + * nothing. If you pass 0, you should probably disable it. + */ + void setRule(const FilterRule* rule); + /** + * Return a reference to the currently worked-on @ref FilterRule. + */ + FilterRule* rule() const; + /** + * Resets the rule currently worked on and updates the widget accordingly. + */ + void reset(); + +signals: + void signalModified(); + +public slots: + void setFocus(); + +protected slots: + void slotEditRegExp(); + void slotRuleFieldChanged(int which); + void slotRuleFunctionChanged(int which); + +private: + void initLists(); + void initWidget(); + + KComboBox* m_ruleField; + KComboBox* m_ruleFunc; + KLineEdit* m_ruleValue; + KPushButton* m_editRegExp; + QDialog* m_editRegExpDialog; + QStringList m_ruleFieldList; + QStringList m_ruleFuncList; +}; + +class FilterRuleWidgetLister : public KWidgetLister { +Q_OBJECT + +public: + FilterRuleWidgetLister(QWidget* parent, const char* name=0); + + const QPtrList<QWidget>& widgetList() const; + void setFilter(Filter::Ptr filter); + +public slots: + void reset(); + virtual void setFocus(); + +signals: + void signalModified(); + +protected: + virtual void clearWidget(QWidget* widget); + virtual QWidget* createWidget(QWidget* parent); +}; + +/** + * @author Robby Stephenson + */ +class FilterDialog : public KDialogBase { +Q_OBJECT + +public: + enum Mode { + CreateFilter, + ModifyFilter + }; + + /** + * The constructor sets up the dialog. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + FilterDialog(Mode mode, QWidget* parent, const char* name=0); + + FilterPtr currentFilter(); + void setFilter(FilterPtr filter); + +public slots: + void slotClear(); + +protected slots: + virtual void slotOk(); + virtual void slotApply(); + void slotShrink(); + void slotFilterChanged(); + void slotSaveFilter(); + +signals: + void signalUpdateFilter(Tellico::FilterPtr); + void signalCollectionModified(); + +private: + void init(); + + FilterPtr m_filter; + const Mode m_mode; + QRadioButton* m_matchAll; + QRadioButton* m_matchAny; + FilterRuleWidgetLister* m_ruleLister; + KLineEdit* m_filterName; + KPushButton* m_saveFilter; +}; + +} // end namespace +#endif diff --git a/src/filteritem.cpp b/src/filteritem.cpp new file mode 100644 index 0000000..68f5eaf --- /dev/null +++ b/src/filteritem.cpp @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 2005-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 "filteritem.h" +#include "tellico_kernel.h" + +#include <kiconloader.h> + +#include <qpixmap.h> + +using Tellico::FilterItem; + +FilterItem::FilterItem(GUI::ListView* parent_, Filter::Ptr filter_) + : GUI::CountedItem(parent_), m_filter(filter_) { + setText(0, filter_->name()); + setPixmap(0, SmallIcon(QString::fromLatin1("filter"))); +} + +void FilterItem::updateFilter(Filter::Ptr filter_) { + m_filter = filter_; + setText(0, m_filter->name()); +} + +void FilterItem::doubleClicked() { + Kernel::self()->modifyFilter(m_filter); +} diff --git a/src/filteritem.h b/src/filteritem.h new file mode 100644 index 0000000..c777b5a --- /dev/null +++ b/src/filteritem.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef FILTERITEM_H +#define FILTERITEM_H + +#include "gui/counteditem.h" +#include "filter.h" + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class FilterItem : public GUI::CountedItem { +public: + FilterItem(GUI::ListView* parent, Tellico::Filter::Ptr filter); + + virtual bool isFilterItem() const { return true; } + Filter::Ptr filter() { return m_filter; } + void updateFilter(Filter::Ptr filter); + + virtual void doubleClicked(); + +private: + Filter::Ptr m_filter; +}; + +} + +#endif diff --git a/src/filterview.cpp b/src/filterview.cpp new file mode 100644 index 0000000..8bb9deb --- /dev/null +++ b/src/filterview.cpp @@ -0,0 +1,266 @@ +/*************************************************************************** + copyright : (C) 2005-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 "filterview.h" +#include "controller.h" +#include "entry.h" +#include "collection.h" +#include "document.h" +#include "entryitem.h" +#include "tellico_kernel.h" +#include "listviewcomparison.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kpopupmenu.h> +#include <kiconloader.h> + +#include <qheader.h> + +using Tellico::FilterView; + +FilterView::FilterView(QWidget* parent_, const char* name_) : GUI::ListView(parent_, name_), m_notSortedYet(true) { + addColumn(i18n("Filter")); + header()->setStretchEnabled(true, 0); + setResizeMode(QListView::NoColumn); + setRootIsDecorated(true); + setShowSortIndicator(true); + setTreeStepSize(15); + setFullWidth(true); + + connect(this, SIGNAL(contextMenuRequested(QListViewItem*, const QPoint&, int)), + SLOT(contextMenuRequested(QListViewItem*, const QPoint&, int))); +} + +bool FilterView::isSelectable(GUI::ListViewItem* item_) const { + if(!GUI::ListView::isSelectable(item_)) { + return false; + } + + // because the popup menu has modify and delete, only + // allow one filter item to get selected + if(item_->isFilterItem()) { + return selectedItems().isEmpty(); + } + + return true; +} + +void FilterView::contextMenuRequested(QListViewItem* item_, const QPoint& point_, int) { + if(!item_) { + return; + } + + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(item_); + if(item->isFilterItem()) { + KPopupMenu menu(this); + menu.insertItem(SmallIconSet(QString::fromLatin1("filter")), + i18n("Modify Filter"), this, SLOT(slotModifyFilter())); + menu.insertItem(SmallIconSet(QString::fromLatin1("editdelete")), + i18n("Delete Filter"), this, SLOT(slotDeleteFilter())); + menu.exec(point_); + } +} + +// this gets called when header() is clicked, so cycle through +void FilterView::setSorting(int col_, bool asc_) { + if(asc_ && !m_notSortedYet) { + if(sortStyle() == ListView::SortByText) { + setSortStyle(ListView::SortByCount); + } else { + setSortStyle(ListView::SortByText); + } + } + if(sortStyle() == ListView::SortByText) { + setColumnText(0, i18n("Filter")); + } else { + setColumnText(0, i18n("Filter (Sort by Count)")); + } + m_notSortedYet = false; + ListView::setSorting(col_, asc_); +} + +void FilterView::addCollection(Data::CollPtr coll_) { + FilterVec filters = coll_->filters(); + for(FilterVec::Iterator it = filters.begin(); it != filters.end(); ++it) { + addFilter(it); + } + Data::FieldPtr f = coll_->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } +} + +void FilterView::addEntries(Data::EntryVec entries_) { + for(QListViewItem* item = firstChild(); item; item = item->nextSibling()) { + Filter::Ptr filter = static_cast<FilterItem*>(item)->filter(); + for(Data::EntryVecIt it = entries_.begin(); it != entries_.end(); ++it) { + if(filter && filter->matches(it.data())) { + new EntryItem(static_cast<FilterItem*>(item), it); + } + } + } +} + +void FilterView::modifyEntries(Data::EntryVec entries_) { + for(Data::EntryVecIt it = entries_.begin(); it != entries_.end(); ++it) { + modifyEntry(it); + } +} + +void FilterView::modifyEntry(Data::EntryPtr entry_) { + for(QListViewItem* item = firstChild(); item; item = item->nextSibling()) { + bool hasEntry = false; + QListViewItem* entryItem = 0; + // iterate over all children and find item with matching entry pointers + for(QListViewItem* i = item->firstChild(); i; i = i->nextSibling()) { + if(static_cast<EntryItem*>(i)->entry() == entry_) { + i->setText(0, entry_->title()); + // only one item per filter will match + hasEntry = true; + entryItem = i; + break; + } + } + // now, if the entry was there but no longer matches, delete it + // if the entry was not there but does match, add it + Filter::Ptr filter = static_cast<FilterItem*>(item)->filter(); + if(hasEntry && !filter->matches(static_cast<EntryItem*>(entryItem)->entry())) { + delete entryItem; + } else if(!hasEntry && filter->matches(entry_)) { + new EntryItem(static_cast<FilterItem*>(item), entry_); + } + } +} + +void FilterView::removeEntries(Data::EntryVec entries_) { + // the group modified signal gets handles separately, this is just for filters + for(QListViewItem* item = firstChild(); item; item = item->nextSibling()) { + // iterate over all children and delete items with matching entry pointers + QListViewItem* c1 = item->firstChild(); + while(c1) { + if(entries_.contains(static_cast<EntryItem*>(c1)->entry())) { + QListViewItem* c2 = c1; + c1 = c1->nextSibling(); + delete c2; + } else { + c1 = c1->nextSibling(); + } + } + } +} + +void FilterView::addField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void FilterView::modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr) { + resetComparisons(); +} + +void FilterView::removeField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void FilterView::addFilter(FilterPtr filter_) { + FilterItem* filterItem = new FilterItem(this, filter_); + + Data::EntryVec entries = Data::Document::self()->filteredEntries(filter_); + for(Data::EntryVecIt it = entries.begin(); it != entries.end(); ++it) { + new EntryItem(filterItem, it); // text gets set in constructor + } +} + +void FilterView::slotModifyFilter() { + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(currentItem()); + if(!item || !item->isFilterItem()) { + return; + } + + Kernel::self()->modifyFilter(static_cast<FilterItem*>(item)->filter()); +} + +void FilterView::slotDeleteFilter() { + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(currentItem()); + if(!item || !item->isFilterItem()) { + return; + } + + Kernel::self()->removeFilter(static_cast<FilterItem*>(item)->filter()); +} + +void FilterView::removeFilter(FilterPtr filter_) { + // paranoia + if(!filter_) { + return; + } + + // find the item for this filter + // cheating a bit, it's probably the current one + GUI::ListViewItem* found = 0; + GUI::ListViewItem* cur = static_cast<GUI::ListViewItem*>(currentItem()); + if(cur && cur->isFilterItem() && static_cast<FilterItem*>(cur)->filter() == filter_) { + // found it! + found = cur; + } else { + // iterate over all filter items + for(QListViewItem* item = firstChild(); item; item = item->nextSibling()) { + if(static_cast<FilterItem*>(item)->filter() == filter_) { + found = static_cast<FilterItem*>(item); + break; + } + } + } + + // not found + if(!found) { + myDebug() << "GroupView::modifyFilter() - not found" << endl; + return; + } + + delete found; +} + +void FilterView::slotSelectionChanged() { + GUI::ListView::slotSelectionChanged(); + + GUI::ListViewItem* item = selectedItems().getFirst(); + if(item && item->isFilterItem()) { + Controller::self()->slotUpdateFilter(static_cast<FilterItem*>(item)->filter()); + } +} + +void FilterView::resetComparisons() { + // this is only allowed when the view is not empty, so we can grab a collection ptr + if(childCount() == 0) { + return; + } + QListViewItem* item = firstChild(); + while(item && item->childCount() == 0) { + item = item->nextSibling(); + } + if(!item) { + return; + } + item = item->firstChild(); + Data::CollPtr coll = static_cast<EntryItem*>(item)->entry()->collection(); + if(!coll) { + return; + } + Data::FieldPtr f = coll->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } +} + +#include "filterview.moc" diff --git a/src/filterview.h b/src/filterview.h new file mode 100644 index 0000000..85674e5 --- /dev/null +++ b/src/filterview.h @@ -0,0 +1,83 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_FILTERVIEW_H +#define TELLICO_FILTERVIEW_H + +#include "gui/listview.h" +#include "observer.h" +#include "filteritem.h" + +#include <qdict.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class FilterView : public GUI::ListView, public Observer { +Q_OBJECT + +public: + FilterView(QWidget* parent, const char* name=0); + + virtual bool isSelectable(GUI::ListViewItem*) const; + + void addCollection(Data::CollPtr coll); + + virtual void addEntries(Data::EntryVec entries); + virtual void modifyEntry(Data::EntryPtr entry); + virtual void modifyEntries(Data::EntryVec entries); + virtual void removeEntries(Data::EntryVec entries); + + virtual void addField(Data::CollPtr, Data::FieldPtr); + virtual void modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr); + virtual void removeField(Data::CollPtr, Data::FieldPtr); + + virtual void addFilter(FilterPtr filter); + virtual void modifyFilter(FilterPtr) {} + virtual void removeFilter(FilterPtr filter); + +protected slots: + virtual void slotSelectionChanged(); + +private slots: + /** + * Handles the appearance of the popup menu. + * + * @param item A pointer to the item underneath the mouse + * @param point The location point + * @param col The column number, not currently used + */ + void contextMenuRequested(QListViewItem* item, const QPoint& point, int col); + + /** + * Modify a saved filter + */ + void slotModifyFilter(); + /** + * Delete a saved filter + */ + void slotDeleteFilter(); + +private: + virtual void setSorting(int column, bool ascending = true); + void resetComparisons(); + + bool m_notSortedYet; + QDict<FilterItem> m_itemDict; +}; + +} // end namespace + +#endif diff --git a/src/groupiterator.cpp b/src/groupiterator.cpp new file mode 100644 index 0000000..21f0db2 --- /dev/null +++ b/src/groupiterator.cpp @@ -0,0 +1,34 @@ +/*************************************************************************** + copyright : (C) 2003-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 "groupiterator.h" +#include "entrygroupitem.h" + +using Tellico::GroupIterator; + +GroupIterator::GroupIterator(const QListView* view_) { + // groups are the first children in the group view + m_item = static_cast<GUI::ListViewItem*>(view_->firstChild()); +} + +GroupIterator& GroupIterator::operator++() { + m_item = static_cast<GUI::ListViewItem*>(m_item->nextSibling()); + return *this; +} + +Tellico::Data::EntryGroup* GroupIterator::group() { + if(!m_item || !m_item->isEntryGroupItem()) { + return 0; + } + return static_cast<EntryGroupItem*>(m_item)->group(); +} diff --git a/src/groupiterator.h b/src/groupiterator.h new file mode 100644 index 0000000..a77b7ea --- /dev/null +++ b/src/groupiterator.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GROUPITERATOR_H +#define TELLICO_GROUPITERATOR_H + +class QListView; + +namespace Tellico { + namespace Data { + class EntryGroup; + } + namespace GUI { + class ListViewItem; + } + +/** + * @author Robby Stephenson + */ +class GroupIterator{ +public: + GroupIterator(const QListView* view); + + GroupIterator& operator++(); + Data::EntryGroup* group(); + +private: + GUI::ListViewItem* m_item; +}; + +} + +#endif diff --git a/src/groupview.cpp b/src/groupview.cpp new file mode 100644 index 0000000..67b1a40 --- /dev/null +++ b/src/groupview.cpp @@ -0,0 +1,495 @@ +/*************************************************************************** + copyright : (C) 2001-2007 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 "groupview.h" +#include "collection.h" +#include "document.h" +#include "field.h" +#include "filter.h" +#include "controller.h" +#include "entryitem.h" +#include "entrygroupitem.h" +#include "entry.h" +#include "field.h" +#include "filter.h" +#include "tellico_kernel.h" +#include "listviewcomparison.h" +#include "../tellico_debug.h" + +#include <kpopupmenu.h> +#include <klocale.h> +#include <kiconloader.h> +#include <kaction.h> + +#include <qstringlist.h> +#include <qcolor.h> +#include <qregexp.h> +#include <qheader.h> + +using Tellico::GroupView; + +GroupView::GroupView(QWidget* parent_, const char* name_/*=0*/) + : GUI::ListView(parent_, name_), m_notSortedYet(true), m_coll(0) { + addColumn(QString::null); // header text gets updated later + header()->setStretchEnabled(true, 0); + setResizeMode(QListView::NoColumn); + setRootIsDecorated(true); + setShowSortIndicator(true); + setTreeStepSize(15); + setFullWidth(true); + + connect(this, SIGNAL(contextMenuRequested(QListViewItem*, const QPoint&, int)), + SLOT(contextMenuRequested(QListViewItem*, const QPoint&, int))); + + connect(this, SIGNAL(expanded(QListViewItem*)), + SLOT(slotExpanded(QListViewItem*))); + + connect(this, SIGNAL(collapsed(QListViewItem*)), + SLOT(slotCollapsed(QListViewItem*))); + + m_groupOpenPixmap = SmallIcon(QString::fromLatin1("folder_open")); + m_groupClosedPixmap = SmallIcon(QString::fromLatin1("folder")); +} + +Tellico::EntryGroupItem* GroupView::addGroup(Data::EntryGroup* group_) { + if(group_->isEmpty()) { + return 0; + } + int type = -1; + if(m_coll && m_coll->hasField(group_->fieldName())) { + type = m_coll->fieldByName(group_->fieldName())->type(); + } + EntryGroupItem* item = new EntryGroupItem(this, group_, type); + if(group_->groupName() == i18n(Data::Collection::s_emptyGroupTitle)) { + item->setPixmap(0, SmallIcon(QString::fromLatin1("folder_red"))); + item->setSortWeight(10); + } else { + item->setPixmap(0, m_groupClosedPixmap); + } + + m_groupDict.insert(group_->groupName(), item); + item->setExpandable(!group_->isEmpty()); + + return item; +} + +void GroupView::slotReset() { + clear(); + m_groupDict.clear(); +} + +void GroupView::removeCollection(Data::CollPtr coll_) { + if(!coll_) { + kdWarning() << "GroupView::removeCollection() - null coll pointer!" << endl; + return; + } + +// myDebug() << "GroupView::removeCollection() - " << coll_->title() << endl; + + blockSignals(true); + slotReset(); + blockSignals(false); +} + +void GroupView::slotModifyGroups(Data::CollPtr coll_, PtrVector<Data::EntryGroup> groups_) { + if(!coll_ || groups_.isEmpty()) { + kdWarning() << "GroupView::slotModifyGroups() - null coll or group pointer!" << endl; + return; + } + + for(PtrVector<Data::EntryGroup>::Iterator group = groups_.begin(); group != groups_.end(); ++group) { + // if the entries aren't grouped by field of the modified group, + // we don't care, so return + if(m_groupBy != group->fieldName()) { + continue; + } + +// myDebug() << "GroupView::slotModifyGroups() - " << group->fieldName() << "/" << group->groupName() << endl; + EntryGroupItem* par = m_groupDict.find(group->groupName()); + if(par) { + if(group->isEmpty()) { + m_groupDict.remove(par->text(0)); + delete par; + continue; + } + // the group might get deleted and recreated out from under us, + // so do a sanity check + par->setGroup(group.ptr()); + } else { + if(group->isEmpty()) { + myDebug() << "GroupView::slotModifyGroups() - trying to add empty group" << endl; + continue; + } + par = addGroup(group.ptr()); + } + + setUpdatesEnabled(false); + bool open = par->isOpen(); + par->setOpen(false); // closing and opening the item will clear the items + par->setOpen(open); + setUpdatesEnabled(true); + } + // don't want any selected + clearSelection(); + sort(); // in case the count changed, or group name +} + +// don't 'shadow' QListView::setSelected +void GroupView::setEntrySelected(Data::EntryPtr entry_) { +// myDebug() << "GroupView::slotSetSelected()" << endl; + // if entry_ is null pointer, set no selected + if(!entry_) { + // don't move this one outside the block since it calls setCurrentItem(0) + clearSelection(); + return; + } + + // if the selected entry is the same as the current one, just return + GUI::ListViewItem* it = static_cast<GUI::ListViewItem*>(currentItem()); + if(it && it->isEntryItem() && entry_ == static_cast<EntryItem*>(it)->entry()) { + return; + } + + // have to find a group whose field is the same as currently shown + if(m_groupBy.isEmpty()) { + myDebug() << "GroupView::slotSetSelected() - no group field" << endl; + return; + } + + const Data::EntryGroup* group = 0; + for(PtrVector<Data::EntryGroup>::ConstIterator it = entry_->groups().begin(); it != entry_->groups().end(); ++it) { + if(it->fieldName() == m_groupBy) { + group = it.ptr(); + break; + } + } + if(!group) { + myDebug() << "GroupView::slotSetSelected() - entry is not in any current groups!" << endl; + return; + } + + EntryGroupItem* groupItem = m_groupDict.find(group->groupName()); + if(!groupItem) { + return; + } + + clearSelection(); + for(QListViewItem* item = groupItem->firstChild(); item; item = item->nextSibling()) { + EntryItem* entryItem = static_cast<EntryItem*>(item); + if(entryItem->entry() == entry_) { + blockSignals(true); + setSelected(item, true); + setCurrentItem(item); + blockSignals(false); + ensureItemVisible(item); + return; + } + } +} + +void GroupView::slotExpandAll(int depth_/*=-1*/) { + if(childCount() == 0) { + return; + } + setSiblingsOpen(depth_, true); +} + +void GroupView::slotCollapseAll(int depth_/*=-1*/) { + if(childCount() == 0) { + return; + } + setSiblingsOpen(depth_, false); +} + +void GroupView::setSiblingsOpen(int depth_, bool open_) { + QListViewItem* item = 0; + + if(depth_ == -1) { + item = currentItem(); + if(!item) { + return; + } + depth_ = item->depth(); + } + + switch(depth_) { + case 0: + item = firstChild(); + break; + + case 1: + item = firstChild()->firstChild(); + break; + + default: + return; + } + + for( ; item; item = item->nextSibling()) { + item->setOpen(open_); + } +} + +void GroupView::contextMenuRequested(QListViewItem* item_, const QPoint& point_, int) { + if(!item_) { + return; + } + + KPopupMenu menu(this); + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(item_); + if(item->isEntryGroupItem()) { + menu.insertItem(SmallIconSet(QString::fromLatin1("2downarrow")), + i18n("Expand All Groups"), this, SLOT(slotExpandAll())); + menu.insertItem(SmallIconSet(QString::fromLatin1("2uparrow")), + i18n("Collapse All Groups"), this, SLOT(slotCollapseAll())); + menu.insertItem(SmallIconSet(QString::fromLatin1("filter")), + i18n("Filter by Group"), this, SLOT(slotFilterGroup())); + } else if(item->isEntryItem()) { + Controller::self()->plugEntryActions(&menu); + } + menu.exec(point_); +} + +void GroupView::slotCollapsed(QListViewItem* item_) { + // only change icon for group items + if(static_cast<GUI::ListViewItem*>(item_)->isEntryGroupItem()) { + if(item_->text(0) == i18n(Data::Collection::s_emptyGroupTitle)) { + item_->setPixmap(0, SmallIcon(QString::fromLatin1("folder_red"))); + } else { + item_->setPixmap(0, m_groupClosedPixmap); + } + static_cast<GUI::ListViewItem*>(item_)->clear(); + } +} + +void GroupView::slotExpanded(QListViewItem* item_) { + EntryGroupItem* item = static_cast<EntryGroupItem*>(item_); + // only change icon for group items + if(!item->isEntryGroupItem()) { + return; + } + + setUpdatesEnabled(false); + if(item->text(0) == i18n(Data::Collection::s_emptyGroupTitle)) { + item->setPixmap(0, SmallIcon(QString::fromLatin1("folder_red_open"))); + } else { + item->setPixmap(0, m_groupOpenPixmap); + } + + Data::EntryGroup* group = item->group(); + if(!group) { + myDebug() << "GroupView::slotExpanded() - no entry group! - " << item->text(0) << endl; + } else { + for(Data::EntryVecIt entryIt = group->begin(); entryIt != group->end(); ++entryIt) { + new EntryItem(item, entryIt); + } + } + + setUpdatesEnabled(true); + triggerUpdate(); +} + +void GroupView::addCollection(Data::CollPtr coll_) { +// myDebug() << "GroupView::addCollection()" << endl; + if(!coll_) { + kdWarning() << "GroupView::addCollection() - null coll pointer!" << endl; + return; + } + + m_coll = coll_; + // if the collection doesn't have the grouped field, and it's not the pseudo-group, + // change it to default + if(m_groupBy.isEmpty() || (!coll_->hasField(m_groupBy) && m_groupBy != Data::Collection::s_peopleGroupName)) { + m_groupBy = coll_->defaultGroupField(); + } + + // when the coll gets set for the first time, the pixmaps need to be updated + if((m_coll->hasField(m_groupBy) && m_coll->fieldByName(m_groupBy)->formatFlag() == Data::Field::FormatName) + || m_groupBy == Data::Collection::s_peopleGroupName) { + m_groupOpenPixmap = UserIcon(QString::fromLatin1("person-open")); + m_groupClosedPixmap = UserIcon(QString::fromLatin1("person")); + } + + Data::FieldPtr f = coll_->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } + + updateHeader(); + populateCollection(); + + slotCollapseAll(); +// myDebug() << "GroupView::addCollection - done" << endl; +} + +void GroupView::setGroupField(const QString& groupField_) { +// myDebug() << "GroupView::setGroupField - " << groupField_ << endl; + if(groupField_.isEmpty() || groupField_ == m_groupBy) { + return; + } + + m_groupBy = groupField_; + if(!m_coll) { + return; // can't do anything yet, but still need to set the variable + } + if((m_coll->hasField(groupField_) && m_coll->fieldByName(groupField_)->formatFlag() == Data::Field::FormatName) + || groupField_ == Data::Collection::s_peopleGroupName) { + m_groupOpenPixmap = UserIcon(QString::fromLatin1("person-open")); + m_groupClosedPixmap = UserIcon(QString::fromLatin1("person")); + } else { + m_groupOpenPixmap = SmallIcon(QString::fromLatin1("folder_open")); + m_groupClosedPixmap = SmallIcon(QString::fromLatin1("folder")); + } + updateHeader(); + populateCollection(); +} + +void GroupView::populateCollection() { + if(!m_coll) { + return; + } + +// myDebug() << "GroupView::populateCollection() - " << m_groupBy << endl; + if(m_groupBy.isEmpty()) { + m_groupBy = m_coll->defaultGroupField(); + } + + setUpdatesEnabled(false); + clear(); // delete all groups + m_groupDict.clear(); + + // if there's no group field, just return + if(m_groupBy.isEmpty()) { + setUpdatesEnabled(true); + return; + } + + Data::EntryGroupDict* dict = m_coll->entryGroupDictByName(m_groupBy); + if(!dict) { // could happen if m_groupBy is non empty, but there are no entries with a value + return; + } + + // iterate over all the groups in the dict + // e.g. if the dict is "author", loop over all the author groups + for(QDictIterator<Data::EntryGroup> it(*dict); it.current(); ++it) { + addGroup(it.current()); + } + + setUpdatesEnabled(true); + triggerUpdate(); +} + +void GroupView::slotFilterGroup() { + const GUI::ListViewItemList& items = selectedItems(); + GUI::ListViewItem* item = items.getFirst(); + // only works for entry groups + if(!item || !item->isEntryGroupItem()) { + return; + } + + FilterPtr filter = new Filter(Filter::MatchAny); + + for(GUI::ListViewItemListIt it(items); it.current(); ++it) { + if(static_cast<EntryGroupItem*>(it.current())->count() == 0) { //ignore empty items + continue; + } + // need to check for people group + if(m_groupBy == Data::Collection::s_peopleGroupName) { + Data::EntryPtr entry = static_cast<EntryItem*>(it.current()->firstChild())->entry(); + Data::FieldVec fields = entry->collection()->peopleFields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + filter->append(new FilterRule(fIt->name(), it.current()->text(0), FilterRule::FuncContains)); + } + } else { + QString s = it.current()->text(0); + if(s != i18n(Data::Collection::s_emptyGroupTitle)) { + filter->append(new FilterRule(m_groupBy, it.current()->text(0), FilterRule::FuncContains)); + } + } + } + + if(!filter->isEmpty()) { + emit signalUpdateFilter(filter); + } +} + +// this gets called when header() is clicked, so cycle through +void GroupView::setSorting(int col_, bool asc_) { + if(asc_ && !m_notSortedYet) { // cycle through after ascending + if(sortStyle() == ListView::SortByText) { + setSortStyle(ListView::SortByCount); + } else { + setSortStyle(ListView::SortByText); + } + } + updateHeader(); + m_notSortedYet = false; + ListView::setSorting(col_, asc_); +} + +void GroupView::addField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void GroupView::modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr newField_) { + if(newField_->name() == m_groupBy) { + updateHeader(newField_); + } + // if the grouping changed at all, our groups got deleted out from under us + // so check first pointer, too. The groups could be deleted if the format type + // changes, too, so not enough just to check group flag + if(childCount() > 0 && static_cast<EntryGroupItem*>(firstChild())->group() == 0) { + populateCollection(); + } + resetComparisons(); +} + +void GroupView::removeField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void GroupView::updateHeader(Data::FieldPtr field_/*=0*/) { + QString t = field_ ? field_->title() : groupTitle(); + if(sortStyle() == ListView::SortByText) { + setColumnText(0, t); + } else { + setColumnText(0, i18n("%1 (Sort by Count)").arg(t)); + } +} + +QString GroupView::groupTitle() { + QString title; + if(!m_coll || m_groupBy.isEmpty()) { + title = i18n("Group Name Header", "Group"); + } else { + Data::FieldPtr f = m_coll->fieldByName(m_groupBy); + if(f) { + title = f->title(); + } else if(m_groupBy == Data::Collection::s_peopleGroupName) { + title = i18n("People"); + } + } + return title; +} + +void GroupView::resetComparisons() { + if(!m_coll) { + return; + } + Data::FieldPtr f = m_coll->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } +} + +#include "groupview.moc" diff --git a/src/groupview.h b/src/groupview.h new file mode 100644 index 0000000..4ead256 --- /dev/null +++ b/src/groupview.h @@ -0,0 +1,198 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef GROUPVIEW_H +#define GROUPVIEW_H + +#include "gui/listview.h" +#include "observer.h" + +#include <qdict.h> +#include <qpixmap.h> + +namespace Tellico { + namespace Data { + class EntryGroup; + } + class Filter; + class EntryGroupItem; + class GroupIterator; + +/** + * The GroupView shows the entries grouped, as well as the saved filters. + * + * There is one root item for each collection in the document. The entries are grouped + * by the field defined by each collection. A @ref QDict is used to keep track of the + * group items. + * + * @see Tellico::Data::Collection + * + * @author Robby Stephenson + */ +class GroupView : public GUI::ListView, public Observer { +Q_OBJECT + +public: + /** + * The constructor sets up the single column, and initializes the popup menu. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + GroupView(QWidget* parent, const char* name=0); + + /** + * Returns the name of the field by which the entries are grouped + * + * @return The field name + */ + const QString& groupBy() const { return m_groupBy; } + /** + * Sets the name of the field by which the entries are grouped + * + * @param groupFieldName The field name + */ + void setGroupField(const QString& groupFieldName); + /** + * Adds a collection, along with all all the groups for the collection in + * the groupFieldribute. This method gets called as well when the groupFieldribute + * is changed, since it essentially repopulates the listview. + * + * @param coll A pointer to the collection being added + */ + void addCollection(Data::CollPtr coll); + /** + * Removes a root collection item, and all of its children. + * + * @param coll A pointer to the collection + */ + void removeCollection(Data::CollPtr coll); + /** + * Selects the first item which refers to a certain entry. + * + * @param entry A pointer to the entry + */ + void setEntrySelected(Data::EntryPtr entry); + /** + * Refresh all the items for the collection. + * + * @return The item for the collection + */ + void populateCollection(); + + virtual void addField(Data::CollPtr, Data::FieldPtr); + virtual void modifyField(Data::CollPtr coll, Data::FieldPtr oldField, Data::FieldPtr newField); + virtual void removeField(Data::CollPtr, Data::FieldPtr); + +public slots: + /** + * Resets the list view, clearing and deleting all items. + */ + void slotReset(); + /** + * Adds or removes listview items when groups are modified. + * + * @param coll A pointer to the collection of the gorup + * @param groups A vector of pointers to the modified groups + */ + void slotModifyGroups(Tellico::Data::CollPtr coll, PtrVector<Tellico::Data::EntryGroup> groups); + /** + * Expands all items at a certain depth. If depth is -1, the current selected item + * is expanded. If depth is equal to either 0 or 1, then all items at that depth + * are expanded. + * + * @param depth The depth value + */ + void slotExpandAll(int depth=-1); + /** + * Collapses all items at a certain depth. If depth is -1, the current selected item + * is collapsed. If depth is equal to either 0 or 1, then all items at that depth + * are collapsed. + * + * @param depth The depth value + */ + void slotCollapseAll(int depth=-1); + +private: + /** + * Inserts a listviewitem for a given group + * + * @param group The group to be added + * @return A pointer to the created @ ref ParentItem + */ + EntryGroupItem* addGroup(Data::EntryGroup* group); + /** + * Traverse all siblings at a certain depth, setting them open or closed. If depth is -1, + * then the depth of the @ref currentItem() is used. + * + * @param depth Desired depth + * @param open Whether the item should be open or not + */ + void setSiblingsOpen(int depth, bool open); + +private slots: + /** + * Handles the appearance of the popup menu, determining which of the three (collection, + * group, or entry) menus to display. + * + * @param item A pointer to the item underneath the mouse + * @param point The location point + * @param col The column number, not currently used + */ + void contextMenuRequested(QListViewItem* item, const QPoint& point, int col); + /** + * Handles changing the icon when an item is expanded, depended on whether it refers + * to a collection, a group, or an entry. + * + * @param item A pointer to the expanded list item + */ + void slotExpanded(QListViewItem* item); + /** + * Handles changing the icon when an item is collapsed, depended on whether it refers + * to a collection, a group, or an entry. + * + * @param item A pointer to the collapse list item + */ + void slotCollapsed(QListViewItem* item); + /** + * Filter by group + */ + void slotFilterGroup(); + +signals: + /** + * Signals a desire to filter the view. + * + * @param filter A pointer to the filter + */ + void signalUpdateFilter(Tellico::FilterPtr filter); + +private: + friend class GroupIterator; + + virtual void setSorting(int column, bool ascending = true); + void resetComparisons(); + QString groupTitle(); + void updateHeader(Data::FieldPtr field=0); + + bool m_notSortedYet; + Data::CollPtr m_coll; + QDict<EntryGroupItem> m_groupDict; + QString m_groupBy; + + QPixmap m_groupOpenPixmap; + QPixmap m_groupClosedPixmap; +}; + +} // end namespace +#endif diff --git a/src/gui/Makefile.am b/src/gui/Makefile.am new file mode 100644 index 0000000..cb6f5e8 --- /dev/null +++ b/src/gui/Makefile.am @@ -0,0 +1,43 @@ +AM_CPPFLAGS = $(all_includes) + +noinst_LIBRARIES = libgui.a +libgui_a_SOURCES = combobox.cpp counteditem.cpp datewidget.cpp \ + tabcontrol.cpp kwidgetlister.cpp stringmapdialog.cpp listview.cpp richtextlabel.cpp \ + lineedit.cpp boolfieldwidget.cpp choicefieldwidget.cpp linefieldwidget.cpp \ + numberfieldwidget.cpp parafieldwidget.cpp urlfieldwidget.cpp tablefieldwidget.cpp \ + imagefieldwidget.cpp datefieldwidget.cpp imagewidget.cpp fieldwidget.cpp ratingwidget.cpp \ + ratingfieldwidget.cpp overlaywidget.cpp progress.cpp listboxtext.cpp collectiontypecombo.cpp \ + previewdialog.cpp + +libgui_a_METASOURCES = AUTO +KDE_OPTIONS = noautodist +EXTRA_DIST = combobox.h combobox.cpp \ +counteditem.h counteditem.cpp \ +datewidget.h datewidget.cpp \ +kwidgetlister.h kwidgetlister.cpp \ +listview.h listview.cpp \ +richtextlabel.h richtextlabel.cpp \ +stringmapdialog.h stringmapdialog.cpp \ +tabcontrol.h tabcontrol.cpp \ +lineedit.h lineedit.cpp \ +boolfieldwidget.h boolfieldwidget.cpp \ +choicefieldwidget.h choicefieldwidget.cpp \ +datefieldwidget.h datefieldwidget.cpp \ +imagefieldwidget.h imagefieldwidget.cpp \ +linefieldwidget.h linefieldwidget.cpp \ +numberfieldwidget.h numberfieldwidget.cpp \ +parafieldwidget.h parafieldwidget.cpp \ +tablefieldwidget.h tablefieldwidget.cpp \ +urlfieldwidget.h urlfieldwidget.cpp \ +ratingwidget.h ratingwidget.cpp \ +imagewidget.h imagewidget.cpp \ +fieldwidget.h fieldwidget.cpp \ +ratingfieldwidget.h ratingfieldwidget.cpp \ +overlaywidget.h overlaywidget.cpp \ +progress.h progress.cpp \ +listboxtext.h listboxtext.cpp \ +collectiontypecombo.h collectiontypecombo.cpp \ +previewdialog.h previewdialog.cpp + +CLEANFILES = *~ + diff --git a/src/gui/boolfieldwidget.cpp b/src/gui/boolfieldwidget.cpp new file mode 100644 index 0000000..5a8e88b --- /dev/null +++ b/src/gui/boolfieldwidget.cpp @@ -0,0 +1,61 @@ +/*************************************************************************** + copyright : (C) 2005-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 "boolfieldwidget.h" +#include "../field.h" +#include "../latin1literal.h" + +#include <qlabel.h> +#include <qcheckbox.h> +#include <qlayout.h> + +using Tellico::GUI::BoolFieldWidget; + +BoolFieldWidget::BoolFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + + m_checkBox = new QCheckBox(this); + connect(m_checkBox, SIGNAL(clicked()), SIGNAL(modified())); + registerWidget(); +} + +QString BoolFieldWidget::text() const { + if(m_checkBox->isChecked()) { + return QString::fromLatin1("true"); + } + + return QString(); +} + +void BoolFieldWidget::setText(const QString& text_) { + blockSignals(true); + + m_checkBox->blockSignals(true); + // be lax, don't have to check for "1" or "true" + // just check for a non-empty string + m_checkBox->setChecked(!text_.isEmpty()); + m_checkBox->blockSignals(false); + + blockSignals(false); +} + +void BoolFieldWidget::clear() { + m_checkBox->setChecked(false); + editMultiple(false); +} + +QWidget* BoolFieldWidget::widget() { + return m_checkBox; +} + +#include "boolfieldwidget.moc" diff --git a/src/gui/boolfieldwidget.h b/src/gui/boolfieldwidget.h new file mode 100644 index 0000000..81af5a6 --- /dev/null +++ b/src/gui/boolfieldwidget.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef BOOLFIELDWIDGET_H +#define BOOLFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +class QCheckBox; +class QString; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class BoolFieldWidget : public FieldWidget { +Q_OBJECT + +public: + BoolFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~BoolFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + +private: + QCheckBox* m_checkBox; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/choicefieldwidget.cpp b/src/gui/choicefieldwidget.cpp new file mode 100644 index 0000000..e9c6870 --- /dev/null +++ b/src/gui/choicefieldwidget.cpp @@ -0,0 +1,69 @@ +/*************************************************************************** + copyright : (C) 2005-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 "choicefieldwidget.h" +#include "../field.h" + +#include <kcombobox.h> + +#include <qlabel.h> +#include <qlayout.h> + +using Tellico::GUI::ChoiceFieldWidget; + +ChoiceFieldWidget::ChoiceFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_), m_comboBox(0) { + + m_comboBox = new KComboBox(this); + connect(m_comboBox, SIGNAL(activated(int)), SIGNAL(modified())); + // always have empty choice + m_comboBox->insertItem(QString::null); + m_comboBox->insertStringList(field_->allowed()); + m_comboBox->setMinimumWidth(5*fontMetrics().maxWidth()); + + registerWidget(); +} + +QString ChoiceFieldWidget::text() const { + return m_comboBox->currentText(); +} + +void ChoiceFieldWidget::setText(const QString& text_) { + blockSignals(true); + + m_comboBox->blockSignals(true); + m_comboBox->setCurrentItem(text_); + m_comboBox->blockSignals(false); + + blockSignals(false); +} + +void ChoiceFieldWidget::clear() { + m_comboBox->setCurrentItem(0); // first item is empty + editMultiple(false); +} + +void ChoiceFieldWidget::updateFieldHook(Data::FieldPtr, Data::FieldPtr newField_) { + QString value = text(); + m_comboBox->clear(); + // always have empty choice + m_comboBox->insertItem(QString::null); + m_comboBox->insertStringList(newField_->allowed()); + m_comboBox->setCurrentText(value); +} + +QWidget* ChoiceFieldWidget::widget() { + return m_comboBox; +} + +#include "choicefieldwidget.moc" diff --git a/src/gui/choicefieldwidget.h b/src/gui/choicefieldwidget.h new file mode 100644 index 0000000..a2c40f5 --- /dev/null +++ b/src/gui/choicefieldwidget.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef CHOICEFIELDWIDGET_H +#define CHOICEFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +class KComboBox; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class ChoiceFieldWidget : public FieldWidget { +Q_OBJECT + +public: + ChoiceFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~ChoiceFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +private: + KComboBox* m_comboBox; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/collectiontypecombo.cpp b/src/gui/collectiontypecombo.cpp new file mode 100644 index 0000000..66749a3 --- /dev/null +++ b/src/gui/collectiontypecombo.cpp @@ -0,0 +1,53 @@ +/*************************************************************************** + copyright : (C) 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 "collectiontypecombo.h" +#include "../collection.h" +#include "../collectionfactory.h" + +using Tellico::GUI::CollectionTypeCombo; + +CollectionTypeCombo::CollectionTypeCombo(QWidget* parent_) : ComboBox(parent_) { + reset(); +} + +void CollectionTypeCombo::reset() { + clear(); + // I want to sort the collection names + const CollectionNameMap nameMap = CollectionFactory::nameMap(); + QMap<QString, int> rNameMap; + for(CollectionNameMap::ConstIterator it = nameMap.begin(); it != nameMap.end(); ++it) { + rNameMap.insert(it.data(), it.key()); + } + const QValueList<int> collTypes = rNameMap.values(); + const QStringList collNames = rNameMap.keys(); + int custom = -1; + const int total = collTypes.count(); + // when i equals the size, then go back and do custom + for(int i = 0; i <= total; ++i) { + // put custom last + if(custom > -1 && count() >= total) { + break; // already done it! + } else if(i == total) { + i = custom; + } else if(collTypes[i] == Data::Collection::Base) { + custom = i; + continue; + } + insertItem(collNames[i], collTypes[i]); + } +} + +void CollectionTypeCombo::setCurrentType(int type_) { + setCurrentData(type_); +} diff --git a/src/gui/collectiontypecombo.h b/src/gui/collectiontypecombo.h new file mode 100644 index 0000000..8d3ef80 --- /dev/null +++ b/src/gui/collectiontypecombo.h @@ -0,0 +1,33 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_COLLECTIONTYPECOMBO_H +#define TELLICO_GUI_COLLECTIONTYPECOMBO_H + +#include "combobox.h" + +namespace Tellico { + +namespace GUI { + +class CollectionTypeCombo : public ComboBox { +public: + CollectionTypeCombo(QWidget* parent); + void reset(); + void setCurrentType(int type); + int currentType() const { return currentData().toInt(); } +}; + + } +} +#endif diff --git a/src/gui/combobox.cpp b/src/gui/combobox.cpp new file mode 100644 index 0000000..1aff29b --- /dev/null +++ b/src/gui/combobox.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + 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 "combobox.h" + +#include <kdebug.h> + +using Tellico::GUI::ComboBox; + +ComboBox::ComboBox(QWidget* parent_) : KComboBox(parent_) { + setEditable(false); +} + +void ComboBox::clear() { + KComboBox::clear(); + m_data.clear(); +} + +void ComboBox::insertItem(const QString& s_, const QVariant& t_, int idx_/* =-1 */) { + KComboBox::insertItem(s_, idx_); + if(idx_ < 0) { + m_data.push_back(t_); + } else { + while(idx_ > static_cast<int>(m_data.count())) { + m_data.push_back(QVariant()); + } + m_data.insert(m_data.at(idx_), t_); + } +} + +void ComboBox::insertItems(const QStringList& s_, const QValueList<QVariant>& t_, int idx_ /*=-1*/) { + if(s_.count() != t_.count()) { + kdWarning() << "ComboBox::insertItems() - must have equal number of items in list!" << endl; + return; + } + + for(uint i = 0; i < s_.count(); ++i) { + insertItem(s_[i], t_[i], idx_+i); + } +} + +const QVariant& ComboBox::currentData() const { + return data(currentItem()); +} + +const QVariant& ComboBox::data(uint idx_) const { + if(idx_ >= m_data.count()) { + static QVariant t; // inescapable + return t; + } + return m_data[idx_]; +} + +void ComboBox::setCurrentData(const QVariant& data_) { + for(uint i = 0; i < m_data.count(); ++i) { + if(m_data[i] == data_) { + setCurrentItem(i); + break; + } + } +} diff --git a/src/gui/combobox.h b/src/gui/combobox.h new file mode 100644 index 0000000..d02dbb8 --- /dev/null +++ b/src/gui/combobox.h @@ -0,0 +1,52 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_COMBOBOX_H +#define TELLICO_GUI_COMBOBOX_H + +#include <kcombobox.h> + +#include <qvariant.h> +#include <qvaluelist.h> + +class QString; + +namespace Tellico { + namespace GUI { + +/** + * A combobox for mapping a QVariant to each item. + * + * @author Robby Stephenson + */ +class ComboBox : public KComboBox { +public: + ComboBox(QWidget* parent_); + + void clear(); + const QVariant& currentData() const; + const QVariant& data(uint index) const; + void insertItem(const QString& string, const QVariant& datum, int index = -1); + void insertItems(const QStringList& strings, const QValueList<QVariant>& data, int index = -1); + + // set current item to match data + void setCurrentData(const QVariant& data); + +private: + QValueList<QVariant> m_data; +}; + + } // end namespace +} //end namespace + +#endif diff --git a/src/gui/counteditem.cpp b/src/gui/counteditem.cpp new file mode 100644 index 0000000..08b4f25 --- /dev/null +++ b/src/gui/counteditem.cpp @@ -0,0 +1,113 @@ +/*************************************************************************** + copyright : (C) 2005-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 "counteditem.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" + +#include <kglobalsettings.h> +#include <kstringhandler.h> + +#include <qpainter.h> +#include <qpixmap.h> + +using Tellico::GUI::CountedItem; + +int CountedItem::compare(QListViewItem* item_, int col_, bool asc_) const { + GUI::ListView* lv = listView(); + GUI::CountedItem* item = static_cast<GUI::CountedItem*>(item_); + if(lv->sortStyle() == GUI::ListView::SortByCount) { + if(count() < item->count()) { + return -1; + } else if(count() > item->count()) { + return 1; + } else { + return GUI::ListViewItem::compare(item, col_, asc_); + } + } + // for now, only other style is by text + return GUI::ListViewItem::compare(item, col_, asc_); +} + +void CountedItem::paintCell(QPainter* p_, const QColorGroup& cg_, + int column_, int width_, int align_) { + if(!p_) { + return; + } + + // always paint the cell + + // show count is only for first column + if(column_ != 0) { + ListViewItem::paintCell(p_, cg_, column_, width_, align_); + return; + } + + // set a catchable text so that we can have our own implementation (see further down) + // but still benefit from KListView::paintCell + QString oldText = text(column_); +// if(oldText.isEmpty()) { + if(oldText == '\t') { + return; // avoid endless loop! + } + + setText(column_, QChar('\t')); + ListViewItem::paintCell(p_, cg_, column_, width_, align_); + setText(column_, oldText); + + int marg = listView()->itemMargin(); + int r = marg; + const QPixmap* icon = pixmap(column_); + if(icon) { + r += icon->width() + marg; + } + + QFontMetrics fm = p_->fontMetrics(); + QString numText = QString::fromLatin1(" (%1)").arg(count()); + // don't call CountedListViewItem::width() because that includes the count already + int w = ListViewItem::width(fm, listView(), column_); + int countWidth = fm.width(numText); + if(w+marg+r+countWidth > width_) { + oldText = KStringHandler::rPixelSqueeze(oldText, fm, width_-marg-r-countWidth); + } + if(isSelected()) { + p_->setPen(cg_.highlightedText()); + } else { + p_->setPen(cg_.text()); + } + QRect br(0, height(), r, 0); + if(!oldText.isEmpty() && !oldText.startsWith(QChar('\t'))) { + p_->drawText(r, 0, width_-marg-r, height(), align_ | AlignVCenter, oldText, -1, &br); + } + + if(isSelected()) { + p_->setPen(cg_.highlightedText()); + } else { + if(!Tellico::contrastColor.isValid()) { + updateContrastColor(cg_); + } + p_->setPen(Tellico::contrastColor); + } + p_->drawText(br.right(), 0, width_-marg-br.right(), height(), align_ | Qt::AlignVCenter, numText); +} + +int CountedItem::width(const QFontMetrics& fm_, const QListView* lv_, int column_) const { + int w = ListViewItem::width(fm_, lv_, column_); + + // show count is only for first column + if(column_ == 0) { + QString numText = QString::fromLatin1(" (%1)").arg(count()); + w += fm_.width(numText) + 2; // add a little pad + } + return w; +} diff --git a/src/gui/counteditem.h b/src/gui/counteditem.h new file mode 100644 index 0000000..9ea0138 --- /dev/null +++ b/src/gui/counteditem.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef COUNTEDITEM_H +#define COUNTEDITEM_H + +#include "listview.h" + +class QPainter; +class QColorGroup; +class QFontMetrics; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class CountedItem : public GUI::ListViewItem { +public: + CountedItem(ListView* parent) : ListViewItem(parent) {} + CountedItem(ListViewItem* parent) : ListViewItem(parent) {} + + virtual int compare(QListViewItem* item, int col, bool ascending) const; + /** + * Paints the cell, adding the number count. + */ + virtual void paintCell(QPainter* p, const QColorGroup& cg, + int column, int width, int align); + virtual int width(const QFontMetrics& fm, const QListView* lv, int c) const; + + virtual int count() const { return childCount(); } + virtual int realChildCount() const { return count(); } +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/datefieldwidget.cpp b/src/gui/datefieldwidget.cpp new file mode 100644 index 0000000..d0609d6 --- /dev/null +++ b/src/gui/datefieldwidget.cpp @@ -0,0 +1,52 @@ +/*************************************************************************** + copyright : (C) 2005-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 "datefieldwidget.h" +#include "datewidget.h" +#include "../field.h" + +using Tellico::GUI::DateFieldWidget; + +DateFieldWidget::DateFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + + m_widget = new DateWidget(this); + connect(m_widget, SIGNAL(signalModified()), SIGNAL(modified())); + + registerWidget(); +} + +QString DateFieldWidget::text() const { + return m_widget->text(); +} + +void DateFieldWidget::setText(const QString& text_) { + blockSignals(true); + m_widget->blockSignals(true); + + m_widget->setDate(text_); + + m_widget->blockSignals(false); + blockSignals(false); +} + +void DateFieldWidget::clear() { + m_widget->clear(); + editMultiple(false); +} + +QWidget* DateFieldWidget::widget() { + return m_widget; +} + +#include "datefieldwidget.moc" diff --git a/src/gui/datefieldwidget.h b/src/gui/datefieldwidget.h new file mode 100644 index 0000000..e3c2d55 --- /dev/null +++ b/src/gui/datefieldwidget.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef DATEFIELDWIDGET_H +#define DATEFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +class QString; + +namespace Tellico { + namespace GUI { + class DateWidget; + +/** + * @author Robby Stephenson + */ +class DateFieldWidget : public FieldWidget { +Q_OBJECT + +public: + DateFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~DateFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + +private: + DateWidget* m_widget; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/datewidget.cpp b/src/gui/datewidget.cpp new file mode 100644 index 0000000..42e2d4c --- /dev/null +++ b/src/gui/datewidget.cpp @@ -0,0 +1,279 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +// this class borrows heavily from kdateedit.h in the kdepim module +// which is Copyright (c) 2002 Cornelius Schumacher <schumacher@kde.org> +// and published under the LGPL + +#include "datewidget.h" + +#include <kdebug.h> +#include <kcombobox.h> +#include <kpushbutton.h> +#include <kdatepicker.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kglobalsettings.h> +#include <kcalendarsystem.h> + +#include <qvbox.h> +#include <qlayout.h> + +using Tellico::GUI::SpinBox; +using Tellico::GUI::DateWidget; + +SpinBox::SpinBox(int min, int max, QWidget *parent) : QSpinBox(min, max, 1, parent) +{ + editor()->setAlignment(AlignRight); + // I want to be able to omit the day + // an empty string just removes the special value, so set white space + setSpecialValueText(QChar(' ')); +} + +DateWidget::DateWidget(QWidget* parent_, const char* name_) : QWidget(parent_, name_) { + QHBoxLayout* l = new QHBoxLayout(this, 0, 4); + + KLocale* locale = KGlobal::locale(); + + // 0 allows empty value + m_daySpin = new SpinBox(0, 31, this); + l->addWidget(m_daySpin, 1); + + m_monthCombo = new KComboBox(false, this); + l->addWidget(m_monthCombo, 1); + // allow empty item + m_monthCombo->insertItem(QString::null); + QDate d; + for(int i = 1; ; ++i) { + QString str = locale->calendar()->monthName(i, locale->calendar()->year(d)); + if(str.isNull()) { + break; + } + m_monthCombo->insertItem(str); + } + + m_yearSpin = new SpinBox(locale->calendar()->minValidYear(), + locale->calendar()->maxValidYear(), this); + l->addWidget(m_yearSpin, 1); + + connect(m_daySpin, SIGNAL(valueChanged(int)), SLOT(slotDateChanged())); + connect(m_monthCombo, SIGNAL(activated(int)), SLOT(slotDateChanged())); + connect(m_yearSpin, SIGNAL(valueChanged(int)), SLOT(slotDateChanged())); + + m_dateButton = new KPushButton(this); + m_dateButton->setIconSet(SmallIconSet(QString::fromLatin1("date"))); + connect(m_dateButton, SIGNAL(clicked()), SLOT(slotShowPicker())); + l->addWidget(m_dateButton, 0); + + m_frame = new QVBox(0, 0, WType_Popup); + m_frame->setFrameStyle(QFrame::PopupPanel | QFrame::Raised); + m_frame->setLineWidth(3); + m_frame->hide(); + + m_picker = new KDatePicker(m_frame, 0); // must include name to get correct constructor + connect(m_picker, SIGNAL(dateEntered(QDate)), SLOT(slotDateEntered(QDate))); + connect(m_picker, SIGNAL(dateSelected(QDate)), SLOT(slotDateSelected(QDate))); +} + +void DateWidget::slotDateChanged() { + int day = m_daySpin->value(); + day = QMIN(QMAX(day, m_daySpin->minValue()), m_daySpin->maxValue()); + + int m = m_monthCombo->currentItem(); + m = QMIN(QMAX(m, 0), m_monthCombo->count()-1); + + int y = m_yearSpin->value(); + y = QMIN(QMAX(y, m_yearSpin->minValue()), m_yearSpin->maxValue()); + + // if all are valid, set this date + if(day > m_daySpin->minValue() && m > 0 && y > m_yearSpin->minValue()) { + QDate d(y, m, day); + setDate(d); + } + emit signalModified(); +} + +QDate DateWidget::date() const { + // possible for either day, month, or year to be empty + // in which case a null date is returned + int day = m_daySpin->value(); + // min value is the empty one + if(day == m_daySpin->minValue()) { + return QDate(); + } + int month = m_monthCombo->currentItem(); + if(month == 0) { + return QDate(); + } + int year = m_yearSpin->value(); + if(year == m_yearSpin->minValue()) { + return QDate(); + } + return QDate(year, month, day); +} + +QString DateWidget::text() const { + // possible for either day, month, or year to be empty + // but not all three + bool empty = true; + // format is "year-month-day" + QString s; + if(m_yearSpin->value() > m_yearSpin->minValue()) { + s += QString::number(m_yearSpin->value()); + empty = false; + } + s += '-'; + // first item is empty + if(m_monthCombo->currentItem() > 0) { + s += QString::number(m_monthCombo->currentItem()); + empty = false; + } + s += '-'; + if(m_daySpin->value() > m_daySpin->minValue()) { + s += QString::number(m_daySpin->value()); + empty = false; + } + return empty ? QString() : s; +} + +void DateWidget::setDate(const QDate& date_) { + m_daySpin->blockSignals(true); + m_monthCombo->blockSignals(true); + m_yearSpin->blockSignals(true); + + const KCalendarSystem * calendar = KGlobal::locale()->calendar(); + m_daySpin->setMaxValue(calendar->daysInMonth(date_)); + m_daySpin->setValue(calendar->day(date_)); + m_monthCombo->setCurrentItem(calendar->month(date_)); // don't subtract 1 since there's the blank first item + m_yearSpin->setValue(calendar->year(date_)); + + m_daySpin->blockSignals(false); + m_monthCombo->blockSignals(false); + m_yearSpin->blockSignals(false); +} + +void DateWidget::setDate(const QString& date_) { + m_daySpin->blockSignals(true); + m_monthCombo->blockSignals(true); + m_yearSpin->blockSignals(true); + + QStringList s = QStringList::split('-', date_, true); + bool ok = true; + int y = s.count() > 0 ? s[0].toInt(&ok) : m_yearSpin->minValue(); + if(!ok) { + y = m_yearSpin->minValue(); + ok = true; + } + y = QMIN(QMAX(y, m_yearSpin->minValue()), m_yearSpin->maxValue()); + m_yearSpin->setValue(y); + + int m = s.count() > 1 ? s[1].toInt(&ok) : 0; + if(!ok) { + m = 0; + ok = true; + } + m = QMIN(QMAX(m, 0), m_monthCombo->count()-1); + m_monthCombo->setCurrentItem(m); + + // need to update number of days in month + // for now set date to 1 + QDate date(y, (m == 0 ? 1 : m), 1); + m_daySpin->blockSignals(true); + m_daySpin->setMaxValue(KGlobal::locale()->calendar()->daysInMonth(date)); + m_daySpin->blockSignals(false); + + int day = s.count() > 2 ? s[2].toInt(&ok) : m_daySpin->minValue(); + if(!ok) { + day = m_daySpin->minValue(); + } + day = QMIN(QMAX(day, m_daySpin->minValue()), m_daySpin->maxValue()); + m_daySpin->setValue(day); + + m_daySpin->blockSignals(false); + m_monthCombo->blockSignals(false); + m_yearSpin->blockSignals(false); + + // if all are valid, set this date + if(day > m_daySpin->minValue() && m > 0 && y > m_yearSpin->minValue()) { + QDate d(y, m, day); + m_picker->blockSignals(true); + m_picker->setDate(d); + m_picker->blockSignals(false); + } +} + +void DateWidget::clear() { + m_daySpin->blockSignals(true); + m_monthCombo->blockSignals(true); + m_yearSpin->blockSignals(true); + m_picker->blockSignals(true); + + m_daySpin->setValue(m_daySpin->minValue()); + m_monthCombo->setCurrentItem(0); + m_yearSpin->setValue(m_yearSpin->minValue()); + m_picker->setDate(QDate::currentDate()); + + m_daySpin->blockSignals(false); + m_monthCombo->blockSignals(false); + m_yearSpin->blockSignals(false); + m_picker->blockSignals(false); +} + +void DateWidget::slotShowPicker() { + QRect desk = KGlobalSettings::desktopGeometry(this); + QPoint popupPoint = mapToGlobal(QPoint(0, 0)); + + int dateFrameHeight = m_frame->sizeHint().height(); + if(popupPoint.y() + height() + dateFrameHeight > desk.bottom()) { + popupPoint.setY(popupPoint.y() - dateFrameHeight); + } else { + popupPoint.setY(popupPoint.y() + height()); + } + int dateFrameWidth = m_frame->sizeHint().width(); + if(popupPoint.x() + dateFrameWidth > desk.right()) { + popupPoint.setX(desk.right() - dateFrameWidth); + } + + if(popupPoint.x() < desk.left()) { + popupPoint.setX( desk.left()); + } + if(popupPoint.y() < desk.top()) { + popupPoint.setY(desk.top()); + } + + m_frame->move(popupPoint); + + QDate d = date(); + if(d.isValid()) { + m_picker->setDate(d); + } + + m_frame->show(); +} + +void DateWidget::slotDateSelected(QDate date_) { + if(date_.isValid()) { + setDate(date_); + emit signalModified(); + m_frame->hide(); + } +} + +void DateWidget::slotDateEntered(QDate date_) { + if(date_.isValid()) { + setDate(date_); + emit signalModified(); + } +} + +#include "datewidget.moc" diff --git a/src/gui/datewidget.h b/src/gui/datewidget.h new file mode 100644 index 0000000..93d7bcb --- /dev/null +++ b/src/gui/datewidget.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICODATEWIDGET_H +#define TELLICODATEWIDGET_H + +#include <qspinbox.h> +#include <qdatetime.h> + +class KComboBox; +class KPushButton; +class KDatePicker; + +class QVBox; +class QString; + +namespace Tellico { + namespace GUI { + +class SpinBox : public QSpinBox { +Q_OBJECT + +public: + SpinBox(int min, int max, QWidget *parent); +}; + +/** + * @author Robby Stephenson + */ +class DateWidget : public QWidget { +Q_OBJECT + +public: + DateWidget(QWidget* parent, const char* name = 0); + ~DateWidget() {} + + QDate date() const; + QString text() const; + void setDate(const QDate& date); + void setDate(const QString& date); + void clear(); + +signals: + void signalModified(); + +private slots: + void slotDateChanged(); + void slotShowPicker(); + void slotDateSelected(QDate newDate); + void slotDateEntered(QDate newDate); + +private: + SpinBox* m_daySpin; + KComboBox* m_monthCombo; + SpinBox* m_yearSpin; + KPushButton* m_dateButton; + + QVBox* m_frame; + KDatePicker* m_picker; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/gui/fieldwidget.cpp b/src/gui/fieldwidget.cpp new file mode 100644 index 0000000..6a9fc66 --- /dev/null +++ b/src/gui/fieldwidget.cpp @@ -0,0 +1,201 @@ +/*************************************************************************** + copyright : (C) 2003-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 "fieldwidget.h" +#include "linefieldwidget.h" +#include "parafieldwidget.h" +#include "boolfieldwidget.h" +#include "choicefieldwidget.h" +#include "numberfieldwidget.h" +#include "urlfieldwidget.h" +#include "imagefieldwidget.h" +#include "datefieldwidget.h" +#include "tablefieldwidget.h" +#include "ratingfieldwidget.h" +#include "../field.h" + +#include <kdebug.h> +#include <kurllabel.h> +#include <klocale.h> + +#include <qlayout.h> +#include <qwhatsthis.h> +#include <qregexp.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qstyle.h> +#include <qtimer.h> + +namespace { + // if you change this, update numberfieldwidget, too + const int FIELD_EDIT_WIDGET_INDEX = 2; +} + +using Tellico::GUI::FieldWidget; + +const QRegExp FieldWidget::s_semiColon = QRegExp(QString::fromLatin1("\\s*;\\s*")); + +FieldWidget* FieldWidget::create(Data::FieldPtr field_, QWidget* parent_, const char* name_) { + switch (field_->type()) { + case Data::Field::Line: + return new GUI::LineFieldWidget(field_, parent_, name_); + + case Data::Field::Para: + return new GUI::ParaFieldWidget(field_, parent_, name_); + + case Data::Field::Bool: + return new GUI::BoolFieldWidget(field_, parent_, name_); + + case Data::Field::Number: + return new GUI::NumberFieldWidget(field_, parent_, name_); + + case Data::Field::Choice: + return new GUI::ChoiceFieldWidget(field_, parent_, name_); + + case Data::Field::Table: + case Data::Field::Table2: + return new GUI::TableFieldWidget(field_, parent_, name_); + + case Data::Field::Date: + return new GUI::DateFieldWidget(field_, parent_, name_); + + case Data::Field::URL: + return new GUI::URLFieldWidget(field_, parent_, name_); + + case Data::Field::Image: + return new GUI::ImageFieldWidget(field_, parent_, name_); + + case Data::Field::Rating: + return new GUI::RatingFieldWidget(field_, parent_, name_); + + case Data::Field::ReadOnly: + case Data::Field::Dependent: + kdWarning() << "FieldWidget::create() - read-only/dependent field, this shouldn't have been called" << endl; + return 0; + + default: + kdWarning() << "FieldWidget::create() - unknown field type = " << field_->type() << endl; + return 0; + } +} + +FieldWidget::FieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : QWidget(parent_, name_), m_field(field_) { + QHBoxLayout* l = new QHBoxLayout(this, 2, 2); // parent, margin, spacing + l->addSpacing(4); // add some more space in the columns between widgets + if(QCString(style().name()).lower().find("keramik", 0, false) > -1) { + l->setMargin(1); + } + + Data::Field::Type type = field_->type(); + QString s = i18n("Edit Label", "%1:").arg(field_->title()); + if(type == Data::Field::URL) { + // set URL to null for now + m_label = new KURLLabel(QString::null, s, this); + } else { + m_label = new QLabel(s, this); + } + m_label->setFixedWidth(m_label->sizeHint().width()); + l->addWidget(m_label); + + // expands indicates if the edit widget should expand to full width of widget + m_expands = (type == Data::Field::Line + || type == Data::Field::Para + || type == Data::Field::Number + || type == Data::Field::URL + || type == Data::Field::Table + || type == Data::Field::Table2 + || type == Data::Field::Image + || type == Data::Field::Date); + + m_editMultiple = new QCheckBox(this); + m_editMultiple->setChecked(true); + m_editMultiple->setFixedWidth(m_editMultiple->sizeHint().width()); // don't let it have any extra space + connect(m_editMultiple, SIGNAL(toggled(bool)), SLOT(setEnabled(bool))); + l->addWidget(m_editMultiple); + + QWhatsThis::add(this, field_->description()); + // after letting the subclass get created, insert default value + QTimer::singleShot(0, this, SLOT(insertDefault())); +} + +void FieldWidget::insertDefault() { + setText(m_field->defaultValue()); +} + +bool FieldWidget::isEnabled() { + return widget()->isEnabled(); +} + +void FieldWidget::setEnabled(bool enabled_) { + if(enabled_ == isEnabled()) { + return; + } + + widget()->setEnabled(enabled_); + m_editMultiple->setChecked(enabled_); +} + +int FieldWidget::labelWidth() const { + return m_label->sizeHint().width(); +} + +void FieldWidget::setLabelWidth(int width_) { + m_label->setFixedWidth(width_); +} + +bool FieldWidget::expands() const { + return m_expands; +} + +void FieldWidget::editMultiple(bool show_) { + if(show_ == m_editMultiple->isShown()) { + return; + } + + // FIXME: maybe modified should only be signaled when the button is toggle on + if(show_) { + m_editMultiple->show(); + connect(m_editMultiple, SIGNAL(clicked()), this, SIGNAL(modified())); + } else { + m_editMultiple->hide(); + disconnect(m_editMultiple, SIGNAL(clicked()), this, SIGNAL(modified())); + } + // the widget length needs to be updated since it gets shorter + widget()->updateGeometry(); +} + +void FieldWidget::registerWidget() { + QWidget* w = widget(); + m_label->setBuddy(w); + if(w->isFocusEnabled()) { + setFocusProxy(w); + } + + QHBoxLayout* l = static_cast<QHBoxLayout*>(layout()); + l->insertWidget(FIELD_EDIT_WIDGET_INDEX, w, m_expands ? 1 : 0 /*stretch*/); + if(!m_expands) { + l->insertStretch(FIELD_EDIT_WIDGET_INDEX+1, 1 /*stretch*/); + } + updateGeometry(); +} + +void FieldWidget::updateField(Data::FieldPtr oldField_, Data::FieldPtr newField_) { + m_field = newField_; + m_label->setText(i18n("Edit Label", "%1:").arg(newField_->title())); + updateGeometry(); + QWhatsThis::add(this, newField_->description()); + updateFieldHook(oldField_, newField_); +} + +#include "fieldwidget.moc" diff --git a/src/gui/fieldwidget.h b/src/gui/fieldwidget.h new file mode 100644 index 0000000..dd34ebb --- /dev/null +++ b/src/gui/fieldwidget.h @@ -0,0 +1,91 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef FIELDWIDGET_H +#define FIELDWIDGET_H + +#include "../datavectors.h" + +#include <qwidget.h> +#include <qregexp.h> + +class QLabel; +class QCheckBox; +class QString; + +namespace Tellico { + namespace Data { + class Field; + } + namespace GUI { + +/** + * The FieldWidget class is a box that shows a label, then a widget which depends + * on the field type, and then a checkbox for multiple editing. + * + * @author Robby Stephenson + */ +class FieldWidget : public QWidget { +Q_OBJECT + +public: + FieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~FieldWidget() {} + + Data::FieldPtr field() const { return m_field; } + virtual QString text() const = 0; + virtual void setText(const QString& text) = 0; + + int labelWidth() const; + void setLabelWidth(int width); + bool isEnabled(); + bool expands() const; + void editMultiple(bool show); + // calls updateFieldHook() + void updateField(Data::FieldPtr oldField, Data::FieldPtr newField); + + // only used by LineFieldWidget, really + virtual void addCompletionObjectItem(const QString&) {} + + // factory function + static FieldWidget* create(Data::FieldPtr field, QWidget* parent, const char* name=0); + +public slots: + virtual void insertDefault(); + virtual void clear() = 0; + void setEnabled(bool enabled); + +signals: + virtual void modified(); + +protected: + QLabel* label() { return m_label; } // needed so the URLField can handle clicks on the label + virtual QWidget* widget() = 0; + void registerWidget(); + + // not all widgets have to be updated when the field changes + virtual void updateFieldHook(Data::FieldPtr, Data::FieldPtr) {} + + static const QRegExp s_semiColon; + +private: + Data::FieldPtr m_field; + QLabel* m_label; + QCheckBox* m_editMultiple; + + bool m_expands; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/imagefieldwidget.cpp b/src/gui/imagefieldwidget.cpp new file mode 100644 index 0000000..2cb9b40 --- /dev/null +++ b/src/gui/imagefieldwidget.cpp @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2005-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 "imagefieldwidget.h" +#include "imagewidget.h" +#include "../field.h" +#include "../latin1literal.h" + +using Tellico::GUI::ImageFieldWidget; + +ImageFieldWidget::ImageFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + + m_widget = new ImageWidget(this); + m_widget->setLinkOnlyChecked(field_->property(QString::fromLatin1("link")) == Latin1Literal("true")); + connect(m_widget, SIGNAL(signalModified()), SIGNAL(modified())); + + registerWidget(); +} + +QString ImageFieldWidget::text() const { + return m_widget->id(); +} + +void ImageFieldWidget::setText(const QString& text_) { + blockSignals(true); + m_widget->blockSignals(true); + + m_widget->setImage(text_); + + m_widget->blockSignals(false); + blockSignals(false); +} + +void ImageFieldWidget::clear() { + m_widget->slotClear(); + editMultiple(false); +} + +QWidget* ImageFieldWidget::widget() { + return m_widget; +} + +#include "imagefieldwidget.moc" diff --git a/src/gui/imagefieldwidget.h b/src/gui/imagefieldwidget.h new file mode 100644 index 0000000..b5ebea5 --- /dev/null +++ b/src/gui/imagefieldwidget.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef IMAGEFIELDWIDGET_H +#define IMAGEFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +class QString; + +namespace Tellico { + namespace GUI { + class ImageWidget; + +/** + * @author Robby Stephenson + */ +class ImageFieldWidget : public FieldWidget { +Q_OBJECT + +public: + ImageFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~ImageFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + +private: + ImageWidget* m_widget; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/imagewidget.cpp b/src/gui/imagewidget.cpp new file mode 100644 index 0000000..92f75c3 --- /dev/null +++ b/src/gui/imagewidget.cpp @@ -0,0 +1,257 @@ +/*************************************************************************** + copyright : (C) 2003-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 "imagewidget.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../filehandler.h" +#include "../tellico_debug.h" +#include "../tellico_utils.h" + +#include <kfiledialog.h> +#include <klocale.h> +#include <kbuttonbox.h> +#include <kurldrag.h> +#include <kmessagebox.h> + +#include <qwmatrix.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qdragobject.h> +#include <qapplication.h> // needed for drag distance + +namespace { + static const uint IMAGE_WIDGET_BUTTON_MARGIN = 8; + static const uint IMAGE_WIDGET_IMAGE_MARGIN = 4; + static const uint MAX_UNSCALED_WIDTH = 640; + static const uint MAX_UNSCALED_HEIGHT = 640; +} + +using Tellico::GUI::ImageWidget; + +ImageWidget::ImageWidget(QWidget* parent_, const char* name_) : QWidget(parent_, name_) { + QHBoxLayout* l = new QHBoxLayout(this); + l->setMargin(IMAGE_WIDGET_BUTTON_MARGIN); + m_label = new QLabel(this); + m_label->setSizePolicy(QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored)); + m_label->setFrameStyle(QFrame::Panel | QFrame::Sunken); + m_label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + l->addWidget(m_label, 1); + l->addSpacing(IMAGE_WIDGET_BUTTON_MARGIN); + + QVBoxLayout* boxLayout = new QVBoxLayout(l); + boxLayout->addStretch(1); + + KButtonBox* box = new KButtonBox(this, Vertical); + box->addButton(i18n("Select Image..."), this, SLOT(slotGetImage())); + box->addButton(i18n("Clear"), this, SLOT(slotClear())); + box->layout(); + boxLayout->addWidget(box); + + boxLayout->addSpacing(8); + m_cbLinkOnly = new QCheckBox(i18n("Save link only"), this); + connect(m_cbLinkOnly, SIGNAL(clicked()), SLOT(slotLinkOnlyClicked())); + boxLayout->addWidget(m_cbLinkOnly); + + boxLayout->addStretch(1); + slotClear(); + + // accept image drops + setAcceptDrops(true); +} + +void ImageWidget::setImage(const QString& id_) { + if(id_.isEmpty()) { + slotClear(); + return; + } + m_imageID = id_; + m_pixmap = ImageFactory::pixmap(id_, MAX_UNSCALED_WIDTH, MAX_UNSCALED_HEIGHT); + const bool link = ImageFactory::imageInfo(id_).linkOnly; + m_cbLinkOnly->setChecked(link); + m_cbLinkOnly->setEnabled(link); + // if we're using a link, then the original URL _is_ the id + m_originalURL = link ? id_ : KURL(); + m_scaled = QPixmap(); + scale(); + + update(); +} + +void ImageWidget::setLinkOnlyChecked(bool link_) { + m_cbLinkOnly->setChecked(link_); +} + +void ImageWidget::slotClear() { +// m_image = Data::Image(); + m_imageID = QString(); + m_pixmap = QPixmap(); + m_scaled = m_pixmap; + m_originalURL = KURL(); + + m_label->setPixmap(m_scaled); + m_cbLinkOnly->setChecked(false); + m_cbLinkOnly->setEnabled(true); + update(); + emit signalModified(); +} + +void ImageWidget::scale() { + int ww = m_label->width() - 2*IMAGE_WIDGET_IMAGE_MARGIN; + int wh = m_label->height() - 2*IMAGE_WIDGET_IMAGE_MARGIN; + int pw = m_pixmap.width(); + int ph = m_pixmap.height(); + + if(ww < pw || wh < ph) { + int newWidth, newHeight; + if(pw*wh < ph*ww) { + newWidth = static_cast<int>(static_cast<double>(pw)*wh/static_cast<double>(ph)); + newHeight = wh; + } else { + newWidth = ww; + newHeight = static_cast<int>(static_cast<double>(ph)*ww/static_cast<double>(pw)); + } + + QWMatrix wm; + wm.scale(static_cast<double>(newWidth)/pw, static_cast<double>(newHeight)/ph); + m_scaled = m_pixmap.xForm(wm); + } else { + m_scaled = m_pixmap; + } + m_label->setPixmap(m_scaled); +} + +void ImageWidget::resizeEvent(QResizeEvent *) { + if(m_pixmap.isNull()) { + return; + } + + scale(); + update(); +} + +void ImageWidget::slotGetImage() { + KURL url = KFileDialog::getImageOpenURL(QString::null, this); + if(url.isEmpty() || !url.isValid()) { + return; + } + loadImage(url); +} + +void ImageWidget::slotLinkOnlyClicked() { + if(m_imageID.isEmpty()) { + // nothing to do, it has an empty image; + return; + } + + bool link = m_cbLinkOnly->isChecked(); + // if the user is trying to link and can't before there's no information about the url + // the let him know that + if(link && m_originalURL.isEmpty()) { + KMessageBox::sorry(this, i18n("Saving a link is only possible for newly added images.")); + m_cbLinkOnly->setChecked(false); + return; + } + // need to reset image id to be the original url + // if we're linking only, then we want the image id to be the same as the url + // so it needs to be added to the cache all over again + // probably could do this without downloading the image all over again, + // but I'm not going to do that right now + const QString& id = ImageFactory::addImage(m_originalURL, false, KURL(), link); + // same image, so no need to call setImage + m_imageID = id; + emit signalModified(); +} + +void ImageWidget::mousePressEvent(QMouseEvent* event_) { + // Only interested in LMB + if(event_->button() == Qt::LeftButton) { + // Store the position of the mouse press. + // check if position is inside the label + if(m_label->geometry().contains(event_->pos())) { + m_dragStart = event_->pos(); + } else { + m_dragStart = QPoint(); + } + } +} + +void ImageWidget::mouseMoveEvent(QMouseEvent* event_) { + int delay = QApplication::startDragDistance(); + // Only interested in LMB + if(event_->state() & Qt::LeftButton) { + // only allow drag is the image is non-null, and the drag start point isn't null and the user dragged far enough + if(!m_imageID.isEmpty() && !m_dragStart.isNull() && (m_dragStart - event_->pos()).manhattanLength() > delay) { + const Data::Image& img = ImageFactory::imageById(m_imageID); + if(!img.isNull()) { + QImageDrag* drag = new QImageDrag(img, this); + drag->dragCopy(); + } + } + } +} + +void ImageWidget::dragEnterEvent(QDragEnterEvent* event_) { + event_->accept(KURLDrag::canDecode(event_) || QImageDrag::canDecode(event_) || QTextDrag::canDecode(event_)); +} + +void ImageWidget::dropEvent(QDropEvent* event_) { + QImage image; + KURL::List urls; + QString text; + + GUI::CursorSaver cs(Qt::busyCursor); + if(QImageDrag::decode(event_, image)) { + // Qt reads PNG data by default + const QString& id = ImageFactory::addImage(image, QString::fromLatin1("PNG")); + if(!id.isEmpty() && id != m_imageID) { + setImage(id); + emit signalModified(); + } + } else if(KURLDrag::decode(event_, urls)) { + if(urls.isEmpty()) { + return; + } + // only care about the first one + const KURL& url = urls[0]; + if(url.isEmpty() || !url.isValid()) { + return; + } +// kdDebug() << "ImageWidget::dropEvent() - " << url.prettyURL() << endl; + loadImage(url); + } else if(QTextDrag::decode(event_, text)) { + KURL url(text); + if(url.isEmpty() || !url.isValid()) { + return; + } + loadImage(url); + } +} + +void ImageWidget::loadImage(const KURL& url_) { + const bool link = m_cbLinkOnly->isChecked(); + + GUI::CursorSaver cs; + // if we're linking only, then we want the image id to be the same as the url + const QString& id = ImageFactory::addImage(url_, false, KURL(), link); + if(id != m_imageID) { + setImage(id); + emit signalModified(); + } + // at the end, cause setImage() resets it + m_originalURL = url_; + m_cbLinkOnly->setEnabled(true); +} + +#include "imagewidget.moc" diff --git a/src/gui/imagewidget.h b/src/gui/imagewidget.h new file mode 100644 index 0000000..7780be4 --- /dev/null +++ b/src/gui/imagewidget.h @@ -0,0 +1,78 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOIMAGEWIDGET_H +#define TELLICOIMAGEWIDGET_H + +#include <kurl.h> +#include <qwidget.h> +#include <qpixmap.h> +#include <qpoint.h> + +class QLabel; +class QResizeEvent; +class QMouseEvent; +class QDragEnterEvent; +class QDropEvent; +class QCheckBox; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class ImageWidget : public QWidget { +Q_OBJECT + +public: + ImageWidget(QWidget* parent, const char* name = 0); + virtual ~ImageWidget() {} + + const QString& id() const { return m_imageID; } + void setImage(const QString& id); + void setLinkOnlyChecked(bool l); + +public slots: + void slotClear(); + +signals: + void signalModified(); + +protected: + virtual void resizeEvent(QResizeEvent* ev); + virtual void mousePressEvent(QMouseEvent* ev); + virtual void mouseMoveEvent(QMouseEvent* ev); + virtual void dragEnterEvent(QDragEnterEvent* ev); + virtual void dropEvent(QDropEvent* ev); + +private slots: + void slotGetImage(); + void slotLinkOnlyClicked(); + +private: + void scale(); + void loadImage(const KURL& url); + + QString m_imageID; + QPixmap m_pixmap; + QPixmap m_scaled; + QLabel* m_label; + QCheckBox* m_cbLinkOnly; + KURL m_originalURL; + QPoint m_dragStart; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/kwidgetlister.cpp b/src/gui/kwidgetlister.cpp new file mode 100644 index 0000000..80bf31b --- /dev/null +++ b/src/gui/kwidgetlister.cpp @@ -0,0 +1,178 @@ +/* -*- c++ -*- + kwidgetlister.cpp + + This file is part of libkdenetwork. + Copyright (c) 2001 Marc Mutz <mutz@kde.org> + + This library 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 library 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 library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this library with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include "kwidgetlister.h" + +#include <klocale.h> +#include <kdebug.h> +#include <kpushbutton.h> +#include <kiconloader.h> + +#include <qlayout.h> +#include <qhbox.h> + +#include <assert.h> + +KWidgetLister::KWidgetLister( int minWidgets, int maxWidgets, QWidget *parent, const char* name ) + : QWidget( parent, name ) +{ + mWidgetList.setAutoDelete(TRUE); + + mMinWidgets = QMAX( minWidgets, 1 ); + mMaxWidgets = QMAX( maxWidgets, mMinWidgets + 1 ); + + //--------- the button box + mLayout = new QVBoxLayout(this, 0, 4); + mButtonBox = new QHBox(this); + mButtonBox->setSpacing(4); + mLayout->addWidget( mButtonBox ); + + mBtnMore = new KPushButton( i18n("more widgets","More"), mButtonBox ); + mBtnMore->setIconSet(SmallIconSet(QString::fromLatin1("down"))); + mButtonBox->setStretchFactor( mBtnMore, 0 ); + + mBtnFewer = new KPushButton( i18n("fewer widgets","Fewer"), mButtonBox ); + mBtnFewer->setIconSet(SmallIconSet(QString::fromLatin1("up"))); + mButtonBox->setStretchFactor( mBtnFewer, 0 ); + + QWidget *spacer = new QWidget( mButtonBox ); + mButtonBox->setStretchFactor( spacer, 1 ); + + mBtnClear = new KPushButton( i18n("clear widgets","Clear"), mButtonBox ); + mBtnClear->setIconSet(SmallIconSet(QString::fromLatin1("locationbar_erase"))); + mButtonBox->setStretchFactor( mBtnClear, 0 ); + + //---------- connect everything + connect( mBtnMore, SIGNAL(clicked()), + this, SLOT(slotMore()) ); + connect( mBtnFewer, SIGNAL(clicked()), + this, SLOT(slotFewer()) ); + connect( mBtnClear, SIGNAL(clicked()), + this, SLOT(slotClear()) ); + + enableControls(); +} + +KWidgetLister::~KWidgetLister() +{ +} + +void KWidgetLister::slotMore() +{ + // the class should make certain that slotMore can't + // be called when mMaxWidgets are on screen. + assert( (int)mWidgetList.count() < mMaxWidgets ); + + addWidgetAtEnd(); + // adjustSize(); + enableControls(); +} + +void KWidgetLister::slotFewer() +{ + // the class should make certain that slotFewer can't + // be called when mMinWidgets are on screen. + assert( (int)mWidgetList.count() > mMinWidgets ); + + removeLastWidget(); + // adjustSize(); + enableControls(); +} + +void KWidgetLister::slotClear() +{ + setNumberOfShownWidgetsTo( mMinWidgets ); + + // clear remaining widgets + QPtrListIterator<QWidget> it( mWidgetList ); + for ( it.toFirst() ; it.current() ; ++it ) + clearWidget( (*it) ); + + // adjustSize(); + enableControls(); + emit clearWidgets(); +} + +void KWidgetLister::addWidgetAtEnd(QWidget *w) +{ + if (!w) w = this->createWidget(this); + + mLayout->insertWidget( mLayout->findWidget( mButtonBox ), w ); + mWidgetList.append( w ); + w->show(); + enableControls(); + emit widgetAdded(); + emit widgetAdded(w); +} + +void KWidgetLister::removeLastWidget() +{ + // The layout will take care that the + // widget is removed from screen, too. + mWidgetList.removeLast(); + enableControls(); + emit widgetRemoved(); +} + +void KWidgetLister::clearWidget( QWidget* /*aWidget*/ ) +{ +} + +QWidget* KWidgetLister::createWidget( QWidget* parent ) +{ + return new QWidget( parent ); +} + +void KWidgetLister::setNumberOfShownWidgetsTo( int aNum ) +{ + int superfluousWidgets = QMAX( (int)mWidgetList.count() - aNum, 0 ); + int missingWidgets = QMAX( aNum - (int)mWidgetList.count(), 0 ); + + // remove superfluous widgets + for ( ; superfluousWidgets ; superfluousWidgets-- ) + removeLastWidget(); + + // add missing widgets + for ( ; missingWidgets ; missingWidgets-- ) + addWidgetAtEnd(); +} + +void KWidgetLister::enableControls() +{ + int count = mWidgetList.count(); + bool isMaxWidgets = ( count >= mMaxWidgets ); + bool isMinWidgets = ( count <= mMinWidgets ); + + mBtnMore->setEnabled( !isMaxWidgets ); + mBtnFewer->setEnabled( !isMinWidgets ); +} + +#include "kwidgetlister.moc" diff --git a/src/gui/kwidgetlister.h b/src/gui/kwidgetlister.h new file mode 100644 index 0000000..e02b54c --- /dev/null +++ b/src/gui/kwidgetlister.h @@ -0,0 +1,150 @@ +/* -*- c++ -*- + kwidgetlister.h + + This file is part of libkdenetwork. + Copyright (c) 2001 Marc Mutz <mutz@kde.org> + + This library 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 library 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 library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this library with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef _KWIDGETLISTER_H_ +#define _KWIDGETLISTER_H_ + +#include <qwidget.h> +#include <qptrlist.h> + +class KPushButton; +class QVBoxLayout; +class QHBox; + +/** Simple widget that nonetheless does a lot of the dirty work for + the filter edit widgets (@ref KMSearchpatternEdit and @ref + KMFilterActionEdit). It provides a growable and shrinkable area + where widget may be displayed in rows. Widgets can be added by + hitting the provided 'More' button, removed by the 'Fewer' button + and cleared (e.g. reset, if an derived class implements that and + removed for all but @ref mMinWidgets). + + To use this widget, derive from it with the template changed to + the type of widgets this class should list. Then reimplement @ref + addWidgetAtEnd, @ref removeLastWidget, calling the original + implementation as necessary. Instantate an object of the class and + put it in your dialog. + + @short Widget that manages a list of other widgets (incl. 'more', 'fewer' and 'clear' buttons). + @author Marc Mutz <Marc@Mutz.com> + @see KMSearchPatternEdit::WidgetLister KMFilterActionEdit::WidgetLister + +*/ + +class KWidgetLister : public QWidget +{ + Q_OBJECT +public: + KWidgetLister( int minWidgets=1, int maxWidgets=8, QWidget* parent=0, const char* name=0 ); + virtual ~KWidgetLister(); + +protected slots: + /** Called whenever the user clicks on the 'more' button. + Reimplementations should call this method, because this + implementation does all the dirty work with adding the widgets + to the layout (through @ref addWidgetAtEnd) and enabling/disabling + the control buttons. */ + virtual void slotMore(); + /** Called whenever the user clicks on the 'fewer' button. + Reimplementations should call this method, because this + implementation does all the dirty work with removing the widgets + from the layout (through @ref removelastWidget) and + enabling/disabling the control buttons. */ + virtual void slotFewer(); + /** Called whenever the user clicks on the 'clear' button. + Reimplementations should call this method, because this + implementation does all the dirty work with removing all but + @ref mMinWidets widgets from the layout and enabling/disabling + the control buttons. */ + virtual void slotClear(); + + + +protected: + /** Adds a single widget. Doesn't care if there are already @ref + mMaxWidgets on screen and whether it should enable/disable any + controls. It simply does what it is asked to do. You want to + reimplement this method if you want to initialize the the widget + when showing it on screen. Make sure you call this + implementaion, though, since you cannot put the widget on screen + from derived classes (@p mLayout is private). + Make sure the parent of the QWidget to add is this KWidgetLister. */ + virtual void addWidgetAtEnd(QWidget *w =0); + /** Removes a single (always the last) widget. Doesn't care if there + are still only @ref mMinWidgets left on screen and whether it + should enable/disable any controls. It simply does what it is + asked to do. You want to reimplement this method if you want to + save the the widget's state before removing it from screen. Make + sure you call this implementaion, though, since you should not + remove the widget from screen from derived classes. */ + virtual void removeLastWidget(); + /** Called to clear a given widget. The default implementation does + nothing. */ + virtual void clearWidget( QWidget* ); + /** Because QT 2.x does not support signals/slots in template + classes, we are forced to emulate this by forcing the + implementers of subclasses of KWidgetLister to reimplement this + function which replaces the "@p new @p T" call. */ + virtual QWidget* createWidget( QWidget *parent ); + /** Sets the number of widgets on scrren to exactly @p aNum. Doesn't + check if @p aNum is inside the range @p + [mMinWidgets,mMaxWidgets]. */ + virtual void setNumberOfShownWidgetsTo( int aNum ); + /** The list of widgets. Note that this list is set to auto-delete, + meaning that widgets that are removed from the screen by either + @ref slotFewer or @ref slotClear will be destroyed! */ + QPtrList<QWidget> mWidgetList; + /** The minimum number of widgets that are to stay on screen. */ + int mMinWidgets; + /** The maximum number of widgets that are to be shown on screen. */ + int mMaxWidgets; + +signals: + /** This signal is emitted whenever a widget was added */ + void widgetAdded(); + /** This signal is emitted whenever a widget was added */ + void widgetAdded(QWidget *); + /** This signal is emitted whenever a widget was removed */ + void widgetRemoved(); + /** This signal is emitted whenever the clear button is clicked */ + void clearWidgets(); + +private: + void enableControls(); + + KPushButton *mBtnMore, *mBtnFewer, *mBtnClear; + QVBoxLayout *mLayout; + QHBox *mButtonBox; +}; + + + +#endif /* _KWIDGETLISTER_H_ */ diff --git a/src/gui/lineedit.cpp b/src/gui/lineedit.cpp new file mode 100644 index 0000000..6248880 --- /dev/null +++ b/src/gui/lineedit.cpp @@ -0,0 +1,150 @@ +/*************************************************************************** + copyright : (C) 2005-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 "lineedit.h" + +#include <kstdaction.h> +#include <kactioncollection.h> +#include <kspell.h> + +#include <qapplication.h> +#include <qpainter.h> +#include <qpopupmenu.h> + +using Tellico::GUI::LineEdit; + +LineEdit::LineEdit(QWidget* parent_, const char* name_) : KLineEdit(parent_, name_) + , m_drawHint(false) + , m_allowSpellCheck(false) + , m_enableSpellCheck(true) + , m_spell(0) { + m_spellAction = KStdAction::spelling(this, SLOT(slotCheckSpelling()), new KActionCollection(this)); +} + +void LineEdit::clear() { + KLineEdit::clear(); + m_drawHint = true; + repaint(); +} + +void LineEdit::setText(const QString& text_) { + m_drawHint = text_.isEmpty(); + repaint(); + KLineEdit::setText(text_); +} + +void LineEdit::setHint(const QString& hint_) { + m_hint = hint_; + m_drawHint = text().isEmpty(); + repaint(); +} + +void LineEdit::focusInEvent(QFocusEvent* event_) { + if(m_drawHint) { + m_drawHint = false; + repaint(); + } + KLineEdit::focusInEvent(event_); +} + +void LineEdit::focusOutEvent(QFocusEvent* event_) { + if(text().isEmpty()) { + m_drawHint = true; + repaint(); + } + KLineEdit::focusOutEvent(event_); +} + +void LineEdit::drawContents(QPainter* painter_) { + // draw the regular line edit first + KLineEdit::drawContents(painter_); + + // no need to draw anything else if in focus or no hint + if(hasFocus() || !m_drawHint || m_hint.isEmpty() || !text().isEmpty()) { + return; + } + + // save current pen + QPen oldPen = painter_->pen(); + + // follow lead of kdepim and amarok, use disabled text color + painter_->setPen(palette().color(QPalette::Disabled, QColorGroup::Text)); + + QRect rect = contentsRect(); + // again, follow kdepim and amarok lead, and pad by 2 pixels + rect.rLeft() += 2; + painter_->drawText(rect, AlignAuto | AlignVCenter, m_hint); + + // reset pen + painter_->setPen(oldPen); +} + +QPopupMenu* LineEdit::createPopupMenu() { + QPopupMenu* popup = KLineEdit::createPopupMenu(); + + if(!popup) { + return popup; + } + + if(m_allowSpellCheck && echoMode() == QLineEdit::Normal && !isReadOnly()) { + popup->insertSeparator(); + + m_spellAction->plug(popup); + m_spellAction->setEnabled(m_enableSpellCheck && !text().isEmpty()); + } + + return popup; +} + +void LineEdit::slotCheckSpelling() { + delete m_spell; + // re-use the action string to get translations + m_spell = new KSpell(this, m_spellAction->text(), + this, SLOT(slotSpellCheckReady(KSpell*)), 0, true, true); + + connect(m_spell, SIGNAL(death()), + SLOT(spellCheckerFinished())); + connect(m_spell, SIGNAL(misspelling( const QString &, const QStringList &, unsigned int)), + SLOT(spellCheckerMisspelling( const QString &, const QStringList &, unsigned int))); + connect(m_spell, SIGNAL(corrected(const QString &, const QString &, unsigned int)), + SLOT(spellCheckerCorrected(const QString &, const QString &, unsigned int))); +} + +void LineEdit::slotSpellCheckReady(KSpell* spell) { + spell->check(text()); + connect(spell, SIGNAL(done(const QString&)), SLOT(slotSpellCheckDone(const QString&))); +} + +void LineEdit::slotSpellCheckDone(const QString& newText) { + if(newText != text()) { + setText(newText); + } +} + +void LineEdit::spellCheckerFinished() { + delete m_spell; + m_spell = 0; +} + +void LineEdit::spellCheckerMisspelling(const QString &text, const QStringList&, unsigned int pos) { + setSelection(pos, pos + text.length()); +} + +void LineEdit::spellCheckerCorrected(const QString& oldWord, const QString& newWord, unsigned int pos) { + if(oldWord != newWord) { + setSelection(pos, pos + oldWord.length()); + insert(newWord); + } +} + +#include "lineedit.moc" diff --git a/src/gui/lineedit.h b/src/gui/lineedit.h new file mode 100644 index 0000000..af7d81b --- /dev/null +++ b/src/gui/lineedit.h @@ -0,0 +1,72 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUILINEEDIT_H +#define TELLICO_GUILINEEDIT_H + +#include <klineedit.h> + +#include <qstring.h> + +class KAction; +class KSpell; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class LineEdit : public KLineEdit { +Q_OBJECT + +public: + LineEdit(QWidget* parent = 0, const char* name = 0); + + virtual void setText(const QString& text); + void setHint(const QString& hint); + + // by default, spell check is not allowed, and no popupmenu item is created + void setAllowSpellCheck(bool b) { m_allowSpellCheck = b; } + // spell check may be allowed but disabled + void setEnableSpellCheck(bool b) { m_enableSpellCheck = b; } + +public slots: + void clear(); + +protected: + virtual void focusInEvent(QFocusEvent* event); + virtual void focusOutEvent(QFocusEvent* event); + virtual void drawContents(QPainter* painter); + virtual QPopupMenu* createPopupMenu(); + +private slots: + void slotCheckSpelling(); + void slotSpellCheckReady(KSpell* spell); + void slotSpellCheckDone(const QString& text); + void spellCheckerMisspelling(const QString& text, const QStringList&, unsigned int pos); + void spellCheckerCorrected(const QString& oldText, const QString& newText, unsigned int pos); + void spellCheckerFinished(); + +private: + QString m_hint; + bool m_drawHint; + KAction* m_spellAction; + bool m_allowSpellCheck; + bool m_enableSpellCheck; + KSpell* m_spell; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/gui/linefieldwidget.cpp b/src/gui/linefieldwidget.cpp new file mode 100644 index 0000000..1535832 --- /dev/null +++ b/src/gui/linefieldwidget.cpp @@ -0,0 +1,90 @@ +/*************************************************************************** + copyright : (C) 2005-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 "linefieldwidget.h" +#include "../field.h" +#include "../isbnvalidator.h" +#include "../fieldcompletion.h" +#include "../latin1literal.h" +#include "../tellico_kernel.h" +#include "../gui/lineedit.h" + +using Tellico::GUI::LineFieldWidget; + +LineFieldWidget::LineFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + + m_lineEdit = new GUI::LineEdit(this); + m_lineEdit->setAllowSpellCheck(true); + m_lineEdit->setEnableSpellCheck(field_->formatFlag() != Data::Field::FormatName); + connect(m_lineEdit, SIGNAL(textChanged(const QString&)), SIGNAL(modified())); + + registerWidget(); + + if(field_->flags() & Data::Field::AllowCompletion) { + FieldCompletion* completion = new FieldCompletion(field_->flags() & Data::Field::AllowMultiple); + completion->setItems(Kernel::self()->valuesByFieldName(field_->name())); + completion->setIgnoreCase(true); + m_lineEdit->setCompletionObject(completion); + m_lineEdit->setAutoDeleteCompletionObject(true); + } + + if(field_->name() == Latin1Literal("isbn")) { + m_lineEdit->setValidator(new ISBNValidator(this)); + } +} + +QString LineFieldWidget::text() const { + QString text = m_lineEdit->text(); + if(field()->flags() & Data::Field::AllowMultiple) { + text.replace(s_semiColon, QString::fromLatin1("; ")); + } + return text.stripWhiteSpace(); +} + +void LineFieldWidget::setText(const QString& text_) { + blockSignals(true); + m_lineEdit->blockSignals(true); + m_lineEdit->setText(text_); + m_lineEdit->blockSignals(false); + blockSignals(false); +} + +void LineFieldWidget::clear() { + m_lineEdit->clear(); + editMultiple(false); +} + +void LineFieldWidget::addCompletionObjectItem(const QString& text_) { + m_lineEdit->completionObject()->addItem(text_); +} + +void LineFieldWidget::updateFieldHook(Data::FieldPtr oldField_, Data::FieldPtr newField_) { + bool wasComplete = (oldField_->flags() & Data::Field::AllowCompletion); + bool isComplete = (newField_->flags() & Data::Field::AllowCompletion); + if(!wasComplete && isComplete) { + FieldCompletion* completion = new FieldCompletion(isComplete); + completion->setItems(Kernel::self()->valuesByFieldName(newField_->name())); + completion->setIgnoreCase(true); + m_lineEdit->setCompletionObject(completion); + m_lineEdit->setAutoDeleteCompletionObject(true); + } else if(wasComplete && !isComplete) { + m_lineEdit->completionObject()->clear(); + } +} + +QWidget* LineFieldWidget::widget() { + return m_lineEdit; +} + +#include "linefieldwidget.moc" diff --git a/src/gui/linefieldwidget.h b/src/gui/linefieldwidget.h new file mode 100644 index 0000000..70dc175 --- /dev/null +++ b/src/gui/linefieldwidget.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef LINEFIELDWIDGET_H +#define LINEFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +namespace Tellico { + namespace GUI { + class LineEdit; + +/** + * @author Robby Stephenson + */ +class LineFieldWidget : public FieldWidget { +Q_OBJECT + +public: + LineFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~LineFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + virtual void addCompletionObjectItem(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +private: + GUI::LineEdit* m_lineEdit; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/listboxtext.cpp b/src/gui/listboxtext.cpp new file mode 100644 index 0000000..8ec6fa2 --- /dev/null +++ b/src/gui/listboxtext.cpp @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2005-2006 by Robby Stephenson + email : $EMAIL + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "listboxtext.h" +#include "../tellico_utils.h" + +#include <qpainter.h> + +using Tellico::GUI::ListBoxText; + +ListBoxText::ListBoxText(QListBox* listbox_, const QString& text_) + : QListBoxText(listbox_, text_), m_colored(false) { +} + +ListBoxText::ListBoxText(QListBox* listbox_, const QString& text_, QListBoxItem* after_) + : QListBoxText(listbox_, text_, after_), m_colored(false) { +} + +int ListBoxText::width(const QListBox* listbox_) const { + if(m_colored) { + QFont font = listbox_->font(); + font.setBold(true); + font.setItalic(true); + QFontMetrics fm(font); + return fm.width(text()) + 6; + } else { + return QListBoxText::width(listbox_); + } +} + +// I don't want the height to change when colored +// so all the items are at the same level for multi-column boxes +int ListBoxText::height(const QListBox* listbox_) const { + return QListBoxText::height(listbox_); +} + +void ListBoxText::setColored(bool colored_) { + if(m_colored != colored_) { + m_colored = colored_; + listBox()->triggerUpdate(false); + } +} + +void ListBoxText::setText(const QString& text_) { + QListBoxText::setText(text_); + listBox()->triggerUpdate(true); +} + +// mostly copied from QListBoxText::paint() in Qt 3.1.1 +void ListBoxText::paint(QPainter* painter_) { + if(m_colored) { + QFont font = painter_->font(); + font.setBold(true); + font.setItalic(true); + painter_->setFont(font); + if(!isSelected()) { + painter_->setPen(Tellico::contrastColor); + } + } + int itemHeight = height(listBox()); + QFontMetrics fm = painter_->fontMetrics(); + int yPos = ((itemHeight - fm.height()) / 2) + fm.ascent(); + painter_->drawText(3, yPos, text()); +} diff --git a/src/gui/listboxtext.h b/src/gui/listboxtext.h new file mode 100644 index 0000000..3cfe2db --- /dev/null +++ b/src/gui/listboxtext.h @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_LISTBOXTEXT_H +#define TELLICO_GUI_LISTBOXTEXT_H + +#include <qlistbox.h> + +namespace Tellico { + namespace GUI { + +/** + * ListBoxText subclasses QListBoxText so that @ref setText() can be made public, + * and the font color can be changed + * + * @author Robby Stephenson + */ +class ListBoxText : public QListBoxText { +public: + ListBoxText(QListBox* listbox, const QString& text); + ListBoxText(QListBox* listbox, const QString& text, QListBoxItem* after); + + virtual int width(const QListBox* box) const; + virtual int height(const QListBox* box) const; + + bool isColored() const { return m_colored; } + void setColored(bool colored); + void setText(const QString& text); + +protected: + virtual void paint(QPainter* painter); + +private: + bool m_colored; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/listview.cpp b/src/gui/listview.cpp new file mode 100644 index 0000000..d93174c --- /dev/null +++ b/src/gui/listview.cpp @@ -0,0 +1,347 @@ +/*************************************************************************** + 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 "listview.h" +#include "../controller.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" + +#include <kapplication.h> + +#include <qpainter.h> +#include <qpixmap.h> +#include <qheader.h> + +using Tellico::GUI::ListView; +using Tellico::GUI::ListViewItem; + +ListView::ListView(QWidget* parent_, const char* name_) : KListView(parent_, name_/*=0*/), + m_sortStyle(SortByText), m_isClear(true) { + setSelectionMode(QListView::Extended); + connect(this, SIGNAL(selectionChanged()), + SLOT(slotSelectionChanged())); + connect(this, SIGNAL(doubleClicked(QListViewItem*)), + SLOT(slotDoubleClicked(QListViewItem*))); +#if !KDE_IS_VERSION(3,3,90) + m_shadeSortColumn = false; + // call it once to initialize + slotUpdateColors(); +#endif + connect(kapp, SIGNAL(kdisplayPaletteChanged()), SLOT(slotUpdateColors())); + m_comparisons.setAutoDelete(true); +} + +ListView::~ListView() { +} + +void ListView::clearSelection() { + if(m_selectedItems.isEmpty()) { + // nothing to do; + return; + } + bool b = signalsBlocked(); + blockSignals(true); + selectAll(false); + blockSignals(b); + emit selectionChanged(); +} + +void ListView::updateSelected(ListViewItem* item_, bool selected_) { + if(selected_) { + m_selectedItems.append(item_); + } else { + m_selectedItems.removeRef(item_); + } +} + +bool ListView::isSelectable(ListViewItem* item_) const { + // don't allow hidden items to be selected + if(!item_->isVisible()) { + return false; + } + + // selecting multiple items is ok + // only when parent is open. Be careful to check for existence of parent + if(item_->parent() && !item_->parent()->isOpen()) { + return false; + } + + // just selecting a single item is always ok + if(m_selectedItems.isEmpty()) { + return true; + } + + // not allowed is something other than an entry is selected and current is entry + if(m_selectedItems.getFirst()->isEntryItem() != item_->isEntryItem()) { + return false; + } + + return true; +} + +int ListView::firstVisibleColumn() const { + int col = 0; + while(col < columns() && columnWidth(header()->mapToSection(col)) == 0) { + ++col; + } + if(col == columns()) { + return -1; + } + return header()->mapToSection(col); +} + +int ListView::lastVisibleColumn() const { + int col = columns()-1; + while(col < columns() && columnWidth(header()->mapToSection(col)) == 0) { + --col; + } + if(col == columns()) { + return -1; + } + return header()->mapToSection(col); +} + +void ListView::setColumnText(int column, const QString& label) { + ListViewComparison* comp = m_comparisons.take(columnText(column)); + KListView::setColumnText(column, label); + if(comp) { + m_comparisons.insert(columnText(column), comp); + } +} + +void ListView::setComparison(int column, ListViewComparison* comp) { + if(comp) { + m_comparisons.replace(columnText(column), comp); + } +} + +void ListView::removeComparison(int column) { + m_comparisons.remove(columnText(column)); +} + +void ListView::clearComparisons() { + m_comparisons.clear(); +} + +int ListView::compare(int col, const GUI::ListViewItem* item1, GUI::ListViewItem* item2, bool asc) { + if(col >= 0 && col < static_cast<int>(m_comparisons.count())) { + ListViewComparison* com = m_comparisons.find(columnText(col)); + if(com) { + return com->compare(col, item1, item2, asc); + } + } + return 0; +} + +#if !KDE_IS_VERSION(3,3,90) +void ListView::setShadeSortColumn(bool shade_) { + if(m_shadeSortColumn != shade_) { + m_shadeSortColumn = shade_; + repaint(); + } +} +#endif + +void ListView::slotUpdateColors() { +#if !KDE_IS_VERSION(3,3,90) + m_backColor2 = viewport()->colorGroup().base(); + if(m_backColor2 == Qt::black) { + m_backColor2 = QColor(50, 50, 50); // dark gray + } else { + int h,s,v; + m_backColor2.hsv(&h, &s, &v); + if(v > 175) { + m_backColor2 = m_backColor2.dark(105); + } else { + m_backColor2 = m_backColor2.light(120); + } + } + + m_altColor2 = alternateBackground(); + if(m_altColor2 == Qt::black) { + m_altColor2 = QColor(50, 50, 50); // dark gray + } else { + int h,s,v; + m_altColor2.hsv(&h, &s, &v); + if(v > 175) { + m_altColor2 = m_altColor2.dark(105); + } else { + m_altColor2 = m_altColor2.light(120); + } + } +#endif + Tellico::updateContrastColor(viewport()->colorGroup()); + repaint(); +} + +void ListView::slotSelectionChanged() { + if(m_selectedItems.isEmpty()) { + if(m_isClear) { + return; // nothing to do + } + m_isClear = true; + Controller::self()->slotClearSelection(); + return; + } + m_isClear = false; + + Data::EntryVec entries; + // now just find all the children or grandchildren that are entry items + for(GUI::ListViewItemListIt it(m_selectedItems); it.current(); ++it) { + Data::EntryVec more = it.current()->entries(); + for(Data::EntryVecIt entry = more.begin(); entry != more.end(); ++entry) { + if(!entries.contains(entry)) { + entries.append(entry); + } + } + } +// Controller::self()->slotUpdateCurrent(entries); // just update current, don't change selection + Controller::self()->slotUpdateSelection(this, entries); +} + +void ListView::slotDoubleClicked(QListViewItem* item_) { + if(!item_) { + return; + } + + // if it has children, just open it + // but some items delay children creation + if(static_cast<ListViewItem*>(item_)->realChildCount() > 0) { + item_->setOpen(!item_->isOpen()); + } + + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(item_); + item->doubleClicked(); +} + +void ListView::drawContentsOffset(QPainter* p, int ox, int oy, int cx, int cy, int cw, int ch) { + bool oldUpdatesEnabled = isUpdatesEnabled(); + setUpdatesEnabled(false); + KListView::drawContentsOffset(p, ox, oy, cx, cy, cw, ch); + setUpdatesEnabled(oldUpdatesEnabled); +} + +/* ****************** ListViewItem ********************* */ + +ListViewItem::~ListViewItem() { + // I think there's a bug in qt where the children of this item are deleted after the item itself + // as a result, there is no listView() pointer for the children, that obvious causes + // a problem with updating the selection. So we MUST call clear() here ourselves! + clear(); + // be sure to remove from selected list when it's deleted + ListView* lv = listView(); + if(lv) { + lv->updateSelected(this, false); + } +} + +void ListViewItem::clear() { + QListViewItem* item = firstChild(); + while(item) { + delete item; + item = firstChild(); + } +} + +int ListViewItem::compare(QListViewItem* item_, int col_, bool asc_) const { + int res = compareWeight(item_, col_, asc_); + if(res != 0) { + return res; + } + res = listView()->compare(col_, this, static_cast<GUI::ListViewItem*>(item_), asc_); + return res == 0 ? KListViewItem::compare(item_, col_, asc_) : res; +} + +int ListViewItem::compareWeight(QListViewItem* item_, int col_, bool asc_) const { + Q_UNUSED(col_); + // I want the sorting to be independent of sort order + GUI::ListViewItem* i = static_cast<GUI::ListViewItem*>(item_); + int res = 0; + if(m_sortWeight < i->sortWeight()) { + res = -1; + } else if(m_sortWeight > i->sortWeight()) { + res = 1; + } + if(asc_) { + res *= -1; // reverse, heavier weights will come first always + } + return res; +} + +void ListViewItem::setSelected(bool s_) { + ListView* lv = listView(); + if(!lv) { + return; + } + if(s_ && !lv->isSelectable(this)) { + return; + } + if(s_ != isSelected()) { + lv->updateSelected(this, s_); + KListViewItem::setSelected(s_); + } +} + +QColor ListViewItem::backgroundColor(int column_) { +#if KDE_IS_VERSION(3,3,90) + return KListViewItem::backgroundColor(column_); +#else + ListView* view = listView(); + if(view->columns() > 1 && view->shadeSortColumn() && column_ == view->sortColumn()) { + return isAlternate() ? view->alternateBackground2() : view->background2(); + } + return isAlternate() ? view->alternateBackground() : view->viewport()->colorGroup().base(); +#endif +} + +void ListViewItem::paintCell(QPainter* p_, const QColorGroup& cg_, + int column_, int width_, int align_) { + // taken from klistview.cpp + // I can't call KListViewItem::paintCell since KListViewItem::backgroundCOlor(int) is + // not virtual. I need to be sure to call ListViewItem::backgroundColor(int); + QColorGroup cg = cg_; + const QPixmap* pm = listView()->viewport()->backgroundPixmap(); + if(pm && !pm->isNull()) { + cg.setBrush(QColorGroup::Base, QBrush(backgroundColor(column_), *pm)); + QPoint o = p_->brushOrigin(); + p_->setBrushOrigin(o.x()-listView()->contentsX(), o.y()-listView()->contentsY()); + } else { + cg.setColor(listView()->viewport()->backgroundMode() == Qt::FixedColor ? + QColorGroup::Background : QColorGroup::Base, + backgroundColor(column_)); + } + + // don't call KListViewItem::paintCell() since that also does alternate painting, etc... + QListViewItem::paintCell(p_, cg, column_, width_, align_); + + // borrowed from amarok, draw line to left of cell + if(!isSelected()) { + p_->setPen(QPen(listView()->alternateBackground(), 0, Qt::SolidLine)); + p_->drawLine(width_-1, 0, width_-1, height()-1); + } +} + +Tellico::Data::EntryVec ListViewItem::entries() const { + Data::EntryVec entries; + for(QListViewItem* child = firstChild(); child; child = child->nextSibling()) { + Data::EntryVec more = static_cast<GUI::ListViewItem*>(child)->entries(); + for(Data::EntryVecIt entry = more.begin(); entry != more.end(); ++entry) { + if(!entries.contains(entry)) { + entries.append(entry); + } + } + } + return entries; +} + +#include "listview.moc" diff --git a/src/gui/listview.h b/src/gui/listview.h new file mode 100644 index 0000000..4c5ade7 --- /dev/null +++ b/src/gui/listview.h @@ -0,0 +1,180 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_LISTVIEW_H +#define TELLICO_LISTVIEW_H + +#include "../datavectors.h" +#include "../listviewcomparison.h" + +#include <klistview.h> +#include <kdeversion.h> + +#include <qdict.h> + +namespace Tellico { + class ListViewComparison; + namespace GUI { + +class ListViewItem; +typedef QPtrList<ListViewItem> ListViewItemList; +typedef QPtrListIterator<ListViewItem> ListViewItemListIt; + +/** + * A ListView keeps track of the selected items and allows subclasses to determine + * whether child items may be selected or not. In addition, it provides alternating + * background colors and shaded sort columns. + * + * @see GroupView + * @see DetailedListView + * + * @author Robby Stephenson + */ +class ListView : public KListView { +Q_OBJECT + +friend class ListViewItem; // needed so the ListViewItem d'tor can update selection list + +public: + enum SortStyle { + SortByText = 0, // don't change these values, they're saved as ints in the config + SortByCount = 1 + }; + + ListView(QWidget* parent, const char* name = 0); + virtual ~ListView(); + + void clearSelection(); + /** + * Returns a list of the currently selected items; + * + * @return The list of selected items + */ + const ListViewItemList& selectedItems() const { return m_selectedItems; } + /** + * Used to determine whether an item may be selected without having to setSelectable on + * every child item. + * + * @return Whether the item may be selected + */ + virtual bool isSelectable(ListViewItem*) const; + SortStyle sortStyle() const { return m_sortStyle; } + void setSortStyle(SortStyle style) { m_sortStyle = style; } + + int firstVisibleColumn() const; + int lastVisibleColumn() const; + + virtual void setColumnText(int column, const QString& label); + + void setComparison(int column, ListViewComparison* comp); + void removeComparison(int column); + void clearComparisons(); + virtual int compare(int col, const GUI::ListViewItem* item1, GUI::ListViewItem* item2, bool asc); + +#if !KDE_IS_VERSION(3,3,90) + // taken from KDE bug 59791 + void setShadeSortColumn(bool shade_); + bool shadeSortColumn() const { return m_shadeSortColumn; } + const QColor& background2() const { return m_backColor2; } + const QColor& alternateBackground2() const { return m_altColor2; } +#endif + +protected slots: + /** + * Handles everything when an item is selected. The proper signal is emitted, depending + * on whether the item refers to a collection, a group, or a entry. + */ + virtual void slotSelectionChanged(); + +protected: + virtual void drawContentsOffset(QPainter* p, int ox, int oy, int cx, int cy, int cw, int ch); + +private slots: + void slotUpdateColors(); + void slotDoubleClicked(QListViewItem* item); + +private: + /** + * Updates the pointer list. + * + * @param item The item being selected or deselected + * @param s Selected or not + */ + void updateSelected(ListViewItem* item, bool s); + + SortStyle m_sortStyle; + bool m_isClear; + ListViewItemList m_selectedItems; + QDict<ListViewComparison> m_comparisons; +#if !KDE_IS_VERSION(3,3,90) + bool m_shadeSortColumn; + QColor m_backColor2; + QColor m_altColor2; +#endif +}; + +/** + * The ListViewItem keeps track of what kind of specialized listview item it is, as + * well as taking care of the selection tracking. + * + * @author Robby Stephenson + */ +class ListViewItem : public KListViewItem { +public: + ListViewItem(ListView* parent) : KListViewItem(parent), m_sortWeight(-1) {} + ListViewItem(ListView* parent, QListViewItem* after) : KListViewItem(parent, after), m_sortWeight(-1) {} + ListViewItem(ListViewItem* parent) : KListViewItem(parent), m_sortWeight(-1) {} + ListViewItem(ListView* parent, const QString& text) : KListViewItem(parent, text), m_sortWeight(-1) {} + ListViewItem(ListViewItem* parent, const QString& text) : KListViewItem(parent, text), m_sortWeight(-1) {} + virtual ~ListViewItem(); + + virtual int realChildCount() const { return childCount(); } + virtual void clear(); + + virtual bool isEntryGroupItem() const { return false; } + virtual bool isEntryItem() const { return false; } + virtual bool isFilterItem() const { return false; } + virtual bool isBorrowerItem() const { return false; } + virtual bool isLoanItem() const { return false; } + + int sortWeight() const { return m_sortWeight; } + void setSortWeight(int w) { m_sortWeight = w; } + virtual int compare(QListViewItem* item, int col, bool ascending) const; + + virtual void setSelected(bool selected); + /** + * Returns the background color for the column, which depends on whether the item + * is an alternate row and whether the column is selected. + * + * @param column The column number + * @param alternate The alternate row color can be forced + */ + virtual QColor backgroundColor(int column); // not virtual in KListViewItem!!! + virtual void paintCell(QPainter* painter, const QColorGroup& colorGroup, + int column, int width, int alignment); + + ListView* listView () const { return static_cast<ListView*>(KListViewItem::listView()); } + + virtual void doubleClicked() {} + virtual Data::EntryVec entries() const; + +private: + int compareWeight(QListViewItem* item, int col, bool ascending) const; + + int m_sortWeight; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/numberfieldwidget.cpp b/src/gui/numberfieldwidget.cpp new file mode 100644 index 0000000..027b1e9 --- /dev/null +++ b/src/gui/numberfieldwidget.cpp @@ -0,0 +1,143 @@ +/*************************************************************************** + copyright : (C) 2005-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 "numberfieldwidget.h" +#include "datewidget.h" +#include "../field.h" + +#include <klineedit.h> + +#include <qlayout.h> +#include <qvalidator.h> + +using Tellico::GUI::NumberFieldWidget; + +NumberFieldWidget::NumberFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_), m_lineEdit(0), m_spinBox(0) { + + if(field_->flags() & Data::Field::AllowMultiple) { + initLineEdit(); + } else { + initSpinBox(); + } + + registerWidget(); +} + +void NumberFieldWidget::initLineEdit() { + m_lineEdit = new KLineEdit(this); + connect(m_lineEdit, SIGNAL(textChanged(const QString&)), SIGNAL(modified())); + // connect(kl, SIGNAL(returnPressed(const QString&)), this, SLOT(slotHandleReturn())); + + // regexp is any number of digits followed optionally by any number of + // groups of a semi-colon followed optionally by a space, followed by digits + QRegExp rx(QString::fromLatin1("-?\\d*(; ?-?\\d*)*")); + m_lineEdit->setValidator(new QRegExpValidator(rx, this)); +} + +void NumberFieldWidget::initSpinBox() { + // intentionally allow only positive numbers + m_spinBox = new GUI::SpinBox(-1, INT_MAX, this); + connect(m_spinBox, SIGNAL(valueChanged(int)), SIGNAL(modified())); + // QSpinBox doesn't emit valueChanged if you edit the value with + // the lineEdit until you change the keyboard focus + connect(m_spinBox->child("qt_spinbox_edit"), SIGNAL(textChanged(const QString&)), SIGNAL(modified())); + // I want to allow no value, so set space as special text. Empty text is ignored + m_spinBox->setSpecialValueText(QChar(' ')); +} + +QString NumberFieldWidget::text() const { + if(isSpinBox()) { + // minValue = special empty text + if(m_spinBox->value() > m_spinBox->minValue()) { + return QString::number(m_spinBox->value()); + } + return QString(); + } + + QString text = m_lineEdit->text(); + if(field()->flags() & Data::Field::AllowMultiple) { + text.replace(s_semiColon, QString::fromLatin1("; ")); + } + return text.simplifyWhiteSpace(); +} + +void NumberFieldWidget::setText(const QString& text_) { + blockSignals(true); + + if(isSpinBox()) { + bool ok; + int n = text_.toInt(&ok); + if(ok) { + // did just allow positive + if(n < 0) { + m_spinBox->setMinValue(INT_MIN+1); + } + m_spinBox->blockSignals(true); + m_spinBox->setValue(n); + m_spinBox->blockSignals(false); + } + } else { + m_lineEdit->blockSignals(true); + m_lineEdit->setText(text_); + m_lineEdit->blockSignals(false); + } + + blockSignals(false); +} + +void NumberFieldWidget::clear() { + if(isSpinBox()) { + // show empty special value text + m_spinBox->setValue(m_spinBox->minValue()); + } else { + m_lineEdit->clear(); + } + editMultiple(false); +} + +void NumberFieldWidget::updateFieldHook(Data::FieldPtr, Data::FieldPtr newField_) { + bool wasLineEdit = !isSpinBox(); + bool nowLineEdit = newField_->flags() & Data::Field::AllowMultiple; + + if(wasLineEdit == nowLineEdit) { + return; + } + + QString value = text(); + if(wasLineEdit && !nowLineEdit) { + layout()->remove(m_lineEdit); + delete m_lineEdit; + m_lineEdit = 0; + initSpinBox(); + } else if(!wasLineEdit && nowLineEdit) { + layout()->remove(m_spinBox); + delete m_spinBox; + m_spinBox = 0; + initLineEdit(); + } + + // should really be FIELD_EDIT_WIDGET_INDEX from fieldwidget.cpp + static_cast<QBoxLayout*>(layout())->insertWidget(2, widget(), 1 /*stretch*/); + widget()->show(); + setText(value); +} + +QWidget* NumberFieldWidget::widget() { + if(isSpinBox()) { + return m_spinBox; + } + return m_lineEdit; +} + +#include "numberfieldwidget.moc" diff --git a/src/gui/numberfieldwidget.h b/src/gui/numberfieldwidget.h new file mode 100644 index 0000000..c0fed9d --- /dev/null +++ b/src/gui/numberfieldwidget.h @@ -0,0 +1,57 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef NUMBERFIELDWIDGET_H +#define NUMBERFIELDWIDGET_H + +class KLineEdit; + +#include "fieldwidget.h" +#include "../datavectors.h" + +namespace Tellico { + namespace GUI { + class SpinBox; + +/** + * @author Robby Stephenson + */ +class NumberFieldWidget : public FieldWidget { +Q_OBJECT + +public: + NumberFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~NumberFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +private: + bool isSpinBox() const { return (m_spinBox != 0); } + void initLineEdit(); + void initSpinBox(); + + KLineEdit* m_lineEdit; + SpinBox* m_spinBox; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/overlaywidget.cpp b/src/gui/overlaywidget.cpp new file mode 100644 index 0000000..6214ca8 --- /dev/null +++ b/src/gui/overlaywidget.cpp @@ -0,0 +1,104 @@ +/*************************************************************************** + copyright : (C) 2005-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 "overlaywidget.h" + +#include <qlayout.h> + +using Tellico::GUI::OverlayWidget; + +OverlayWidget::OverlayWidget(QWidget* parent, QWidget* anchor) : QFrame(parent) + , m_anchor(anchor) + , m_corner(TopRight) { + m_anchor->installEventFilter(this); + reposition(); + hide(); +} + +void OverlayWidget::setCorner(Corner corner_) { + if(corner_ == m_corner) { + return; + } + m_corner = corner_; + reposition(); +} + +void OverlayWidget::addWidget(QWidget* widget_) { + layout()->add(widget_); + adjustSize(); +} + +void OverlayWidget::reposition() { + if(!m_anchor) { + return; + } + + setMaximumSize(parentWidget()->size()); + adjustSize(); + + QPoint p; + + switch(m_corner) { + case BottomLeft: + p.setX(0); + p.setY(m_anchor->height()); + break; + + case BottomRight: + p.setX(m_anchor->width() - width()); + p.setY(m_anchor->height()); + break; + + case TopLeft: + p.setX(0); + p.setY(-1 * height()); + break; + + case TopRight: + p.setX(m_anchor->width() - width()); + p.setY(-1 * height()); + } + + // Position in the toplevelwidget's coordinates + QPoint pTopLevel = m_anchor->mapTo(topLevelWidget(), p); + // Position in the widget's parentWidget coordinates + QPoint pParent = parentWidget()->mapFrom(topLevelWidget(), pTopLevel); + // keep it on the screen + if(pParent.x() < 0) { + pParent.rx() = 0; + } + move(pParent); +} + +bool OverlayWidget::eventFilter(QObject* object_, QEvent* event_) { + if(object_ == m_anchor && (event_->type() == QEvent::Move || event_->type() == QEvent::Resize)) { + reposition(); + } + + return QFrame::eventFilter(object_, event_); +} + +void OverlayWidget::resizeEvent(QResizeEvent* event_) { + reposition(); + QFrame::resizeEvent(event_); +} + +bool OverlayWidget::event(QEvent* event_) { + if(event_->type() == QEvent::ChildInserted) { + adjustSize(); + } + + return QFrame::event(event_); +} + +#include "overlaywidget.moc" diff --git a/src/gui/overlaywidget.h b/src/gui/overlaywidget.h new file mode 100644 index 0000000..7b4453e --- /dev/null +++ b/src/gui/overlaywidget.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +// much of this code is adapted from libkdepim +// which is GPL licensed, Copyright (c) 2004 David Faure + +#ifndef TELLICO_GUI_OVERLAYWIDGET_H +#define TELLICO_GUI_OVERLAYWIDGET_H + +#include <qframe.h> + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class OverlayWidget : public QFrame { +Q_OBJECT + +public: + OverlayWidget(QWidget* parent, QWidget* anchor); + + void setCorner(Corner corner); + Corner corner() const { return m_corner; } + + void addWidget(QWidget* widget); + +protected: + void resizeEvent(QResizeEvent* event); + bool eventFilter(QObject* object, QEvent* event); + bool event(QEvent* event); + +private: + void reposition(); + + QWidget* m_anchor; + Corner m_corner; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/parafieldwidget.cpp b/src/gui/parafieldwidget.cpp new file mode 100644 index 0000000..9941e34 --- /dev/null +++ b/src/gui/parafieldwidget.cpp @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2005-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 "parafieldwidget.h" +#include "../field.h" +#include "../latin1literal.h" + +#include <ktextedit.h> + +using Tellico::GUI::ParaFieldWidget; + +ParaFieldWidget::ParaFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + + m_textEdit = new KTextEdit(this); + m_textEdit->setTextFormat(Qt::PlainText); + if(field_->property(QString::fromLatin1("spellcheck")) != Latin1Literal("false")) { + m_textEdit->setCheckSpellingEnabled(true); + } + connect(m_textEdit, SIGNAL(textChanged()), SIGNAL(modified())); + + registerWidget(); +} + +QString ParaFieldWidget::text() const { + QString text = m_textEdit->text(); + text.replace('\n', QString::fromLatin1("<br/>")); + return text; +} + +void ParaFieldWidget::setText(const QString& text_) { + blockSignals(true); + m_textEdit->blockSignals(true); + + QRegExp rx(QString::fromLatin1("<br/?>"), false /*case-sensitive*/); + QString s = text_; + s.replace(rx, QChar('\n')); + m_textEdit->setText(s); + + m_textEdit->blockSignals(false); + blockSignals(false); +} + +void ParaFieldWidget::clear() { + m_textEdit->clear(); + editMultiple(false); +} + +QWidget* ParaFieldWidget::widget() { + return m_textEdit; +} + +#include "parafieldwidget.moc" diff --git a/src/gui/parafieldwidget.h b/src/gui/parafieldwidget.h new file mode 100644 index 0000000..ac9801f --- /dev/null +++ b/src/gui/parafieldwidget.h @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef PARAFIELDWIDGET_H +#define PARAFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +class KTextEdit; + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class ParaFieldWidget : public FieldWidget { +Q_OBJECT + +public: + ParaFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~ParaFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + +private: + KTextEdit* m_textEdit; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/previewdialog.cpp b/src/gui/previewdialog.cpp new file mode 100644 index 0000000..4c0d8e1 --- /dev/null +++ b/src/gui/previewdialog.cpp @@ -0,0 +1,56 @@ +/*************************************************************************** + copyright : (C) 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 "previewdialog.h" +#include "../entryview.h" +#include "../entry.h" +#include "../imagefactory.h" // for StyleOptions + +#include <klocale.h> +#include <ktempdir.h> +#include <khtmlview.h> + +using Tellico::GUI::PreviewDialog; + +PreviewDialog::PreviewDialog(QWidget* parent_) + : KDialogBase(parent_, "template preview dialog", false /* modal */, + i18n("Template Preview"), KDialogBase::Ok) + , m_tempDir(new KTempDir()) { + m_tempDir->setAutoDelete(true); + connect(this, SIGNAL(finished()), SLOT(delayedDestruct())); + + m_view = new EntryView(this); + setMainWidget(m_view->view()); + setInitialSize(QSize(600, 500)); +} + +PreviewDialog::~PreviewDialog() { + delete m_tempDir; + m_tempDir = 0; +} + +void PreviewDialog::setXSLTFile(const QString& file_) { + m_view->setXSLTFile(file_); +} + +void PreviewDialog::setXSLTOptions(StyleOptions options_) { + options_.imgDir = m_tempDir->name(); // images always get written to temp dir + ImageFactory::createStyleImages(options_); + m_view->setXSLTOptions(options_); +} + +void PreviewDialog::showEntry(Data::EntryPtr entry_) { + m_view->showEntry(entry_); +} + +#include "previewdialog.moc" diff --git a/src/gui/previewdialog.h b/src/gui/previewdialog.h new file mode 100644 index 0000000..a386a94 --- /dev/null +++ b/src/gui/previewdialog.h @@ -0,0 +1,47 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_PREVIEWDIALOG_H +#define TELLICO_GUI_PREVIEWDIALOG_H + +#include <kdialogbase.h> + +#include "../datavectors.h" + +class KTempDir; + +namespace Tellico { + class EntryView; + class StyleOptions; + + namespace GUI { + +class PreviewDialog : public KDialogBase { +Q_OBJECT + +public: + PreviewDialog(QWidget* parent); + ~PreviewDialog(); + + void setXSLTFile(const QString& file); + void setXSLTOptions(StyleOptions options); + void showEntry(Data::EntryPtr entry); + +private: + KTempDir* m_tempDir; + EntryView* m_view; +}; + + } +} +#endif diff --git a/src/gui/progress.cpp b/src/gui/progress.cpp new file mode 100644 index 0000000..36baaf1 --- /dev/null +++ b/src/gui/progress.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + copyright : (C) 2005-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 "progress.h" +#include "../tellico_debug.h" + +using Tellico::GUI::Progress; + +Progress::Progress(QWidget* parent_) : KProgress(parent_) { +} + +Progress::Progress(int totalSteps_, QWidget* parent_) : KProgress(totalSteps_, parent_) { +} + +bool Progress::isDone() const { + return progress() == totalSteps(); +} + +void Progress::setDone() { + setProgress(totalSteps()); +} + +#include "progress.moc" diff --git a/src/gui/progress.h b/src/gui/progress.h new file mode 100644 index 0000000..b58de2f --- /dev/null +++ b/src/gui/progress.h @@ -0,0 +1,39 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_PROGRESS_H +#define TELLICO_GUI_PROGRESS_H + +#include <kprogress.h> + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class Progress : public KProgress { +Q_OBJECT + +public: + Progress(QWidget* parent); + Progress(int totalSteps, QWidget* parent); + + bool isDone() const; + void setDone(); +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/ratingfieldwidget.cpp b/src/gui/ratingfieldwidget.cpp new file mode 100644 index 0000000..4b0de71 --- /dev/null +++ b/src/gui/ratingfieldwidget.cpp @@ -0,0 +1,59 @@ +/*************************************************************************** + copyright : (C) 2005-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 "ratingfieldwidget.h" +#include "ratingwidget.h" +#include "../field.h" + +using Tellico::GUI::RatingFieldWidget; + +RatingFieldWidget::RatingFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_) { + m_rating = new RatingWidget(field_, this); + connect(m_rating, SIGNAL(modified()), SIGNAL(modified())); + + registerWidget(); +} + +QString RatingFieldWidget::text() const { + return m_rating->text(); +} + +void RatingFieldWidget::setText(const QString& text_) { + blockSignals(true); + + m_rating->blockSignals(true); + m_rating->setText(text_); + m_rating->blockSignals(false); + + blockSignals(false); + + if(m_rating->text() != text_) { + emit modified(); + } +} + +void RatingFieldWidget::clear() { + m_rating->clear(); + editMultiple(false); +} + +void RatingFieldWidget::updateFieldHook(Data::FieldPtr, Data::FieldPtr newField_) { + m_rating->updateField(newField_); +} + +QWidget* RatingFieldWidget::widget() { + return m_rating; +} + +#include "ratingfieldwidget.moc" diff --git a/src/gui/ratingfieldwidget.h b/src/gui/ratingfieldwidget.h new file mode 100644 index 0000000..369dbec --- /dev/null +++ b/src/gui/ratingfieldwidget.h @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef RATINGFIELDWIDGET_H +#define RATINGFIELDWIDGET_H + +#include "fieldwidget.h" +#include "../datavectors.h" + +namespace Tellico { + namespace GUI { + class RatingWidget; + +/** + * @author Robby Stephenson + */ +class RatingFieldWidget : public FieldWidget { +Q_OBJECT + +public: + RatingFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~RatingFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +private: + RatingWidget* m_rating; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/ratingwidget.cpp b/src/gui/ratingwidget.cpp new file mode 100644 index 0000000..4921f21 --- /dev/null +++ b/src/gui/ratingwidget.cpp @@ -0,0 +1,170 @@ +/*************************************************************************** + copyright : (C) 2003-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 "ratingwidget.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" + +#include <kiconloader.h> + +#include <qintdict.h> +#include <qlayout.h> + +namespace { + static const int RATING_WIDGET_MAX_ICONS = 10; // same as in Field::ratingValues() + static const int RATING_WIDGET_MAX_STAR_SIZE = 24; +} + +using Tellico::GUI::RatingWidget; + +const QPixmap& RatingWidget::pixmap(const QString& value_) { + static QIntDict<QPixmap> pixmaps; + if(pixmaps.isEmpty()) { + pixmaps.insert(-1, new QPixmap()); + } + bool ok; + int n = Tellico::toUInt(value_, &ok); + if(!ok || n < 1 || n > 10) { + return *pixmaps[-1]; + } + if(pixmaps[n]) { + return *pixmaps[n]; + } + + QString picName = QString::fromLatin1("stars%1").arg(n); + QPixmap* pix = new QPixmap(UserIcon(picName)); + pixmaps.insert(n, pix); + return *pix; +} + +RatingWidget::RatingWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : QHBox(parent_, name_), m_field(field_), m_currIndex(-1) { + m_pixOn = UserIcon(QString::fromLatin1("star_on")); + m_pixOff = UserIcon(QString::fromLatin1("star_off")); + setSpacing(0); + + // find maximum width and height + int w = QMAX(RATING_WIDGET_MAX_STAR_SIZE, QMAX(m_pixOn.width(), m_pixOff.width())); + int h = QMAX(RATING_WIDGET_MAX_STAR_SIZE, QMAX(m_pixOn.height(), m_pixOff.height())); + for(int i = 0; i < RATING_WIDGET_MAX_ICONS; ++i) { + QLabel* l = new QLabel(this); + l->setFixedSize(w, h); + m_widgets.append(l); + } + init(); + + QBoxLayout* l = dynamic_cast<QBoxLayout*>(layout()); + if(l) { + l->addStretch(1); + } +} + +void RatingWidget::init() { + updateBounds(); + m_total = QMIN(m_max, static_cast<int>(m_widgets.count())); + uint i = 0; + for( ; static_cast<int>(i) < m_total; ++i) { + m_widgets.at(i)->setPixmap(m_pixOff); + } + for( ; i < m_widgets.count(); ++i) { + m_widgets.at(i)->setPixmap(QPixmap()); + } + update(); +} + +void RatingWidget::updateBounds() { + bool ok; // not used; + m_min = Tellico::toUInt(m_field->property(QString::fromLatin1("minimum")), &ok); + m_max = Tellico::toUInt(m_field->property(QString::fromLatin1("maximum")), &ok); + if(m_max > RATING_WIDGET_MAX_ICONS) { + myDebug() << "RatingWidget::updateBounds() - max is too high: " << m_max << endl; + m_max = RATING_WIDGET_MAX_ICONS; + } + if(m_min < 1) { + m_min = 1; + } +} + +void RatingWidget::update() { + int i = 0; + for( ; i <= m_currIndex; ++i) { + m_widgets.at(i)->setPixmap(m_pixOn); + } + for( ; i < m_total; ++i) { + m_widgets.at(i)->setPixmap(m_pixOff); + } + + QHBox::update(); +} + +void RatingWidget::mousePressEvent(QMouseEvent* event_) { + // only react to left button + if(event_->button() != Qt::LeftButton) { + return; + } + + int idx; + QWidget* child = childAt(event_->pos()); + if(child) { + idx = m_widgets.findRef(static_cast<QLabel*>(child)); + // if the widget is clicked beyond the maximum value, clear it + // remember total and min are values, but index is zero-based! + if(idx > m_total-1) { + idx = -1; + } else if(idx < m_min-1) { + idx = m_min-1; // limit to minimum, remember index is zero-based + } + } else { + idx = -1; + } + if(m_currIndex != idx) { + m_currIndex = idx; + update(); + emit modified(); + } +} + +void RatingWidget::clear() { + m_currIndex = -1; + update(); +} + +QString RatingWidget::text() const { + // index is index of the list, which is zero-based. Add 1! + return m_currIndex == -1 ? QString::null : QString::number(m_currIndex+1); +} + +void RatingWidget::setText(const QString& text_) { + bool ok; + // text is value, subtract one to get index + m_currIndex = Tellico::toUInt(text_, &ok)-1; + if(ok) { + if(m_currIndex > m_total-1) { + m_currIndex = -1; + } else if(m_currIndex < m_min-1) { + m_currIndex = m_min-1; // limit to minimum, remember index is zero-based + } + } else { + m_currIndex = -1; + } + update(); +} + +void RatingWidget::updateField(Data::FieldPtr field_) { + m_field = field_; + init(); +} + +#include "ratingwidget.moc" diff --git a/src/gui/ratingwidget.h b/src/gui/ratingwidget.h new file mode 100644 index 0000000..99665b3 --- /dev/null +++ b/src/gui/ratingwidget.h @@ -0,0 +1,75 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef RATINGWIDGET_H +#define RATINGWIDGET_H + +#include "../datavectors.h" + +#include <qhbox.h> +#include <qptrlist.h> +#include <qlabel.h> +#include <qpixmap.h> + +namespace Tellico { + namespace Data { + class Field; + } + namespace GUI { + +/** + * @author Robby Stephenson + */ +class RatingWidget : public QHBox { +Q_OBJECT + +typedef QPtrList<QLabel> LabelList; + +public: + RatingWidget(Data::FieldPtr field, QWidget* parent, const char* name = 0); + + void clear(); + QString text() const; + void setText(const QString& text); + void updateField(Data::FieldPtr field); + + static const QPixmap& pixmap(const QString& value); + +public slots: + void update(); + +signals: + void modified(); + +protected: + virtual void mousePressEvent(QMouseEvent* e); + +private: + void init(); + void updateBounds(); + + Data::ConstFieldPtr m_field; + LabelList m_widgets; + + int m_currIndex; + int m_total; + int m_min; + int m_max; + + QPixmap m_pixOn; + QPixmap m_pixOff; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/richtextlabel.cpp b/src/gui/richtextlabel.cpp new file mode 100644 index 0000000..a4cdde4 --- /dev/null +++ b/src/gui/richtextlabel.cpp @@ -0,0 +1,47 @@ +/*************************************************************************** + 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 "richtextlabel.h" + +#include <kdebug.h> + +#include <qlayout.h> + +using Tellico::GUI::RichTextLabel; + +RichTextLabel::RichTextLabel(QWidget* parent) : QTextEdit(parent) { + init(); +} + +RichTextLabel::RichTextLabel(const QString& text, QWidget* parent) : QTextEdit(text, QString::null, parent) { + init(); +} + +QSize RichTextLabel::sizeHint() const { + return minimumSizeHint(); +} + +void RichTextLabel::init() { + setReadOnly(true); + setTextFormat(Qt::RichText); + + setFrameShape(QFrame::NoFrame); + viewport()->setMouseTracking(false); + + setPaper(colorGroup().background()); + + setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); + viewport()->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); +} + +#include "richtextlabel.moc" diff --git a/src/gui/richtextlabel.h b/src/gui/richtextlabel.h new file mode 100644 index 0000000..f45a328 --- /dev/null +++ b/src/gui/richtextlabel.h @@ -0,0 +1,46 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_GUI_RICHTEXTLABEL_H +#define TELLICO_GUI_RICHTEXTLABEL_H + +#include <qtextedit.h> + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class RichTextLabel : public QTextEdit { +Q_OBJECT + +public: + RichTextLabel(QWidget* parent); + RichTextLabel(const QString& text, QWidget* parent); + + virtual QSize sizeHint() const; + +private: + void init(); + // redefine these to disable selection + void contentsMousePressEvent(QMouseEvent*) {} + void contentsMouseMoveEvent(QMouseEvent*) {} + void contentsMouseReleaseEvent(QMouseEvent*) {} + void contentsMouseDoubleClickEvent(QMouseEvent*) {} +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/gui/stringmapdialog.cpp b/src/gui/stringmapdialog.cpp new file mode 100644 index 0000000..4a5374f --- /dev/null +++ b/src/gui/stringmapdialog.cpp @@ -0,0 +1,125 @@ +/*************************************************************************** + copyright : (C) 2003-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 "stringmapdialog.h" + +#include <klistview.h> +#include <klocale.h> +#include <klineedit.h> +#include <kbuttonbox.h> +#include <kiconloader.h> + +#include <qlayout.h> +#include <qheader.h> +#include <qhbox.h> +#include <qwhatsthis.h> +#include <qpushbutton.h> + +using Tellico::StringMapDialog; + +StringMapDialog::StringMapDialog(const QMap<QString, QString>& map_, QWidget* parent_, const char* name_/*=0*/, bool modal_/*=false*/) + : KDialogBase(parent_, name_, modal_, QString::null, Ok|Cancel) { + QWidget* page = new QWidget(this); + QVBoxLayout* l = new QVBoxLayout(page, 0, KDialog::spacingHint()); + + m_listView = new KListView(page); + m_listView->setAllColumnsShowFocus(true); + m_listView->setShowSortIndicator(true); + m_listView->addColumn(QString::null); + m_listView->addColumn(QString::null); + m_listView->header()->hide(); // hide header since neither column has a label initially + m_listView->setColumnWidthMode(0, QListView::Maximum); + m_listView->setColumnWidthMode(1, QListView::Maximum); + m_listView->setResizeMode(QListView::AllColumns); + connect(m_listView, SIGNAL(currentChanged(QListViewItem*)), SLOT(slotUpdate(QListViewItem*))); + connect(m_listView, SIGNAL(clicked(QListViewItem*)), SLOT(slotUpdate(QListViewItem*))); + l->addWidget(m_listView); + + QHBox* box = new QHBox(page); + box->setMargin(4); + box->setSpacing(KDialog::spacingHint()); + + m_edit1 = new KLineEdit(box); + m_edit1->setFocus(); + m_edit2 = new KLineEdit(box); + KButtonBox* bb = new KButtonBox(box); + bb->addStretch(); + QPushButton* btn = bb->addButton(i18n("&Set"), this, SLOT(slotAdd())); + btn->setIconSet(BarIcon(QString::fromLatin1("filenew"), KIcon::SizeSmall)); + btn = bb->addButton(i18n("&Delete"), this, SLOT(slotDelete())); + btn->setIconSet(BarIcon(QString::fromLatin1("editdelete"), KIcon::SizeSmall)); + + l->addWidget(box); + l->addStretch(1); + setMainWidget(page); + + for(QMap<QString, QString>::ConstIterator it = map_.begin(); it != map_.end(); ++it) { + if(!it.data().isEmpty()) { + (void) new KListViewItem(m_listView, it.key(), it.data()); + } + } + + setMinimumWidth(400); + enableButtonSeparator(true); +} + +void StringMapDialog::slotAdd() { + QString s1 = m_edit1->text(); + QString s2 = m_edit2->text(); + if(s1.isEmpty() && s2.isEmpty()) { + return; + } + QListViewItem* item = m_listView->currentItem(); + if(item && s1 == item->text(0)) { // only update values if same key + item->setText(1, s2); + } else { + item = new KListViewItem(m_listView, s1, s2); + } + m_listView->ensureItemVisible(item); + m_listView->setSelected(item, true); + m_listView->setCurrentItem(item); +} + +void StringMapDialog::slotDelete() { + delete m_listView->currentItem(); + m_edit1->clear(); + m_edit2->clear(); + m_listView->setSelected(m_listView->currentItem(), true); +} + +void StringMapDialog::slotUpdate(QListViewItem* item_) { + if(item_) { + m_edit1->setText(item_->text(0)); + m_edit2->setText(item_->text(1)); + m_listView->header()->adjustHeaderSize(); + } else { + m_edit1->clear(); + m_edit2->clear(); + } +} + +void StringMapDialog::setLabels(const QString& label1_, const QString& label2_) { + m_listView->header()->setLabel(0, label1_); + m_listView->header()->setLabel(1, label2_); + m_listView->header()->show(); +} + +QMap<QString, QString> StringMapDialog::stringMap() { + QMap<QString, QString> map; + for(QListViewItem* item = m_listView->firstChild(); item; item = item->nextSibling()) { + map.insert(item->text(0), item->text(1)); + } + return map; +} + +#include "stringmapdialog.moc" diff --git a/src/gui/stringmapdialog.h b/src/gui/stringmapdialog.h new file mode 100644 index 0000000..311df70 --- /dev/null +++ b/src/gui/stringmapdialog.h @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef STRINGMAPDIALOG_H +#define STRINGMAPDIALOG_H + +class KLineEdit; +class KListView; +class QListViewItem; + +#include <kdialogbase.h> + +template <typename T1, typename T2> +class QMap; + +namespace Tellico { + +/** + * @short A simple dialog for editing a map between two strings. + * + * A \ref KListView is used with the map keys in the first column and + * the map values in the second. Two edit boxes are below the list view. + * When an item is selected, the key-value is pair is placed in the edit + * boxes. Add and Delete buttons are used to add a new pair, or to remove + * an existing one. + * + * @author Robby Stephenson + */ +class StringMapDialog : public KDialogBase { +Q_OBJECT + +public: + StringMapDialog(const QMap<QString, QString>& stringMap, QWidget* parent, const char* name=0, bool modal=false); + + /** + * Sets the titles for the key and value columns. + * + * @param label1 The name of the key string + * @param label2 The name of the value string + */ + void setLabels(const QString& label1, const QString& label2); + /** + * Returns the modified string map. + * + * @return The modified string map + */ + QMap<QString, QString> stringMap(); + +private slots: + void slotAdd(); + void slotDelete(); + void slotUpdate(QListViewItem* item); + +protected: + KListView* m_listView; + KLineEdit* m_edit1; + KLineEdit* m_edit2; +}; + +} // end namespace +#endif diff --git a/src/gui/tabcontrol.cpp b/src/gui/tabcontrol.cpp new file mode 100644 index 0000000..145f632 --- /dev/null +++ b/src/gui/tabcontrol.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + copyright : (C) 2002-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 "tabcontrol.h" + +#include <qtabbar.h> +#include <qobjectlist.h> + +using Tellico::GUI::TabControl; + +TabControl::TabControl(QWidget* parent_, const char* name_/*=0*/) + : QTabWidget(parent_, name_) { +} + +QTabBar* TabControl::tabBar() const { + return QTabWidget::tabBar(); +} + +void TabControl::setFocusToFirstChild() { + QWidget* page = currentPage(); + Q_ASSERT(page); + QObjectList* list = page->queryList("QWidget"); + for(QObjectListIt it(*list); it.current(); ++it) { + QWidget* w = static_cast<QWidget*>(it.current()); + if(w->isFocusEnabled()) { + w->setFocus(); + break; + } + } + delete list; +} + +// have to loop backwards since count() gets decremented on delete +void TabControl::clear() { + for(int i = count(); i > 0; --i) { + QWidget* w = page(i-1); + if(w) { + removePage(w); + delete w; + } + } +} + +void TabControl::setTabBarHidden(bool hide_) { + QWidget* rightcorner = cornerWidget(TopRight); + QWidget* leftcorner = cornerWidget(TopLeft); + + if(hide_) { + if(leftcorner) { + leftcorner->hide(); + } + if(rightcorner) { + rightcorner->hide(); + } + tabBar()->hide(); + } else { + tabBar()->show(); + if(leftcorner) { + leftcorner->show(); + } + if(rightcorner) { + rightcorner->show(); + } + } +} + +#include "tabcontrol.moc" diff --git a/src/gui/tabcontrol.h b/src/gui/tabcontrol.h new file mode 100644 index 0000000..20d22f5 --- /dev/null +++ b/src/gui/tabcontrol.h @@ -0,0 +1,48 @@ +/*************************************************************************** + copyright : (C) 2002-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; * + * * + ***************************************************************************/ + +#ifndef TABCONTROL_H +#define TABCONTROL_H + +#include <qtabwidget.h> + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class TabControl : public QTabWidget { +Q_OBJECT + +public: + /** + * Constructor + */ + TabControl(QWidget* parent, const char* name=0); + + QTabBar* tabBar() const; + void setTabBarHidden(bool hide); + + /** + * Sets the focus to the first focusable widget on the current page. + */ + void setFocusToFirstChild(); + +public slots: + void clear(); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/gui/tablefieldwidget.cpp b/src/gui/tablefieldwidget.cpp new file mode 100644 index 0000000..30f89b4 --- /dev/null +++ b/src/gui/tablefieldwidget.cpp @@ -0,0 +1,330 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tablefieldwidget.h" +#include "../field.h" +#include "../tellico_utils.h" +#include "../tellico_kernel.h" + +#include <klocale.h> +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <kinputdialog.h> + +#include <qtable.h> + +namespace { + static const int MIN_TABLE_ROWS = 5; + static const int MAX_TABLE_COLS = 10; +} + +using Tellico::GUI::TableFieldWidget; + +TableFieldWidget::TableFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_), m_field(field_), m_row(-1), m_col(-1) { + + bool ok; + m_columns = Tellico::toUInt(field_->property(QString::fromLatin1("columns")), &ok); + if(!ok) { + m_columns = 1; + } else { + m_columns = QMIN(m_columns, MAX_TABLE_COLS); // max of 5 columns + } + + m_table = new QTable(MIN_TABLE_ROWS, m_columns, this); + labelColumns(m_field); + // allow renaming of column titles + m_table->horizontalHeader()->setClickEnabled(true); + m_table->horizontalHeader()->installEventFilter(this); + + m_table->verticalHeader()->setClickEnabled(true); + m_table->verticalHeader()->installEventFilter(this); + connect(m_table->verticalHeader(), SIGNAL(indexChange(int, int, int)), SIGNAL(modified())); + + m_table->setDragEnabled(false); + m_table->setFocusStyle(QTable::FollowStyle); + m_table->setRowMovingEnabled(true); // rows can be moved + m_table->setColumnMovingEnabled(false); // columns remain fixed + + m_table->setColumnStretchable(m_columns-1, true); + m_table->adjustColumn(m_columns-1); + m_table->setSelectionMode(QTable::NoSelection); + m_table->setHScrollBarMode(QScrollView::AlwaysOff); + + connect(m_table, SIGNAL(valueChanged(int, int)), SIGNAL(modified())); + connect(m_table, SIGNAL(currentChanged(int, int)), SLOT(slotCheckRows(int, int))); + connect(m_table, SIGNAL(valueChanged(int, int)), SLOT(slotResizeColumn(int, int))); + connect(m_table, SIGNAL(contextMenuRequested(int, int, const QPoint&)), SLOT(contextMenu(int, int, const QPoint&))); + + registerWidget(); +} + +QString TableFieldWidget::text() const { + QString text, str, rstack, cstack, rowStr; + for(int row = 0; row < m_table->numRows(); ++row) { + rowStr.truncate(0); + cstack.truncate(0); + for(int col = 0; col < m_table->numCols(); ++col) { + str = m_table->text(row, col).simplifyWhiteSpace(); + if(str.isEmpty()) { + cstack += QString::fromLatin1("::"); + } else { + rowStr += cstack + str + QString::fromLatin1("::"); + cstack.truncate(0); + } + } + if(rowStr.isEmpty()) { + rstack += QString::fromLatin1("; "); + } else { + rowStr.truncate(rowStr.length()-2); // remove last semi-colon and space + text += rstack + rowStr + QString::fromLatin1("; "); + rstack.truncate(0); + } + } + if(!text.isEmpty()) { + text.truncate(text.length()-2); // remove last semi-colon and space + } + + // now reduce number of rows if necessary + bool loop = true; + for(int row = m_table->numRows()-1; loop && row > MIN_TABLE_ROWS; --row) { + bool empty = true; + for(int col = 0; col < m_table->numCols(); ++col) { + if(!m_table->text(row, col).isEmpty()) { + empty = false; + break; + } + } + if(empty) { + m_table->removeRow(row); + } else { + loop = false; + } + } + return text; +} + +void TableFieldWidget::setText(const QString& text_) { + QStringList list = Data::Field::split(text_, true); + // add additional rows if needed + if(static_cast<int>(list.count()) > m_table->numRows()) { + m_table->insertRows(m_table->numRows(), list.count()-m_table->numRows()); + } + int row; + for(row = 0; row < static_cast<int>(list.count()); ++row) { + for(int col = 0; col < m_table->numCols(); ++col) { + m_table->setText(row, col, list[row].section(QString::fromLatin1("::"), col, col)); + } + m_table->showRow(row); + } + // remove any un-needed rows + int minRow = QMAX(row, MIN_TABLE_ROWS); + for(row = m_table->numRows()-1; row >= minRow; --row) { + m_table->removeRow(row); + } + // adjust all columns + for(int col = 0; col < m_table->numCols()-1; ++col) { + m_table->adjustColumn(col); + } +} + +void TableFieldWidget::clear() { + bool wasEmpty = true; + for(int row = 0; row < m_table->numRows(); ++row) { + if(!emptyRow(row)) { + wasEmpty = false; + } + for(int col = 0; col < m_table->numCols(); ++col) { + m_table->setText(row, col, QString::null); + } + if(row >= MIN_TABLE_ROWS) { + m_table->removeRow(row); + --row; + } + } + editMultiple(false); + if(!wasEmpty) { + emit modified(); + } +} + +QWidget* TableFieldWidget::widget() { + return m_table; +} + +void TableFieldWidget::slotCheckRows(int row_, int) { + if(row_ == m_table->numRows()-1 && !emptyRow(row_)) { // if is last row and row above is not empty + m_table->insertRows(m_table->numRows()); + } +} + +void TableFieldWidget::slotResizeColumn(int, int col_) { + m_table->adjustColumn(col_); +} + +void TableFieldWidget::slotRenameColumn() { + if(m_col < 0 || m_col >= m_columns) { + return; + } + QString name = m_table->horizontalHeader()->label(m_col); + bool ok; + QString newName = KInputDialog::getText(i18n("Rename Column"), i18n("New column name:"), + name, &ok, this); + if(ok && !newName.isEmpty()) { + Data::FieldPtr newField = new Data::Field(*m_field); + newField->setProperty(QString::fromLatin1("column%1").arg(m_col+1), newName); + if(Kernel::self()->modifyField(newField)) { + m_field = newField; + labelColumns(m_field); + } + } +} + +bool TableFieldWidget::emptyRow(int row_) const { + for(int col = 0; col < m_table->numCols(); ++col) { + if(!m_table->text(row_, col).isEmpty()) { + return false; + } + } + return true; +} + +void TableFieldWidget::labelColumns(Data::FieldPtr field_) { + for(int i = 0; i < m_columns; ++i) { + QString s = field_->property(QString::fromLatin1("column%1").arg(i+1)); + if(s.isEmpty()) { + s = i18n("Column %1").arg(i+1); + } + m_table->horizontalHeader()->setLabel(i, s); + } +} + +void TableFieldWidget::updateFieldHook(Data::FieldPtr, Data::FieldPtr newField_) { + bool ok; + m_columns = Tellico::toUInt(newField_->property(QString::fromLatin1("columns")), &ok); + if(!ok) { + m_columns = 1; + } else { + m_columns = QMIN(m_columns, MAX_TABLE_COLS); // max of 5 columns + } + if(m_columns != m_table->numCols()) { + m_table->setNumCols(m_columns); + } + m_table->horizontalHeader()->adjustHeaderSize(); + labelColumns(newField_); +} + +bool TableFieldWidget::eventFilter(QObject* obj_, QEvent* ev_) { + if(ev_->type() == QEvent::MouseButtonPress + && static_cast<QMouseEvent*>(ev_)->button() == Qt::RightButton) { + if(obj_ == m_table->horizontalHeader()) { + QMouseEvent* ev = static_cast<QMouseEvent*>(ev_); + // might be scrolled + int pos = ev->x() + m_table->horizontalHeader()->offset(); + int col = m_table->horizontalHeader()->sectionAt(pos); + if(col >= m_columns) { + return false; + } + m_row = -1; + m_col = col; + KPopupMenu menu(this); + menu.insertItem(SmallIconSet(QString::fromLatin1("edit")), i18n("Rename Column..."), + this, SLOT(slotRenameColumn())); + menu.exec(ev->globalPos()); + return true; + } else if(obj_ == m_table->verticalHeader()) { + QMouseEvent* ev = static_cast<QMouseEvent*>(ev_); + // might be scrolled + int pos = ev->y() + m_table->verticalHeader()->offset(); + int row = m_table->verticalHeader()->sectionAt(pos); + if(row < 0 || row > m_table->numRows()-1) { + return false; + } + m_row = row; + m_col = -1; + // show regular right-click menu + contextMenu(m_row, m_col, ev->globalPos()); + return true; + } + } + return FieldWidget::eventFilter(obj_, ev_); +} + +void TableFieldWidget::contextMenu(int row_, int col_, const QPoint& p_) { + // might get called with col == -1 for clicking on vertical header + // but a negative row means clicking outside bounds of table + if(row_ < 0) { + return; + } + m_row = row_; + m_col = col_; + + int id; + KPopupMenu menu(this); + menu.insertItem(SmallIconSet(QString::fromLatin1("insrow")), i18n("Insert Row"), + this, SLOT(slotInsertRow())); + menu.insertItem(SmallIconSet(QString::fromLatin1("remrow")), i18n("Remove Row"), + this, SLOT(slotRemoveRow())); + id = menu.insertItem(SmallIconSet(QString::fromLatin1("1uparrow")), i18n("Move Row Up"), + this, SLOT(slotMoveRowUp())); + if(m_row == 0) { + menu.setItemEnabled(id, false); + } + id = menu.insertItem(SmallIconSet(QString::fromLatin1("1downarrow")), i18n("Move Row Down"), + this, SLOT(slotMoveRowDown())); + if(m_row == m_table->numRows()-1) { + menu.setItemEnabled(id, false); + } + menu.insertSeparator(); + id = menu.insertItem(SmallIconSet(QString::fromLatin1("edit")), i18n("Rename Column..."), + this, SLOT(slotRenameColumn())); + if(m_col < 0 || m_col > m_columns-1) { + menu.setItemEnabled(id, false); + } + menu.insertSeparator(); + menu.insertItem(SmallIconSet(QString::fromLatin1("locationbar_erase")), i18n("Clear Table"), + this, SLOT(clear())); + menu.exec(p_); +} + +void TableFieldWidget::slotInsertRow() { + if(m_row > -1) { + m_table->insertRows(m_row); + emit modified(); + } +} + +void TableFieldWidget::slotRemoveRow() { + if(m_row > -1) { + m_table->removeRow(m_row); + emit modified(); + } +} + +void TableFieldWidget::slotMoveRowUp() { + if(m_row > 0) { + m_table->swapRows(m_row, m_row-1, true); + m_table->updateContents(); + emit modified(); + } +} + +void TableFieldWidget::slotMoveRowDown() { + if(m_row < m_table->numRows()-1) { + m_table->swapRows(m_row, m_row+1, true); + m_table->updateContents(); + emit modified(); + } +} + +#include "tablefieldwidget.moc" diff --git a/src/gui/tablefieldwidget.h b/src/gui/tablefieldwidget.h new file mode 100644 index 0000000..da9157d --- /dev/null +++ b/src/gui/tablefieldwidget.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TABLEFIELDWIDGET_H +#define TABLEFIELDWIDGET_H + +class QTable; +class QEvent; + +#include "fieldwidget.h" +#include "../datavectors.h" + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class TableFieldWidget : public FieldWidget { +Q_OBJECT + +public: + TableFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~TableFieldWidget() {} + + virtual QString text() const; + virtual void setText(const QString& text); + + /** + * Event filter used to popup the menu + */ + bool eventFilter(QObject* obj, QEvent* ev); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +private slots: + void contextMenu(int row, int col, const QPoint& p); + void slotCheckRows(int row, int col); + void slotResizeColumn(int row, int col); + void slotRenameColumn(); + void slotInsertRow(); + void slotRemoveRow(); + void slotMoveRowUp(); + void slotMoveRowDown(); + +private: + bool emptyRow(int row) const; + void labelColumns(Data::FieldPtr field); + + QTable* m_table; + int m_columns; + Data::FieldPtr m_field; + int m_row; + int m_col; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/gui/urlfieldwidget.cpp b/src/gui/urlfieldwidget.cpp new file mode 100644 index 0000000..f3c2afd --- /dev/null +++ b/src/gui/urlfieldwidget.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + copyright : (C) 2005-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 "urlfieldwidget.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../tellico_kernel.h" + +#include <klineedit.h> +#include <kurlrequester.h> +#include <kurllabel.h> + +using Tellico::GUI::URLFieldWidget; + +// subclass of KURLCompletion is needed so the KURLLabel +// can open relative links. I don't want to have to have to update +// the base directory of the completion every time a new document is opened +QString URLFieldWidget::URLCompletion::makeCompletion(const QString& text_) { + // KURLCompletion::makeCompletion() uses an internal variable instead + // of calling KURLCompletion::dir() so need to set the base dir before completing + setDir(Kernel::self()->URL().directory()); + return KURLCompletion::makeCompletion(text_); +} + +URLFieldWidget::URLFieldWidget(Data::FieldPtr field_, QWidget* parent_, const char* name_/*=0*/) + : FieldWidget(field_, parent_, name_), m_run(0) { + + m_requester = new KURLRequester(this); + m_requester->lineEdit()->setCompletionObject(new URLCompletion()); + m_requester->lineEdit()->setAutoDeleteCompletionObject(true); + connect(m_requester, SIGNAL(textChanged(const QString&)), SIGNAL(modified())); + connect(m_requester, SIGNAL(textChanged(const QString&)), label(), SLOT(setURL(const QString&))); + connect(label(), SIGNAL(leftClickedURL(const QString&)), SLOT(slotOpenURL(const QString&))); + registerWidget(); + + // special case, remember if it's a relative url + m_isRelative = field_->property(QString::fromLatin1("relative")) == Latin1Literal("true"); +} + +URLFieldWidget::~URLFieldWidget() { + if(m_run) { + m_run->abort(); + } +} + +QString URLFieldWidget::text() const { + if(m_isRelative) { + return KURL::relativeURL(Kernel::self()->URL(), m_requester->url()); + } + // for comparison purposes and to be consistent with the file listing importer + // I want the full url here, including the protocol + // the requester only returns the path, so create a KURL + return KURL(m_requester->url()).url(); +} + +void URLFieldWidget::setText(const QString& text_) { + blockSignals(true); + + m_requester->blockSignals(true); + m_requester->setURL(text_); + m_requester->blockSignals(false); + static_cast<KURLLabel*>(label())->setURL(text_); + + blockSignals(false); +} + +void URLFieldWidget::clear() { + m_requester->clear(); + editMultiple(false); +} + +void URLFieldWidget::updateFieldHook(Data::FieldPtr, Data::FieldPtr newField_) { + m_isRelative = newField_->property(QString::fromLatin1("relative")) == Latin1Literal("true"); +} + +void URLFieldWidget::slotOpenURL(const QString& url_) { + if(url_.isEmpty()) { + return; + } + // just in case, interpret string relative to document url + m_run = new KRun(KURL(Kernel::self()->URL(), url_)); +} + +QWidget* URLFieldWidget::widget() { + return m_requester; +} + +#include "urlfieldwidget.moc" diff --git a/src/gui/urlfieldwidget.h b/src/gui/urlfieldwidget.h new file mode 100644 index 0000000..70e9505 --- /dev/null +++ b/src/gui/urlfieldwidget.h @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef URLFIELDWIDGET_H +#define URLFIELDWIDGET_H + +class KURLRequester; + +#include "fieldwidget.h" + +#include <krun.h> +#include <kurlcompletion.h> + +#include <qguardedptr.h> + +namespace Tellico { + namespace GUI { + +/** + * @author Robby Stephenson + */ +class URLFieldWidget : public FieldWidget { +Q_OBJECT + +public: + URLFieldWidget(Data::FieldPtr field, QWidget* parent, const char* name=0); + virtual ~URLFieldWidget(); + + virtual QString text() const; + virtual void setText(const QString& text); + +public slots: + virtual void clear(); + +protected: + virtual QWidget* widget(); + virtual void updateFieldHook(Data::FieldPtr oldField, Data::FieldPtr newField); + +protected slots: + void slotOpenURL(const QString& url); + +private: + class URLCompletion : public KURLCompletion { + public: + URLCompletion() : KURLCompletion() {} + virtual QString makeCompletion(const QString& text); + }; + + KURLRequester* m_requester; + bool m_isRelative : 1; + QGuardedPtr<KRun> m_run; +}; + + } // end GUI namespace +} // end namespace +#endif diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 0000000..35e14cb --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,162 @@ +/*************************************************************************** + copyright : (C) 2003-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 "image.h" +#include "imagefactory.h" +#include "tellico_debug.h" + +#include <kmdcodec.h> +#include <kpixmapio.h> +#include <kstaticdeleter.h> + +#include <qbuffer.h> +#include <qregexp.h> + +using Tellico::Data::Image; +using Tellico::Data::ImageInfo; + +KPixmapIO* Image::s_pixmapIO = 0; +static KStaticDeleter<KPixmapIO> staticKPixmapIODeleter; +KPixmapIO* Image::io() { + if(!s_pixmapIO) { + staticKPixmapIODeleter.setObject(s_pixmapIO, new KPixmapIO()); + } + return s_pixmapIO; +} + +Image::Image() : QImage(), m_id(QString::null), m_linkOnly(false) { +} + +// I'm using the MD5 hash as the id. I consider it rather unlikely that two images in one +// collection could ever have the same hash, and this lets me do a fast comparison of two images +// simply by comparing their ids. +Image::Image(const QString& filename_) : QImage(filename_), m_linkOnly(false) { + m_format = QImage::imageFormat(filename_); + calculateID(); +} + +Image::Image(const QImage& img_, const QString& format_) : QImage(img_), m_format(format_), m_linkOnly(false) { + calculateID(); +} + +Image::Image(const QByteArray& data_, const QString& format_, const QString& id_) + : QImage(data_), m_id(idClean(id_)), m_format(format_), m_linkOnly(false) { + if(isNull()) { + m_id = QString(); + } +} + +Image::~Image() { +} + +QByteArray Image::byteArray() const { + return byteArray(*this, outputFormat(m_format)); +} + +bool Image::isNull() const { + // 1x1 images are considered null for Tellico. Amazon returns some like that. + return QImage::isNull() || (width() < 2 && height() < 2); +} + +QPixmap Image::convertToPixmap() const { + return io()->convertToPixmap(*this); +} + +QPixmap Image::convertToPixmap(int w_, int h_) const { + if(w_ < width() || h_ < height()) { + return io()->convertToPixmap(this->smoothScale(w_, h_, ScaleMin)); + } else { + return io()->convertToPixmap(*this); + } +} + +QCString Image::outputFormat(const QCString& inputFormat) { + QStrList list = QImage::outputFormats(); + for(QStrListIterator it(list); it.current(); ++it) { + if(inputFormat == it.current()) { + return inputFormat; + } + } +// myDebug() << "Image::outputFormat() - writing " << inputFormat << " as PNG" << endl; + return "PNG"; +} + +QByteArray Image::byteArray(const QImage& img_, const QCString& outputFormat_) { + QByteArray ba; + QBuffer buf(ba); + buf.open(IO_WriteOnly); + QImageIO iio(&buf, outputFormat_); + iio.setImage(img_); + iio.write(); + buf.close(); + return ba; +} + +QString Image::idClean(const QString& id_) { + static const QRegExp rx('[' + QRegExp::escape(QString::fromLatin1("/@<>#\"&%?={}|^~[]'`\\:+")) + ']'); + QString clean = id_; + return clean.remove(rx); +} + +void Image::setID(const QString& id_) { + m_id = id_; +} + +void Image::calculateID() { + // the id will eventually be used as a filename + if(!isNull()) { + KMD5 md5(byteArray()); + m_id = QString::fromLatin1(md5.hexDigest()) + QString::fromLatin1(".") + QString::fromLatin1(m_format).lower(); + m_id = idClean(m_id); + } +} + +/******************************************************/ + +ImageInfo::ImageInfo(const Image& img_) + : id(img_.id()) + , format(img_.format()) + , linkOnly(img_.linkOnly()) + , m_width(img_.width()) + , m_height(img_.height()) { +} + +ImageInfo::ImageInfo(const QString& id_, const QCString& format_, int w_, int h_, bool l_) + : id(id_) + , format(format_) + , linkOnly(l_) + , m_width(w_) + , m_height(h_) { +} + +int ImageInfo::width(bool loadIfNecessary) const { + if(m_width < 1 && loadIfNecessary) { + const Image& img = ImageFactory::imageById(id); + if(!img.isNull()) { + m_width = img.width(); + m_height = img.height(); + } + } + return m_width; +} + +int ImageInfo::height(bool loadIfNecessary) const { + if(m_height < 1 && loadIfNecessary) { + const Image& img = ImageFactory::imageById(id); + if(!img.isNull()) { + m_width = img.width(); + m_height = img.height(); + } + } + return m_height; +} diff --git a/src/image.h b/src/image.h new file mode 100644 index 0000000..9545500 --- /dev/null +++ b/src/image.h @@ -0,0 +1,99 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef IMAGE_H +#define IMAGE_H + +#include <qimage.h> +#include <qstring.h> + +class KPixmapIO; + +namespace Tellico { + class ImageFactory; + class FileHandler; + + namespace Data { + +/** + * @author Robby Stephenson + */ +class Image : public QImage { + +friend class Tellico::ImageFactory; +friend class Tellico::FileHandler; + +public: + ~Image(); + + const QString& id() const { return m_id; }; + const QCString& format() const { return m_format; }; + QByteArray byteArray() const; + bool isNull() const; + bool linkOnly() const { return m_linkOnly; } + void setLinkOnly(bool l) { m_linkOnly = l; } + + QPixmap convertToPixmap() const; + QPixmap convertToPixmap(int width, int height) const; + + static QCString outputFormat(const QCString& inputFormat); + static QByteArray byteArray(const QImage& img, const QCString& outputFormat); + static QString idClean(const QString& id); + +private: + Image(); + explicit Image(const QString& filename); + Image(const QImage& image, const QString& format); + Image(const QByteArray& data, const QString& format, const QString& id); + + //disable copy + Image(const Image&); + Image& operator=(const Image&); + + void setID(const QString& id); + void calculateID(); + + QString m_id; + QCString m_format; + bool m_linkOnly : 1; + + static KPixmapIO* s_pixmapIO; + static KPixmapIO* io(); +}; + +class ImageInfo { +public: + ImageInfo() {} + explicit ImageInfo(const Image& img); + ImageInfo(const QString& id, const QCString& format, int w, int h, bool link); + bool isNull() const { return id.isEmpty(); } + QString id; + QCString format; + bool linkOnly : 1; + + int width(bool loadIfNecessary=true) const; + int height(bool loadIfNecessary=true) const; + +private: + mutable int m_width; + mutable int m_height; +}; + + } // end namespace +} // end namespace + +inline bool operator== (const Tellico::Data::Image& img1, const Tellico::Data::Image& img2) { + return img1.id() == img2.id(); +} + +#endif diff --git a/src/imagefactory.cpp b/src/imagefactory.cpp new file mode 100644 index 0000000..00a980a --- /dev/null +++ b/src/imagefactory.cpp @@ -0,0 +1,611 @@ +/*************************************************************************** + copyright : (C) 2003-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 "imagefactory.h" +#include "image.h" +#include "document.h" +#include "filehandler.h" +#include "tellico_utils.h" +#include "tellico_kernel.h" +#include "core/tellico_config.h" +#include "tellico_debug.h" + +#include <ktempdir.h> +#include <kapplication.h> +#include <kimageeffect.h> + +#include <qfile.h> +#include <qdir.h> + +#define RELEASE_IMAGES + +using Tellico::ImageFactory; + +bool ImageFactory::s_needInit = true; +const Tellico::Data::Image ImageFactory::s_null; + +QDict<Tellico::Data::Image> ImageFactory::s_imageDict; +// since most images get turned into pixmaps quickly, use 10 megs +// for images and 10 megs for pixmaps +QCache<Tellico::Data::Image> ImageFactory::s_imageCache(10 * 1024 * 1024); +QCache<QPixmap> ImageFactory::s_pixmapCache(10 * 1024 * 1024); +// this image info map is just for big images that don't fit +// in the cache, so that don't have to be continually reloaded to get info +QMap<QString, Tellico::Data::ImageInfo> ImageFactory::s_imageInfoMap; +Tellico::StringSet ImageFactory::s_imagesInTmpDir; +Tellico::StringSet ImageFactory::s_imagesToRelease; +KTempDir* ImageFactory::s_tmpDir = 0; +QString ImageFactory::s_localDir; + +void ImageFactory::init() { + if(!s_needInit) { + return; + } + s_imageDict.setAutoDelete(true); + s_imageCache.setAutoDelete(true); + s_imageCache.setMaxCost(Config::imageCacheSize()); + s_pixmapCache.setAutoDelete(true); + s_needInit = false; +} + +QString ImageFactory::tempDir() { + if(!s_tmpDir) { + s_tmpDir = new KTempDir(); + s_tmpDir->setAutoDelete(true); + } + return s_tmpDir->name(); +} + +QString ImageFactory::dataDir() { + static const QString dataDir = Tellico::saveLocation(QString::fromLatin1("data/")); + return dataDir; +} + +QString ImageFactory::localDir() { + if(s_localDir.isEmpty()) { + return dataDir(); + } + return s_localDir; +} + +QString ImageFactory::addImage(const KURL& url_, bool quiet_, const KURL& refer_, bool link_) { + return addImageImpl(url_, quiet_, refer_, link_).id(); +} + +const Tellico::Data::Image& ImageFactory::addImageImpl(const KURL& url_, bool quiet_, const KURL& refer_, bool link_) { + if(url_.isEmpty() || !url_.isValid()) { + return s_null; + } +// myLog() << "ImageFactory::addImageImpl(KURL) - " << url_.prettyURL() << endl; + Data::Image* img = refer_.isEmpty() + ? FileHandler::readImageFile(url_, quiet_) + : FileHandler::readImageFile(url_, quiet_, refer_); + if(!img) { + myLog() << "ImageFactory::addImageImpl() - image not found: " << url_.prettyURL() << endl; + return s_null; + } + if(img->isNull()) { + delete img; + return s_null; + } + + if(link_) { + img->setLinkOnly(true); + img->setID(url_.url()); + } + + if(hasImage(img->id())) { +// myDebug() << "### ImageFactory::addImageImpl() - hasImage() is true!" << endl; + const Data::Image& img2 = imageById(img->id()); + if(!img2.isNull()) { + delete img; + return img2; + } + } + + if(!link_) { + s_imageDict.insert(img->id(), img); + } + s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img)); + return *img; +} + +QString ImageFactory::addImage(const QImage& image_, const QString& format_) { + return addImageImpl(image_, format_).id(); +} + +QString ImageFactory::addImage(const QPixmap& pix_, const QString& format_) { + return addImageImpl(pix_.convertToImage(), format_).id(); +} + +const Tellico::Data::Image& ImageFactory::addImageImpl(const QImage& image_, const QString& format_) { + Data::Image* img = new Data::Image(image_, format_); + if(hasImage(img->id())) { + const Data::Image& img2 = imageById(img->id()); + if(!img2.isNull()) { + delete img; + return img2; + } + } + if(img->isNull()) { + delete img; + return s_null; + } + s_imageDict.insert(img->id(), img); + s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img)); + return *img; +} + +QString ImageFactory::addImage(const QByteArray& data_, const QString& format_, const QString& id_) { + return addImageImpl(data_, format_, id_).id(); +} + +const Tellico::Data::Image& ImageFactory::addImageImpl(const QByteArray& data_, const QString& format_, + const QString& id_) { + if(id_.isEmpty()) { + return s_null; + } + + // do not call imageById(), it causes infinite looping with Document::loadImage() + Data::Image* img = s_imageCache.find(id_); + if(img) { + myLog() << "ImageFactory::addImageImpl(QByteArray) - already exists in cache: " << id_ << endl; + return *img; + } + + img = s_imageDict.find(id_); + if(img) { + myLog() << "ImageFactory::addImageImpl(QByteArray) - already exists in dict: " << id_ << endl; + return *img; + } + + img = new Data::Image(data_, format_, id_); + if(img->isNull()) { + myDebug() << "ImageFactory::addImageImpl(QByteArray) - NULL IMAGE!!!!!" << endl; + delete img; + return s_null; + } + +// myLog() << "ImageFactory::addImageImpl(QByteArray) - " << data_.size() +// << " bytes, format = " << format_ +// << ", id = "<< img->id() << endl; + + s_imageDict.insert(img->id(), img); + s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img)); + return *img; +} + +const Tellico::Data::Image& ImageFactory::addCachedImageImpl(const QString& id_, CacheDir dir_) { +// myLog() << "ImageFactory::addCachedImageImpl() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" ) +// << "; id = " << id_ << endl; + KURL u; + if(dir_ == DataDir) { + u.setPath(dataDir() + id_); + } else if(dir_ == LocalDir) { + u.setPath(localDir() + id_); + } else{ // Temp + u.setPath(tempDir() + id_); + } + + QString newID = addImage(u, true); + if(newID.isEmpty()) { + myLog() << "ImageFactory::addCachedImageImpl() - null image loaded" << endl; + return s_null; + } + + // the id probably got changed, so reset it + // addImage() already inserted it in the dict + Data::Image* img = s_imageDict.take(newID); + if(!img) { + kdWarning() << "ImageFactory::addCachedImageImpl() - no image in dict - very bad!" << endl; + return s_null; + } + if(img->isNull()) { + kdWarning() << "ImageFactory::addCachedImageImpl() - null image in dict, should never happen!" << endl; + delete img; + return s_null; + } + img->setID(id_); + s_imageInfoMap.remove(newID); + s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img)); + + if(s_imageCache.insert(img->id(), img, img->numBytes())) { +// myLog() << "ImageFactory::addCachedImageImpl() - removing from dict: " << img->id() << endl; + } else { + // can't hold it in the cache + kdWarning() << "Tellico's image cache is unable to hold the image, it might be too big!" << endl; + kdWarning() << "Image name is " << img->id() << endl; + kdWarning() << "Image size is " << img->numBytes() << endl; + kdWarning() << "Max cache size is " << s_imageCache.maxCost() << endl; + + // add it back to the dict, but add the image to the list of + // images to release later. Necessary to avoid a memory leak since new Image() + // was called, we need to keep the pointer + s_imageDict.insert(img->id(), img); + s_imagesToRelease.add(img->id()); + } + return *img; +} + +bool ImageFactory::writeImage(const QString& id_, const KURL& targetDir_, bool force_) { +// myLog() << "ImageFactory::writeImage() - target = " << targetDir_.url() << id_ << endl; + if(targetDir_.isEmpty()) { + myDebug() << "ImageFactory::writeImage() - empty target dir!" << endl; + return false; + } + + const Data::Image& img = imageById(id_); + if(img.isNull()) { +// myDebug() << "ImageFactory::writeImage() - null image: " << id_ << endl; + return false; + } + + if(img.linkOnly()) { +// myLog() << "ImageFactory::writeImage() - " << id_ << ": link only, not writing!" << endl; + return true; + } + + KURL target = targetDir_; + target.addPath(id_); + + return FileHandler::writeDataURL(target, img.byteArray(), force_); +} + +bool ImageFactory::writeCachedImage(const QString& id_, CacheDir dir_, bool force_ /*=false*/) { + if(id_.isEmpty()) { + return false; + } +// myLog() << "ImageFactory::writeCachedImage() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" ) +// << "; id = " << id_ << endl; + + QString path = ( dir_ == DataDir ? dataDir() : dir_ == TempDir ? tempDir() : localDir() ); + + // images in the temp directory are erased every session, so we can track + // whether they've already been written with a simple string set. + // images in the data directory are persistent, so we have to check the + // actual file existence + bool exists = ( dir_ == TempDir ? s_imagesInTmpDir.has(id_) : QFile::exists(path + id_)); + + if(!force_ && exists) { +// myDebug() << "...writeCachedImage() - exists = true: " << id_ << endl; + } else if(!force_ && !exists && dir_ == LocalDir) { + QDir dir(localDir()); + if(!dir.exists()) { + myDebug() << "ImageFactory::writeCachedImage() - creating " << s_localDir << endl; + dir.mkdir(localDir()); + } + } else { +// myLog() << "ImageFactory::writeCachedImage() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" ) +// << "; id = " << id_ << endl; + } + // only write if it doesn't exist + bool success = (!force_ && exists) || writeImage(id_, path, true /* force */); + + if(success) { + if(dir_ == TempDir) { + s_imagesInTmpDir.add(id_); + } + + // remove from dict and add to cache + // it might not be in dict though + Data::Image* img = s_imageDict.take(id_); + if(img && s_imageCache.insert(img->id(), img, img->numBytes())) { + s_imageInfoMap.remove(id_); + } else if(img) { +// myLog() << "ImageFactory::writeCachedImage() - failed writing image to cache: " << id_ << endl; +// myLog() << "ImageFactory::writeCachedImage() - removed from dict, deleting: " << img->id() << endl; +// myLog() << "ImageFactory::writeCachedImage() - current dict size: " << s_imageDict.count() << endl; + // can't insert it in the cache, so put it back in the dict + // No, it's written to disk now, so we're safe +// s_imageDict.insert(img->id(), img); + delete img; + } + } + return success; +} + +const Tellico::Data::Image& ImageFactory::imageById(const QString& id_) { + if(id_.isEmpty()) { + myDebug() << "ImageFactory::imageById() - empty id" << endl; + return s_null; + } +// myLog() << "ImageFactory::imageById() - " << id_ << endl; + + // can't think of a better place to regularly check for images to release + // but don't release image that just got asked for + s_imagesToRelease.remove(id_); + releaseImages(); + + // first check the cache, used for images that are in the data file, or are only temporary + // then the dict, used for images downloaded, but not yet saved anywhere + Data::Image* img = s_imageCache.find(id_); + if(img) { +// myLog() << "...imageById() - found in cache" << endl; + return *img; + } + + img = s_imageDict.find(id_); + if(img) { +// myLog() << "...imageById() - found in dict" << endl; + return *img; + } + + // if the image is link only, we need to load it + // but can't call imageInfo() since that might recurse into imageById() + // also, the image info cache might not have it so check if the + // id is a valid absolute url + // yeah, it's probably slow + if((s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly) || !KURL::isRelativeURL(id_)) { + KURL u = id_; + if(u.isValid()) { + return addImageImpl(u, false, KURL(), true); + } + } + + // the document does a delayed loading of the images, sometimes + // so an image could be in the tmp dir and not be in the cache + // or it could be too big for the cache + if(s_imagesInTmpDir.has(id_)) { + const Data::Image& img2 = addCachedImageImpl(id_, TempDir); + if(!img2.isNull()) { +// myLog() << "...imageById() - found in tmp dir" << endl; + return img2; + } else { + myLog() << "ImageFactory::imageById() - img in tmpDir list but not actually there: " << id_ << endl; + s_imagesInTmpDir.remove(id_); + } + } + + // try to do a delayed loading of the image + if(Data::Document::self()->loadImage(id_)) { + // loadImage() could insert in either the cache or the dict! + img = s_imageCache.find(id_); + if(!img) { + img = s_imageDict.find(id_); + } + if(img) { +// myLog() << "...imageById() - found in doc" << endl; + // go ahead and write image to disk so we don't have to keep it in memory + // calling pixmap() could be loading all the covers, and we don't want one + // to get pushed out of the cache yet + if(!s_imagesInTmpDir.has(id_)) { + writeCachedImage(id_, TempDir); + } + return *img; + } + } + + // don't check Config::writeImagesInFile(), someday we might have problems + // and the image will exist in the data dir, but the app thinks everything should + // be in the zip file instead + bool exists = QFile::exists(dataDir() + id_); + if(exists) { + // if we're loading from the application data dir, but images are being saved in the + // data file instead, then consider the document to be modified since it needs + // the image saved + if(Config::imageLocation() != Config::ImagesInAppDir) { + Data::Document::self()->slotSetModified(true); + } + const Data::Image& img2 = addCachedImageImpl(id_, DataDir); + if(img2.isNull()) { + myDebug() << "ImageFactory::imageById() - tried to add from DataDir, but failed: " << id_ << endl; + } else { +// myLog() << "...imageById() - found in data dir" << endl; + return img2; + } + } + // if localDir() == DataDir(), then there's nothing left to check + if(localDir() == dataDir()) { + return s_null; + } + exists = QFile::exists(localDir() + id_); + if(exists) { + // if we're loading from the application data dir, but images are being saved in the + // data file instead, then consider the document to be modified since it needs + // the image saved + if(Config::imageLocation() != Config::ImagesInLocalDir) { + Data::Document::self()->slotSetModified(true); + } + const Data::Image& img2 = addCachedImageImpl(id_, LocalDir); + if(img2.isNull()) { + myDebug() << "ImageFactory::imageById() - tried to add from LocalDir, but failed: " << id_ << endl; + } else { +// myLog() << "...imageById() - found in data dir" << endl; + return img2; + } + } + myDebug() << "***ImageFactory::imageById() - not found: " << id_ << endl; + return s_null; +} + +Tellico::Data::ImageInfo ImageFactory::imageInfo(const QString& id_) { + if(s_imageInfoMap.contains(id_)) { + return s_imageInfoMap[id_]; + } + + const Data::Image& img = imageById(id_); + if(img.isNull()) { + return Data::ImageInfo(); + } + return Data::ImageInfo(img); +} + +void ImageFactory::cacheImageInfo(const Data::ImageInfo& info) { + s_imageInfoMap.insert(info.id, info); +} + +bool ImageFactory::validImage(const QString& id_) { + // don't try s_imageInfoMap[id_] cause it inserts an empty image info + return s_imageInfoMap.contains(id_) || hasImage(id_) || !imageById(id_).isNull(); +} + +QPixmap ImageFactory::pixmap(const QString& id_, int width_, int height_) { + if(id_.isEmpty()) { + return QPixmap(); + } + + const QString key = id_ + '|' + QString::number(width_) + '|' + QString::number(height_); + QPixmap* pix = s_pixmapCache.find(key); + if(pix) { + return *pix; + } + + const Data::Image& img = imageById(id_); + if(img.isNull()) { + return QPixmap(); + } + + if(width_ > 0 && height_ > 0) { + pix = new QPixmap(img.convertToPixmap(width_, height_)); + } else { + pix = new QPixmap(img.convertToPixmap()); + } + + // pixmap size is w x h x d, divided by 8 bits + if(!s_pixmapCache.insert(key, pix, pix->width()*pix->height()*pix->depth()/8)) { + kdWarning() << "ImageFactory::pixmap() - can't save in cache: " << id_ << endl; + kdWarning() << "### Current pixmap size is " << (pix->width()*pix->height()*pix->depth()/8) << endl; + kdWarning() << "### Max pixmap cache size is " << s_pixmapCache.maxCost() << endl; + QPixmap pix2(*pix); + delete pix; + return pix2; + } + return *pix; +} + +void ImageFactory::clean(bool deleteTempDirectory_) { + // the dict and caches all auto-delete + s_imagesToRelease.clear(); + s_imageDict.clear(); + s_imageInfoMap.clear(); + s_imageCache.clear(); + s_pixmapCache.clear(); + if(deleteTempDirectory_) { + s_imagesInTmpDir.clear(); + delete s_tmpDir; + s_tmpDir = 0; + } +} + +void ImageFactory::createStyleImages(const StyleOptions& opt_) { + const int collType = Kernel::self()->collectionType(); + + const QColor& baseColor = opt_.baseColor.isValid() + ? opt_.baseColor + : Config::templateBaseColor(collType); + const QColor& highColor = opt_.highlightedBaseColor.isValid() + ? opt_.highlightedBaseColor + : Config::templateHighlightedBaseColor(collType); + + const QColor& bgc1 = Tellico::blendColors(baseColor, highColor, 30); + const QColor& bgc2 = Tellico::blendColors(baseColor, highColor, 50); + + const QString bgname = QString::fromLatin1("gradient_bg.png"); + QImage bgImage = KImageEffect::gradient(QSize(400, 1), bgc1, baseColor, + KImageEffect::PipeCrossGradient); + bgImage = KImageEffect::rotate(bgImage, KImageEffect::Rotate90); + + const QString hdrname = QString::fromLatin1("gradient_header.png"); + QImage hdrImage = KImageEffect::unbalancedGradient(QSize(1, 10), highColor, bgc2, + KImageEffect::VerticalGradient, 100, -100); + + if(opt_.imgDir.isEmpty()) { + // write the style images both to the tmp dir and the data dir + // doesn't really hurt and lets the user switch back and forth + ImageFactory::removeImage(bgname, true /*delete */); + ImageFactory::addImageImpl(Data::Image::byteArray(bgImage, "PNG"), QString::fromLatin1("PNG"), bgname); + ImageFactory::writeCachedImage(bgname, DataDir, true /*force*/); + ImageFactory::writeCachedImage(bgname, TempDir, true /*force*/); + + ImageFactory::removeImage(hdrname, true /*delete */); + ImageFactory::addImageImpl(Data::Image::byteArray(hdrImage, "PNG"), QString::fromLatin1("PNG"), hdrname); + ImageFactory::writeCachedImage(hdrname, DataDir, true /*force*/); + ImageFactory::writeCachedImage(hdrname, TempDir, true /*force*/); + } else { + bgImage.save(opt_.imgDir + bgname, "PNG"); + hdrImage.save(opt_.imgDir + hdrname, "PNG"); + } +} + +void ImageFactory::removeImage(const QString& id_, bool deleteImage_) { + //be careful using this + s_imageDict.remove(id_); + s_imageCache.remove(id_); + s_imagesInTmpDir.remove(id_); + + if(deleteImage_) { + // remove from both data dir and temp dir + QFile::remove(dataDir() + id_); + QFile::remove(tempDir() + id_); + } +} + +Tellico::StringSet ImageFactory::imagesNotInCache() { + StringSet set; + for(QDictIterator<Tellico::Data::Image> it(s_imageDict); it.current(); ++it) { + if(s_imageCache.find(it.currentKey()) == 0) { + set.add(it.currentKey()); + } + } + return set; +} + +bool ImageFactory::hasImage(const QString& id_) { + return s_imageCache.find(id_, false) || s_imageDict.find(id_); +} + +// the purpose here is to remove images from the dict if they're is on the disk somewhere, +// either in tempDir() or in dataDir(). The use for this is for calling pixmap() on an +// image too big to stay in the cache. Then it stays in the dict forever. +void ImageFactory::releaseImages() { +#ifdef RELEASE_IMAGES + if(s_imagesToRelease.isEmpty()) { + return; + } + + const QStringList images = s_imagesToRelease.toList(); + for(QStringList::ConstIterator it = images.begin(); it != images.end(); ++it) { + s_imagesToRelease.remove(*it); + if(!s_imageDict.find(*it)) { + continue; + } +// myLog() << "ImageFactory::releaseImage() - id = " << *it << endl; + if(QFile::exists(dataDir() + *it)) { +// myDebug() << "...exists in dataDir() - removing from dict" << endl; + s_imageDict.remove(*it); + } else if(QFile::exists(tempDir() + *it)) { +// myDebug() << "...exists in tempDir() - removing from dict" << endl; + s_imageDict.remove(*it); + } + } +#endif +} + +void ImageFactory::setLocalDirectory(const KURL& url_) { + if(url_.isEmpty()) { + return; + } + if(!url_.isLocalFile()) { + myWarning() << "ImageFactory::setLocalDirectory() - Tellico can only save images to local disk" << endl; + myWarning() << "unable to save to " << url_ << endl; + } else { + s_localDir = url_.directory(false); + // could have already been set once + if(!url_.fileName().contains(QString::fromLatin1("_files"))) { + s_localDir += url_.fileName().section('.', 0, 0) + QString::fromLatin1("_files/"); + } + myLog() << "ImageFactory::setLocalDirectory() - local dir = " << s_localDir << endl; + } +} + +#undef RELEASE_IMAGES diff --git a/src/imagefactory.h b/src/imagefactory.h new file mode 100644 index 0000000..efb3009 --- /dev/null +++ b/src/imagefactory.h @@ -0,0 +1,195 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef IMAGEFACTORY_H +#define IMAGEFACTORY_H + +#include "stringset.h" + +#include <kurl.h> + +#include <qcolor.h> +#include <qcache.h> + +class KTempDir; + +namespace Tellico { + namespace Data { + class Image; + class ImageInfo; + } + +class StyleOptions { +public: + QString fontFamily; + int fontSize; + QColor baseColor; + QColor textColor; + QColor highlightedBaseColor; + QColor highlightedTextColor; + QString imgDir; +}; + +/** + * @author Robby Stephenson + */ +class ImageFactory { +public: + enum CacheDir { + TempDir, + DataDir, + LocalDir + }; + + /** + * setup some of the static members + */ + static void init(); + + /** + * Returns the temporary directory where image files are saved + * + * @return The full path + */ + static QString tempDir(); + static QString dataDir(); + + /** + * Add an image, reading it from a URL, which is the case when adding a new image from the + * @ref ImageWidget. + * + * @param url The URL of the image, anything KIO can handle + * @param quiet If any error should not be reported. + * @return The image id, empty if null + */ + static QString addImage(const KURL& url, bool quiet=false, + const KURL& referrer = KURL(), bool linkOnly=false); + /** + * Add an image, reading it from a regular QImage, which is the case when dragging and dropping + * an image in the @ref ImageWidget. The format has to be included, since the QImage doesn't + * 'know' what format it came from. + * + * @param image The qimage + * @param format The image format, probably "PNG" + * @return The image id, empty if null + */ + static QString addImage(const QImage& image, const QString& format); + static QString addImage(const QPixmap& image, const QString& format); + /** + * Add an image, reading it from data, which is the case when reading from the data file. The + * @p id isn't strictly needed, since it can be reconstructed from the image data and format, but + * since it's already known, go ahead and use it. + * + * @param data The image data + * @param format The image format, from Qt's output format list + * @param id The internal id of the image + * @return The image id, empty if null + */ + static QString addImage(const QByteArray& data, const QString& format, const QString& id); + + /** + * Writes an image to a file. ImageFactory keeps track of which images were already written + * if the location is the same as the tempdir. + * + * @param id The ID of the image to be written + * @param targetDir The directory to write the image to, if empty, the tempdir is used. + * @param force Force the image to be written, even if it already has been + * @return Whether the save was successful + */ + static bool writeImage(const QString& id, const KURL& targetDir, bool force=false); + static bool writeCachedImage(const QString& id, CacheDir dir, bool force = false); + + /** + * Returns an image reference given its id. If none is found, a null image + * is returned. + * + * @param id The image id + * @return The image referencenter + */ + static const Data::Image& imageById(const QString& id); + static Data::ImageInfo imageInfo(const QString& id); + static void cacheImageInfo(const Data::ImageInfo& info); + // basically returns !imageById().isNull() + static bool validImage(const QString& id); + + static QPixmap pixmap(const QString& id, int w, int h); + + /** + * Clear the image cache and dict + * if deleteTempDirectory = true, then clean the temp dir and remove all temporary image files + */ + static void clean(bool deleteTempDirectory); + /** + * Creates the gradient images used in the entry view. + */ + static void createStyleImages(const StyleOptions& options = StyleOptions()); + + static void removeImage(const QString& id_, bool deleteImage); + static StringSet imagesNotInCache(); + + static void setLocalDirectory(const KURL& url); + // local save directory + static QString localDir(); + +private: + /** + * Add an image, reading it from a URL, which is the case when adding a new image from the + * @ref ImageWidget. + * + * @param url The URL of the image, anything KIO can handle + * @param quiet If any error should not be reported. + * @return The image + */ + static const Data::Image& addImageImpl(const KURL& url, bool quiet=false, + const KURL& referrer = KURL(), bool linkOnly = false); + /** + * Add an image, reading it from a regular QImage, which is the case when dragging and dropping + * an image in the @ref ImageWidget. The format has to be included, since the QImage doesn't + * 'know' what format it came from. + * + * @param image The qimage + * @param format The image format, probably "PNG" + * @return The image + */ + static const Data::Image& addImageImpl(const QImage& image, const QString& format); + /** + * Add an image, reading it from data, which is the case when reading from the data file. The + * @p id isn't strictly needed, since it can be reconstructed from the image data and format, but + * since it's already known, go ahead and use it. + * + * @param data The image data + * @param format The image format, from Qt's output format list + * @param id The internal id of the image + * @return The image + */ + static const Data::Image& addImageImpl(const QByteArray& data, const QString& format, const QString& id); + + static const Data::Image& addCachedImageImpl(const QString& id, CacheDir dir); + static bool hasImage(const QString& id); + static void releaseImages(); + + static bool s_needInit; + static QDict<Data::Image> s_imageDict; + static QCache<Data::Image> s_imageCache; + static QCache<QPixmap> s_pixmapCache; + static QMap<QString, Data::ImageInfo> s_imageInfoMap; + static StringSet s_imagesInTmpDir; // the id's of the images written to tmp directory + static StringSet s_imagesToRelease; + static KTempDir* s_tmpDir; + static QString s_localDir; + static const Data::Image s_null; +}; + +} // end namespace + +#endif diff --git a/src/importdialog.cpp b/src/importdialog.cpp new file mode 100644 index 0000000..07e0ec1 --- /dev/null +++ b/src/importdialog.cpp @@ -0,0 +1,382 @@ +/*************************************************************************** + copyright : (C) 2003-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 "importdialog.h" +#include "document.h" +#include "tellico_kernel.h" +#include "tellico_debug.h" +#include "collection.h" + +#include "translators/importer.h" +#include "translators/tellicoimporter.h" +#include "translators/bibteximporter.h" +#include "translators/bibtexmlimporter.h" +#include "translators/csvimporter.h" +#include "translators/xsltimporter.h" +#include "translators/audiofileimporter.h" +#include "translators/alexandriaimporter.h" +#include "translators/freedbimporter.h" +#include "translators/risimporter.h" +#include "translators/gcfilmsimporter.h" +#include "translators/filelistingimporter.h" +#include "translators/amcimporter.h" +#include "translators/griffithimporter.h" +#include "translators/pdfimporter.h" +#include "translators/referencerimporter.h" +#include "translators/deliciousimporter.h" + +#include <klocale.h> +#include <kstandarddirs.h> + +#include <qlayout.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qcheckbox.h> +#include <qwhatsthis.h> +#include <qtimer.h> + +// really according to taste or computer speed +const unsigned Tellico::Import::Importer::s_stepSize = 20; + +using Tellico::ImportDialog; + +ImportDialog::ImportDialog(Import::Format format_, const KURL::List& urls_, QWidget* parent_, const char* name_) + : KDialogBase(parent_, name_, true /*modal*/, i18n("Import Options"), Ok|Cancel), + m_coll(0), + m_importer(importer(format_, urls_)) { + QWidget* widget = new QWidget(this); + QVBoxLayout* topLayout = new QVBoxLayout(widget, 0, spacingHint()); + + QButtonGroup* bg = new QButtonGroup(1, Qt::Horizontal, i18n("Import Options"), widget); + topLayout->addWidget(bg, 0); + m_radioReplace = new QRadioButton(i18n("&Replace current collection"), bg); + QWhatsThis::add(m_radioReplace, i18n("Replace the current collection with the contents " + "of the imported file.")); + m_radioAppend = new QRadioButton(i18n("A&ppend to current collection"), bg); + QWhatsThis::add(m_radioAppend, i18n("Append the contents of the imported file to the " + "current collection. This is only possible when the " + "collection types match.")); + m_radioMerge = new QRadioButton(i18n("&Merge with current collection"), bg); + QWhatsThis::add(m_radioMerge, i18n("Merge the contents of the imported file to the " + "current collection. This is only possible when the " + "collection types match. Entries must match exactly " + "in order to be merged.")); + if(m_importer->canImport(Kernel::self()->collectionType())) { + // append by default? + m_radioAppend->setChecked(true); + } else { + m_radioReplace->setChecked(true); + m_radioAppend->setEnabled(false); + m_radioMerge->setEnabled(false); + } + + QWidget* w = m_importer->widget(widget, "importer_widget"); +// m_importer->readOptions(KGlobal::config()); + if(w) { + topLayout->addWidget(w, 0); + } + + connect(bg, SIGNAL(clicked(int)), m_importer, SLOT(slotActionChanged(int))); + + topLayout->addStretch(); + setMainWidget(widget); + + KGuiItem ok = KStdGuiItem::ok(); + ok.setText(i18n("&Import")); + setButtonOK(ok); + + // want to grab default button action, too + // since the importer might do something with widgets, don't just call it, do it after layout is done + QTimer::singleShot(0, this, SLOT(slotUpdateAction())); +} + +ImportDialog::~ImportDialog() { + delete m_importer; + m_importer = 0; +} + +Tellico::Data::CollPtr ImportDialog::collection() { + if(m_importer && !m_coll) { + m_coll = m_importer->collection(); + } + return m_coll; +} + +QString ImportDialog::statusMessage() const { + return m_importer ? m_importer->statusMessage() : QString::null; +} + +Tellico::Import::Action ImportDialog::action() const { + if(m_radioReplace->isChecked()) { + return Import::Replace; + } else if(m_radioAppend->isChecked()) { + return Import::Append; + } else { + return Import::Merge; + } +} + +// static +Tellico::Import::Importer* ImportDialog::importer(Import::Format format_, const KURL::List& urls_) { +#define CHECK_SIZE if(urls_.size() > 1) kdWarning() << "ImportDialog::importer() - only importing first URL" << endl + KURL firstURL = urls_.isEmpty() ? KURL() : urls_[0]; + Import::Importer* importer = 0; + switch(format_) { + case Import::TellicoXML: + CHECK_SIZE; + importer = new Import::TellicoImporter(firstURL); + break; + + case Import::Bibtex: + importer = new Import::BibtexImporter(urls_); + break; + + case Import::Bibtexml: + CHECK_SIZE; + importer = new Import::BibtexmlImporter(firstURL); + break; + + case Import::CSV: + CHECK_SIZE; + importer = new Import::CSVImporter(firstURL); + break; + + case Import::XSLT: + CHECK_SIZE; + importer = new Import::XSLTImporter(firstURL); + break; + + case Import::MODS: + CHECK_SIZE; + importer = new Import::XSLTImporter(firstURL); + { + QString xsltFile = locate("appdata", QString::fromLatin1("mods2tellico.xsl")); + if(!xsltFile.isEmpty()) { + KURL u; + u.setPath(xsltFile); + static_cast<Import::XSLTImporter*>(importer)->setXSLTURL(u); + } else { + kdWarning() << "ImportDialog::importer - unable to find mods2tellico.xml!" << endl; + } + } + break; + + case Import::AudioFile: + CHECK_SIZE; + importer = new Import::AudioFileImporter(firstURL); + break; + + case Import::Alexandria: + CHECK_SIZE; + importer = new Import::AlexandriaImporter(); + break; + + case Import::FreeDB: + CHECK_SIZE; + importer = new Import::FreeDBImporter(); + break; + + case Import::RIS: + importer = new Import::RISImporter(urls_); + break; + + case Import::GCfilms: + CHECK_SIZE; + importer = new Import::GCfilmsImporter(firstURL); + break; + + case Import::FileListing: + CHECK_SIZE; + importer = new Import::FileListingImporter(firstURL); + break; + + case Import::AMC: + CHECK_SIZE; + importer = new Import::AMCImporter(firstURL); + break; + + case Import::Griffith: + importer = new Import::GriffithImporter(); + break; + + case Import::PDF: + importer = new Import::PDFImporter(urls_); + break; + + case Import::Referencer: + CHECK_SIZE; + importer = new Import::ReferencerImporter(firstURL); + break; + + case Import::Delicious: + CHECK_SIZE; + importer = new Import::DeliciousImporter(firstURL); + break; + + case Import::GRS1: + myDebug() << "ImportDialog::importer() - GRS1 not implemented" << endl; + break; + } +#ifndef NDEBUG + if(!importer) { + kdWarning() << "ImportDialog::importer() - importer not created!" << endl; + } +#endif + return importer; +#undef CHECK_SIZE +} + +// static +QString ImportDialog::fileFilter(Import::Format format_) { + QString text; + switch(format_) { + case Import::TellicoXML: + text = i18n("*.tc *.bc|Tellico Files (*.tc)") + QChar('\n'); + text += i18n("*.xml|XML Files (*.xml)") + QChar('\n'); + break; + + case Import::Bibtex: + text = i18n("*.bib|Bibtex Files (*.bib)") + QChar('\n'); + break; + + case Import::CSV: + text = i18n("*.csv|CSV Files (*.csv)") + QChar('\n'); + break; + + case Import::Bibtexml: + case Import::XSLT: + text = i18n("*.xml|XML Files (*.xml)") + QChar('\n'); + break; + + case Import::MODS: + case Import::Delicious: + text = i18n("*.xml|XML Files (*.xml)") + QChar('\n'); + break; + + case Import::RIS: + text = i18n("*.ris|RIS Files (*.ris)") + QChar('\n'); + break; + + case Import::GCfilms: + text = i18n("*.gcs|GCstar Data Files (*.gcs)") + QChar('\n'); + text += i18n("*.gcf|GCfilms Data Files (*.gcf)") + QChar('\n'); + break; + + case Import::AMC: + text = i18n("*.amc|AMC Data Files (*.amc)") + QChar('\n'); + break; + + case Import::PDF: + text = i18n("*.pdf|PDF Files (*.pdf)") + QChar('\n'); + break; + + case Import::Referencer: + text = i18n("*.reflib|Referencer Files (*.reflib)") + QChar('\n'); + break; + + case Import::AudioFile: + case Import::Alexandria: + case Import::FreeDB: + case Import::FileListing: + case Import::GRS1: + case Import::Griffith: + break; + } + + return text + i18n("*|All Files"); +} + +// audio files are imported by directory +// alexandria is a defined location, as is freedb +// all others are files +Tellico::Import::Target ImportDialog::importTarget(Import::Format format_) { + switch(format_) { + case Import::AudioFile: + case Import::FileListing: + return Import::Dir; + case Import::Griffith: + case Import::Alexandria: + case Import::FreeDB: + return Import::None; + default: + return Import::File; + } +} + +Tellico::Import::FormatMap ImportDialog::formatMap() { + // at one point, these were translated, but after some thought + // I decided they were likely to be the same in any language + // transliteration is unlikely + Import::FormatMap map; + map[Import::TellicoXML] = QString::fromLatin1("Tellico"); + map[Import::Bibtex] = QString::fromLatin1("Bibtex"); + map[Import::Bibtexml] = QString::fromLatin1("Bibtexml"); +// map[Import::CSV] = QString::fromLatin1("CSV"); + map[Import::MODS] = QString::fromLatin1("MODS"); + map[Import::RIS] = QString::fromLatin1("RIS"); + map[Import::GCfilms] = QString::fromLatin1("GCstar"); + map[Import::AMC] = QString::fromLatin1("AMC"); + map[Import::Griffith] = QString::fromLatin1("Griffith"); + map[Import::PDF] = QString::fromLatin1("PDF"); + map[Import::Referencer] = QString::fromLatin1("Referencer"); + map[Import::Delicious ] = QString::fromLatin1("Delicious Library"); + return map; +} + +bool ImportDialog::formatImportsText(Import::Format format_) { + return format_ != Import::AMC && + format_ != Import::Griffith && + format_ != Import::PDF; +} + +QString ImportDialog::startDir(Import::Format format_) { + if(format_ == Import::GCfilms) { + QDir dir = QDir::home(); + // able to cd if exists and readable + if(dir.cd(QString::fromLatin1(".local/share/gcfilms/"))) { + return dir.absPath(); + } + } + return QString::fromLatin1(":import"); +} + +void ImportDialog::slotOk() { + // some importers, like the CSV importer, can validate their settings + if(!m_importer || m_importer->validImport()) { + KDialogBase::slotOk(); + } else { + myLog() << "ImportDialog::slotOk() - not a valid import" << endl; + } +} + +void ImportDialog::slotUpdateAction() { + // distasteful hack + // selectedId() is new in QT 3.2 +// m_importer->slotActionChanged(dynamic_cast<QButtonGroup*>(m_radioAppend->parentWidget())->selectedId()); + QButtonGroup* bg = static_cast<QButtonGroup*>(m_radioAppend->parentWidget()); + m_importer->slotActionChanged(bg->id(bg->selected())); +} + +// static +Tellico::Data::CollPtr ImportDialog::importURL(Import::Format format_, const KURL& url_) { + Import::Importer* imp = importer(format_, url_); + Data::CollPtr c = imp->collection(); + if(!c && !imp->statusMessage().isEmpty()) { + Kernel::self()->sorry(imp->statusMessage()); + } + delete imp; + return c; +} + + +#include "importdialog.moc" diff --git a/src/importdialog.h b/src/importdialog.h new file mode 100644 index 0000000..3802436 --- /dev/null +++ b/src/importdialog.h @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef IMPORTDIALOG_H +#define IMPORTDIALOG_H + +#include "translators/translators.h" +#include "datavectors.h" + +#include <kdialogbase.h> +#include <kurl.h> + +class QRadioButton; +class QCheckBox; +class QShowEvent; + +namespace Tellico { + namespace Import { + class Importer; + typedef QMap<Import::Format, QString> FormatMap; + } + +/** + * @author Robby Stephenson + */ +class ImportDialog : public KDialogBase { +Q_OBJECT + +public: + ImportDialog(Import::Format format, const KURL::List& urls, QWidget* parent, const char* name); + ~ImportDialog(); + + Data::CollPtr collection(); + QString statusMessage() const; + Import::Action action() const; + + static QString fileFilter(Import::Format format); + static Import::Target importTarget(Import::Format format); + static QString startDir(Import::Format format); + static Import::FormatMap formatMap(); + static bool formatImportsText(Import::Format format); + + static Import::Importer* importer(Import::Format format, const KURL::List& urls); + static Data::CollPtr importURL(Import::Format format, const KURL& url); + +protected: + virtual void slotOk(); + +private slots: + void slotUpdateAction(); + +private: + Data::CollPtr m_coll; + Import::Importer* m_importer; + QRadioButton* m_radioAppend; + QRadioButton* m_radioReplace; + QRadioButton* m_radioMerge; +}; + +} // end namespace +#endif diff --git a/src/isbnvalidator.cpp b/src/isbnvalidator.cpp new file mode 100644 index 0000000..91ed099 --- /dev/null +++ b/src/isbnvalidator.cpp @@ -0,0 +1,478 @@ +/*************************************************************************** + copyright : (C) 2002-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 "isbnvalidator.h" +#include "tellico_debug.h" + +using Tellico::ISBNValidator; + +//static +QString ISBNValidator::isbn10(QString isbn13) { + if(!isbn13.startsWith(QString::fromLatin1("978"))) { + myDebug() << "ISBNValidator::isbn10() - can't convert, must start with 978: " << isbn13 << endl; + return isbn13; + } + isbn13 = isbn13.mid(3); + isbn13.remove('-'); + // remove checksum + isbn13.truncate(isbn13.length()-1); + // add new checksum + isbn13 += checkSum10(isbn13); + staticFixup(isbn13); + return isbn13; +} + +QString ISBNValidator::isbn13(QString isbn10) { + isbn10.remove('-'); + if(isbn10.startsWith(QString::fromLatin1("978")) || + isbn10.startsWith(QString::fromLatin1("979"))) { + return isbn10; + } + // remove checksum + isbn10.truncate(isbn10.length()-1); + // begins with 978 + isbn10.prepend(QString::fromLatin1("978")); + // add new checksum + isbn10 += checkSum13(isbn10); + staticFixup(isbn10); + return isbn10; +} + +QString ISBNValidator::cleanValue(QString isbn) { + isbn.remove(QRegExp(QString::fromLatin1("[^xX0123456789]"))); + return isbn; +} + +ISBNValidator::ISBNValidator(QObject* parent_, const char* name_/*=0*/) + : QValidator(parent_, name_) { +} + +QValidator::State ISBNValidator::validate(QString& input_, int& pos_) const { + if(input_.startsWith(QString::fromLatin1("978")) || + input_.startsWith(QString::fromLatin1("979"))) { + return validate13(input_, pos_); + } else { + return validate10(input_, pos_); + } +} + +void ISBNValidator::fixup(QString& input_) const { + return staticFixup(input_); +} + +void ISBNValidator::staticFixup(QString& input_) { + if((input_.startsWith(QString::fromLatin1("978")) + || input_.startsWith(QString::fromLatin1("979"))) + && input_.contains(QRegExp(QString::fromLatin1("\\d"))) > 10) { + return fixup13(input_); + } + return fixup10(input_); +} + +QValidator::State ISBNValidator::validate10(QString& input_, int& pos_) const { + // first check to see if it's a "perfect" ISBN + // A perfect ISBN has 9 digits plus either an 'X' or another digit + // A perfect ISBN may have 2 or 3 hyphens + // The final digit or 'X' is the correct check sum + static const QRegExp isbn(QString::fromLatin1("(\\d-?){9,11}-[\\dX]")); + uint len = input_.length(); +/* + // Don't do this since the hyphens may be in the wrong place, can't put that in a regexp + if(isbn.exactMatch(input_) // put the exactMatch() first since I use matchedLength() later + && (len == 12 || len == 13) + && input_[len-1] == checkSum(input_)) { + return QValidator::Acceptable; + } +*/ + // two easy invalid cases are too many hyphens and the 'X' not in the last position + if(input_.contains('-') > 3 + || input_.contains('X', false) > 1 + || (input_.find('X', 0, false) != -1 && input_[len-1].upper() != 'X')) { + return QValidator::Invalid; + } + + // remember if the cursor is at the end + bool atEnd = (pos_ == static_cast<int>(len)); + + // fix the case where the user attempts to delete a character from a non-checksum + // position; the solution is to delete the checksum, but only if it's X + if(!atEnd && input_[len-1].upper() == 'X') { + input_.truncate(len-1); + --len; + } + + // fix the case where the user attempts to delete the checksum; the + // solution is to delete the last digit as well + static const QRegExp digit(QString::fromLatin1("\\d")); + if(atEnd && input_.contains(digit) == 9 && input_[len-1] == '-') { + input_.truncate(len-2); + pos_ -= 2; + len -= 2; + } + + // now fixup the hyphens and maybe add a checksum + fixup10(input_); + len = input_.length(); // might have changed in fixup() + if(atEnd) { + pos_ = len; + } + + if(isbn.exactMatch(input_) && (len == 12 || len == 13)) { + return QValidator::Acceptable; + } else { + return QValidator::Intermediate; + } +} + +QValidator::State ISBNValidator::validate13(QString& input_, int& pos_) const { + // first check to see if it's a "perfect" ISBN13 + // A perfect ISBN13 has 13 digits + // A perfect ISBN13 may have 3 or 4 hyphens + // The final digit is the correct check sum + static const QRegExp isbn(QString::fromLatin1("(\\d-?){13,17}")); + uint len = input_.length(); + + const uint countX = input_.contains('X', false); + // two easy invalid cases are too many hyphens or 'X' + if(input_.contains('-') > 4 || countX > 1) { + return QValidator::Invalid; + } + + // now, it's not certain that we're getting a EAN-13, + // it could be a ISBN-10 from Nigeria or Indonesia + if(countX > 0 && (input_[len-1].upper() != 'X' || len > 13)) { + return QValidator::Invalid; + } + + // remember if the cursor is at the end + bool atEnd = (pos_ == static_cast<int>(len)); + + // fix the case where the user attempts to delete a character from a non-checksum + // position; the solution is to delete the checksum, but only if it's X + if(!atEnd && input_[len-1].upper() == 'X') { + input_.truncate(len-1); + --len; + } + + // fix the case where the user attempts to delete the checksum; the + // solution is to delete the last digit as well + static const QRegExp digit(QString::fromLatin1("\\d")); + const uint countN = input_.contains(digit); + if(atEnd && (countN == 12 || countN == 9) && input_[len-1] == '-') { + input_.truncate(len-2); + pos_ -= 2; + len -= 2; + } + + // now fixup the hyphens and maybe add a checksum + if(countN > 10) { + fixup13(input_); + } else { + fixup10(input_); + } + + len = input_.length(); // might have changed in fixup() + if(atEnd) { + pos_ = len; + } + + if(isbn.exactMatch(input_)) { + return QValidator::Acceptable; + } else { + return QValidator::Intermediate; + } +} + +void ISBNValidator::fixup10(QString& input_) { + if(input_.isEmpty()) { + return; + } + + //replace "x" with "X" + input_.replace('x', QString::fromLatin1("X")); + + // remove invalid chars + static const QRegExp badChars(QString::fromLatin1("[^\\d-X]")); + input_.remove(badChars); + + // special case for EAN values that start with 978 or 979. That's the case + // for things like barcode readers that essentially 'type' the string at + // once. The simulated typing has already caused the input to be normalized, + // so strip that off, as well as the generated checksum. Then continue as normal. + // If someone were to input a regular 978- or 979- ISBN _including_ the + // checksum, it will be regarded as barcode input and the input will be stripped accordingly. + // I consider the likelihood that someone wants to input an EAN to be higher than someone + // using a Nigerian ISBN and not noticing that the checksum gets added automatically. + if(input_.length() > 12 + && (input_.startsWith(QString::fromLatin1("978")) + || input_.startsWith(QString::fromLatin1("979")))) { + // Strip the first 4 characters (the invalid publisher) + input_ = input_.right(input_.length() - 3); + } + + // hyphen placement for some languages publishers is well-defined + // remove all hyphens, and insert them ourselves + // some countries have ill-defined second hyphen positions, and if + // the user inserts one, then be sure to put it back + + // Find the first hyphen. If there is none, + // input_.find('-') returns -1 and hyphen2_position = 0 + int hyphen2_position = input_.find('-') + 1; + + // Find the second one. If none, hyphen2_position = -2 + hyphen2_position = input_.find('-', hyphen2_position) - 1; + + // The second hyphen can not be in the last characters + if(hyphen2_position >= 9) { + hyphen2_position = 0; + } + + // Remove all existing hyphens. We will insert ours. + input_.remove('-'); + // the only place that 'X' can be is last spot + for(int xpos = input_.find('X'); xpos > -1; xpos = input_.find('X', xpos+1)) { + if(xpos < 9) { // remove if not 10th char + input_.remove(xpos, 1); + --xpos; + } + } + input_.truncate(10); + + // If we can find it, add the checksum + // but only if not started with 978 or 979 + if(input_.length() > 8 + && !input_.startsWith(QString::fromLatin1("978")) + && !input_.startsWith(QString::fromLatin1("979"))) { + input_[9] = checkSum10(input_); + } + + ulong range = input_.leftJustify(9, '0', true).toULong(); + + // now find which band the range falls in + uint band = 0; + while(range >= bands[band].MaxValue) { + ++band; + } + + // if we have space to put the first hyphen, do it + if(input_.length() > bands[band].First) { + input_.insert(bands[band].First, '-'); + } + + //add 1 since one "-" has already been inserted + if(bands[band].Mid != 0) { + hyphen2_position = bands[band].Mid; + if(static_cast<int>(input_.length()) > (hyphen2_position + 1)) { + input_.insert(hyphen2_position + 1, '-'); + } + // or put back user's hyphen + } else if(hyphen2_position > 0 && static_cast<int>(input_.length()) >= (hyphen2_position + 1)) { + input_.insert(hyphen2_position + 1, '-'); + } + + // add a "-" before the checkdigit and another one if the middle "-" exists + uint trueLast = bands[band].Last + 1 + (hyphen2_position > 0 ? 1 : 0); + if(input_.length() > trueLast) { + input_.insert(trueLast, '-'); + } +} + +void ISBNValidator::fixup13(QString& input_) { + if(input_.isEmpty()) { + return; + } + + // remove invalid chars + static const QRegExp badChars(QString::fromLatin1("[^\\d-]")); + input_.remove(badChars); + + // hyphen placement for some languages publishers is well-defined + // remove all hyphens, and insert them ourselves + // some countries have ill-defined second hyphen positions, and if + // the user inserts one, then be sure to put it back + + QString after = input_.mid(3); + if(after[0] == '-') { + after = after.mid(1); + } + + // Find the first hyphen. If there is none, + // input_.find('-') returns -1 and hyphen2_position = 0 + int hyphen2_position = after.find('-') + 1; + + // Find the second one. If none, hyphen2_position = -2 + hyphen2_position = after.find('-', hyphen2_position) - 1; + + // The second hyphen can not be in the last characters + if(hyphen2_position >= 9) { + hyphen2_position = 0; + } + + // Remove all existing hyphens. We will insert ours. + after.remove('-'); + after.truncate(10); + + // add the checksum + if(after.length() > 8) { + after[9] = checkSum13(input_.left(3) + after); + } + + ulong range = after.leftJustify(9, '0', true).toULong(); + + // now find which band the range falls in + uint band = 0; + while(range >= bands[band].MaxValue) { + ++band; + } + + // if we have space to put the first hyphen, do it + if(after.length() > bands[band].First) { + after.insert(bands[band].First, '-'); + } + + //add 1 since one "-" has already been inserted + if(bands[band].Mid != 0) { + hyphen2_position = bands[band].Mid; + if(static_cast<int>(after.length()) > (hyphen2_position + 1)) { + after.insert(hyphen2_position + 1, '-'); + } + // or put back user's hyphen + } else if(hyphen2_position > 0 && static_cast<int>(after.length()) >= (hyphen2_position + 1)) { + after.insert(hyphen2_position + 1, '-'); + } + + // add a "-" before the checkdigit and another one if the middle "-" exists + uint trueLast = bands[band].Last + 1 + (hyphen2_position > 0 ? 1 : 0); + if(after.length() > trueLast) { + after.insert(trueLast, '-'); + } + input_ = input_.left(3) + '-' + after; +} + +QChar ISBNValidator::checkSum10(const QString& input_) { + uint sum = 0; + uint multiplier = 10; + + // hyphens are already gone, only use first nine digits + for(uint i = 0; i < input_.length() && multiplier > 1; ++i) { + sum += input_[i].digitValue() * multiplier--; + } + sum %= 11; + sum = 11-sum; + + QChar c; + if(sum == 10) { + c = 'X'; + } else if(sum == 11) { + c = '0'; + } else { + c = QString::number(sum)[0]; + } + return c; +} + +QChar ISBNValidator::checkSum13(const QString& input_) { + uint sum = 0; + + const uint len = QMIN(12, input_.length()); + // hyphens are already gone, only use first twelve digits + for(uint i = 0; i < len; ++i) { + sum += input_[i].digitValue() * (1 + 2*(i%2)); + // multiplier goes 1, 3, 1, 3, etc... + } + sum %= 10; + sum = 10-sum; + + QChar c; + if(sum == 10) { + c = '0'; + } else { + c = QString::number(sum)[0]; + } + return c; +} + +// ISBN code from Regis Boudin +#define ISBNGRP_1DIGIT(digit, max, middle, last) \ + {((digit)*100000000) + (max), 1, middle, last} +#define ISBNGRP_2DIGIT(digit, max, middle, last) \ + {((digit)*10000000) + ((max)/10), 2, middle, last} +#define ISBNGRP_3DIGIT(digit, max, middle, last) \ + {((digit)*1000000) + ((max)/100), 3, middle, last} +#define ISBNGRP_4DIGIT(digit, max, middle, last) \ + {((digit)*100000) + ((max)/1000), 4, middle, last} +#define ISBNGRP_5DIGIT(digit, max, middle, last) \ + {((digit)*10000) + ((max)/10000), 5, middle, last} + +#define ISBNPUB_2DIGIT(grp) (((grp)+1)*1000000) +#define ISBNPUB_3DIGIT(grp) (((grp)+1)*100000) +#define ISBNPUB_4DIGIT(grp) (((grp)+1)*10000) +#define ISBNPUB_5DIGIT(grp) (((grp)+1)*1000) +#define ISBNPUB_6DIGIT(grp) (((grp)+1)*100) +#define ISBNPUB_7DIGIT(grp) (((grp)+1)*10) +#define ISBNPUB_8DIGIT(grp) (((grp)+1)*1) + +// how to format an ISBN, after categorising it into a range of numbers. +struct ISBNValidator::isbn_band ISBNValidator::bands[] = { + /* Groups 0 & 1 : English */ + ISBNGRP_1DIGIT(0, ISBNPUB_2DIGIT(19), 3, 9), + ISBNGRP_1DIGIT(0, ISBNPUB_3DIGIT(699), 4, 9), + ISBNGRP_1DIGIT(0, ISBNPUB_4DIGIT(8499), 5, 9), + ISBNGRP_1DIGIT(0, ISBNPUB_5DIGIT(89999), 6, 9), + ISBNGRP_1DIGIT(0, ISBNPUB_6DIGIT(949999), 7, 9), + ISBNGRP_1DIGIT(0, ISBNPUB_7DIGIT(9999999), 8, 9), + + ISBNGRP_1DIGIT(1, ISBNPUB_5DIGIT(54999), 6, 9), + ISBNGRP_1DIGIT(1, ISBNPUB_5DIGIT(86979), 6, 9), + ISBNGRP_1DIGIT(1, ISBNPUB_6DIGIT(998999), 7, 9), + ISBNGRP_1DIGIT(1, ISBNPUB_7DIGIT(9999999), 8, 9), + /* Group 2 : French */ + ISBNGRP_1DIGIT(2, ISBNPUB_2DIGIT(19), 3, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_3DIGIT(349), 4, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_5DIGIT(39999), 6, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_3DIGIT(699), 4, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_4DIGIT(8399), 5, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_5DIGIT(89999), 6, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_6DIGIT(949999), 7, 9), + ISBNGRP_1DIGIT(2, ISBNPUB_7DIGIT(9999999), 8, 9), + + /* Group 2 : German */ + ISBNGRP_1DIGIT(3, ISBNPUB_2DIGIT(19), 3, 9), + ISBNGRP_1DIGIT(3, ISBNPUB_3DIGIT(699), 4, 9), + ISBNGRP_1DIGIT(3, ISBNPUB_4DIGIT(8499), 5, 9), + ISBNGRP_1DIGIT(3, ISBNPUB_5DIGIT(89999), 6, 9), + ISBNGRP_1DIGIT(3, ISBNPUB_6DIGIT(949999), 7, 9), + ISBNGRP_1DIGIT(3, ISBNPUB_7DIGIT(9999999), 8, 9), + + ISBNGRP_1DIGIT(7, ISBNPUB_2DIGIT(99), 0, 9), + /* Group 80 : Czech */ + ISBNGRP_2DIGIT(80, ISBNPUB_2DIGIT(19), 4, 9), + ISBNGRP_2DIGIT(80, ISBNPUB_3DIGIT(699), 5, 9), + ISBNGRP_2DIGIT(80, ISBNPUB_4DIGIT(8499), 6, 9), + ISBNGRP_2DIGIT(80, ISBNPUB_5DIGIT(89999), 7, 9), + ISBNGRP_2DIGIT(80, ISBNPUB_6DIGIT(949999), 8, 9), + + /* Group 90 * Netherlands */ + ISBNGRP_2DIGIT(90, ISBNPUB_2DIGIT(19), 4, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_3DIGIT(499), 5, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_4DIGIT(6999), 6, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_5DIGIT(79999), 7, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_6DIGIT(849999), 8, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_4DIGIT(8999), 6, 9), + ISBNGRP_2DIGIT(90, ISBNPUB_7DIGIT(9999999), 9, 9), + + ISBNGRP_2DIGIT(94, ISBNPUB_2DIGIT(99), 0, 9), + ISBNGRP_3DIGIT(993, ISBNPUB_2DIGIT(99), 0, 9), + ISBNGRP_4DIGIT(9989, ISBNPUB_2DIGIT(99), 0, 9), + ISBNGRP_5DIGIT(99999, ISBNPUB_2DIGIT(99), 0, 9) +}; diff --git a/src/isbnvalidator.h b/src/isbnvalidator.h new file mode 100644 index 0000000..763f7ff --- /dev/null +++ b/src/isbnvalidator.h @@ -0,0 +1,148 @@ +/*************************************************************************** + copyright : (C) 2002-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; * + * * + ***************************************************************************/ + +#ifndef ISBNVALIDATOR_H +#define ISBNVALIDATOR_H + +#include <qvalidator.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + * + * @see http://www.isbn.org/standards/home/isbn/international/hyphenation-instructions.asp + * @see http://www.eblong.com/zarf/bookscan/ + * @see http://doc.trolltech.com/qq/qq01-seriously-weird-qregexp.html + */ +class ISBNValidator : public QValidator { +public: + ISBNValidator(QObject* parent, const char* name=0); + + /** + * Certain conditions are checked. Character, length and position + * restrictions are checked. Certain cases where the user is deleting + * characters are caught and compensated for. The string is then passed to + * @ref fixup. Finally, the text is @ref Valid if it is a certain length and + * @ref Intermediate if not. + * + * @param input The text to validate + * @param pos The position of the cursor + * @return The condition of the text + */ + virtual QValidator::State validate(QString& input, int& pos) const; + + /** + * The input string is examined. Hyphens are inserted appropriately, + * and the checksum is calculated. + * + * For correct presentation, the 10 digits of an ISBN must + * be divided, by hyphens, into four parts: + * @li Part 1: The country or group of countries identifier + * @li Part 2: The publisher identifier + * @li Part 3: The title identifier + * @li Part 4: The check digit + * For details + * @see http://www.isbn.org/standards/home/isbn/international/hyphenation-instructions.asp + * For info on group codes + * @see http://www.isbn.spk-berlin.de/html/prefix/allpref.htm + * For info on French language publisher codes + * @see http://www.afnil.org + * <pre> + * Group Identifiers First Hyphen After + * ----------------------------------------- + * 0........7 1st digit + * 80.......94 2nd " + * 950......993 3rd " + * 9940.....9989 4th " + * 99900....99999 5th " + * + * Group Insert Hyphens + * Identifier "0" After + * ----------------------------------------- + * 00.......19 1st 3rd 9th digit + * 200......699 " 4th " + * 7000.....8499 " 5th " + * 85000....89999 " 6th " + * 900000...949999 " 7th " + * 9500000..9999999 " 8th " + * + * + * Group Insert Hyphens + * Identifier "1" After + * ---------------------------------------- + * 0........54999 illegal + * 55000....86979 1st 6th 9th digit + * 869800...998999 " 7th " + * 9990000..9999999 " 8th " + * + * + * Group Insert Hyphens + * Identifier "2" After + * ----------------------------------------- + * 00.......19 1st 3rd 9th digit + * 200......349 " 4th " + * 34000....39999 " 6th " + * 400......699 " 4th " + * 7000.....8399 " 5th " + * 84000....89999 " 6th " + * 900000...949999 " 7th " + * 9500000..9999999 " 8th " + * + * The position of the hyphens are determined by the publisher + * prefix range established by each national agency in accordance + * with the industry needs. The knowledge of the prefix ranges for + * each country or group of countries is necessary to develop the + * hyphenation output program. For groups 3 through 99999, the hyphenation + * rules are currently unknown. So just leave out the hyphen between + * the publisher and title for now, but allow it if the user inserts it. + * </pre> + * + * @param input The raw string, hyphens included + */ + virtual void fixup(QString& input) const; + static void staticFixup(QString& input); + + static QString isbn10(QString isbn13); + static QString isbn13(QString isbn10); + static QString cleanValue(QString isbn); + +private: + static struct isbn_band { + unsigned long MaxValue; + unsigned int First; + unsigned int Mid; + unsigned int Last; + } bands[]; + + QValidator::State validate10(QString& input, int& pos) const; + QValidator::State validate13(QString& input, int& pos) const; + + static void fixup10(QString& input); + static void fixup13(QString& input); + + /** + * This function calculates and returns the ISBN checksum. The + * algorithm is based on some code by Andrew Plotkin, available at + * http://www.eblong.com/zarf/bookscan/ + * + * @see http://www.eblong.com/zarf/bookscan/ + * + * @param input The raw string, with no hyphens + */ + static QChar checkSum10(const QString& input); + static QChar checkSum13(const QString& input); +}; + +} // end namespace +#endif diff --git a/src/iso5426converter.cpp b/src/iso5426converter.cpp new file mode 100644 index 0000000..bac132a --- /dev/null +++ b/src/iso5426converter.cpp @@ -0,0 +1,883 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// This class is adapted from Iso5426ToUnicode from the MARC4J project, available +// from http://marc4j.tigris.org, with the following notice: +// * Copyright (C) 2002 Bas Peters (mail@bpeters.com) +// * Copyright (C) 2002 Yves Pratter (ypratter@club-internet.fr) +// +// That source was released under the terms of the GNU Lesser General Public +// License, version 2.1. In accordance with Condition 3 of that license, +// I am applying the terms of the GNU General Public License to the source +// code, and including a large portion of it here + +#include "iso5426converter.h" +#include "tellico_debug.h" + +#include <qstring.h> + +using Tellico::Iso5426Converter; + +QString Iso5426Converter::toUtf8(const QCString& text_) { + const uint len = text_.length(); + QString result; + result.reserve(len); + uint pos = 0; + for(uint i = 0; i < len; ++i) { + uchar c = text_[i]; + if(isAscii(c)) { + result[pos++] = c; + } else if(isCombining(c) && hasNext(i, len)) { + // this is a hack + // use the diaeresis instead of umlaut + // works for SUDOC + if(c == 0xC9) { + c = 0xC8; + } + QChar d = getCombiningChar(c * 256 + text_[i + 1]); + if(!d.isNull()) { + result[pos++] = d; + ++i; + } else { + result[pos++] = getChar(c); + } + } else { + result[pos++] = getChar(c); + } + } + result.squeeze(); + return result; +} + +inline +bool Iso5426Converter::hasNext(uint pos, uint len) { + return pos < (len - 1); +} + +inline +bool Iso5426Converter::isAscii(uchar c) { + return c <= 0x7F; +} + +inline +bool Iso5426Converter::isCombining(uchar c) { + return c >= 0xC0 && c <= 0xDF; +} + +// Source : http://www.itscj.ipsj.or.jp/ISO-IR/053.pdf +QChar Iso5426Converter::getChar(uchar c) { + switch(c) { + case 0xA1: + return 0x00A1; // 2/1 inverted exclamation mark + case 0xA2: + return 0x201C; // 2/2 left low double quotation mark + case 0xA3: + return 0x00A3; // 2/3 pound sign + case 0xA4: + return 0x0024; // 2/4 dollar sign + case 0xA5: + return 0x00A5; // 2/5 yen sign + case 0xA6: + return 0x2020; // 2/6 single dagger + case 0xA7: + return 0x00A7; // 2/7 paragraph (section) + case 0xA8: + return 0x2032; // 2/8 prime + case 0xA9: + return 0x2018; // 2/9 left high single quotation mark + case 0xAA: + return 0x201C; // 2/10 left high double quotation mark + case 0xAB: + return 0x00AB; // 2/11 left angle quotation mark + case 0xAC: + return 0x266D; // 2/12 music flat + case 0xAD: + return 0x00A9; // 2/13 copyright sign + case 0xAE: + return 0x2117; // 2/14 sound recording copyright sign + case 0xAF: + return 0x00AE; // 2/15 trade mark sign + + case 0xB0: + return 0x0639; // 3/0 ayn [ain] + case 0xB1: + return 0x0623; // 3/1 alif/hamzah [alef with hamza above] + case 0xB2: + return 0x2018; // 3/2 left low single quotation mark + // 3/3 (this position shall not be used) + // 3/4 (this position shall not be used) + // 3/5 (this position shall not be used) + case 0xB6: + return 0x2021; // 3/6 double dagger + case 0xB7: + return 0x00B7; // 3/7 middle dot + case 0xB8: + return 0x2033; // 3/8 double prime + case 0xB9: + return 0x2019; // 3/9 right high single quotation mark + case 0xBA: + return 0x201D; // 3/10 right high double quotation mark + case 0xBB: + return 0x00BB; // 3/11 right angle quotation mark + case 0xBC: + return 0x266F; // 3/12 musical sharp + case 0xBD: + return 0x02B9; // 3/13 mjagkij znak + case 0xBE: + return 0x02BA; // 3/14 tverdyj znak + case 0xBF: + return 0x00BF; // 3/15 inverted question mark + + // 4/0 to 5/15 diacritic characters + + // 6/0 (this position shall not be used) + case 0xE1: + return 0x00C6; // 6/1 CAPITAL DIPHTHONG A WITH E + case 0xE2: + return 0x0110; // 6/2 CAPITAL LETTER D WITH STROKE + // 6/3 (this position shall not be used) + // 6/4 (this position shall not be used) + // 6/5 (this position shall not be used) + case 0xE6: + return 0x0132; // 6/6 CAPITAL LETTER IJ + // 6/7 (this position shall not be used) + case 0xE8: + return 0x0141; // 6/8 CAPITAL LETTER L WITH STROKE + case 0xE9: + return 0x00D8; // 6/9 CAPITAL LETTER O WITH SOLIDUS [oblique stroke] + case 0xEA: + return 0x0152; // 6/10 CAPITAL DIPHTONG OE + // 6/11 (this position shall not be used) + case 0xEC: + return 0x00DE; // 6/12 CAPITAL LETTER THORN + // 6/13 (this position shall not be used) + // 6/14 (this position shall not be used) + // 6/15 (this position shall not be used) + + // 7/0 (this position shall not be used) + case 0xF1: + return 0x00E6; // 7/1 small diphthong a with e + // 7/4 (this position shall not be used) + case 0xF5: + return 0x0131; // 7/5 small letter i without dot + case 0xF6: + return 0x0133; // 7/6 small letter ij + // 7/7 (this position shall not be used) + case 0xF8: + return 0x0142; // 7/8 small letter l with stroke + case 0xF9: + return 0x00F8; // 7/9 small letter o with solidus (oblique stroke) + case 0xFA: + return 0x0153; // 7/10 small diphtong oe + case 0xFB: + return 0x00DF; // 7/11 small letter sharp s + case 0xFC: + return 0x00FE; // 7/12 small letter thorn + // 7/13 (this position shall not be used) + // 7/14 (this position shall not be used) + default: + return c; + } +} + +QChar Iso5426Converter::getCombiningChar(uint c) { + switch(c) { + // 4/0 low rising tone mark + case 0xC041: + return 0x1EA2; // CAPITAL A WITH HOOK ABOVE + case 0xC045: + return 0x1EBA; // CAPITAL E WITH HOOK ABOVE + case 0xC049: + return 0x1EC8; // CAPITAL I WITH HOOK ABOVE + case 0xC04F: + return 0x1ECE; // CAPITAL O WITH HOOK ABOVE + case 0xC055: + return 0x1EE6; // CAPITAL U WITH HOOK ABOVE + case 0xC059: + return 0x1EF6; // CAPITAL Y WITH HOOK ABOVE + case 0xC061: + return 0x1EA3; // small a with hook above + case 0xC065: + return 0x1EBB; // small e with hook above + case 0xC069: + return 0x1EC9; // small i with hook above + case 0xC06F: + return 0x1ECF; // small o with hook above + case 0xC075: + return 0x1EE7; // small u with hook above + case 0xC079: + return 0x1EF7; // small y with hook above + + // 4/1 grave accent + case 0xC141: + return 0x00C0; // CAPITAL A WITH GRAVE ACCENT + case 0xC145: + return 0x00C8; // CAPITAL E WITH GRAVE ACCENT + case 0xC149: + return 0x00CC; // CAPITAL I WITH GRAVE ACCENT + case 0xC14F: + return 0x00D2; // CAPITAL O WITH GRAVE ACCENT + case 0xC155: + return 0x00D9; // CAPITAL U WITH GRAVE ACCENT + case 0xC157: + return 0x1E80; // CAPITAL W WITH GRAVE + case 0xC159: + return 0x1EF2; // CAPITAL Y WITH GRAVE + case 0xC161: + return 0x00E0; // small a with grave accent + case 0xC165: + return 0x00E8; // small e with grave accent + case 0xC169: + return 0x00EC; // small i with grave accent + case 0xC16F: + return 0x00F2; // small o with grave accent + case 0xC175: + return 0x00F9; // small u with grave accent + case 0xC177: + return 0x1E81; // small w with grave + case 0xC179: + return 0x1EF3; // small y with grave + + // 4/2 acute accent + case 0xC241: + return 0x00C1; // CAPITAL A WITH ACUTE ACCENT + case 0xC243: + return 0x0106; // CAPITAL C WITH ACUTE ACCENT + case 0xC245: + return 0x00C9; // CAPITAL E WITH ACUTE ACCENT + case 0xC247: + return 0x01F4; // CAPITAL G WITH ACUTE + case 0xC249: + return 0x00CD; // CAPITAL I WITH ACUTE ACCENT + case 0xC24B: + return 0x1E30; // CAPITAL K WITH ACUTE + case 0xC24C: + return 0x0139; // CAPITAL L WITH ACUTE ACCENT + case 0xC24D: + return 0x1E3E; // CAPITAL M WITH ACUTE + case 0xC24E: + return 0x0143; // CAPITAL N WITH ACUTE ACCENT + case 0xC24F: + return 0x00D3; // CAPITAL O WITH ACUTE ACCENT + case 0xC250: + return 0x1E54; // CAPITAL P WITH ACUTE + case 0xC252: + return 0x0154; // CAPITAL R WITH ACUTE ACCENT + case 0xC253: + return 0x015A; // CAPITAL S WITH ACUTE ACCENT + case 0xC255: + return 0x00DA; // CAPITAL U WITH ACUTE ACCENT + case 0xC257: + return 0x1E82; // CAPITAL W WITH ACUTE + case 0xC259: + return 0x00DD; // CAPITAL Y WITH ACUTE ACCENT + case 0xC25A: + return 0x0179; // CAPITAL Z WITH ACUTE ACCENT + case 0xC261: + return 0x00E1; // small a with acute accent + case 0xC263: + return 0x0107; // small c with acute accent + case 0xC265: + return 0x00E9; // small e with acute accent + case 0xC267: + return 0x01F5; // small g with acute + case 0xC269: + return 0x00ED; // small i with acute accent + case 0xC26B: + return 0x1E31; // small k with acute + case 0xC26C: + return 0x013A; // small l with acute accent + case 0xC26D: + return 0x1E3F; // small m with acute + case 0xC26E: + return 0x0144; // small n with acute accent + case 0xC26F: + return 0x00F3; // small o with acute accent + case 0xC270: + return 0x1E55; // small p with acute + case 0xC272: + return 0x0155; // small r with acute accent + case 0xC273: + return 0x015B; // small s with acute accent + case 0xC275: + return 0x00FA; // small u with acute accent + case 0xC277: + return 0x1E83; // small w with acute + case 0xC279: + return 0x00FD; // small y with acute accent + case 0xC27A: + return 0x017A; // small z with acute accent + case 0xC2E1: + return 0x01FC; // CAPITAL AE WITH ACUTE + case 0xC2F1: + return 0x01FD; // small ae with acute + + // 4/3 circumflex accent + case 0xC341: + return 0x00C2; // CAPITAL A WITH CIRCUMFLEX ACCENT + case 0xC343: + return 0x0108; // CAPITAL C WITH CIRCUMFLEX + case 0xC345: + return 0x00CA; // CAPITAL E WITH CIRCUMFLEX ACCENT + case 0xC347: + return 0x011C; // CAPITAL G WITH CIRCUMFLEX + case 0xC348: + return 0x0124; // CAPITAL H WITH CIRCUMFLEX + case 0xC349: + return 0x00CE; // CAPITAL I WITH CIRCUMFLEX ACCENT + case 0xC34A: + return 0x0134; // CAPITAL J WITH CIRCUMFLEX + case 0xC34F: + return 0x00D4; // CAPITAL O WITH CIRCUMFLEX ACCENT + case 0xC353: + return 0x015C; // CAPITAL S WITH CIRCUMFLEX + case 0xC355: + return 0x00DB; // CAPITAL U WITH CIRCUMFLEX + case 0xC357: + return 0x0174; // CAPITAL W WITH CIRCUMFLEX + case 0xC359: + return 0x0176; // CAPITAL Y WITH CIRCUMFLEX + case 0xC35A: + return 0x1E90; // CAPITAL Z WITH CIRCUMFLEX + case 0xC361: + return 0x00E2; // small a with circumflex accent + case 0xC363: + return 0x0109; // small c with circumflex + case 0xC365: + return 0x00EA; // small e with circumflex accent + case 0xC367: + return 0x011D; // small g with circumflex + case 0xC368: + return 0x0125; // small h with circumflex + case 0xC369: + return 0x00EE; // small i with circumflex accent + case 0xC36A: + return 0x0135; // small j with circumflex + case 0xC36F: + return 0x00F4; // small o with circumflex accent + case 0xC373: + return 0x015D; // small s with circumflex + case 0xC375: + return 0x00FB; // small u with circumflex + case 0xC377: + return 0x0175; // small w with circumflex + case 0xC379: + return 0x0177; // small y with circumflex + case 0xC37A: + return 0x1E91; // small z with circumflex + + // 4/4 tilde + case 0xC441: + return 0x00C3; // CAPITAL A WITH TILDE + case 0xC445: + return 0x1EBC; // CAPITAL E WITH TILDE + case 0xC449: + return 0x0128; // CAPITAL I WITH TILDE + case 0xC44E: + return 0x00D1; // CAPITAL N WITH TILDE + case 0xC44F: + return 0x00D5; // CAPITAL O WITH TILDE + case 0xC455: + return 0x0168; // CAPITAL U WITH TILDE + case 0xC456: + return 0x1E7C; // CAPITAL V WITH TILDE + case 0xC459: + return 0x1EF8; // CAPITAL Y WITH TILDE + case 0xC461: + return 0x00E3; // small a with tilde + case 0xC465: + return 0x1EBD; // small e with tilde + case 0xC469: + return 0x0129; // small i with tilde + case 0xC46E: + return 0x00F1; // small n with tilde + case 0xC46F: + return 0x00F5; // small o with tilde + case 0xC475: + return 0x0169; // small u with tilde + case 0xC476: + return 0x1E7D; // small v with tilde + case 0xC479: + return 0x1EF9; // small y with tilde + + // 4/5 macron + case 0xC541: + return 0x0100; // CAPITAL A WITH MACRON + case 0xC545: + return 0x0112; // CAPITAL E WITH MACRON + case 0xC547: + return 0x1E20; // CAPITAL G WITH MACRON + case 0xC549: + return 0x012A; // CAPITAL I WITH MACRON + case 0xC54F: + return 0x014C; // CAPITAL O WITH MACRON + case 0xC555: + return 0x016A; // CAPITAL U WITH MACRON + case 0xC561: + return 0x0101; // small a with macron + case 0xC565: + return 0x0113; // small e with macron + case 0xC567: + return 0x1E21; // small g with macron + case 0xC569: + return 0x012B; // small i with macron + case 0xC56F: + return 0x014D; // small o with macron + case 0xC575: + return 0x016B; // small u with macron + case 0xC5E1: + return 0x01E2; // CAPITAL AE WITH MACRON + case 0xC5F1: + return 0x01E3; // small ae with macron + + // 4/6 breve + case 0xC641: + return 0x0102; // CAPITAL A WITH BREVE + case 0xC645: + return 0x0114; // CAPITAL E WITH BREVE + case 0xC647: + return 0x011E; // CAPITAL G WITH BREVE + case 0xC649: + return 0x012C; // CAPITAL I WITH BREVE + case 0xC64F: + return 0x014E; // CAPITAL O WITH BREVE + case 0xC655: + return 0x016C; // CAPITAL U WITH BREVE + case 0xC661: + return 0x0103; // small a with breve + case 0xC665: + return 0x0115; // small e with breve + case 0xC667: + return 0x011F; // small g with breve + case 0xC669: + return 0x012D; // small i with breve + case 0xC66F: + return 0x014F; // small o with breve + case 0xC675: + return 0x016D; // small u with breve + + // 4/7 dot above + case 0xC742: + return 0x1E02; // CAPITAL B WITH DOT ABOVE + case 0xC743: + return 0x010A; // CAPITAL C WITH DOT ABOVE + case 0xC744: + return 0x1E0A; // CAPITAL D WITH DOT ABOVE + case 0xC745: + return 0x0116; // CAPITAL E WITH DOT ABOVE + case 0xC746: + return 0x1E1E; // CAPITAL F WITH DOT ABOVE + case 0xC747: + return 0x0120; // CAPITAL G WITH DOT ABOVE + case 0xC748: + return 0x1E22; // CAPITAL H WITH DOT ABOVE + case 0xC749: + return 0x0130; // CAPITAL I WITH DOT ABOVE + case 0xC74D: + return 0x1E40; // CAPITAL M WITH DOT ABOVE + case 0xC74E: + return 0x1E44; // CAPITAL N WITH DOT ABOVE + case 0xC750: + return 0x1E56; // CAPITAL P WITH DOT ABOVE + case 0xC752: + return 0x1E58; // CAPITAL R WITH DOT ABOVE + case 0xC753: + return 0x1E60; // CAPITAL S WITH DOT ABOVE + case 0xC754: + return 0x1E6A; // CAPITAL T WITH DOT ABOVE + case 0xC757: + return 0x1E86; // CAPITAL W WITH DOT ABOVE + case 0xC758: + return 0x1E8A; // CAPITAL X WITH DOT ABOVE + case 0xC759: + return 0x1E8E; // CAPITAL Y WITH DOT ABOVE + case 0xC75A: + return 0x017B; // CAPITAL Z WITH DOT ABOVE + case 0xC762: + return 0x1E03; // small b with dot above + case 0xC763: + return 0x010B; // small c with dot above + case 0xC764: + return 0x1E0B; // small d with dot above + case 0xC765: + return 0x0117; // small e with dot above + case 0xC766: + return 0x1E1F; // small f with dot above + case 0xC767: + return 0x0121; // small g with dot above + case 0xC768: + return 0x1E23; // small h with dot above + case 0xC76D: + return 0x1E41; // small m with dot above + case 0xC76E: + return 0x1E45; // small n with dot above + case 0xC770: + return 0x1E57; // small p with dot above + case 0xC772: + return 0x1E59; // small r with dot above + case 0xC773: + return 0x1E61; // small s with dot above + case 0xC774: + return 0x1E6B; // small t with dot above + case 0xC777: + return 0x1E87; // small w with dot above + case 0xC778: + return 0x1E8B; // small x with dot above + case 0xC779: + return 0x1E8F; // small y with dot above + case 0xC77A: + return 0x017C; // small z with dot above + + // 4/8 trema, diaresis + case 0xC820: + return 0x00A8; // diaeresis + case 0xC841: + return 0x00C4; // CAPITAL A WITH DIAERESIS + case 0xC845: + return 0x00CB; // CAPITAL E WITH DIAERESIS + case 0xC848: + return 0x1E26; // CAPITAL H WITH DIAERESIS + case 0xC849: + return 0x00CF; // CAPITAL I WITH DIAERESIS + case 0xC84F: + return 0x00D6; // CAPITAL O WITH DIAERESIS + case 0xC855: + return 0x00DC; // CAPITAL U WITH DIAERESIS + case 0xC857: + return 0x1E84; // CAPITAL W WITH DIAERESIS + case 0xC858: + return 0x1E8C; // CAPITAL X WITH DIAERESIS + case 0xC859: + return 0x0178; // CAPITAL Y WITH DIAERESIS + case 0xC861: + return 0x00E4; // small a with diaeresis + case 0xC865: + return 0x00EB; // small e with diaeresis + case 0xC868: + return 0x1E27; // small h with diaeresis + case 0xC869: + return 0x00EF; // small i with diaeresis + case 0xC86F: + return 0x00F6; // small o with diaeresis + case 0xC874: + return 0x1E97; // small t with diaeresis + case 0xC875: + return 0x00FC; // small u with diaeresis + case 0xC877: + return 0x1E85; // small w with diaeresis + case 0xC878: + return 0x1E8D; // small x with diaeresis + case 0xC879: + return 0x00FF; // small y with diaeresis + + // 4/9 umlaut + case 0xC920: + return 0x00A8; // [diaeresis] + + // 4/10 circle above + case 0xCA41: + return 0x00C5; // CAPITAL A WITH RING ABOVE + case 0xCAAD: + return 0x016E; // CAPITAL U WITH RING ABOVE + case 0xCA61: + return 0x00E5; // small a with ring above + case 0xCA75: + return 0x016F; // small u with ring above + case 0xCA77: + return 0x1E98; // small w with ring above + case 0xCA79: + return 0x1E99; // small y with ring above + + // 4/11 high comma off centre + + // 4/12 inverted high comma centred + + // 4/13 double acute accent + case 0xCD4F: + return 0x0150; // CAPITAL O WITH DOUBLE ACUTE + case 0xCD55: + return 0x0170; // CAPITAL U WITH DOUBLE ACUTE + case 0xCD6F: + return 0x0151; // small o with double acute + case 0xCD75: + return 0x0171; // small u with double acute + + // 4/14 horn + case 0xCE54: + return 0x01A0; // LATIN CAPITAL LETTER O WITH HORN + case 0xCE55: + return 0x01AF; // LATIN CAPITAL LETTER U WITH HORN + case 0xCE74: + return 0x01A1; // latin small letter o with horn + case 0xCE75: + return 0x01B0; // latin small letter u with horn + + // 4/15 caron (hacek) + case 0xCF41: + return 0x01CD; // CAPITAL A WITH CARON + case 0xCF43: + return 0x010C; // CAPITAL C WITH CARON + case 0xCF44: + return 0x010E; // CAPITAL D WITH CARON + case 0xCF45: + return 0x011A; // CAPITAL E WITH CARON + case 0xCF47: + return 0x01E6; // CAPITAL G WITH CARON + case 0xCF49: + return 0x01CF; // CAPITAL I WITH CARON + case 0xCF4B: + return 0x01E8; // CAPITAL K WITH CARON + case 0xCF4C: + return 0x013D; // CAPITAL L WITH CARON + case 0xCF4E: + return 0x0147; // CAPITAL N WITH CARON + case 0xCF4F: + return 0x01D1; // CAPITAL O WITH CARON + case 0xCF52: + return 0x0158; // CAPITAL R WITH CARON + case 0xCF53: + return 0x0160; // CAPITAL S WITH CARON + case 0xCF54: + return 0x0164; // CAPITAL T WITH CARON + case 0xCF55: + return 0x01D3; // CAPITAL U WITH CARON + case 0xCF5A: + return 0x017D; // CAPITAL Z WITH CARON + case 0xCF61: + return 0x01CE; // small a with caron + case 0xCF63: + return 0x010D; // small c with caron + case 0xCF64: + return 0x010F; // small d with caron + case 0xCF65: + return 0x011B; // small e with caron + case 0xCF67: + return 0x01E7; // small g with caron + case 0xCF69: + return 0x01D0; // small i with caron + case 0xCF6A: + return 0x01F0; // small j with caron + case 0xCF6B: + return 0x01E9; // small k with caron + case 0xCF6C: + return 0x013E; // small l with caron + case 0xCF6E: + return 0x0148; // small n with caron + case 0xCF6F: + return 0x01D2; // small o with caron + case 0xCF72: + return 0x0159; // small r with caron + case 0xCF73: + return 0x0161; // small s with caron + case 0xCF74: + return 0x0165; // small t with caron + case 0xCF75: + return 0x01D4; // small u with caron + case 0xCF7A: + return 0x017E; // small z with caron + + // 5/0 cedilla + case 0xD020: + return 0x00B8; // cedilla + case 0xD043: + return 0x00C7; // CAPITAL C WITH CEDILLA + case 0xD044: + return 0x1E10; // CAPITAL D WITH CEDILLA + case 0xD047: + return 0x0122; // CAPITAL G WITH CEDILLA + case 0xD048: + return 0x1E28; // CAPITAL H WITH CEDILLA + case 0xD04B: + return 0x0136; // CAPITAL K WITH CEDILLA + case 0xD04C: + return 0x013B; // CAPITAL L WITH CEDILLA + case 0xD04E: + return 0x0145; // CAPITAL N WITH CEDILLA + case 0xD052: + return 0x0156; // CAPITAL R WITH CEDILLA + case 0xD053: + return 0x015E; // CAPITAL S WITH CEDILLA + case 0xD054: + return 0x0162; // CAPITAL T WITH CEDILLA + case 0xD063: + return 0x00E7; // small c with cedilla + case 0xD064: + return 0x1E11; // small d with cedilla + case 0xD067: + return 0x0123; // small g with cedilla + case 0xD068: + return 0x1E29; // small h with cedilla + case 0xD06B: + return 0x0137; // small k with cedilla + case 0xD06C: + return 0x013C; // small l with cedilla + case 0xD06E: + return 0x0146; // small n with cedilla + case 0xD072: + return 0x0157; // small r with cedilla + case 0xD073: + return 0x015F; // small s with cedilla + case 0xD074: + return 0x0163; // small t with cedilla + + // 5/1 rude + + // 5/2 hook to left + + // 5/3 ogonek (hook to right) + case 0xD320: + return 0x02DB; // ogonek + case 0xD341: + return 0x0104; // CAPITAL A WITH OGONEK + case 0xD345: + return 0x0118; // CAPITAL E WITH OGONEK + case 0xD349: + return 0x012E; // CAPITAL I WITH OGONEK + case 0xD34F: + return 0x01EA; // CAPITAL O WITH OGONEK + case 0xD355: + return 0x0172; // CAPITAL U WITH OGONEK + case 0xD361: + return 0x0105; // small a with ogonek + case 0xD365: + return 0x0119; // small e with ogonek + case 0xD369: + return 0x012F; // small i with ogonek + case 0xD36F: + return 0x01EB; // small o with ogonek + case 0xD375: + return 0x0173; // small u with ogonek + + // 5/4 circle below + case 0xD441: + return 0x1E00; // CAPITAL A WITH RING BELOW + case 0xD461: + return 0x1E01; // small a with ring below + + // 5/5 half circle below + case 0xF948: + return 0x1E2A; // CAPITAL H WITH BREVE BELOW + case 0xF968: + return 0x1E2B; // small h with breve below + + // 5/6 dot below + case 0xD641: + return 0x1EA0; // CAPITAL A WITH DOT BELOW + case 0xD642: + return 0x1E04; // CAPITAL B WITH DOT BELOW + case 0xD644: + return 0x1E0C; // CAPITAL D WITH DOT BELOW + case 0xD645: + return 0x1EB8; // CAPITAL E WITH DOT BELOW + case 0xD648: + return 0x1E24; // CAPITAL H WITH DOT BELOW + case 0xD649: + return 0x1ECA; // CAPITAL I WITH DOT BELOW + case 0xD64B: + return 0x1E32; // CAPITAL K WITH DOT BELOW + case 0xD64C: + return 0x1E36; // CAPITAL L WITH DOT BELOW + case 0xD64D: + return 0x1E42; // CAPITAL M WITH DOT BELOW + case 0xD64E: + return 0x1E46; // CAPITAL N WITH DOT BELOW + case 0xD64F: + return 0x1ECC; // CAPITAL O WITH DOT BELOW + case 0xD652: + return 0x1E5A; // CAPITAL R WITH DOT BELOW + case 0xD653: + return 0x1E62; // CAPITAL S WITH DOT BELOW + case 0xD654: + return 0x1E6C; // CAPITAL T WITH DOT BELOW + case 0xD655: + return 0x1EE4; // CAPITAL U WITH DOT BELOW + case 0xD656: + return 0x1E7E; // CAPITAL V WITH DOT BELOW + case 0xD657: + return 0x1E88; // CAPITAL W WITH DOT BELOW + case 0xD659: + return 0x1EF4; // CAPITAL Y WITH DOT BELOW + case 0xD65A: + return 0x1E92; // CAPITAL Z WITH DOT BELOW + case 0xD661: + return 0x1EA1; // small a with dot below + case 0xD662: + return 0x1E05; // small b with dot below + case 0xD664: + return 0x1E0D; // small d with dot below + case 0xD665: + return 0x1EB9; // small e with dot below + case 0xD668: + return 0x1E25; // small h with dot below + case 0xD669: + return 0x1ECB; // small i with dot below + case 0xD66B: + return 0x1E33; // small k with dot below + case 0xD66C: + return 0x1E37; // small l with dot below + case 0xD66D: + return 0x1E43; // small m with dot below + case 0xD66E: + return 0x1E47; // small n with dot below + case 0xD66F: + return 0x1ECD; // small o with dot below + case 0xD672: + return 0x1E5B; // small r with dot below + case 0xD673: + return 0x1E63; // small s with dot below + case 0xD674: + return 0x1E6D; // small t with dot below + case 0xD675: + return 0x1EE5; // small u with dot below + case 0xD676: + return 0x1E7F; // small v with dot below + case 0xD677: + return 0x1E89; // small w with dot below + case 0xD679: + return 0x1EF5; // small y with dot below + case 0xD67A: + return 0x1E93; // small z with dot below + + // 5/7 double dot below + case 0xD755: + return 0x1E72; // CAPITAL U WITH DIAERESIS BELOW + case 0xD775: + return 0x1E73; // small u with diaeresis below + + // 5/8 underline + case 0xD820: + return 0x005F; // underline + + // 5/9 double underline + case 0xD920: + return 0x2017; // double underline + + // 5/10 small low vertical bar + case 0xDA20: + return 0x02CC; // + + // 5/11 circumflex below + + // 5/12 (this position shall not be used) + + // 5/13 left half of ligature sign and of double tilde + + // 5/14 right half of ligature sign + + // 5/15 right half of double tilde + + default: + myDebug() << "Iso5426Converter::getCombiningChar() - no match for " << c << endl; + return QChar(); + } +} diff --git a/src/iso5426converter.h b/src/iso5426converter.h new file mode 100644 index 0000000..6bd1fd3 --- /dev/null +++ b/src/iso5426converter.h @@ -0,0 +1,43 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ISO5426CONVERTER_H +#define TELLICO_ISO5426CONVERTER_H + +class QCString; +class QString; +class QChar; + +#include <qglobal.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class Iso5426Converter { +public: + static QString toUtf8(const QCString& text); + +private: + static bool hasNext(uint pos, uint len); + static bool isAscii(uchar c); + static bool isCombining(uchar c); + + static QChar getChar(uchar c); + static QChar getCombiningChar(uint i); +}; + +} // end namespace + +#endif diff --git a/src/iso6937converter.cpp b/src/iso6937converter.cpp new file mode 100644 index 0000000..3a3bcfb --- /dev/null +++ b/src/iso6937converter.cpp @@ -0,0 +1,597 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// This class is adapted from Iso6937ToUnicode from the MARC4J project, available +// from http://marc4j.tigris.org, with the following notice: +// * Copyright (C) 2002 Bas Peters (mail@bpeters.com) +// * Copyright (C) 2002 Yves Pratter (ypratter@club-internet.fr) +// +// That source was released under the terms of the GNU Lesser General Public +// License, version 2.1. In accordance with Condition 3 of that license, +// I am applying the terms of the GNU General Public License to the source +// code, and including a large portion of it here + +#include "iso6937converter.h" +#include "tellico_debug.h" + +#include <qstring.h> + +using Tellico::Iso6937Converter; + +QString Iso6937Converter::toUtf8(const QCString& text_) { + const uint len = text_.length(); + QString result; + result.reserve(len); + uint pos = 0; + for(uint i = 0; i < len; ++i) { + uchar c = text_[i]; + if(isAscii(c)) { + result[pos++] = c; + } else if(isCombining(c) && hasNext(i, len)) { + QChar d = getCombiningChar(c * 256 + text_[i + 1]); + if(!d.isNull()) { + result[pos++] = d; + ++i; + } else { + result[pos++] = getChar(c); + } + } else { + result[pos++] = getChar(c); + } + } + result.squeeze(); + return result; +} + +inline +bool Iso6937Converter::hasNext(uint pos, uint len) { + return pos < (len - 1); +} + +inline +bool Iso6937Converter::isAscii(uchar c) { + return c <= 0x7F; +} + +inline +bool Iso6937Converter::isCombining(uchar c) { + return c >= 0xC0 && c <= 0xDF; +} + +// Source : http://anubis.dkuug.dk/JTC1/SC2/WG3/docs/6937cd.pdf +QChar Iso6937Converter::getChar(uchar c) { + switch(c) { + case 0xA0: + return 0x00A0; // 10/00 NO-BREAK SPACE + case 0xA1: + return 0x00A1; // 10/01 INVERTED EXCLAMATION MARK + case 0xA2: + return 0x00A2; // 10/02 CENT SIGN + case 0xA3: + return 0x00A3; // 10/03 POUND SIGN + // 10/04 (This position shall not be used) + case 0xA5: + return 0x00A5; // 10/05 YEN SIGN + // 10/06 (This position shall not be used) + case 0xA7: + return 0x00A7; // 10/07 SECTION SIGN + case 0xA8: + return 0x00A4; // 10/08 CURRENCY SIGN + case 0xA9: + return 0x2018; // 10/09 LEFT SINGLE QUOTATION MARK + case 0xAA: + return 0x201C; // 10/10 LEFT DOUBLE QUOTATION MARK + case 0xAB: + return 0x00AB; // 10/11 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + case 0xAC: + return 0x2190; // 10/12 LEFTWARDS ARROW + case 0xAD: + return 0x2191; // 10/13 UPWARDS ARROW + case 0xAE: + return 0x2192; // 10/14 RIGHTWARDS ARROW + case 0xAF: + return 0x2193; // 10/15 DOWNWARDS ARROW + + case 0xB0: + return 0x00B0; // 11/00 DEGREE SIGN + case 0xB1: + return 0x00B1; // 11/01 PLUS-MINUS SIGN + case 0xB2: + return 0x00B2; // 11/02 SUPERSCRIPT TWO + case 0xB3: + return 0x00B3; // 11/03 SUPERSCRIPT THREE + case 0xB4: + return 0x00D7; // 11/04 MULTIPLICATION SIGN + case 0xB5: + return 0x00B5; // 11/05 MICRO SIGN + case 0xB6: + return 0x00B6; // 11/06 PILCROW SIGN + case 0xB7: + return 0x00B7; // 11/07 MIDDLE DOT + case 0xB8: + return 0x00F7; // 11/08 DIVISION SIGN + case 0xB9: + return 0x2019; // 11/09 RIGHT SINGLE QUOTATION MARK + case 0xBA: + return 0x201D; // 11/10 RIGHT DOUBLE QUOTATION MARK + case 0xBB: + return 0x00BB; // 11/11 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + case 0xBC: + return 0x00BC; // 11/12 VULGAR FRACTION ONE QUARTER + case 0xBD: + return 0x00BD; // 11/13 VULGAR FRACTION ONE HALF + case 0xBE: + return 0x00BE; // 11/14 VULGAR FRACTION THREE QUARTERS + case 0xBF: + return 0x00BF; // 11/15 INVERTED QUESTION MARK + + // 4/0 to 5/15 diacritic characters + + case 0xD0: + return 0x2015; // 13/00 HORIZONTAL BAR + case 0xD1: + return 0x00B9; // 13/01 SUPERSCRIPT ONE + case 0xD2: + return 0x2117; // 13/02 REGISTERED SIGN + case 0xD3: + return 0x00A9; // 13/03 COPYRIGHT SIGN + case 0xD4: + return 0x00AE; // 13/04 TRADE MARK SIGN + case 0xD5: + return 0x266A; // 13/05 EIGHTH NOTE + case 0xD6: + return 0x00AC; // 13/06 NOT SIGN + case 0xD7: + return 0x00A6; // 13/07 BROKEN BAR + // 13/08 (This position shall not be used) + // 13/09 (This position shall not be used) + // 13/10 (This position shall not be used) + // 13/11 (This position shall not be used) + case 0xDC: + return 0x215B; // 13/12 VULGAR FRACTION ONE EIGHTH + case 0xDF: + return 0x215E; // 13/15 VULGAR FRACTION SEVEN EIGHTHS + + case 0xE0: + return 0x2126; // 14/00 OHM SIGN + case 0xE1: + return 0x00C6; // 14/01 LATIN CAPITAL LETTER AE + case 0xE2: + return 0x0110; // 14/02 LATIN CAPITAL LETTER D WITH STROKE + case 0xE3: + return 0x00AA; // 14/03 FEMININE ORDINAL INDICATOR + case 0xE4: + return 0x0126; // 14/04 LATIN CAPITAL LETTER H WITH STROKE + // 14/05 (This position shall not be used) + case 0xE6: + return 0x0132; // 14/06 LATIN CAPITAL LIGATURE IJ + case 0xE7: + return 0x013F; // 14/07 LATIN CAPITAL LETTER L WITH MIDDLE DOT + case 0xE8: + return 0x0141; // 14/08 LATIN CAPITAL LETTER L WITH STROKE + case 0xE9: + return 0x00D8; // 14/09 LATIN CAPITAL LETTER O WITH STROKE + case 0xEA: + return 0x0152; // 14/10 LATIN CAPITAL LIGATURE OE + case 0xEB: + return 0x00BA; // 14/11 MASCULINE ORDINAL INDICATOR + case 0xEC: + return 0x00DE; // 14/12 LATIN CAPITAL LETTER THORN + case 0xED: + return 0x0166; // 14/13 LATIN CAPITAL LETTER T WITH STROKE + case 0xEE: + return 0x014A; // 14/14 LATIN CAPITAL LETTER ENG + case 0xEF: + return 0x0149; // 14/15 LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + + case 0xF0: + return 0x0138; // 15/00 LATIN SMALL LETTER KRA + case 0xF1: + return 0x00E6; // 15/01 LATIN SMALL LETTER AE + case 0xF2: + return 0x0111; // 15/02 LATIN SMALL LETTER D WITH STROKE + case 0xF3: + return 0x00F0; // 15/03 LATIN SMALL LETTER ETH + case 0xF4: + return 0x0127; // 15/04 LATIN SMALL LETTER H WITH STROKE + case 0xF5: + return 0x0131; // 15/05 LATIN SMALL LETTER DOTLESS I + case 0xF6: + return 0x0133; // 15/06 LATIN SMALL LIGATURE IJ + case 0xF7: + return 0x0140; // 15/07 LATIN SMALL LETTER L WITH MIDDLE DOT + case 0xF8: + return 0x0142; // 15/08 LATIN SMALL LETTER L WITH STROKE + case 0xF9: + return 0x00F8; // 15/09 LATIN SMALL LETTER O WITH STROKE + case 0xFA: + return 0x0153; // 15/10 LATIN SMALL LIGATURE OE + case 0xFB: + return 0x00DF; // 15/11 LATIN SMALL LETTER SHARP S + case 0xFC: + return 0x00FE; // 15/12 LATIN SMALL LETTER THORN + case 0xFD: + return 0x0167; // 15/13 LATIN SMALL LETTER T WITH STROKE + case 0xFE: + return 0x014B; // 15/14 LATIN SMALL LETTER ENG + case 0xFF: + return 0x00AD; // 15/15 SOFT HYPHEN$ + default: + return c; + } +} + +QChar Iso6937Converter::getCombiningChar(uint c) { + switch(c) { + // 12/00 (This position shall not be used) + + // 12/01 non-spacing grave accent + case 0xC141: + return 0x00C0; // LATIN CAPITAL LETTER A WITH GRAVE + case 0xC145: + return 0x00C8; // LATIN CAPITAL LETTER E WITH GRAVE + case 0xC149: + return 0x00CC; // LATIN CAPITAL LETTER I WITH GRAVE + case 0xC14F: + return 0x00D2; // LATIN CAPITAL LETTER O WITH GRAVE + case 0xC155: + return 0x00D9; // LATIN CAPITAL LETTER U WITH GRAVE + case 0xC161: + return 0x00E0; // LATIN SMALL LETTER A WITH GRAVE + case 0xC165: + return 0x00E8; // LATIN SMALL LETTER E WITH GRAVE + case 0xC169: + return 0x00EC; // LATIN SMALL LETTER I WITH GRAVE + case 0xC16F: + return 0x00F2; // LATIN SMALL LETTER O WITH GRAVE + case 0xC175: + return 0x00F9; // LATIN SMALL LETTER U WITH GRAVE + + // 12/02 non-spacing acute accent + case 0xC220: + return 0x00B4; // ACUTE ACCENT + case 0xC241: + return 0x00C1; // LATIN CAPITAL LETTER A WITH ACUTE + case 0xC243: + return 0x0106; // LATIN CAPITAL LETTER C WITH ACUTE + case 0xC245: + return 0x00C9; // LATIN CAPITAL LETTER E WITH ACUTE + case 0xC249: + return 0x00CD; // LATIN CAPITAL LETTER I WITH ACUTE + case 0xC24C: + return 0x0139; // LATIN CAPITAL LETTER L WITH ACUTE + case 0xC24E: + return 0x0143; // LATIN CAPITAL LETTER N WITH ACUTE + case 0xC24F: + return 0x00D3; // LATIN CAPITAL LETTER O WITH ACUTE + case 0xC252: + return 0x0154; // LATIN CAPITAL LETTER R WITH ACUTE + case 0xC253: + return 0x015A; // LATIN CAPITAL LETTER S WITH ACUTE + case 0xC255: + return 0x00DA; // LATIN CAPITAL LETTER U WITH ACUTE + case 0xC259: + return 0x00DD; // LATIN CAPITAL LETTER Y WITH ACUTE + case 0xC25A: + return 0x0179; // LATIN CAPITAL LETTER Z WITH ACUTE + case 0xC261: + return 0x00E1; // LATIN SMALL LETTER A WITH ACUTE + case 0xC263: + return 0x0107; // LATIN SMALL LETTER C WITH ACUTE + case 0xC265: + return 0x00E9; // LATIN SMALL LETTER E WITH ACUTE + case 0xC267: + return 0x01F5; // LATIN SMALL LETTER G WITH CEDILLA(4) + case 0xC269: + return 0x00ED; // LATIN SMALL LETTER I WITH ACUTE + case 0xC26C: + return 0x013A; // LATIN SMALL LETTER L WITH ACUTE + case 0xC26E: + return 0x0144; // LATIN SMALL LETTER N WITH ACUTE + case 0xC26F: + return 0x00F3; // LATIN SMALL LETTER O WITH ACUTE + case 0xC272: + return 0x0155; // LATIN SMALL LETTER R WITH ACUTE + case 0xC273: + return 0x015B; // LATIN SMALL LETTER S WITH ACUTE + case 0xC275: + return 0x00FA; // LATIN SMALL LETTER U WITH ACUTE + case 0xC279: + return 0x00FD; // LATIN SMALL LETTER Y WITH ACUTE + case 0xC27A: + return 0x017A; // LATIN SMALL LETTER Z WITH ACUTE + + // 12/03 non-spacing circumflex accent + case 0xC341: + return 0x00C2; // LATIN CAPITAL LETTER A WITH CIRCUMFLEX + case 0xC343: + return 0x0108; // LATIN CAPITAL LETTER C WITH CIRCUMFLEX + case 0xC345: + return 0x00CA; // LATIN CAPITAL LETTER E WITH CIRCUMFLEX + case 0xC347: + return 0x011C; // LATIN CAPITAL LETTER G WITH CIRCUMFLEX + case 0xC348: + return 0x0124; // LATIN CAPITAL LETTER H WITH CIRCUMFLEX + case 0xC349: + return 0x00CE; // LATIN CAPITAL LETTER I WITH CIRCUMFLEX + case 0xC34A: + return 0x0134; // LATIN CAPITAL LETTER J WITH CIRCUMFLEX + case 0xC34F: + return 0x00D4; // LATIN CAPITAL LETTER O WITH CIRCUMFLEX + case 0xC353: + return 0x015C; // LATIN CAPITAL LETTER S WITH CIRCUMFLEX + case 0xC355: + return 0x00DB; // LATIN CAPITAL LETTER U WITH CIRCUMFLEX + case 0xC357: + return 0x0174; // LATIN CAPITAL LETTER W WITH CIRCUMFLEX + case 0xC359: + return 0x0176; // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX + case 0xC361: + return 0x00E2; // LATIN SMALL LETTER A WITH CIRCUMFLEX + case 0xC363: + return 0x0109; // LATIN SMALL LETTER C WITH CIRCUMFLEX + case 0xC365: + return 0x00EA; // LATIN SMALL LETTER E WITH CIRCUMFLEX + case 0xC367: + return 0x011D; // LATIN SMALL LETTER G WITH CIRCUMFLEX + case 0xC368: + return 0x0125; // LATIN SMALL LETTER H WITH CIRCUMFLEX + case 0xC369: + return 0x00EE; // LATIN SMALL LETTER I WITH CIRCUMFLEX + case 0xC36A: + return 0x0135; // LATIN SMALL LETTER J WITH CIRCUMFLEX + case 0xC36F: + return 0x00F4; // LATIN SMALL LETTER O WITH CIRCUMFLEX + case 0xC373: + return 0x015D; // LATIN SMALL LETTER S WITH CIRCUMFLEX + case 0xC375: + return 0x00FB; // LATIN SMALL LETTER U WITH CIRCUMFLEX + case 0xC377: + return 0x0175; // LATIN SMALL LETTER W WITH CIRCUMFLEX + case 0xC379: + return 0x0177; // LATIN SMALL LETTER Y WITH CIRCUMFLEX + + // 12/04 non-spacing tilde + case 0xC441: + return 0x00C3; // LATIN CAPITAL LETTER A WITH TILDE + case 0xC449: + return 0x0128; // LATIN CAPITAL LETTER I WITH TILDE + case 0xC44E: + return 0x00D1; // LATIN CAPITAL LETTER N WITH TILDE + case 0xC44F: + return 0x00D5; // LATIN CAPITAL LETTER O WITH TILDE + case 0xC455: + return 0x0168; // LATIN CAPITAL LETTER U WITH TILDE + case 0xC461: + return 0x00E3; // LATIN SMALL LETTER A WITH TILDE + case 0xC469: + return 0x0129; // LATIN SMALL LETTER I WITH TILDE + case 0xC46E: + return 0x00F1; // LATIN SMALL LETTER N WITH TILDE + case 0xC46F: + return 0x00F5; // LATIN SMALL LETTER O WITH TILDE + case 0xC475: + return 0x0169; // LATIN SMALL LETTER U WITH TILDE + + // 12/05 non-spacing macron + case 0xC541: + return 0x0100; // LATIN CAPITAL LETTER A WITH MACRON + case 0xC545: + return 0x0112; // LATIN CAPITAL LETTER E WITH MACRON + case 0xC549: + return 0x012A; // LATIN CAPITAL LETTER I WITH MACRON + case 0xC54F: + return 0x014C; // LATIN CAPITAL LETTER O WITH MACRON + case 0xC555: + return 0x016A; // LATIN CAPITAL LETTER U WITH MACRON + case 0xC561: + return 0x0101; // LATIN SMALL LETTER A WITH MACRON + case 0xC565: + return 0x0113; // LATIN SMALL LETTER E WITH MACRON + case 0xC569: + return 0x012B; // LATIN SMALL LETTER I WITH MACRON + case 0xC56F: + return 0x014D; // LATIN SMALL LETTER O WITH MACRON + case 0xC575: + return 0x016B; // LATIN SMALL LETTER U WITH MACRON + + // 12/06 non-spacing breve + case 0xC620: + return 0x02D8; // BREVE + case 0xC641: + return 0x0102; // LATIN CAPITAL LETTER A WITH BREVE + case 0xC647: + return 0x011E; // LATIN CAPITAL LETTER G WITH BREVE + case 0xC655: + return 0x016C; // LATIN CAPITAL LETTER U WITH BREVE + case 0xC661: + return 0x0103; // LATIN SMALL LETTER A WITH BREVE + case 0xC667: + return 0x011F; // LATIN SMALL LETTER G WITH BREVE + case 0xC675: + return 0x016D; // LATIN SMALL LETTER U WITH BREVE + + // 12/07 non-spacing dot above + case 0xC743: + return 0x010A; // LATIN CAPITAL LETTER C WITH DOT ABOVE + case 0xC745: + return 0x0116; // LATIN CAPITAL LETTER E WITH DOT ABOVE + case 0xC747: + return 0x0120; // LATIN CAPITAL LETTER G WITH DOT ABOVE + case 0xC749: + return 0x0130; // LATIN CAPITAL LETTER I WITH DOT ABOVE + case 0xC75A: + return 0x017B; // LATIN CAPITAL LETTER Z WITH DOT ABOVE + case 0xC763: + return 0x010B; // LATIN SMALL LETTER C WITH DOT ABOVE + case 0xC765: + return 0x0117; // LATIN SMALL LETTER E WITH DOT ABOVE + case 0xC767: + return 0x0121; // LATIN SMALL LETTER G WITH DOT ABOVE + case 0xC77A: + return 0x017C; // LATIN SMALL LETTER Z WITH DOT ABOVE + + // 12/08 non-spacing diaeresis + case 0xC820: + return 0x00A8; // DIAERESIS + case 0xC841: + return 0x00C4; // LATIN CAPITAL LETTER A WITH DIAERESIS + case 0xC845: + return 0x00CB; // LATIN CAPITAL LETTER E WITH DIAERESIS + case 0xC849: + return 0x00CF; // LATIN CAPITAL LETTER I WITH DIAERESIS + case 0xC84F: + return 0x00D6; // LATIN CAPITAL LETTER O WITH DIAERESIS + case 0xC855: + return 0x00DC; // LATIN CAPITAL LETTER U WITH DIAERESIS + case 0xC859: + return 0x0178; // LATIN CAPITAL LETTER Y WITH DIAERESIS + case 0xC861: + return 0x00E4; // LATIN SMALL LETTER A WITH DIAERESIS + case 0xC865: + return 0x00EB; // LATIN SMALL LETTER E WITH DIAERESIS + case 0xC869: + return 0x00EF; // LATIN SMALL LETTER I WITH DIAERESIS + case 0xC86F: + return 0x00F6; // LATIN SMALL LETTER O WITH DIAERESIS + case 0xC875: + return 0x00FC; // LATIN SMALL LETTER U WITH DIAERESIS + case 0xC879: + return 0x00FF; // LATIN SMALL LETTER Y WITH DIAERESIS + + // 12/09 (This position shall not be used) + + // 12/10 non-spacing ring above + case 0xCA20: + return 0x02DA; // RING ABOVE + case 0xCA41: + return 0x00C5; // LATIN CAPITAL LETTER A WITH RING ABOVE + case 0xCAAD: + return 0x016E; // LATIN CAPITAL LETTER U WITH RING ABOVE + case 0xCA61: + return 0x00E5; // LATIN SMALL LETTER A WITH RING ABOVE + case 0xCA75: + return 0x016F; // LATIN SMALL LETTER U WITH RING ABOVE + + // 12/11 non-spacing cedilla + case 0xCB20: + return 0x00B8; // CEDILLA + case 0xCB43: + return 0x00C7; // LATIN CAPITAL LETTER C WITH CEDILLA + case 0xCB47: + return 0x0122; // LATIN CAPITAL LETTER G WITH CEDILLA + case 0xCB4B: + return 0x0136; // LATIN CAPITAL LETTER K WITH CEDILLA + case 0xCB4C: + return 0x013B; // LATIN CAPITAL LETTER L WITH CEDILLA + case 0xCB4E: + return 0x0145; // LATIN CAPITAL LETTER N WITH CEDILLA + case 0xCB52: + return 0x0156; // LATIN CAPITAL LETTER R WITH CEDILLA + case 0xCB53: + return 0x015E; // LATIN CAPITAL LETTER S WITH CEDILLA + case 0xCB54: + return 0x0162; // LATIN CAPITAL LETTER T WITH CEDILLA + case 0xCB63: + return 0x00E7; // LATIN SMALL LETTER C WITH CEDILLA + // case 0xCB67: return 0x0123; // small g with cedilla + case 0xCB6B: + return 0x0137; // LATIN SMALL LETTER K WITH CEDILLA + case 0xCB6C: + return 0x013C; // LATIN SMALL LETTER L WITH CEDILLA + case 0xCB6E: + return 0x0146; // LATIN SMALL LETTER N WITH CEDILLA + case 0xCB72: + return 0x0157; // LATIN SMALL LETTER R WITH CEDILLA + case 0xCB73: + return 0x015F; // LATIN SMALL LETTER S WITH CEDILLA + case 0xCB74: + return 0x0163; // LATIN SMALL LETTER T WITH CEDILLA + + // 12/12 (This position shall not be used) + + // 12/13 non-spacing double acute accent + case 0xCD4F: + return 0x0150; // LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + case 0xCD55: + return 0x0170; // LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + case 0xCD6F: + return 0x0151; // LATIN SMALL LETTER O WITH DOUBLE ACUTE + case 0xCD75: + return 0x0171; // LATIN SMALL LETTER U WITH DOUBLE ACUTE + + // 12/14 non-spacing ogonek + case 0xCE20: + return 0x02DB; // ogonek + case 0xCE41: + return 0x0104; // LATIN CAPITAL LETTER A WITH OGONEK + case 0xCE45: + return 0x0118; // LATIN CAPITAL LETTER E WITH OGONEK + case 0xCE49: + return 0x012E; // LATIN CAPITAL LETTER I WITH OGONEK + case 0xCE55: + return 0x0172; // LATIN CAPITAL LETTER U WITH OGONEK + case 0xCE61: + return 0x0105; // LATIN SMALL LETTER A WITH OGONEK + case 0xCE65: + return 0x0119; // LATIN SMALL LETTER E WITH OGONEK + case 0xCE69: + return 0x012F; // LATIN SMALL LETTER I WITH OGONEK + case 0xCE75: + return 0x0173; // LATIN SMALL LETTER U WITH OGONEK + + // 12/15 non-spacing caron + case 0xCF20: + return 0x02C7; // CARON + case 0xCF43: + return 0x010C; // LATIN CAPITAL LETTER C WITH CARON + case 0xCF44: + return 0x010E; // LATIN CAPITAL LETTER D WITH CARON + case 0xCF45: + return 0x011A; // LATIN CAPITAL LETTER E WITH CARON + case 0xCF4C: + return 0x013D; // LATIN CAPITAL LETTER L WITH CARON + case 0xCF4E: + return 0x0147; // LATIN CAPITAL LETTER N WITH CARON + case 0xCF52: + return 0x0158; // LATIN CAPITAL LETTER R WITH CARON + case 0xCF53: + return 0x0160; // LATIN CAPITAL LETTER S WITH CARON + case 0xCF54: + return 0x0164; // LATIN CAPITAL LETTER T WITH CARON + case 0xCF5A: + return 0x017D; // LATIN CAPITAL LETTER Z WITH CARON + case 0xCF63: + return 0x010D; // LATIN SMALL LETTER C WITH CARON + case 0xCF64: + return 0x010F; // LATIN SMALL LETTER D WITH CARON + case 0xCF65: + return 0x011B; // LATIN SMALL LETTER E WITH CARON + case 0xCF6C: + return 0x013E; // LATIN SMALL LETTER L WITH CARON + case 0xCF6E: + return 0x0148; // LATIN SMALL LETTER N WITH CARON + case 0xCF72: + return 0x0159; // LATIN SMALL LETTER R WITH CARON + case 0xCF73: + return 0x0161; // LATIN SMALL LETTER S WITH CARON + case 0xCF74: + return 0x0165; // LATIN SMALL LETTER T WITH CARON + case 0xCF7A: + return 0x017E; // LATIN SMALL LETTER Z WITH CARON + + default: + myDebug() << "Iso6937Converter::getCombiningChar() - no match for " << c << endl; + return QChar(); + } +} diff --git a/src/iso6937converter.h b/src/iso6937converter.h new file mode 100644 index 0000000..1d168ba --- /dev/null +++ b/src/iso6937converter.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_ISO6937CONVERTER_H +#define TELLICO_ISO6937CONVERTER_H + +class QCString; +class QString; +class QChar; + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class Iso6937Converter { +public: + static QString toUtf8(const QCString& text); + +private: + static bool hasNext(unsigned int pos, unsigned int len); + static bool isAscii(unsigned char c); + static bool isCombining(unsigned char c); + + static QChar getChar(unsigned char c); + static QChar getCombiningChar(unsigned int c); +}; + +} // end namespace + +#endif diff --git a/src/latin1literal.h b/src/latin1literal.h new file mode 100644 index 0000000..03e2293 --- /dev/null +++ b/src/latin1literal.h @@ -0,0 +1,90 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +// this code was original published to the kde-core-devel email list +// copyright 2003 Harri Porten <porten@kde.org> +// Originally licensed under LGPL, included here under GPL v2 + +#ifndef LATIN1LITERAL_H +#define LATIN1LITERAL_H + +#include <qstring.h> + +namespace Tellico { + +/** + * A class for explicit marking of string literals encoded in the ISO + * 8859-1 character set. Allows for efficient, still (in terms of the + * chosen encoding) safe comparison with QString instances. To be used + * like this: + * + * \code + * QString s = ..... + * if (s == Latin1Literal("o")) { ..... } + * \endcode + * + */ +#define Latin1Literal(s) \ + Tellico::Latin1LiteralInternal((s), sizeof(s)/sizeof(char)-1) + +class Latin1LiteralInternal { + +public: + Latin1LiteralInternal(const char* s, size_t l) + : str(s), len(s ? l : (size_t)-1) { } + + // this is lazy, leave these public since I can't figure out + // how to declare a friend function that works for gcc 2.95 + const char* str; + size_t len; +}; + +} // end namespace + +inline +bool operator==(const QString& s1, const Tellico::Latin1LiteralInternal& s2) { + const QChar* uc = s1.unicode(); + const char* c = s2.str; + if(!c || !uc) { + return (!c && !uc); + } + + const size_t& l = s2.len; + if(s1.length() != l) { + return false; + } + + for(size_t i = 0; i < l; ++i, ++uc, ++c) { + if(uc->unicode() != static_cast<uchar>(*c)) { + return false; + } + } + return true; +} + +inline +bool operator!=(const QString& s1, const Tellico::Latin1LiteralInternal& s2) { + return !(s1 == s2); +} + +inline +bool operator==(const Tellico::Latin1LiteralInternal& s1, const QString& s2) { + return s2 == s1; +} + +inline +bool operator!=(const Tellico::Latin1LiteralInternal& s1, const QString& s2) { + return !(s2 == s1); +} + +#endif diff --git a/src/lccnvalidator.cpp b/src/lccnvalidator.cpp new file mode 100644 index 0000000..8c47931 --- /dev/null +++ b/src/lccnvalidator.cpp @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2008 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 "lccnvalidator.h" +#include "tellico_debug.h" + +using Tellico::LCCNValidator; + +LCCNValidator::LCCNValidator(QObject* parent_) : QRegExpValidator(parent_) { + QRegExp rx(QString::fromLatin1("[a-z ]{0,3}" + "(" + "\\d{2}-?\\d{1,6}" + "|" + "\\d{4}-?\\d{1,6}" + ")" + " ?\\w*")); + setRegExp(rx); +} + +// static +QString LCCNValidator::formalize(const QString& value_) { + const int len = value_.length(); + // first remove alpha prefix + QString alpha; + for(int pos = 0; pos < len; ++pos) { + QChar c = value_.at(pos); + if(c.isNumber()) { + break; + } + alpha += value_.at(pos); + } + QString afterAlpha = value_.mid(alpha.length()); + alpha = alpha.stripWhiteSpace(); // possible to have a space at the end + + QString year; + QString serial; + // have to be able to differentiate 2 and 4-digit years, first check for hyphen position + int pos = afterAlpha.find('-'); + if(pos > -1) { + year = afterAlpha.section('-', 0, 0); + serial = afterAlpha.section('-', 1); + } else { + // make two assumptions, the user will never have a book from the year 1920 + // or from any year after 2100. Reasonable, right? + // so if the string starts with '20' we take the first 4 digits as the year + // otherwise the first 2 + if(afterAlpha.startsWith(QString::fromLatin1("20"))) { + year = afterAlpha.left(4); + serial = afterAlpha.mid(4); + } else { + year = afterAlpha.left(2); + serial = afterAlpha.mid(2); + } + } + + // now check for non digits in the serial + pos = 0; + for( ; pos < serial.length() && serial.at(pos).isNumber(); ++pos) { ; } + QString suffix = serial.mid(pos); + serial = serial.left(pos); + // serial must be left-padded with zeros to 6 characters + serial = serial.rightJustify(6, '0'); + return alpha + year + serial + suffix; +} diff --git a/src/lccnvalidator.h b/src/lccnvalidator.h new file mode 100644 index 0000000..60ab807 --- /dev/null +++ b/src/lccnvalidator.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2008 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_LCCNVALIDATOR_H +#define TELLICO_LCCNVALIDATOR_H + +#include <qvalidator.h> + +namespace Tellico { + +/** + * Library of Congress Controll Number validator + * + * see http://www.loc.gov/marc/lccn_structure.html + * + * These are all valid + * - 89-456 + * - 2001-1114 + * - gm 71-2450 + */ +class LCCNValidator : public QRegExpValidator { + +public: + LCCNValidator(QObject* parent); + + /** + * Returns the formalized version as dictated by LOC search + * http://catalog.loc.gov/help/number.htm + */ + static QString formalize(const QString& value); +}; + +} +#endif diff --git a/src/listviewcomparison.cpp b/src/listviewcomparison.cpp new file mode 100644 index 0000000..9bdd10c --- /dev/null +++ b/src/listviewcomparison.cpp @@ -0,0 +1,279 @@ +/*************************************************************************** + copyright : (C) 2007 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 "listviewcomparison.h" +#include "latin1literal.h" +#include "field.h" +#include "collection.h" +#include "document.h" +#include "entryitem.h" + +#include <qlistview.h> +#include <qiconview.h> +#include <qpixmap.h> +#include <qdatetime.h> + +namespace { + int compareFloat(const QString& s1, const QString& s2) { + bool ok1, ok2; + float n1 = s1.toFloat(&ok1); + float n2 = s2.toFloat(&ok2); + return (ok1 && ok2) ? static_cast<int>(n1-n2) : 0; + } +} + +Tellico::ListViewComparison* Tellico::ListViewComparison::create(Data::FieldPtr field_) { + return create(Data::ConstFieldPtr(field_.data())); +} + +Tellico::ListViewComparison* Tellico::ListViewComparison::create(Data::ConstFieldPtr field_) { + if(field_->type() == Data::Field::Number) { + return new NumberComparison(field_); + } else if(field_->type() == Data::Field::Image || + field_->type() == Data::Field::Rating || + field_->type() == Data::Field::Bool) { + // bool and ratings only have an image + return new PixmapComparison(field_); + } else if(field_->type() == Data::Field::Dependent) { + return new DependentComparison(field_); + } else if(field_->type() == Data::Field::Date || field_->formatFlag() == Data::Field::FormatDate) { + return new ISODateComparison(field_); + } else if(field_->formatFlag() == Data::Field::FormatTitle) { + // Dependent could be title, so put this test after + return new TitleComparison(field_); + } else if(field_->property(QString::fromLatin1("lcc")) == Latin1Literal("true") || + (field_->name() == Latin1Literal("lcc") && + Data::Document::self()->collection() && + (Data::Document::self()->collection()->type() == Data::Collection::Book || + Data::Document::self()->collection()->type() == Data::Collection::Bibtex))) { + // allow LCC comparison if LCC property is set, or if the name is lcc for a book or bibliography collection + return new LCCComparison(field_); + } + return new StringComparison(field_); +} + +Tellico::ListViewComparison::ListViewComparison(Data::ConstFieldPtr field) : m_fieldName(field->name()) { +} + +int Tellico::ListViewComparison::compare(int col_, + const GUI::ListViewItem* item1_, + const GUI::ListViewItem* item2_, + bool asc_) +{ + return compare(item1_->key(col_, asc_), item2_->key(col_, asc_)); +} + +int Tellico::ListViewComparison::compare(const QIconViewItem* item1_, + const QIconViewItem* item2_) +{ + return compare(item1_->key(), item2_->key()); +} + +Tellico::StringComparison::StringComparison(Data::ConstFieldPtr field) : ListViewComparison(field) { +} + +int Tellico::StringComparison::compare(const QString& str1_, const QString& str2_) { + return str1_.localeAwareCompare(str2_); +} + +Tellico::TitleComparison::TitleComparison(Data::ConstFieldPtr field) : ListViewComparison(field) { +} + +int Tellico::TitleComparison::compare(const QString& str1_, const QString& str2_) { + const QString title1 = Data::Field::sortKeyTitle(str1_); + const QString title2 = Data::Field::sortKeyTitle(str2_); + return title1.localeAwareCompare(title2); +} + +Tellico::NumberComparison::NumberComparison(Data::ConstFieldPtr field) : ListViewComparison(field) { +} + +int Tellico::NumberComparison::compare(const QString& str1_, const QString& str2_) { + // by default, an empty string would get sorted before "1" because toFloat() turns it into "0" + // I want the empty strings to be at the end + bool ok1, ok2; + // use section in case of multiple values + float num1 = str1_.section(';', 0, 0).toFloat(&ok1); + float num2 = str2_.section(';', 0, 0).toFloat(&ok2); + if(ok1 && ok2) { + return static_cast<int>(num1 - num2); + } else if(ok1 && !ok2) { + return -1; + } else if(!ok1 && ok2) { + return 1; + } + return 0; +} + +// for details on the LCC comparison, see +// http://www.mcgees.org/2001/08/08/sort-by-library-of-congress-call-number-in-perl/ + +Tellico::LCCComparison::LCCComparison(Data::ConstFieldPtr field) : StringComparison(field), + m_regexp(QString::fromLatin1("^([A-Z]+)(\\d+(?:\\.\\d+)?)\\.?([A-Z]*)(\\d*)\\.?([A-Z]*)(\\d*)(?: (\\d\\d\\d\\d))?")) { +} + +int Tellico::LCCComparison::compare(const QString& str1_, const QString& str2_) { +// myDebug() << "LCCComparison::compare() - " << str1_ << " to " << str2_ << endl; + int pos1 = m_regexp.search(str1_); + const QStringList cap1 = m_regexp.capturedTexts(); + int pos2 = m_regexp.search(str2_); + const QStringList cap2 = m_regexp.capturedTexts(); + if(pos1 > -1 && pos2 > -1) { + int res = compareLCC(cap1, cap2); +// myLog() << "...result = " << res << endl; + return res; + } + return StringComparison::compare(str1_, str2_); +} + +int Tellico::LCCComparison::compareLCC(const QStringList& cap1, const QStringList& cap2) const { + // the first item in the list is the full match, so start array index at 1 + int res = 0; + return (res = cap1[1].compare(cap2[1])) != 0 ? res : + (res = compareFloat(cap1[2], cap2[2])) != 0 ? res : + (res = cap1[3].compare(cap2[3])) != 0 ? res : + (res = compareFloat(QString::fromLatin1("0.") + cap1[4], + QString::fromLatin1("0.") + cap2[4])) != 0 ? res : + (res = cap1[5].compare(cap2[5])) != 0 ? res : + (res = compareFloat(QString::fromLatin1("0.") + cap1[6], + QString::fromLatin1("0.") + cap2[6])) != 0 ? res : + (res = compareFloat(cap1[7], cap2[7])) != 0 ? res : 0; +} + +Tellico::PixmapComparison::PixmapComparison(Data::ConstFieldPtr field) : ListViewComparison(field) { +} + +int Tellico::PixmapComparison::compare(int col_, + const GUI::ListViewItem* item1_, + const GUI::ListViewItem* item2_, + bool asc_) +{ + Q_UNUSED(asc_); + const QPixmap* pix1 = item1_->pixmap(col_); + const QPixmap* pix2 = item2_->pixmap(col_); + if(pix1 && !pix1->isNull()) { + if(pix2 && !pix2->isNull()) { + // large images come first + return pix1->width() - pix2->width(); + } + return 1; + } else if(pix2 && !pix2->isNull()) { + return -1; + } + return 0; +} + +int Tellico::PixmapComparison::compare(const QIconViewItem* item1_, + const QIconViewItem* item2_) +{ + const QPixmap* pix1 = item1_->pixmap(); + const QPixmap* pix2 = item2_->pixmap(); + if(pix1 && !pix1->isNull()) { + if(pix2 && !pix2->isNull()) { + // large images come first + return pix1->width() - pix2->width(); + } + return 1; + } else if(pix2 && !pix2->isNull()) { + return -1; + } + return 0; +} + +Tellico::DependentComparison::DependentComparison(Data::ConstFieldPtr field) : StringComparison(field) { + Data::FieldVec fields = field->dependsOn(Data::Document::self()->collection()); + for(Data::FieldVecIt f = fields.begin(); f != fields.end(); ++f) { + m_comparisons.append(create(f)); + } + m_comparisons.setAutoDelete(true); +} + +int Tellico::DependentComparison::compare(int col_, + const GUI::ListViewItem* item1_, + const GUI::ListViewItem* item2_, + bool asc_) +{ + Q_UNUSED(col_); + Q_UNUSED(asc_); + for(QPtrListIterator<ListViewComparison> it(m_comparisons); it.current(); ++it) { + int res = it.current()->compare(col_, item1_, item2_, asc_); + if(res != 0) { + return res; + } + } + return ListViewComparison::compare(col_, item1_, item2_, asc_); +} + +int Tellico::DependentComparison::compare(const QIconViewItem* item1_, + const QIconViewItem* item2_) +{ + for(QPtrListIterator<ListViewComparison> it(m_comparisons); it.current(); ++it) { + int res = it.current()->compare(item1_, item2_); + if(res != 0) { + return res; + } + } + return ListViewComparison::compare(item1_, item2_); +} + +Tellico::ISODateComparison::ISODateComparison(Data::ConstFieldPtr field) : ListViewComparison(field) { +} + +int Tellico::ISODateComparison::compare(const QString& str1, const QString& str2) { + if(str1.isEmpty()) { + return str2.isEmpty() ? 0 : -1; + } + if(str2.isEmpty()) { // str1 is not + return 1; + } + // modelled after Field::formatDate() + // so dates would sort as expected without padding month and day with zero + // and accounting for "current year - 1 - 1" default scheme + QStringList dlist1 = QStringList::split('-', str1, true); + bool ok = true; + int y1 = dlist1.count() > 0 ? dlist1[0].toInt(&ok) : QDate::currentDate().year(); + if(!ok) { + y1 = QDate::currentDate().year(); + } + int m1 = dlist1.count() > 1 ? dlist1[1].toInt(&ok) : 1; + if(!ok) { + m1 = 1; + } + int d1 = dlist1.count() > 2 ? dlist1[2].toInt(&ok) : 1; + if(!ok) { + d1 = 1; + } + QDate date1(y1, m1, d1); + + QStringList dlist2 = QStringList::split('-', str2, true); + int y2 = dlist2.count() > 0 ? dlist2[0].toInt(&ok) : QDate::currentDate().year(); + if(!ok) { + y2 = QDate::currentDate().year(); + } + int m2 = dlist2.count() > 1 ? dlist2[1].toInt(&ok) : 1; + if(!ok) { + m2 = 1; + } + int d2 = dlist2.count() > 2 ? dlist2[2].toInt(&ok) : 1; + if(!ok) { + d2 = 1; + } + QDate date2(y2, m2, d2); + + if(date1 < date2) { + return -1; + } else if(date1 > date2) { + return 1; + } + return 0; +} diff --git a/src/listviewcomparison.h b/src/listviewcomparison.h new file mode 100644 index 0000000..4c0ed43 --- /dev/null +++ b/src/listviewcomparison.h @@ -0,0 +1,116 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_LISTVIEWCOMPARISON_H +#define TELLICO_LISTVIEWCOMPARISON_H + +#include "datavectors.h" + +#include <qregexp.h> + +class QStringList; +class QIconViewItem; + +namespace Tellico { + namespace GUI { + class ListViewItem; + } + +class ListViewComparison { +public: + ListViewComparison(Data::ConstFieldPtr field); + virtual ~ListViewComparison() {} + + const QString& fieldName() const { return m_fieldName; } + + virtual int compare(int col, const GUI::ListViewItem* item1, const GUI::ListViewItem* item2, bool asc); + virtual int compare(const QIconViewItem* item1, const QIconViewItem* item2); + + static ListViewComparison* create(Data::FieldPtr field); + static ListViewComparison* create(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2) = 0; + +private: + QString m_fieldName; +}; + +class StringComparison : public ListViewComparison { +public: + StringComparison(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2); +}; + +class TitleComparison : public ListViewComparison { +public: + TitleComparison(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2); +}; + +class NumberComparison : public ListViewComparison { +public: + NumberComparison(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2); +}; + +class LCCComparison : public StringComparison { +public: + LCCComparison(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2); + +private: + int compareLCC(const QStringList& cap1, const QStringList& cap2) const; + QRegExp m_regexp; +}; + +class PixmapComparison : public ListViewComparison { +public: + PixmapComparison(Data::ConstFieldPtr field); + + virtual int compare(int col, const GUI::ListViewItem* item1, const GUI::ListViewItem* item2, bool asc); + virtual int compare(const QIconViewItem* item1, const QIconViewItem* item2); + +protected: + virtual int compare(const QString&, const QString&) { return 0; } +}; + +class DependentComparison : public StringComparison { +public: + DependentComparison(Data::ConstFieldPtr field); + + virtual int compare(int col, const GUI::ListViewItem* item1, const GUI::ListViewItem* item2, bool asc); + virtual int compare(const QIconViewItem* item1, const QIconViewItem* item2); + +private: + QPtrList<ListViewComparison> m_comparisons; +}; + +class ISODateComparison : public ListViewComparison { +public: + ISODateComparison(Data::ConstFieldPtr field); + +protected: + virtual int compare(const QString& str1, const QString& str2); +}; + +} +#endif diff --git a/src/loandialog.cpp b/src/loandialog.cpp new file mode 100644 index 0000000..e34ecc7 --- /dev/null +++ b/src/loandialog.cpp @@ -0,0 +1,268 @@ +/*************************************************************************** + copyright : (C) 2005-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 "loandialog.h" +#include "borrowerdialog.h" +#include "gui/datewidget.h" +#include "gui/richtextlabel.h" +#include "collection.h" +#include "commands/addloans.h" +#include "commands/modifyloans.h" + +#include <config.h> + +#include <klocale.h> +#include <klineedit.h> +#include <kpushbutton.h> +#include <ktextedit.h> +#include <kiconloader.h> +#include <kabc/stdaddressbook.h> + +#include <qlayout.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qwhatsthis.h> + +using Tellico::LoanDialog; + +LoanDialog::LoanDialog(const Data::EntryVec& entries_, QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, true, i18n("Loan Dialog"), Ok|Cancel), + m_mode(Add), m_borrower(0), m_entries(entries_), m_loan(0) { + init(); +} + +LoanDialog::LoanDialog(Data::LoanPtr loan_, QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, true, i18n("Modify Loan"), Ok|Cancel), + m_mode(Modify), m_borrower(loan_->borrower()), m_loan(loan_) { + m_entries.append(m_loan->entry()); + + init(); + + m_borrowerEdit->setText(m_loan->borrower()->name()); + m_loanDate->setDate(m_loan->loanDate()); + if(m_loan->dueDate().isValid()) { + m_dueDate->setDate(m_loan->dueDate()); + m_addEvent->setEnabled(true); + if(m_loan->inCalendar()) { + m_addEvent->setChecked(true); + } + } + m_note->setText(m_loan->note()); +} + +void LoanDialog::init() { + QWidget* mainWidget = new QWidget(this, "LoanDialog mainWidget"); + setMainWidget(mainWidget); + QGridLayout* topLayout = new QGridLayout(mainWidget, 7, 2, 0, KDialog::spacingHint()); + + QHBox* hbox = new QHBox(mainWidget); + hbox->setSpacing(KDialog::spacingHint()); + QLabel* pixLabel = new QLabel(hbox); + pixLabel->setPixmap(DesktopIcon(QString::fromLatin1("tellico"), 64)); + pixLabel->setAlignment(Qt::AlignAuto | Qt::AlignTop); + hbox->setStretchFactor(pixLabel, 0); + + QString entryString = QString::fromLatin1("<qt><p>"); + if(m_mode == Add) { + entryString += i18n("The following items are being checked out:"); + entryString += QString::fromLatin1("</p><ol>"); + for(Data::EntryVec::ConstIterator entry = m_entries.constBegin(); entry != m_entries.constEnd(); ++entry) { + entryString += QString::fromLatin1("<li>") + entry->title() + QString::fromLatin1("</li>"); + } + } else { + entryString += i18n("The following item is on-loan:"); + entryString += QString::fromLatin1("</p><ol>"); + entryString += QString::fromLatin1("<li>") + m_loan->entry()->title() + QString::fromLatin1("</li>"); + } + entryString += QString::fromLatin1("</ol></qt>"); + GUI::RichTextLabel* entryLabel = new GUI::RichTextLabel(entryString, hbox); + hbox->setStretchFactor(entryLabel, 1); + + topLayout->addMultiCellWidget(hbox, 0, 0, 0, 1); + + QLabel* l = new QLabel(i18n("&Lend to:"), mainWidget); + topLayout->addWidget(l, 1, 0); + hbox = new QHBox(mainWidget); + hbox->setSpacing(KDialog::spacingHint()); + topLayout->addWidget(hbox, 1, 1); + m_borrowerEdit = new KLineEdit(hbox); + l->setBuddy(m_borrowerEdit); + m_borrowerEdit->completionObject()->setIgnoreCase(true); + connect(m_borrowerEdit, SIGNAL(textChanged(const QString&)), + SLOT(slotBorrowerNameChanged(const QString&))); + actionButton(Ok)->setEnabled(false); // disable until a name is entered + KPushButton* pb = new KPushButton(SmallIconSet(QString::fromLatin1("kaddressbook")), QString::null, hbox); + connect(pb, SIGNAL(clicked()), SLOT(slotGetBorrower())); + QString whats = i18n("Enter the name of the person borrowing the items from you. " + "Clicking the button allows you to select from your address book."); + QWhatsThis::add(l, whats); + QWhatsThis::add(hbox, whats); + // only enable for new loans + if(m_mode == Modify) { + m_borrowerEdit->setEnabled(false); + pb->setEnabled(false); + } + + l = new QLabel(i18n("&Loan date:"), mainWidget); + topLayout->addWidget(l, 2, 0); + m_loanDate = new GUI::DateWidget(mainWidget); + m_loanDate->setDate(QDate::currentDate()); + l->setBuddy(m_loanDate); + topLayout->addWidget(m_loanDate, 2, 1); + whats = i18n("The check-out date is the date that you lent the items. By default, " + "today's date is used."); + QWhatsThis::add(l, whats); + QWhatsThis::add(m_loanDate, whats); + // only enable for new loans + if(m_mode == Modify) { + m_loanDate->setEnabled(false); + } + + l = new QLabel(i18n("D&ue date:"), mainWidget); + topLayout->addWidget(l, 3, 0); + m_dueDate = new GUI::DateWidget(mainWidget); + l->setBuddy(m_dueDate); + topLayout->addWidget(m_dueDate, 3, 1); + // valid due dates will enable the calendar adding checkbox + connect(m_dueDate, SIGNAL(signalModified()), SLOT(slotDueDateChanged())); + whats = i18n("The due date is when the items are due to be returned. The due date " + "is not required, unless you want to add the loan to your active calendar."); + QWhatsThis::add(l, whats); + QWhatsThis::add(m_dueDate, whats); + + l = new QLabel(i18n("&Note:"), mainWidget); + topLayout->addWidget(l, 4, 0); + m_note = new KTextEdit(mainWidget); + l->setBuddy(m_note); + topLayout->addMultiCellWidget(m_note, 5, 5, 0, 1); + topLayout->setRowStretch(5, 1); + whats = i18n("You can add notes about the loan, as well."); + QWhatsThis::add(l, whats); + QWhatsThis::add(m_note, whats); + + m_addEvent = new QCheckBox(i18n("&Add a reminder to the active calendar"), mainWidget); + topLayout->addMultiCellWidget(m_addEvent, 6, 6, 0, 1); + m_addEvent->setEnabled(false); // gets enabled when valid due date is entered + QWhatsThis::add(m_addEvent, i18n("<qt>Checking this box will add a <em>To-do</em> item " + "to your active calendar, which can be viewed using KOrganizer. " + "The box is only active if you set a due date.")); + + resize(configDialogSize(QString::fromLatin1("Loan Dialog Options"))); + + KABC::AddressBook* abook = KABC::StdAddressBook::self(true); + connect(abook, SIGNAL(addressBookChanged(AddressBook*)), + SLOT(slotLoadAddressBook())); + connect(abook, SIGNAL(loadingFinished(Resource*)), + SLOT(slotLoadAddressBook())); + slotLoadAddressBook(); +} + +LoanDialog::~LoanDialog() { + saveDialogSize(QString::fromLatin1("Loan Dialog Options")); +} + +void LoanDialog::slotBorrowerNameChanged(const QString& str_) { + actionButton(Ok)->setEnabled(!str_.isEmpty()); +} + +void LoanDialog::slotDueDateChanged() { +#ifdef HAVE_KCAL + m_addEvent->setEnabled(m_dueDate->date().isValid()); +#endif +} + +void LoanDialog::slotGetBorrower() { + Data::BorrowerPtr borrower = BorrowerDialog::getBorrower(this); + if(borrower) { + m_borrowerEdit->setText(borrower->name()); + m_uid = borrower->uid(); + } +} + +void LoanDialog::slotLoadAddressBook() { + m_borrowerEdit->completionObject()->clear(); + + const KABC::AddressBook* const abook = KABC::StdAddressBook::self(true); + for(KABC::AddressBook::ConstIterator it = abook->begin(), end = abook->end(); + it != end; ++it) { + m_borrowerEdit->completionObject()->addItem((*it).realName()); + } + + // add current borrowers, too + QStringList items = m_borrowerEdit->completionObject()->items(); + Data::BorrowerVec borrowers = m_entries.begin()->collection()->borrowers(); + for(Data::BorrowerVec::ConstIterator it = borrowers.constBegin(), end = borrowers.constEnd(); + it != end; ++it) { + if(items.findIndex(it->name()) == -1) { + m_borrowerEdit->completionObject()->addItem(it->name()); + } + } +} + +KCommand* LoanDialog::createCommand() { + // first, check to see if the borrower is empty + QString name = m_borrowerEdit->text(); + if(name.isEmpty()) { + return 0; + } + + // ok, first handle creating new loans + if(m_mode == Add) { + return addLoansCommand(); + } else { + return modifyLoansCommand(); + } +} + +KCommand* LoanDialog::addLoansCommand() { + if(m_entries.isEmpty()) { + return 0; + } + + const QString name = m_borrowerEdit->text(); + + // see if there's a borrower with this name already + m_borrower = 0; + Data::BorrowerVec borrowers = m_entries.begin()->collection()->borrowers(); + for(Data::BorrowerVec::Iterator it = borrowers.begin(); it != borrowers.end(); ++it) { + if(it->name() == name) { + m_borrower = it; + break; + } + } + + if(!m_borrower) { + m_borrower = new Data::Borrower(name, m_uid); + } + + Data::LoanVec loans; + for(Data::EntryVecIt entry = m_entries.begin(); entry != m_entries.end(); ++entry) { + loans.append(new Data::Loan(entry, m_loanDate->date(), m_dueDate->date(), m_note->text())); + } + + return new Command::AddLoans(m_borrower, loans, m_addEvent->isChecked()); +} + +KCommand* LoanDialog::modifyLoansCommand() { + if(!m_loan) { + return 0; + } + + Data::LoanPtr newLoan = new Data::Loan(*m_loan); + newLoan->setDueDate(m_dueDate->date()); + newLoan->setNote(m_note->text()); + return new Command::ModifyLoans(m_loan, newLoan, m_addEvent->isChecked()); +} + +#include "loandialog.moc" diff --git a/src/loandialog.h b/src/loandialog.h new file mode 100644 index 0000000..b51abf0 --- /dev/null +++ b/src/loandialog.h @@ -0,0 +1,82 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef LOANDIALOG_H +#define LOANDIALOG_H + +class KLineEdit; +class KTextEdit; +class KCommand; +class QCheckBox; + +#include "datavectors.h" +#include "borrower.h" + +#include <kdialogbase.h> + +namespace Tellico { + namespace GUI { + class DateWidget; + } + +/** + * @author Robby Stephenson + */ +class LoanDialog : public KDialogBase { +Q_OBJECT + +public: + /** + * The constructor sets up the dialog. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + LoanDialog(const Data::EntryVec& entries, QWidget* parent, const char* name=0); + LoanDialog(Data::LoanPtr loan, QWidget* parent, const char* name=0); + virtual ~LoanDialog(); + + KCommand* createCommand(); + +private slots: + void slotBorrowerNameChanged(const QString& str); + void slotGetBorrower(); + void slotLoadAddressBook(); + void slotDueDateChanged(); + +private: + void init(); + KCommand* addLoansCommand(); + KCommand* modifyLoansCommand(); + + enum Mode { + Add, + Modify + }; + + const Mode m_mode; + Data::BorrowerPtr m_borrower; + Data::EntryVec m_entries; + Data::LoanPtr m_loan; + + KLineEdit* m_borrowerEdit; + GUI::DateWidget* m_loanDate; + GUI::DateWidget* m_dueDate; + KTextEdit* m_note; + QCheckBox* m_addEvent; + + QString m_uid; +}; + +} // end namespace +#endif diff --git a/src/loanitem.cpp b/src/loanitem.cpp new file mode 100644 index 0000000..744397d --- /dev/null +++ b/src/loanitem.cpp @@ -0,0 +1,26 @@ +/*************************************************************************** + copyright : (C) 2005-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 "loanitem.h" +#include "entry.h" +#include "tellico_kernel.h" + +using Tellico::LoanItem; + +LoanItem::LoanItem(GUI::CountedItem* parent_, Tellico::Data::LoanPtr loan_) + : Tellico::EntryItem(parent_, loan_->entry()), m_loan(loan_) { +} + +void LoanItem::doubleClicked() { + Kernel::self()->modifyLoan(m_loan); +} diff --git a/src/loanitem.h b/src/loanitem.h new file mode 100644 index 0000000..5e79139 --- /dev/null +++ b/src/loanitem.h @@ -0,0 +1,40 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_LOANITEM_H +#define TELLICO_LOANITEM_H + +#include "entryitem.h" +#include "borrower.h" + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class LoanItem : public Tellico::EntryItem { +public: + LoanItem(GUI::CountedItem* parent, Data::LoanPtr loan); + + virtual bool isLoanItem() const { return true; } + Data::LoanPtr loan() { return m_loan; } + + virtual void doubleClicked(); + +private: + Data::LoanPtr m_loan; +}; + +} + +#endif diff --git a/src/loanview.cpp b/src/loanview.cpp new file mode 100644 index 0000000..875f267 --- /dev/null +++ b/src/loanview.cpp @@ -0,0 +1,234 @@ +/*************************************************************************** + copyright : (C) 2005-2007 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 "loanview.h" +#include "loanitem.h" +#include "controller.h" +#include "borrower.h" +#include "entry.h" +#include "collection.h" +#include "tellico_kernel.h" +#include "tellico_debug.h" +#include "listviewcomparison.h" + +#include <klocale.h> +#include <kpopupmenu.h> +#include <kiconloader.h> + +#include <qheader.h> + +using Tellico::LoanView; + +LoanView::LoanView(QWidget* parent_, const char* name_) : GUI::ListView(parent_, name_), m_notSortedYet(true) { + addColumn(i18n("Borrower")); + header()->setStretchEnabled(true, 0); + setResizeMode(QListView::NoColumn); + setRootIsDecorated(true); + setShowSortIndicator(true); + setTreeStepSize(15); + setFullWidth(true); + + connect(this, SIGNAL(contextMenuRequested(QListViewItem*, const QPoint&, int)), + SLOT(contextMenuRequested(QListViewItem*, const QPoint&, int))); + + connect(this, SIGNAL(expanded(QListViewItem*)), + SLOT(slotExpanded(QListViewItem*))); + + connect(this, SIGNAL(collapsed(QListViewItem*)), + SLOT(slotCollapsed(QListViewItem*))); +} + +bool LoanView::isSelectable(GUI::ListViewItem* item_) const { + if(!GUI::ListView::isSelectable(item_)) { + return false; + } + + // because the popup menu has modify, only + // allow one loan item to get selected + if(item_->isLoanItem()) { + return selectedItems().isEmpty(); + } + + return true; +} + +void LoanView::contextMenuRequested(QListViewItem* item_, const QPoint& point_, int) { + if(!item_) { + return; + } + + GUI::ListViewItem* item = static_cast<GUI::ListViewItem*>(item_); + if(item->isLoanItem()) { + KPopupMenu menu(this); + menu.insertItem(SmallIconSet(QString::fromLatin1("2downarrow")), + i18n("Check-in"), this, SLOT(slotCheckIn())); + menu.insertItem(SmallIconSet(QString::fromLatin1("2downarrow")), + i18n("Modify Loan..."), this, SLOT(slotModifyLoan())); + menu.exec(point_); + } +} + +// this gets called when header() is clicked, so cycle through +void LoanView::setSorting(int col_, bool asc_) { + if(asc_ && !m_notSortedYet) { + if(sortStyle() == ListView::SortByText) { + setSortStyle(ListView::SortByCount); + } else { + setSortStyle(ListView::SortByText); + } + } + if(sortStyle() == ListView::SortByText) { + setColumnText(0, i18n("Borrower")); + } else { + setColumnText(0, i18n("Borrower (Sort by Count)")); + } + m_notSortedYet = false; + ListView::setSorting(col_, asc_); +} + +void LoanView::addCollection(Data::CollPtr coll_) { + Data::BorrowerVec borrowers = coll_->borrowers(); + for(Data::BorrowerVec::Iterator it = borrowers.begin(); it != borrowers.end(); ++it) { + addBorrower(it); + } + Data::FieldPtr f = coll_->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } +} + +void LoanView::addField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void LoanView::modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr) { + resetComparisons(); +} + +void LoanView::removeField(Data::CollPtr, Data::FieldPtr) { + resetComparisons(); +} + +void LoanView::addBorrower(Data::BorrowerPtr borrower_) { + if(!borrower_ || borrower_->isEmpty()) { + return; + } + + BorrowerItem* borrowerItem = new BorrowerItem(this, borrower_); + borrowerItem->setExpandable(!borrower_->loans().isEmpty()); + m_itemDict.insert(borrower_->name(), borrowerItem); +} + +void LoanView::modifyBorrower(Data::BorrowerPtr borrower_) { + if(!borrower_) { + return; + } + + BorrowerItem* borrowerItem = m_itemDict[borrower_->name()]; + if(!borrowerItem) { + myDebug() << "LoanView::modifyBorrower() - borrower was never added" << endl; + return; + } + + if(borrower_->isEmpty()) { + m_itemDict.remove(borrower_->name()); + delete borrowerItem; + return; + } + + bool open = borrowerItem->isOpen(); + borrowerItem->setOpen(false); + borrowerItem->setOpen(open); +} + +void LoanView::slotCollapsed(QListViewItem* item_) { + // only change icon for group items + if(static_cast<GUI::ListViewItem*>(item_)->isBorrowerItem()) { + static_cast<GUI::ListViewItem*>(item_)->clear(); + } +} + +void LoanView::slotExpanded(QListViewItem* item_) { + // only change icon for group items + if(!static_cast<GUI::ListViewItem*>(item_)->isBorrowerItem()) { + kdWarning() << "GroupView::slotExpanded() - non entry group item - " << item_->text(0) << endl; + return; + } + + setUpdatesEnabled(false); + + BorrowerItem* item = static_cast<BorrowerItem*>(item_); + Data::LoanVec loans = item->borrower()->loans(); + for(Data::LoanVec::Iterator it = loans.begin(); it != loans.end(); ++it) { + new LoanItem(item, it); + } + + setUpdatesEnabled(true); + triggerUpdate(); +} + +void LoanView::slotCheckIn() { + GUI::ListViewItem* item = selectedItems().getFirst(); + if(!item || !item->isLoanItem()) { + return; + } + + Data::EntryVec entries; + // need a copy since we may be deleting + GUI::ListViewItemList list = selectedItems(); + for(GUI::ListViewItemListIt it(list); it.current(); ++it) { + Data::EntryPtr entry = static_cast<LoanItem*>(it.current())->entry(); + if(!entry) { + myDebug() << "LoanView::slotCheckIn() - no entry!" << endl; + continue; + } + entries.append(entry); + } + + Controller::self()->slotCheckIn(entries); + Controller::self()->slotClearSelection(); // so the checkout menu item gets disabled +} + +void LoanView::slotModifyLoan() { + GUI::ListViewItem* item = selectedItems().getFirst(); + if(!item || !item->isLoanItem()) { + return; + } + + Kernel::self()->modifyLoan(static_cast<LoanItem*>(item)->loan()); +} + +void LoanView::resetComparisons() { + // this is only allowed when the view is not empty, so we can grab a collection ptr + if(childCount() == 0) { + return; + } + Data::EntryVec entries = static_cast<BorrowerItem*>(firstChild())->entries(); + if(entries.isEmpty()) { + return; + } + Data::EntryPtr entry = entries[0]; + if(!entry) { + return; + } + Data::CollPtr coll = entry->collection(); + if(!coll) { + return; + } + Data::FieldPtr f = coll->fieldByName(QString::fromLatin1("title")); + if(f) { + setComparison(0, ListViewComparison::create(f)); + } +} + +#include "loanview.moc" diff --git a/src/loanview.h b/src/loanview.h new file mode 100644 index 0000000..59e4df4 --- /dev/null +++ b/src/loanview.h @@ -0,0 +1,72 @@ +/*************************************************************************** + copyright : (C) 2005-2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_LOANVIEW_H +#define TELLICO_LOANVIEW_H + +#include "gui/listview.h" +#include "observer.h" +#include "borroweritem.h" + +#include <qdict.h> + +namespace Tellico { + namespace Data { + class Borrower; + } + +/** + * @author Robby Stephenson + */ +class LoanView : public GUI::ListView, public Observer { +Q_OBJECT + +public: + LoanView(QWidget* parent, const char* name=0); + + virtual bool isSelectable(GUI::ListViewItem*) const; + + virtual void addCollection(Data::CollPtr coll); + + virtual void addField(Data::CollPtr, Data::FieldPtr); + virtual void modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr); + virtual void removeField(Data::CollPtr, Data::FieldPtr); + + virtual void addBorrower(Data::BorrowerPtr); + virtual void modifyBorrower(Data::BorrowerPtr); + +private slots: + /** + * Handles the appearance of the popup menu. + * + * @param item A pointer to the item underneath the mouse + * @param point The location point + * @param col The column number, not currently used + */ + void contextMenuRequested(QListViewItem* item, const QPoint& point, int col); + void slotExpanded(QListViewItem* item); + void slotCollapsed(QListViewItem* item); + void slotCheckIn(); + void slotModifyLoan(); + +private: + virtual void setSorting(int column, bool ascending = true); + void resetComparisons(); + + bool m_notSortedYet; + QDict<BorrowerItem> m_itemDict; +}; + +} // end namespace + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f0b57c4 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,88 @@ +/*************************************************************************** + 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 "mainwindow.h" +#include "translators/translators.h" // needed for file type enum + +#include <kapplication.h> +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <klocale.h> + +namespace { + static const char* description = I18N_NOOP("Tellico - a collection manager for KDE"); + static const char* version = VERSION; + + static KCmdLineOptions options[] = { + { "nofile", I18N_NOOP("Do not reopen the last open file"), 0 }, + { "bibtex", I18N_NOOP("Import <filename> as a bibtex file"), 0 }, + { "mods", I18N_NOOP("Import <filename> as a MODS file"), 0 }, + { "ris", I18N_NOOP("Import <filename> as a RIS file"), 0 }, + { "+[filename]", I18N_NOOP("File to open"), 0 }, + KCmdLineLastOption + }; +} + +int main(int argc, char* argv[]) { + KAboutData aboutData("tellico", "Tellico", + version, description, KAboutData::License_GPL, + "(c) 2001-2007, Robby Stephenson", 0, + "http://www.periapsis.org/tellico/", "tellico-users@forge.novell.com"); + aboutData.addAuthor("Robby Stephenson", 0, "robby@periapsis.org"); + aboutData.addCredit("Mathias Monnerville", I18N_NOOP("Data source scripts"), + 0, 0); + aboutData.addCredit("Virginie Quesnay", I18N_NOOP("Icons"), + 0, 0); + aboutData.addCredit("Greg Ward", I18N_NOOP("Author of btparse library"), + 0, "http://www.gerg.ca"); + aboutData.addCredit("Amarok", I18N_NOOP("Code examples and general inspiration"), + 0, "http://amarok.kde.org"); + aboutData.addCredit("Robert Gamble", I18N_NOOP("Author of libcsv library"), + 0, 0); + aboutData.addCredit("Valentin Lavrinenko", I18N_NOOP("Author of rtf2html library"), + 0, 0); + + KCmdLineArgs::init(argc, argv, &aboutData); + KCmdLineArgs::addCmdLineOptions(options); + + KApplication app; + + if(app.isRestored()) { + RESTORE(Tellico::MainWindow); + } else { + Tellico::MainWindow* tellico = new Tellico::MainWindow(); + tellico->show(); + tellico->slotShowTipOfDay(false); + + KCmdLineArgs* args = KCmdLineArgs::parsedArgs(); + if(args->count() > 0) { + if(args->isSet("bibtex")) { + tellico->importFile(Tellico::Import::Bibtex, args->url(0), Tellico::Import::Replace); + } else if(args->isSet("mods")) { + tellico->importFile(Tellico::Import::MODS, args->url(0), Tellico::Import::Replace); + } else if(args->isSet("ris")) { + tellico->importFile(Tellico::Import::RIS, args->url(0), Tellico::Import::Replace); + } else { + tellico->slotFileOpen(args->url(0)); + } + } else { + // bit of a hack, I just want the --nofile option + // if --nofile is NOT passed, then the file option is set + // is it's set, then go ahead and check for opening previous file + tellico->initFileOpen(!args->isSet("file")); + } + args->clear(); // some memory savings + } + + return app.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..bb5db7a --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,2391 @@ +/*************************************************************************** + 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 "mainwindow.h" +#include "tellico_kernel.h" +#include "document.h" +#include "detailedlistview.h" +#include "entryeditdialog.h" +#include "groupview.h" +#include "viewstack.h" +#include "collection.h" +#include "entry.h" +#include "configdialog.h" +#include "entryitem.h" +#include "filter.h" +#include "filterdialog.h" +#include "collectionfieldsdialog.h" +#include "controller.h" +#include "importdialog.h" +#include "exportdialog.h" +#include "filehandler.h" // needed so static mainWindow variable can be set +#include "gui/stringmapdialog.h" +#include "translators/htmlexporter.h" // for printing +#include "entryview.h" +#include "entryiconview.h" +#include "imagefactory.h" // needed so tmp files can get cleaned +#include "collections/bibtexcollection.h" // needed for bibtex string macro dialog +#include "translators/bibtexhandler.h" // needed for bibtex options +#include "fetchdialog.h" +#include "reportdialog.h" +#include "tellico_strings.h" +#include "filterview.h" +#include "loanview.h" +#include "gui/tabcontrol.h" +#include "gui/lineedit.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "entryiconfactory.h" +#include "statusbar.h" +#include "fetch/fetchmanager.h" +#include "cite/actionmanager.h" +#include "core/tellico_config.h" +#include "core/drophandler.h" +#include "latin1literal.h" + +#include <kapplication.h> +#include <kcombobox.h> +#include <kiconloader.h> +#include <kfiledialog.h> +#include <kmenubar.h> +#include <ktoolbar.h> +#include <klocale.h> +#include <kconfig.h> +#include <kstdaction.h> +#include <kwin.h> +#include <kprogress.h> +#include <kprinter.h> +#include <khtmlview.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kmessagebox.h> +#include <ktip.h> +#include <krecentdocument.h> +#include <kedittoolbar.h> +#include <kkeydialog.h> +#include <kio/netaccess.h> +#include <dcopclient.h> +#include <kaction.h> + +#include <qsplitter.h> +//#include <qpainter.h> +#include <qsignalmapper.h> +#include <qtimer.h> +#include <qmetaobject.h> // needed for copy, cut, paste slots +#include <qwhatsthis.h> +#include <qvbox.h> + +// the null string and bool are dummy arguments +#define MIME_ICON(s) \ + KMimeType::mimeType(QString::fromLatin1(s))->icon(QString::null, false) + +namespace { + static const int MAIN_WINDOW_MIN_WIDTH = 600; + //static const int PRINTED_PAGE_OVERLAP = 0; + static const int MAX_IMAGES_WARN_PERFORMANCE = 200; +} + +using Tellico::MainWindow; + +MainWindow::MainWindow(QWidget* parent_/*=0*/, const char* name_/*=0*/) : KMainWindow(parent_, name_), + ApplicationInterface(), + m_updateAll(0), + m_statusBar(0), + m_editDialog(0), + m_groupView(0), + m_filterView(0), + m_loanView(0), + m_configDlg(0), + m_filterDlg(0), + m_collFieldsDlg(0), + m_stringMacroDlg(0), + m_fetchDlg(0), + m_reportDlg(0), + m_queuedFilters(0), + m_initialized(false), + m_newDocument(true) { + + if(!kapp->dcopClient()->isRegistered()) { + kapp->dcopClient()->registerAs("tellico"); + kapp->dcopClient()->setDefaultObject(objId()); + } + + m_fetchActions.setAutoDelete(true); // these are the fetcher actions + + Controller::init(this); // the only time this is ever called! + // has to be after controller init + Kernel::init(this); // the only time this is ever called! + + setIcon(DesktopIcon(QString::fromLatin1("tellico"))); + + // initialize the status bar and progress bar + initStatusBar(); + + // create a document, which also creates an empty book collection + // must be done before the different widgets are created + initDocument(); + + // set up all the actions, some connect to the document, so this must be after initDocument() + initActions(); + + // create the different widgets in the view, some widgets connect to actions, so must be after initActions() + initView(); + + // The edit dialog is not created until after the main window is initialized, so it can be a child. + // So don't make any connections, don't read options for it until initFileOpen + readOptions(); + + setAcceptDrops(true); + DropHandler* drophandler = new DropHandler(this); + installEventFilter(drophandler); + + MARK_LINE; + QTimer::singleShot(0, this, SLOT(slotInit())); +} + +void MainWindow::slotInit() { + MARK; + if(m_editDialog) { + return; + } + m_editDialog = new EntryEditDialog(this, "editdialog"); + Controller::self()->addObserver(m_editDialog); + + m_toggleEntryEditor->setChecked(Config::showEditWidget()); + slotToggleEntryEditor(); + + initConnections(); + ImageFactory::init(); + + // disable OOO menu item if library is not available + action("cite_openoffice")->setEnabled(Cite::ActionManager::isEnabled(Cite::CiteOpenOffice)); +} + +void MainWindow::initStatusBar() { + MARK; + m_statusBar = new Tellico::StatusBar(this); +} + +void MainWindow::initActions() { + MARK; + /************************************************* + * File->New menu + *************************************************/ + QSignalMapper* collectionMapper = new QSignalMapper(this); + connect(collectionMapper, SIGNAL(mapped(int)), + this, SLOT(slotFileNew(int))); + + KActionMenu* fileNewMenu = new KActionMenu(actionCollection(), "file_new_collection"); + fileNewMenu->setText(i18n("New")); +// fileNewMenu->setIconSet(BarIconSet(QString::fromLatin1("filenew"))); // doesn't work + fileNewMenu->setIconSet(BarIcon(QString::fromLatin1("filenew"))); + fileNewMenu->setToolTip(i18n("Create a new collection")); + fileNewMenu->setDelayed(false); + + KAction* action = new KAction(actionCollection(), "new_book_collection"); + action->setText(i18n("New &Book Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("book"))); + action->setToolTip(i18n("Create a new book collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Book); + + action = new KAction(actionCollection(), "new_bibtex_collection"); + action->setText(i18n("New B&ibliography")); + action->setIconSet(UserIconSet(QString::fromLatin1("bibtex"))); + action->setToolTip(i18n("Create a new bibtex bibliography")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Bibtex); + + action = new KAction(actionCollection(), "new_comic_book_collection"); + action->setText(i18n("New &Comic Book Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("comic"))); + action->setToolTip(i18n("Create a new comic book collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::ComicBook); + + action = new KAction(actionCollection(), "new_video_collection"); + action->setText(i18n("New &Video Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("video"))); + action->setToolTip(i18n("Create a new video collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Video); + + action = new KAction(actionCollection(), "new_music_collection"); + action->setText(i18n("New &Music Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("album"))); + action->setToolTip(i18n("Create a new music collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Album); + + action = new KAction(actionCollection(), "new_coin_collection"); + action->setText(i18n("New C&oin Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("coin"))); + action->setToolTip(i18n("Create a new coin collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Coin); + + action = new KAction(actionCollection(), "new_stamp_collection"); + action->setText(i18n("New &Stamp Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("stamp"))); + action->setToolTip(i18n("Create a new stamp collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Stamp); + + action = new KAction(actionCollection(), "new_card_collection"); + action->setText(i18n("New C&ard Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("card"))); + action->setToolTip(i18n("Create a new trading card collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Card); + + action = new KAction(actionCollection(), "new_wine_collection"); + action->setText(i18n("New &Wine Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("wine"))); + action->setToolTip(i18n("Create a new wine collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Wine); + + action = new KAction(actionCollection(), "new_game_collection"); + action->setText(i18n("New &Game Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("game"))); + action->setToolTip(i18n("Create a new game collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + + collectionMapper->setMapping(action, Data::Collection::Game); + action = new KAction(actionCollection(), "new_boardgame_collection"); + action->setText(i18n("New Boa&rd Game Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("boardgame"))); + action->setToolTip(i18n("Create a new board game collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::BoardGame); + + action = new KAction(actionCollection(), "new_file_catalog"); + action->setText(i18n("New &File Catalog")); + action->setIconSet(UserIconSet(QString::fromLatin1("file"))); + action->setToolTip(i18n("Create a new file catalog")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::File); + + action = new KAction(actionCollection(), "new_custom_collection"); + action->setText(i18n("New C&ustom Collection")); + action->setIconSet(UserIconSet(QString::fromLatin1("filenew"))); + action->setToolTip(i18n("Create a new custom collection")); + fileNewMenu->insert(action); + connect(action, SIGNAL(activated()), collectionMapper, SLOT(map())); + collectionMapper->setMapping(action, Data::Collection::Base); + + /************************************************* + * File menu + *************************************************/ + action = KStdAction::open(this, SLOT(slotFileOpen()), actionCollection()); + action->setToolTip(i18n("Open an existing document")); + m_fileOpenRecent = KStdAction::openRecent(this, SLOT(slotFileOpenRecent(const KURL&)), actionCollection()); + m_fileOpenRecent->setToolTip(i18n("Open a recently used file")); + m_fileSave = KStdAction::save(this, SLOT(slotFileSave()), actionCollection()); + m_fileSave->setToolTip(i18n("Save the document")); + action = KStdAction::saveAs(this, SLOT(slotFileSaveAs()), actionCollection()); + action->setToolTip(i18n("Save the document as a different file...")); + action = KStdAction::print(this, SLOT(slotFilePrint()), actionCollection()); + action->setToolTip(i18n("Print the contents of the document...")); + action = KStdAction::quit(this, SLOT(slotFileQuit()), actionCollection()); + action->setToolTip(i18n("Quit the application")); + +/**************** Import Menu ***************************/ + + QSignalMapper* importMapper = new QSignalMapper(this); + connect(importMapper, SIGNAL(mapped(int)), + this, SLOT(slotFileImport(int))); + + KActionMenu* importMenu = new KActionMenu(actionCollection(), "file_import"); + importMenu->setText(i18n("&Import")); + importMenu->setIconSet(BarIconSet(QString::fromLatin1("fileimport"))); + importMenu->setToolTip(i18n("Import collection data from other formats")); + importMenu->setDelayed(false); + + action = new KAction(actionCollection(), "file_import_tellico"); + action->setText(i18n("Import Tellico Data...")); + action->setToolTip(i18n("Import another Tellico data file")); + action->setIcon(QString::fromLatin1("tellico")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::TellicoXML); + + action = new KAction(actionCollection(), "file_import_csv"); + action->setText(i18n("Import CSV Data...")); + action->setToolTip(i18n("Import a CSV file")); + action->setIcon(MIME_ICON("text/x-csv")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::CSV); + + action = new KAction(actionCollection(), "file_import_mods"); + action->setText(i18n("Import MODS Data...")); + action->setToolTip(i18n("Import a MODS data file")); + action->setIcon(MIME_ICON("text/xml")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::MODS); + + action = new KAction(actionCollection(), "file_import_alexandria"); + action->setText(i18n("Import Alexandria Data...")); + action->setToolTip(i18n("Import data from the Alexandria book collection manager")); + action->setIcon(QString::fromLatin1("alexandria")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Alexandria); + + action = new KAction(actionCollection(), "file_import_delicious"); + action->setText(i18n("Import Delicious Library Data...")); + action->setToolTip(i18n("Import data from Delicious Library")); + action->setIcon(MIME_ICON("text/xml")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Delicious); + + action = new KAction(actionCollection(), "file_import_referencer"); + action->setText(i18n("Import Referencer Data...")); + action->setToolTip(i18n("Import data from Referencer")); + action->setIcon(QString::fromLatin1("referencer")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Referencer); + + action = new KAction(actionCollection(), "file_import_bibtex"); + action->setText(i18n("Import Bibtex Data...")); + action->setToolTip(i18n("Import a bibtex bibliography file")); + action->setIcon(MIME_ICON("text/x-bibtex")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Bibtex); + + action = new KAction(actionCollection(), "file_import_bibtexml"); + action->setText(i18n("Import Bibtexml Data...")); + action->setToolTip(i18n("Import a Bibtexml bibliography file")); + action->setIcon(MIME_ICON("text/xml")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Bibtexml); + + action = new KAction(actionCollection(), "file_import_ris"); + action->setText(i18n("Import RIS Data...")); + action->setToolTip(i18n("Import an RIS reference file")); + action->setIcon(MIME_ICON("application/x-research-info-systems")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::RIS); + + action = new KAction(actionCollection(), "file_import_pdf"); + action->setText(i18n("Import PDF File...")); + action->setToolTip(i18n("Import a PDF file")); + action->setIcon(MIME_ICON("application/pdf")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::PDF); + + action = new KAction(actionCollection(), "file_import_audiofile"); + action->setText(i18n("Import Audio File Metadata...")); + action->setToolTip(i18n("Import meta-data from audio files")); + action->setIcon(MIME_ICON("audio/x-mp3")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::AudioFile); +#ifndef HAVE_TAGLIB + action->setEnabled(false); +#endif + + action = new KAction(actionCollection(), "file_import_freedb"); + action->setText(i18n("Import Audio CD Data...")); + action->setToolTip(i18n("Import audio CD information")); + action->setIcon(MIME_ICON("media/audiocd")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::FreeDB); +#ifndef HAVE_KCDDB + action->setEnabled(false); +#endif + + action = new KAction(actionCollection(), "file_import_gcfilms"); + action->setText(i18n("Import GCstar Data...")); + action->setToolTip(i18n("Import a GCstar data file")); + action->setIcon(QString::fromLatin1("gcstar")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::GCfilms); + + action = new KAction(actionCollection(), "file_import_griffith"); + action->setText(i18n("Import Griffith Data...")); + action->setToolTip(i18n("Import a Griffith database")); + action->setIcon(QString::fromLatin1("griffith")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::Griffith); + + action = new KAction(actionCollection(), "file_import_amc"); + action->setText(i18n("Import Ant Movie Catalog Data...")); + action->setToolTip(i18n("Import an Ant Movie Catalog data file")); + action->setIcon(MIME_ICON("application/x-crossover-amc")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::AMC); + + action = new KAction(actionCollection(), "file_import_filelisting"); + action->setText(i18n("Import File Listing...")); + action->setToolTip(i18n("Import information about files in a folder")); + action->setIcon(MIME_ICON("inode/directory")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::FileListing); + + action = new KAction(actionCollection(), "file_import_xslt"); + action->setText(i18n("Import XSL Transform...")); + action->setToolTip(i18n("Import using an XSL Transform")); + action->setIcon(MIME_ICON("text/x-xslt")); + importMenu->insert(action); + connect(action, SIGNAL(activated()), importMapper, SLOT(map())); + importMapper->setMapping(action, Import::XSLT); + +/**************** Export Menu ***************************/ + + QSignalMapper* exportMapper = new QSignalMapper(this); + connect(exportMapper, SIGNAL(mapped(int)), + this, SLOT(slotFileExport(int))); + + KActionMenu* exportMenu = new KActionMenu(actionCollection(), "file_export"); + exportMenu->setText(i18n("&Export")); + exportMenu->setIconSet(BarIconSet(QString::fromLatin1("fileexport"))); + exportMenu->setToolTip(i18n("Export the collection data to other formats")); + exportMenu->setDelayed(false); + + action = new KAction(actionCollection(), "file_export_xml"); + action->setText(i18n("Export to XML...")); + action->setToolTip(i18n("Export to a Tellico XML file")); + action->setIcon(QString::fromLatin1("tellico")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::TellicoXML); + + action = new KAction(actionCollection(), "file_export_zip"); + action->setText(i18n("Export to Zip...")); + action->setToolTip(i18n("Export to a Tellico Zip file")); + action->setIcon(QString::fromLatin1("tellico")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::TellicoZip); + + action = new KAction(actionCollection(), "file_export_html"); + action->setText(i18n("Export to HTML...")); + action->setToolTip(i18n("Export to an HTML file")); + action->setIcon(MIME_ICON("text/html")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::HTML); + + action = new KAction(actionCollection(), "file_export_csv"); + action->setText(i18n("Export to CSV...")); + action->setToolTip(i18n("Export to a comma-separated values file")); + action->setIcon(MIME_ICON("text/x-csv")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::CSV); + + action = new KAction(actionCollection(), "file_export_pilotdb"); + action->setText(i18n("Export to PilotDB...")); + action->setToolTip(i18n("Export to a PilotDB database")); + action->setIcon(MIME_ICON("application/vnd.palm")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::PilotDB); + + action = new KAction(actionCollection(), "file_export_alexandria"); + action->setText(i18n("Export to Alexandria...")); + action->setToolTip(i18n("Export to an Alexandria library")); + action->setIcon(QString::fromLatin1("alexandria")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::Alexandria); + + action = new KAction(actionCollection(), "file_export_bibtex"); + action->setText(i18n("Export to Bibtex...")); + action->setToolTip(i18n("Export to a bibtex file")); + action->setIcon(MIME_ICON("text/x-bibtex")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::Bibtex); + + action = new KAction(actionCollection(), "file_export_bibtexml"); + action->setText(i18n("Export to Bibtexml...")); + action->setToolTip(i18n("Export to a Bibtexml file")); + action->setIcon(MIME_ICON("text/xml")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::Bibtexml); + + action = new KAction(actionCollection(), "file_export_onix"); + action->setText(i18n("Export to ONIX...")); + action->setToolTip(i18n("Export to an ONIX file")); + action->setIcon(MIME_ICON("text/xml")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::ONIX); + + action = new KAction(actionCollection(), "file_export_gcfilms"); + action->setText(i18n("Export to GCfilms...")); + action->setToolTip(i18n("Export to a GCfilms data file")); + action->setIcon(QString::fromLatin1("gcstar")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::GCfilms); + +#if 0 + QString dummy1 = i18n("Export to GCstar..."); + QString dummy2 = i18n("Export to a GCstar data file"); +#endif + + action = new KAction(actionCollection(), "file_export_xslt"); + action->setText(i18n("Export XSL Transform...")); + action->setToolTip(i18n("Export using an XSL Transform")); + action->setIcon(MIME_ICON("text/x-xslt")); + exportMenu->insert(action); + connect(action, SIGNAL(activated()), exportMapper, SLOT(map())); + exportMapper->setMapping(action, Export::XSLT); + + /************************************************* + * Edit menu + *************************************************/ + action = KStdAction::cut(this, SLOT(slotEditCut()), actionCollection()); + action->setToolTip(i18n("Cut the selected text and puts it in the clipboard")); + action = KStdAction::copy(this, SLOT(slotEditCopy()), actionCollection()); + action->setToolTip(i18n("Copy the selected text to the clipboard")); + action = KStdAction::paste(this, SLOT(slotEditPaste()), actionCollection()); + action->setToolTip(i18n("Paste the clipboard contents")); + action = KStdAction::selectAll(this, SLOT(slotEditSelectAll()), actionCollection()); + action->setToolTip(i18n("Select all the entries in the collection")); + action = KStdAction::deselect(this, SLOT(slotEditDeselect()), actionCollection()); + action->setToolTip(i18n("Deselect all the entries in the collection")); + + action = new KAction(i18n("Internet Search..."), QString::fromLatin1("wizard"), CTRL + Key_M, + this, SLOT(slotShowFetchDialog()), + actionCollection(), "edit_search_internet"); + action->setToolTip(i18n("Search the internet...")); + + action = new KAction(i18n("Advanced &Filter..."), QString::fromLatin1("filter"), CTRL + Key_J, + this, SLOT(slotShowFilterDialog()), + actionCollection(), "filter_dialog"); + action->setToolTip(i18n("Filter the collection")); + + /************************************************* + * Collection menu + *************************************************/ + m_newEntry = new KAction(i18n("&New Entry..."), QString::fromLatin1("filenew"), CTRL + Key_N, + this, SLOT(slotNewEntry()), + actionCollection(), "coll_new_entry"); + m_newEntry->setToolTip(i18n("Create a new entry")); + m_editEntry = new KAction(i18n("&Edit Entry..."), QString::fromLatin1("edit"), CTRL + Key_E, + this, SLOT(slotShowEntryEditor()), + actionCollection(), "coll_edit_entry"); + m_editEntry->setToolTip(i18n("Edit the selected entries")); + m_copyEntry = new KAction(i18n("D&uplicate Entry"), QString::fromLatin1("editcopy"), CTRL + Key_Y, + Controller::self(), SLOT(slotCopySelectedEntries()), + actionCollection(), "coll_copy_entry"); + m_copyEntry->setToolTip(i18n("Copy the selected entries")); + m_deleteEntry = new KAction(i18n("&Delete Entry"), QString::fromLatin1("editdelete"), CTRL + Key_D, + Controller::self(), SLOT(slotDeleteSelectedEntries()), + actionCollection(), "coll_delete_entry"); + m_deleteEntry->setToolTip(i18n("Delete the selected entries")); + m_mergeEntry = new KAction(i18n("&Merge Entries"), QString::fromLatin1("editcopy"), CTRL + Key_G, + Controller::self(), SLOT(slotMergeSelectedEntries()), + actionCollection(), "coll_merge_entry"); + m_mergeEntry->setToolTip(i18n("Merge the selected entries")); + m_mergeEntry->setEnabled(false); // gets enabled when more than 1 entry is selected + + action = new KAction(i18n("&Generate Reports..."), QString::fromLatin1("document"), 0, this, + SLOT(slotShowReportDialog()), + actionCollection(), "coll_reports"); + action->setToolTip(i18n("Generate collection reports")); + m_checkOutEntry = new KAction(i18n("Check-&out..."), QString::fromLatin1("2uparrow"), 0, + Controller::self(), SLOT(slotCheckOut()), + actionCollection(), "coll_checkout"); + m_checkOutEntry->setToolTip(i18n("Check-out the selected items")); + m_checkInEntry = new KAction(i18n("Check-&in"), QString::fromLatin1("2downarrow"), 0, + Controller::self(), SLOT(slotCheckIn()), + actionCollection(), "coll_checkin"); + m_checkInEntry->setToolTip(i18n("Check-in the selected items")); + + action = new KAction(i18n("&Rename Collection..."), QString::fromLatin1("editclear"), CTRL + Key_R, + this, SLOT(slotRenameCollection()), + actionCollection(), "coll_rename_collection"); + action->setToolTip(i18n("Rename the collection")); + action = new KAction(i18n("Collection &Fields..."), QString::fromLatin1("edit"), CTRL + Key_U, + this, SLOT(slotShowCollectionFieldsDialog()), + actionCollection(), "coll_fields"); + action->setToolTip(i18n("Modify the collection fields")); + action = new KAction(i18n("Convert to &Bibliography"), 0, + this, SLOT(slotConvertToBibliography()), + actionCollection(), "coll_convert_bibliography"); + action->setToolTip(i18n("Convert a book collection to a bibliography")); + action->setIconSet(UserIconSet(QString::fromLatin1("bibtex"))); + action = new KAction(i18n("String &Macros..."), QString::fromLatin1("view_text"), 0, + this, SLOT(slotShowStringMacroDialog()), + actionCollection(), "coll_string_macros"); + action->setToolTip(i18n("Edit the bibtex string macros")); + + QSignalMapper* citeMapper = new QSignalMapper(this); + connect(citeMapper, SIGNAL(mapped(int)), + this, SLOT(slotCiteEntry(int))); + + action = new KAction(actionCollection(), "cite_clipboard"); + action->setText(i18n("Copy Bibtex to Cli&pboard")); + action->setToolTip(i18n("Copy bibtex citations to the clipboard")); + action->setIcon(QString::fromLatin1("editpaste")); + connect(action, SIGNAL(activated()), citeMapper, SLOT(map())); + citeMapper->setMapping(action, Cite::CiteClipboard); + + action = new KAction(actionCollection(), "cite_lyxpipe"); + action->setText(i18n("Cite Entry in &LyX")); + action->setToolTip(i18n("Cite the selected entries in LyX")); + action->setIcon(QString::fromLatin1("lyx")); + connect(action, SIGNAL(activated()), citeMapper, SLOT(map())); + citeMapper->setMapping(action, Cite::CiteLyxpipe); + + action = new KAction(actionCollection(), "cite_openoffice"); + action->setText(i18n("Ci&te Entry in OpenOffice.org")); + action->setToolTip(i18n("Cite the selected entries in OpenOffice.org")); + action->setIcon(QString::fromLatin1("ooo-writer")); + connect(action, SIGNAL(activated()), citeMapper, SLOT(map())); + citeMapper->setMapping(action, Cite::CiteOpenOffice); + + QSignalMapper* updateMapper = new QSignalMapper(this, "update_mapper"); + connect(updateMapper, SIGNAL(mapped(const QString&)), + Controller::self(), SLOT(slotUpdateSelectedEntries(const QString&))); + + m_updateEntryMenu = new KActionMenu(i18n("&Update Entry"), actionCollection(), "coll_update_entry"); +// m_updateEntryMenu->setIconSet(BarIconSet(QString::fromLatin1("fileexport"))); + m_updateEntryMenu->setDelayed(false); + + m_updateAll = new KAction(actionCollection(), "update_entry_all"); + m_updateAll->setText(i18n("All Sources")); + m_updateAll->setToolTip(i18n("Update entry data from all available sources")); +// m_updateEntryMenu->insert(action); + connect(m_updateAll, SIGNAL(activated()), updateMapper, SLOT(map())); + updateMapper->setMapping(m_updateAll, QString::fromLatin1("_all")); + + /************************************************* + * Settings menu + *************************************************/ + setStandardToolBarMenuEnabled(true); + createStandardStatusBarAction(); + KStdAction::configureToolbars(this, SLOT(slotConfigToolbar()), actionCollection()); + KStdAction::keyBindings(this, SLOT(slotConfigKeys()), actionCollection()); + m_toggleGroupWidget = new KToggleAction(i18n("Show Grou&p View"), 0, + this, SLOT(slotToggleGroupWidget()), + actionCollection(), "toggle_group_widget"); + m_toggleGroupWidget->setToolTip(i18n("Enable/disable the group view")); + m_toggleGroupWidget->setCheckedState(i18n("Hide Grou&p View")); + + m_toggleEntryEditor = new KToggleAction(i18n("Show Entry &Editor"), 0, + this, SLOT(slotToggleEntryEditor()), + actionCollection(), "toggle_edit_widget"); + m_toggleEntryEditor->setToolTip(i18n("Enable/disable the editor")); + m_toggleEntryEditor->setCheckedState(i18n("Hide Entry &Editor")); + + m_toggleEntryView = new KToggleAction(i18n("Show Entry &View"), 0, + this, SLOT(slotToggleEntryView()), + actionCollection(), "toggle_entry_view"); + m_toggleEntryView->setToolTip(i18n("Enable/disable the entry view")); + m_toggleEntryView->setCheckedState(i18n("Hide Entry &View")); + + KStdAction::preferences(this, SLOT(slotShowConfigDialog()), actionCollection()); + + /************************************************* + * Help menu + *************************************************/ + KStdAction::tipOfDay(this, SLOT(slotShowTipOfDay()), actionCollection(), "tipOfDay"); + + /************************************************* + * Collection Toolbar + *************************************************/ + (void) new KAction(i18n("Change Grouping"), CTRL + Key_G, + this, SLOT(slotGroupLabelActivated()), + actionCollection(), "change_entry_grouping_accel"); + + m_entryGrouping = new KSelectAction(i18n("&Group Selection"), 0, this, + SLOT(slotChangeGrouping()), + actionCollection(), "change_entry_grouping"); + m_entryGrouping->setToolTip(i18n("Change the grouping of the collection")); + + (void) new KAction(i18n("Filter"), CTRL + Key_F, + this, SLOT(slotFilterLabelActivated()), + actionCollection(), "quick_filter_accel"); + (void) new KAction(i18n("Clear Filter"), QString::fromLatin1("locationbar_erase"), 0, + this, SLOT(slotClearFilter()), + actionCollection(), "quick_filter_clear"); + + m_quickFilter = new GUI::LineEdit(); + m_quickFilter->setHint(i18n("Filter here...")); // same text as kdepim and amarok + // about 10 characters wide + m_quickFilter->setFixedWidth(m_quickFilter->fontMetrics().maxWidth()*10); + // want to update every time the filter text changes + connect(m_quickFilter, SIGNAL(textChanged(const QString&)), + this, SLOT(slotQueueFilter())); + + KWidgetAction* wAction = new KWidgetAction(m_quickFilter, i18n("Filter"), 0, 0, 0, + actionCollection(), "quick_filter"); + wAction->setToolTip(i18n("Filter the collection")); + wAction->setShortcutConfigurable(false); + wAction->setAutoSized(true); + + // show tool tips in status bar + actionCollection()->setHighlightingEnabled(true); + connect(actionCollection(), SIGNAL(actionStatusText(const QString &)), + SLOT(slotStatusMsg(const QString &))); + connect(actionCollection(), SIGNAL(clearStatusText()), + SLOT(slotClearStatus())); + +#ifdef UIFILE + kdWarning() << "MainWindow::initActions() - change createGUI() call!" << endl; + createGUI(UIFILE, false); +#else + createGUI(QString::null, false); +#endif +} + +void MainWindow::initDocument() { + MARK; + Data::Document* doc = Data::Document::self(); + + KConfigGroup config(KGlobal::config(), "General Options"); + doc->setLoadAllImages(config.readBoolEntry("Load All Images", false)); + + // allow status messages from the document + connect(doc, SIGNAL(signalStatusMsg(const QString&)), + SLOT(slotStatusMsg(const QString&))); + + // do stuff that changes when the doc is modified + connect(doc, SIGNAL(signalModified(bool)), + SLOT(slotEnableModifiedActions(bool))); + + connect(Kernel::self()->commandHistory(), SIGNAL(commandExecuted()), + doc, SLOT(slotSetModified())); + connect(Kernel::self()->commandHistory(), SIGNAL(documentRestored()), + doc, SLOT(slotDocumentRestored())); +} + +void MainWindow::initView() { + MARK; + m_split = new QSplitter(Qt::Horizontal, this); + setCentralWidget(m_split); + + m_viewTabs = new GUI::TabControl(m_split); + m_viewTabs->setTabBarHidden(true); + m_groupView = new GroupView(m_viewTabs, "groupview"); + Controller::self()->addObserver(m_groupView); + m_viewTabs->addTab(m_groupView, SmallIcon(QString::fromLatin1("folder")), i18n("Groups")); + QWhatsThis::add(m_groupView, i18n("<qt>The <i>Group View</i> sorts the entries into groupings " + "based on a selected field.</qt>")); + + m_rightSplit = new QSplitter(Qt::Vertical, m_split); + + m_detailedView = new DetailedListView(m_rightSplit, "detailedlistview"); + Controller::self()->addObserver(m_detailedView); + QWhatsThis::add(m_detailedView, i18n("<qt>The <i>Column View</i> shows the value of multiple fields " + "for each entry.</qt>")); + connect(Data::Document::self(), SIGNAL(signalCollectionImagesLoaded(Tellico::Data::CollPtr)), + m_detailedView, SLOT(slotRefreshImages())); + + m_viewStack = new ViewStack(m_rightSplit, "viewstack"); + Controller::self()->addObserver(m_viewStack->iconView()); + connect(m_viewStack->entryView(), SIGNAL(signalAction(const KURL&)), + SLOT(slotURLAction(const KURL&))); + + setMinimumWidth(MAIN_WINDOW_MIN_WIDTH); +} + +void MainWindow::initConnections() { + // have to toggle the menu item if the dialog gets closed + connect(m_editDialog, SIGNAL(finished()), + this, SLOT(slotEditDialogFinished())); + + // let the group view call filters, too + connect(m_groupView, SIGNAL(signalUpdateFilter(Tellico::FilterPtr)), + Controller::self(), SLOT(slotUpdateFilter(Tellico::FilterPtr))); +} + +void MainWindow::initFileOpen(bool nofile_) { + MARK; + slotInit(); + // check to see if most recent file should be opened + bool happyStart = false; + if(!nofile_ && Config::reopenLastFile()) { + // Config::lastOpenFile() is the full URL, protocol included + KURL lastFile(Config::lastOpenFile()); // empty string is actually ok, it gets handled + if(!lastFile.isEmpty() && lastFile.isValid()) { + slotFileOpen(lastFile); + happyStart = true; + } + } + if(!happyStart) { + // the document is created with an initial book collection, continue with that + Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); + + m_fileSave->setEnabled(false); + slotEnableOpenedActions(); + slotEnableModifiedActions(false); + + slotEntryCount(); + + const int type = Kernel::self()->collectionType(); + QString welcomeFile = locate("appdata", QString::fromLatin1("welcome.html")); + QString text = FileHandler::readTextFile(welcomeFile); + text.replace(QString::fromLatin1("$FGCOLOR$"), Config::templateTextColor(type).name()); + text.replace(QString::fromLatin1("$BGCOLOR$"), Config::templateBaseColor(type).name()); + text.replace(QString::fromLatin1("$COLOR1$"), Config::templateHighlightedTextColor(type).name()); + text.replace(QString::fromLatin1("$COLOR2$"), Config::templateHighlightedBaseColor(type).name()); + text.replace(QString::fromLatin1("$IMGDIR$"), QFile::encodeName(ImageFactory::tempDir())); + text.replace(QString::fromLatin1("$BANNER$"), + i18n("Welcome to the Tellico Collection Manager")); + text.replace(QString::fromLatin1("$WELCOMETEXT$"), + i18n("<h3>Tellico is a tool for managing collections of books, " + "videos, music, and whatever else you want to catalog.</h3>" + "<h3>New entries can be added to your collection by " + "<a href=\"tc:///coll_new_entry\">entering data manually</a> or by " + "<a href=\"tc:///edit_search_internet\">downloading data</a> from " + "various Internet sources.</h3>")); + m_viewStack->entryView()->showText(text); + } + m_initialized = true; +} + +// These are general options. +// The options that can be changed in the "Configuration..." dialog +// are taken care of by the ConfigDialog object. +void MainWindow::saveOptions() { +// myDebug() << "MainWindow::saveOptions()" << endl; + + saveMainWindowSettings(KGlobal::config(), QString::fromLatin1("Main Window Options")); + + Config::setShowGroupWidget(m_toggleGroupWidget->isChecked()); + Config::setShowEditWidget(m_toggleEntryEditor->isChecked()); + Config::setShowEntryView(m_toggleEntryView->isChecked()); + + m_fileOpenRecent->saveEntries(KGlobal::config(), QString::fromLatin1("Recent Files")); + if(!isNewDocument()) { + Config::setLastOpenFile(Data::Document::self()->URL().url()); + } + + if(m_groupView->isShown()) { + Config::setMainSplitterSizes(m_split->sizes()); + } + if(m_viewStack->isShown()) { + // badly named option, but no need to change + Config::setSecondarySplitterSizes(m_rightSplit->sizes()); + } + + Config::setGroupViewSortColumn(m_groupView->sortStyle()); // ok to use SortColumn key, save semantics + Config::setGroupViewSortAscending(m_groupView->ascendingSort()); + + if(m_loanView) { + Config::setLoanViewSortAscending(m_loanView->sortStyle()); // ok to use SortColumn key, save semantics + Config::setLoanViewSortAscending(m_loanView->ascendingSort()); + } + + if(m_filterView) { + Config::setFilterViewSortAscending(m_filterView->sortStyle()); // ok to use SortColumn key, save semantics + Config::setFilterViewSortAscending(m_filterView->ascendingSort()); + } + + // this is used in the EntryEditDialog constructor, too + m_editDialog->saveDialogSize(QString::fromLatin1("Edit Dialog Options")); + + saveCollectionOptions(Data::Document::self()->collection()); + Config::writeConfig(); +} + +void MainWindow::readCollectionOptions(Data::CollPtr coll_) { + KConfigGroup group(KGlobal::config(), QString::fromLatin1("Options - %1").arg(coll_->typeName())); + + QString defaultGroup = coll_->defaultGroupField(); + QString entryGroup; + if(coll_->type() != Data::Collection::Base) { + entryGroup = group.readEntry("Group By", defaultGroup); + } else { + KURL url = Kernel::self()->URL(); + for(uint i = 0; i < Config::maxCustomURLSettings(); ++i) { + KURL u = group.readEntry(QString::fromLatin1("URL_%1").arg(i)); + if(url == u) { + entryGroup = group.readEntry(QString::fromLatin1("Group By_%1").arg(i), defaultGroup); + break; + } + } + // fall back to old setting + if(entryGroup.isEmpty()) { + entryGroup = group.readEntry("Group By", defaultGroup); + } + } + if(entryGroup.isEmpty() || !coll_->entryGroups().contains(entryGroup)) { + entryGroup = defaultGroup; + } + m_groupView->setGroupField(entryGroup); + + QString entryXSLTFile = Config::templateName(coll_->type()); + if(entryXSLTFile.isEmpty()) { + entryXSLTFile = QString::fromLatin1("Fancy"); // should never happen, but just in case + } + m_viewStack->entryView()->setXSLTFile(entryXSLTFile + QString::fromLatin1(".xsl")); + + // make sure the right combo element is selected + slotUpdateCollectionToolBar(coll_); +} + +void MainWindow::saveCollectionOptions(Data::CollPtr coll_) { + // don't save initial collection options, or empty collections + if(!coll_ || coll_->entryCount() == 0 || isNewDocument()) { + return; + } + + int configIndex = -1; + KConfigGroup config(KGlobal::config(), QString::fromLatin1("Options - %1").arg(coll_->typeName())); + QString groupName; + if(m_entryGrouping->currentItem() > -1 && + static_cast<int>(coll_->entryGroups().count()) > m_entryGrouping->currentItem()) { + groupName = Kernel::self()->fieldNameByTitle(m_entryGrouping->currentText()); + if(coll_->type() != Data::Collection::Base) { + config.writeEntry("Group By", groupName); + } + } + + if(coll_->type() == Data::Collection::Base) { + // all of this is to have custom settings on a per file basis + KURL url = Kernel::self()->URL(); + QValueList<KURL> urls = QValueList<KURL>() << url; + QStringList groupBys = QStringList() << groupName; + for(uint i = 0; i < Config::maxCustomURLSettings(); ++i) { + KURL u = config.readEntry(QString::fromLatin1("URL_%1").arg(i)); + QString g = config.readEntry(QString::fromLatin1("Group By_%1").arg(i)); + if(!u.isEmpty() && url != u) { + urls.append(u); + groupBys.append(g); + } else if(!u.isEmpty()) { + configIndex = i; + } + } + uint limit = QMIN(urls.count(), Config::maxCustomURLSettings()); + for(uint i = 0; i < limit; ++i) { + config.writeEntry(QString::fromLatin1("URL_%1").arg(i), urls[i].url()); + config.writeEntry(QString::fromLatin1("Group By_%1").arg(i), groupBys[i]); + } + } + m_detailedView->saveConfig(coll_, configIndex); +} + +void MainWindow::readOptions() { +// myDebug() << "MainWindow::readOptions()" << endl; + + applyMainWindowSettings(KGlobal::config(), QString::fromLatin1("Main Window Options")); + + QValueList<int> splitList = Config::mainSplitterSizes(); + if(!splitList.empty()) { + m_split->setSizes(splitList); + } + + splitList = Config::secondarySplitterSizes(); + if(!splitList.empty()) { + m_rightSplit->setSizes(splitList); + } + + m_viewStack->iconView()->setMaxAllowedIconWidth(Config::maxIconSize()); + + connect(toolBar("collectionToolBar"), SIGNAL(modechange()), SLOT(slotUpdateToolbarIcons())); + + m_toggleGroupWidget->setChecked(Config::showGroupWidget()); + slotToggleGroupWidget(); + + m_toggleEntryView->setChecked(Config::showEntryView()); + slotToggleEntryView(); + + // initialize the recent file list + m_fileOpenRecent->loadEntries(KGlobal::config(), QString::fromLatin1("Recent Files")); + + // sort by count if column = 1 + int sortStyle = Config::groupViewSortColumn(); + m_groupView->setSortStyle(static_cast<GUI::ListView::SortStyle>(sortStyle)); + bool sortAscending = Config::groupViewSortAscending(); + m_groupView->setSortOrder(sortAscending ? Qt::Ascending : Qt::Descending); + + m_detailedView->setPixmapSize(Config::maxPixmapWidth(), Config::maxPixmapHeight()); + + bool useBraces = Config::useBraces(); + if(useBraces) { + BibtexHandler::s_quoteStyle = BibtexHandler::BRACES; + } else { + BibtexHandler::s_quoteStyle = BibtexHandler::QUOTES; + } + + // Don't read any options for the edit dialog here, since it's not yet initialized. + // Put them in init() +} + +void MainWindow::saveProperties(KConfig* cfg_) { + if(!isNewDocument() && !Data::Document::self()->isModified()) { + // saving to tempfile not necessary + } else { + KURL url = Data::Document::self()->URL(); + cfg_->writeEntry("filename", url.url()); + cfg_->writeEntry("modified", Data::Document::self()->isModified()); + QString tempname = KURL::encode_string(kapp->tempSaveName(url.url())); + KURL tempurl; + tempurl.setPath(tempname); + Data::Document::self()->saveDocument(tempurl); + } +} + +void MainWindow::readProperties(KConfig* cfg_) { + QString filename = cfg_->readEntry(QString::fromLatin1("filename")); + bool modified = cfg_->readBoolEntry(QString::fromLatin1("modified"), false); + if(modified) { + bool canRecover; + QString tempname = kapp->checkRecoverFile(filename, canRecover); + + if(canRecover) { + KURL tempurl; + tempurl.setPath(tempname); + Data::Document::self()->openDocument(tempurl); + Data::Document::self()->slotSetModified(true); + updateCaption(true); + QFile::remove(tempname); + } + } else { + if(!filename.isEmpty()) { + KURL url; + url.setPath(filename); + Data::Document::self()->openDocument(url); + updateCaption(false); + } + } +} + +bool MainWindow::queryClose() { + // in case we're still loading the images, cancel that + Data::Document::self()->cancelImageWriting(); + return m_editDialog->queryModified() && Data::Document::self()->saveModified(); +} + +bool MainWindow::queryExit() { + FileHandler::clean(); + ImageFactory::clean(true); + saveOptions(); + return true; +} + +void MainWindow::slotFileNew(int type_) { + slotStatusMsg(i18n("Creating new document...")); + + // close the fields dialog + slotHideCollectionFieldsDialog(); + + if(m_editDialog->queryModified() && Data::Document::self()->saveModified()) { + // remove filter and loan tabs, they'll get re-added if needed + if(m_filterView) { + m_viewTabs->removePage(m_filterView); + Controller::self()->removeObserver(m_filterView); + delete m_filterView; + m_filterView = 0; + } + if(m_loanView) { + m_viewTabs->removePage(m_loanView); + Controller::self()->removeObserver(m_loanView); + delete m_loanView; + m_loanView = 0; + } + m_viewTabs->setTabBarHidden(true); + Data::Document::self()->newDocument(type_); + m_fileOpenRecent->setCurrentItem(-1); + slotEnableOpenedActions(); + slotEnableModifiedActions(false); + m_newDocument = true; + ImageFactory::clean(false); + } + + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotFileOpen() { + slotStatusMsg(i18n("Opening file...")); + + if(m_editDialog->queryModified() && Data::Document::self()->saveModified()) { + QString filter = i18n("*.tc *.bc|Tellico Files (*.tc)"); + filter += QString::fromLatin1("\n"); + filter += i18n("*.xml|XML Files (*.xml)"); + filter += QString::fromLatin1("\n"); + filter += i18n("*|All Files"); + // keyword 'open' + KURL url = KFileDialog::getOpenURL(QString::fromLatin1(":open"), filter, + this, i18n("Open File")); + if(!url.isEmpty() && url.isValid()) { + slotFileOpen(url); + } + } + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotFileOpen(const KURL& url_) { + slotStatusMsg(i18n("Opening file...")); + + // close the fields dialog + slotHideCollectionFieldsDialog(); + + // there seems to be a race condition at start between slotInit() and initFileOpen() + // which means the edit dialog might not have been created yet + if((!m_editDialog || m_editDialog->queryModified()) && Data::Document::self()->saveModified()) { + if(openURL(url_)) { + m_fileOpenRecent->addURL(url_); + m_fileOpenRecent->setCurrentItem(-1); + } + } + + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotFileOpenRecent(const KURL& url_) { + slotStatusMsg(i18n("Opening file...")); + + // close the fields dialog + slotHideCollectionFieldsDialog(); + + if(m_editDialog->queryModified() && Data::Document::self()->saveModified()) { + if(!openURL(url_)) { + m_fileOpenRecent->removeURL(url_); + m_fileOpenRecent->setCurrentItem(-1); + } + } else { + // the KAction shouldn't be checked now + m_fileOpenRecent->setCurrentItem(-1); + } + + StatusBar::self()->clearStatus(); +} + +void MainWindow::openFile(const QString& file_) { + KURL url = KURL::fromPathOrURL(file_); + if(!url.isEmpty() && url.isValid()) { + slotFileOpen(url); + } +} + +bool MainWindow::openURL(const KURL& url_) { +// myDebug() << "MainWindow::openURL() - " << url_.prettyURL() << endl; + + // try to open document + GUI::CursorSaver cs(Qt::waitCursor); + + bool success = Data::Document::self()->openDocument(url_); + + if(success) { + m_quickFilter->clear(); + slotEnableOpenedActions(); + m_newDocument = false; + slotEnableModifiedActions(Data::Document::self()->isModified()); // doc might add some stuff + } else if(!m_initialized) { + // special case on startup when openURL() is called with a command line argument + // and that URL can't be opened. The window still needs to be initialized + // the doc object is created with an initial book collection, continue with that + Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); + + m_fileSave->setEnabled(false); + slotEnableOpenedActions(); + slotEnableModifiedActions(false); + + slotEntryCount(); + } + // slotFileOpen(URL) gets called when opening files on the command line + // so go ahead and make sure m_initialized is set. + m_initialized = true; + + // remove filter and loan tabs, they'll get re-added if needed + if(m_filterView && m_filterView->childCount() == 0) { + m_viewTabs->removePage(m_filterView); + Controller::self()->removeObserver(m_filterView); + delete m_filterView; + m_filterView = 0; + } + if(m_loanView && m_loanView->childCount() == 0) { + m_viewTabs->removePage(m_loanView); + Controller::self()->removeObserver(m_loanView); + delete m_loanView; + m_loanView = 0; + } + Controller::self()->hideTabs(); // does conditional check + + return success; +} + +void MainWindow::slotFileSave() { + fileSave(); +} + +bool MainWindow::fileSave() { + if(!m_editDialog->queryModified()) { + return false; + } + slotStatusMsg(i18n("Saving file...")); + + bool ret = true; + if(isNewDocument()) { + ret = fileSaveAs(); + } else { + // special check: if there are more than 200 images AND the "Write Images In File" config key + // is not set, then warn user that performance may suffer, and write result + if(Config::imageLocation() == Config::ImagesInFile && + Config::askWriteImagesInFile() && + Data::Document::self()->imageCount() > MAX_IMAGES_WARN_PERFORMANCE) { + QString msg = i18n("<qt><p>You are saving a file with many images, which causes Tellico to " + "slow down significantly. Do you want to save the images separately in " + "Tellico's data directory to improve performance?</p><p>Your choice can " + "always be changed in the configuration dialog.</p></qt>"); + + KGuiItem yes(i18n("Save Images Separately")); + KGuiItem no(i18n("Save Images in File")); + + int res = KMessageBox::warningYesNo(this, msg, QString::null /* caption */, yes, no); + if(res == KMessageBox::No) { + Config::setImageLocation(Config::ImagesInAppDir); + } + Config::setAskWriteImagesInFile(false); + } + + GUI::CursorSaver cs(Qt::waitCursor); + if(Data::Document::self()->saveDocument(Data::Document::self()->URL())) { + m_newDocument = false; + updateCaption(false); + m_fileSave->setEnabled(false); + m_detailedView->resetEntryStatus(); + } else { + ret = false; + } + } + + StatusBar::self()->clearStatus(); + return ret; +} + +void MainWindow::slotFileSaveAs() { + fileSaveAs(); +} + +bool MainWindow::fileSaveAs() { + if(!m_editDialog->queryModified()) { + return false; + } + + slotStatusMsg(i18n("Saving file with a new filename...")); + + QString filter = i18n("*.tc *.bc|Tellico Files (*.tc)"); + filter += QChar('\n'); + filter += i18n("*|All Files"); + + // keyword 'open' + KFileDialog dlg(QString::fromLatin1(":open"), filter, this, "filedialog", true); + dlg.setCaption(i18n("Save As")); + dlg.setOperationMode(KFileDialog::Saving); + + int result = dlg.exec(); + if(result == QDialog::Rejected) { + StatusBar::self()->clearStatus(); + return false; + } + + bool ret = true; + KURL url = dlg.selectedURL(); + if(!url.isEmpty() && url.isValid()) { + GUI::CursorSaver cs(Qt::waitCursor); + if(Data::Document::self()->saveDocument(url)) { + KRecentDocument::add(url); + m_fileOpenRecent->addURL(url); + updateCaption(false); + m_newDocument = false; + m_fileSave->setEnabled(false); + m_detailedView->resetEntryStatus(); + } else { + ret = false; + } + } + + StatusBar::self()->clearStatus(); + return ret; +} + +void MainWindow::slotFilePrint() { + slotStatusMsg(i18n("Printing...")); + + bool printGrouped = Config::printGrouped(); + bool printHeaders = Config::printFieldHeaders(); + int imageWidth = Config::maxImageWidth(); + int imageHeight = Config::maxImageHeight(); + + // If the collection is being filtered, warn the user + if(m_detailedView->filter() != 0) { + QString str = i18n("The collection is currently being filtered to show a limited subset of " + "the entries. Only the visible entries will be printed. Continue?"); + int ret = KMessageBox::warningContinueCancel(this, str, QString::null, KStdGuiItem::print(), + QString::fromLatin1("WarnPrintVisible")); + if(ret == KMessageBox::Cancel) { + StatusBar::self()->clearStatus(); + return; + } + } + + GUI::CursorSaver cs(Qt::waitCursor); + + Export::HTMLExporter exporter(Data::Document::self()->collection()); + // only print visible entries + exporter.setEntries(m_detailedView->visibleEntries()); + exporter.setXSLTFile(QString::fromLatin1("tellico-printing.xsl")); + exporter.setPrintHeaders(printHeaders); + exporter.setPrintGrouped(printGrouped); + exporter.setGroupBy(Controller::self()->expandedGroupBy()); + if(!printGrouped) { // the sort titles are only used if the entries are not grouped + exporter.setSortTitles(Controller::self()->sortTitles()); + } + exporter.setColumns(m_detailedView->visibleColumns()); + exporter.setMaxImageSize(imageWidth, imageHeight); + + slotStatusMsg(i18n("Processing document...")); + if(Config::printFormatted()) { + exporter.setOptions(Export::ExportUTF8 | Export::ExportFormatted); + } else { + exporter.setOptions(Export::ExportUTF8); + } + QString html = exporter.text(); + if(html.isEmpty()) { + XSLTError(); + StatusBar::self()->clearStatus(); + return; + } + + // don't have busy cursor when showing the print dialog + cs.restore(); +// myDebug() << html << endl; + slotStatusMsg(i18n("Printing...")); + doPrint(html); + + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotFileQuit() { + slotStatusMsg(i18n("Exiting...")); + + // this gets called in queryExit() anyway + //saveOptions(); + close(); + + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotEditCut() { + activateEditSlot(SLOT(cut())); +} + +void MainWindow::slotEditCopy() { + activateEditSlot(SLOT(copy())); +} + +void MainWindow::slotEditPaste() { + activateEditSlot(SLOT(paste())); +} + +void MainWindow::activateEditSlot(const char* slot_) { + // the edit widget is the only one that copies, cuts, and pastes + QWidget* w; + if(m_editDialog->isVisible()) { + w = m_editDialog->focusWidget(); + } else { + w = kapp->focusWidget(); + } + + if(w && w->isVisible()) { + QMetaObject* meta = w->metaObject(); + + int idx = meta->findSlot(slot_ + 1, true); + if(idx > -1) { + w->qt_invoke(idx, 0); + } + } +} + +void MainWindow::slotEditSelectAll() { + m_detailedView->selectAllVisible(); +} + +void MainWindow::slotEditDeselect() { + Controller::self()->slotUpdateSelection(0, Data::EntryVec()); +} + +void MainWindow::slotConfigToolbar() { + saveMainWindowSettings(KGlobal::config(), QString::fromLatin1("Main Window Options")); +#ifdef UIFILE + KEditToolbar dlg(actionCollection(), UIFILE); +#else + KEditToolbar dlg(actionCollection()); +#endif + connect(&dlg, SIGNAL(newToolbarConfig()), this, SLOT(slotNewToolbarConfig())); + dlg.exec(); +} + +void MainWindow::slotNewToolbarConfig() { + applyMainWindowSettings(KGlobal::config(), QString::fromLatin1("Main Window Options")); +#ifdef UIFILE + createGUI(UIFILE, false); +#else + createGUI(QString::null, false); +#endif +} + +void MainWindow::slotConfigKeys() { + KKeyDialog::configure(actionCollection()); +} + +void MainWindow::slotToggleGroupWidget() { + if(m_toggleGroupWidget->isChecked()) { + m_viewTabs->show(); + } else { + m_viewTabs->hide(); + } +} + +void MainWindow::slotToggleEntryEditor() { + if(m_toggleEntryEditor->isChecked()) { + m_editDialog->show(); + } else { + m_editDialog->hide(); + } +} + +void MainWindow::slotToggleEntryView() { + if(m_toggleEntryView->isChecked()) { + m_viewStack->show(); + } else { + m_viewStack->hide(); + } +} + +void MainWindow::slotShowConfigDialog() { + if(!m_configDlg) { + m_configDlg = new ConfigDialog(this); + m_configDlg->show(); + m_configDlg->readConfiguration(); + connect(m_configDlg, SIGNAL(signalConfigChanged()), + SLOT(slotHandleConfigChange())); + connect(m_configDlg, SIGNAL(finished()), + SLOT(slotHideConfigDialog())); + } else { + KWin::activateWindow(m_configDlg->winId()); + m_configDlg->show(); + } +} + +void MainWindow::slotHideConfigDialog() { + if(m_configDlg) { + m_configDlg->delayedDestruct(); + m_configDlg = 0; + } +} + +void MainWindow::slotShowTipOfDay(bool force_/*=true*/) { + QString tipfile = locate("appdata", QString::fromLatin1("tellico.tips")); + KTipDialog::showTip(this, tipfile, force_); +} + +void MainWindow::slotStatusMsg(const QString& text_) { + m_statusBar->setStatus(text_); +} + +void MainWindow::slotClearStatus() { + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotEntryCount() { + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll) { + return; + } + + int count = coll->entryCount(); + QString text = i18n("Total entries: %1").arg(count); + + int selectCount = Controller::self()->selectedEntries().count(); + int filterCount = m_detailedView->visibleItems(); + // if more than one book is selected, add the number of selected books + if(filterCount < count && selectCount > 1) { + text += QChar(' '); + text += i18n("(%1 filtered; %2 selected)").arg(filterCount).arg(selectCount); + } else if(filterCount < count) { + text += QChar(' '); + text += i18n("(%1 filtered)").arg(filterCount); + } else if(selectCount > 1) { + text += QChar(' '); + text += i18n("(%1 selected)").arg(selectCount); + } + + m_statusBar->setCount(text); +} + +void MainWindow::slotEnableOpenedActions() { + slotUpdateToolbarIcons(); + + // collapse all the groups (depth=1) + m_groupView->slotCollapseAll(1); + + updateCollectionActions(); + + // close the filter dialog when a new collection is opened + slotHideFilterDialog(); + slotHideStringMacroDialog(); +} + +void MainWindow::slotEnableModifiedActions(bool modified_ /*= true*/) { + updateCaption(modified_); + updateCollectionActions(); + m_fileSave->setEnabled(modified_); +} + +void MainWindow::slotHandleConfigChange() { + const int imageLocation = Config::imageLocation(); + const bool autoCapitalize = Config::autoCapitalization(); + const bool autoFormat = Config::autoFormat(); + QStringList articles = Config::articleList(); + QStringList nocaps = Config::noCapitalizationList(); + QStringList suffixes = Config::nameSuffixList(); + QStringList prefixes = Config::surnamePrefixList(); + + m_configDlg->saveConfiguration(); + + // only modified if there are entries and image location is changed + if(imageLocation != Config::imageLocation() && !Data::Document::self()->isEmpty()) { + Data::Document::self()->slotSetModified(); + } + + if(autoCapitalize != Config::autoCapitalization() || + autoFormat != Config::autoFormat() || + articles != Config::articleList() || + nocaps != Config::noCapitalizationList() || + suffixes != Config::nameSuffixList() || + prefixes != Config::surnamePrefixList()) { + // invalidate all groups + Data::Document::self()->collection()->invalidateGroups(); + // refreshing the title causes the group view to refresh + Controller::self()->slotRefreshField(Data::Document::self()->collection()->fieldByName(QString::fromLatin1("title"))); + } + + QString entryXSLTFile = Config::templateName(Kernel::self()->collectionType()); + m_viewStack->entryView()->setXSLTFile(entryXSLTFile + QString::fromLatin1(".xsl")); +} + +void MainWindow::slotUpdateCollectionToolBar(Data::CollPtr coll_) { +// myDebug() << "MainWindow::updateCollectionToolBar()" << endl; + + if(!coll_) { + kdWarning() << "MainWindow::slotUpdateCollectionToolBar() - no collection pointer!" << endl; + return; + } + + QString current = m_groupView->groupBy(); + if(current.isEmpty() || !coll_->entryGroups().contains(current)) { + current = coll_->defaultGroupField(); + } + + const QStringList groups = coll_->entryGroups(); + if(groups.isEmpty()) { + m_entryGrouping->clear(); + return; + } + + QMap<QString, QString> groupMap; // use a map so they get sorted + for(QStringList::ConstIterator groupIt = groups.begin(); groupIt != groups.end(); ++groupIt) { + // special case for people "pseudo-group" + if(*groupIt == Data::Collection::s_peopleGroupName) { + groupMap.insert(*groupIt, QString::fromLatin1("<") + i18n("People") + QString::fromLatin1(">")); + } else { + groupMap.insert(*groupIt, coll_->fieldTitleByName(*groupIt)); + } + } + + QStringList names = groupMap.keys(); + int index = names.findIndex(current); + if(index == -1) { + current = names[0]; + index = 0; + } + QStringList titles = groupMap.values(); + m_entryGrouping->setItems(titles); + m_entryGrouping->setCurrentItem(index); + // in case the current grouping field get modified to be non-grouping... + m_groupView->setGroupField(current); // don't call slotChangeGrouping() since it adds an undo item + + // this isn't really proper, but works so the combo box width gets adjusted + const int len = m_entryGrouping->containerCount(); + for(int i = 0; i < len; ++i) { + KToolBar* tb = dynamic_cast<KToolBar*>(m_entryGrouping->container(i)); + if(tb) { + KComboBox* cb = tb->getCombo(m_entryGrouping->itemId(i)); + if(cb) { + // qt caches the combobox size and never recalculates the sizeHint() + // the source code recommends calling setFont to invalidate the sizeHint + cb->setFont(cb->font()); + cb->updateGeometry(); + } + } + } +} + +void MainWindow::slotChangeGrouping() { +// myDebug() << "MainWindow::slotChangeGrouping()" << endl; + QString title = m_entryGrouping->currentText(); + + QString groupName = Kernel::self()->fieldNameByTitle(title); + if(groupName.isEmpty()) { + if(title == QString::fromLatin1("<") + i18n("People") + QString::fromLatin1(">")) { + groupName = Data::Collection::s_peopleGroupName; + } else { + groupName = Data::Document::self()->collection()->defaultGroupField(); + } + } + m_groupView->setGroupField(groupName); + m_viewTabs->showPage(m_groupView); +} + +void MainWindow::slotShowReportDialog() { +// myDebug() << "MainWindow::slotShowReport()" << endl; + if(!m_reportDlg) { + m_reportDlg = new ReportDialog(this); + connect(m_reportDlg, SIGNAL(finished()), + SLOT(slotHideReportDialog())); + } else { + KWin::activateWindow(m_reportDlg->winId()); + } + m_reportDlg->show(); +} + +void MainWindow::slotHideReportDialog() { + if(m_reportDlg) { + m_reportDlg->delayedDestruct(); + m_reportDlg = 0; + } +} + +void MainWindow::doPrint(const QString& html_) { + KHTMLPart w ; + w.setJScriptEnabled(false); + w.setJavaEnabled(false); + w.setMetaRefreshEnabled(false); + w.setPluginsEnabled(false); + w.begin(Data::Document::self()->URL()); + w.write(html_); + w.end(); + +// the problem with doing my own layout is that the text gets truncated, both at the +// top and at the bottom. Even adding the overlap parameter, there were problems. +// KHTMLView takes care of that with a truncatedAt() parameter, but that's hidden in +// the khtml::render_root class. So for now, just use the KHTMLView::print() method. +#if 1 + w.view()->print(); +#else + KPrinter* printer = new KPrinter(QPrinter::PrinterResolution); + + if(printer->setup(this, i18n("Print %1").arg(Data::Document::self()->URL().prettyURL()))) { + printer->setFullPage(false); + printer->setCreator(QString::fromLatin1("Tellico")); + printer->setDocName(Data::Document::self()->URL().prettyURL()); + + QPainter *p = new QPainter; + p->begin(printer); + + // mostly taken from KHTMLView::print() + QString headerLeft = KGlobal::locale()->formatDate(QDate::currentDate(), false); + QString headerRight = Data::Document::self()->URL().prettyURL(); + QString footerMid; + + QFont headerFont(QString::fromLatin1("helvetica"), 8); + p->setFont(headerFont); + const int lspace = p->fontMetrics().lineSpacing(); + const int headerHeight = (lspace * 3) / 2; + + QPaintDeviceMetrics metrics(printer); + const int pageHeight = metrics.height() - 2*headerHeight; + const int pageWidth = metrics.width(); + +// myDebug() << "MainWindow::doPrint() - pageHeight = " << pageHeight << "" +// "; contentsHeight = " << w->view()->contentsHeight() << endl; + + int top = 0; + int page = 1; + + bool more = true; + while(more) { + p->setPen(Qt::black); + p->setFont(headerFont); + + footerMid = i18n("Page %1").arg(page); + + p->drawText(0, 0, pageWidth, lspace, Qt::AlignLeft, headerLeft); + p->drawText(0, 0, pageWidth, lspace, Qt::AlignRight, headerRight); + p->drawText(0, pageHeight+headerHeight, pageWidth, lspace, Qt::AlignHCenter, footerMid); + + w->paint(p, QRect(0, -top + 2*headerHeight, pageWidth, pageHeight+top), top, &more); + + top += pageHeight - PRINTED_PAGE_OVERLAP; + + if(more) { + printer->newPage(); + page++; + } +// p->resetXForm(); + } + // stop painting, this will automatically send the print data to the printer + p->end(); + delete p; + } + + delete printer; +#endif +} + +void MainWindow::XSLTError() { + QString str = i18n("Tellico encountered an error in XSLT processing.") + QChar('\n'); + str += i18n("Please check your installation."); + Kernel::self()->sorry(str); +} + +void MainWindow::slotShowFilterDialog() { + if(!m_filterDlg) { + m_filterDlg = new FilterDialog(FilterDialog::CreateFilter, this); // allow saving + m_filterDlg->setFilter(m_detailedView->filter()); + m_quickFilter->setEnabled(false); + connect(m_filterDlg, SIGNAL(signalCollectionModified()), + Data::Document::self(), SLOT(slotSetModified())); + connect(m_filterDlg, SIGNAL(signalUpdateFilter(Tellico::FilterPtr)), + m_quickFilter, SLOT(clear())); + connect(m_filterDlg, SIGNAL(signalUpdateFilter(Tellico::FilterPtr)), + Controller::self(), SLOT(slotUpdateFilter(Tellico::FilterPtr))); + connect(m_filterDlg, SIGNAL(finished()), + SLOT(slotHideFilterDialog())); + } else { + KWin::activateWindow(m_filterDlg->winId()); + } + m_filterDlg->show(); +} + +void MainWindow::slotHideFilterDialog() { +// m_quickFilter->blockSignals(false); + m_quickFilter->setEnabled(true); + if(m_filterDlg) { + m_filterDlg->delayedDestruct(); + m_filterDlg = 0; + } +} + +void MainWindow::slotQueueFilter() { + m_queuedFilters++; + QTimer::singleShot(200, this, SLOT(slotUpdateFilter())); +} + +void MainWindow::slotUpdateFilter() { + m_queuedFilters--; + if(m_queuedFilters > 0) { + return; + } + + setFilter(m_quickFilter->text()); +} + +void MainWindow::setFilter(const QString& text_) { + QString text = text_.stripWhiteSpace(); + Filter::Ptr filter = 0; + if(!text.isEmpty()) { + filter = new Filter(Filter::MatchAll); + QString fieldName = QString::null; + // if the text contains '=' assume it's a field name or title + if(text.find('=') > -1) { + fieldName = text.section('=', 0, 0).stripWhiteSpace(); + text = text.section('=', 1).stripWhiteSpace(); + // check that the field name might be a title + if(!Data::Document::self()->collection()->hasField(fieldName)) { + fieldName = Data::Document::self()->collection()->fieldNameByTitle(fieldName); + } + } + // if the text contains any non-word characters, assume it's a regexp + // but \W in qt is letter, number, or '_', I want to be a bit less strict + QRegExp rx(QString::fromLatin1("[^\\w\\s-']")); + if(text.find(rx) == -1) { + // split by whitespace, and add rules for each word + QStringList tokens = QStringList::split(QRegExp(QString::fromLatin1("\\s")), text); + for(QStringList::Iterator it = tokens.begin(); it != tokens.end(); ++it) { + // an empty field string means check every field + filter->append(new FilterRule(fieldName, *it, FilterRule::FuncContains)); + } + } else { + // if it isn't valid, hold off on applying the filter + QRegExp tx(text); + if(!tx.isValid()) { + myDebug() << "MainWindow::slotUpdateFilter() - invalid regexp: " << text << endl; + return; + } + filter->append(new FilterRule(fieldName, text, FilterRule::FuncRegExp)); + } + // also want to update the line edit in case the filter was set by DCOP + if(m_quickFilter->text().isEmpty() && m_quickFilter->text() != text_) { + m_quickFilter->setText(text_); + } + } + // only update filter if one exists or did exist + if(filter || m_detailedView->filter()) { + Controller::self()->slotUpdateFilter(filter); + } +} + +void MainWindow::slotShowCollectionFieldsDialog() { + if(!m_collFieldsDlg) { + m_collFieldsDlg = new CollectionFieldsDialog(Data::Document::self()->collection(), this); + connect(m_collFieldsDlg, SIGNAL(finished()), + SLOT(slotHideCollectionFieldsDialog())); + } else { + KWin::activateWindow(m_collFieldsDlg->winId()); + } + m_collFieldsDlg->show(); +} + +void MainWindow::slotHideCollectionFieldsDialog() { + if(m_collFieldsDlg) { + m_collFieldsDlg->delayedDestruct(); + m_collFieldsDlg = 0; + } +} + +void MainWindow::slotFileImport(int format_) { + slotStatusMsg(i18n("Importing data...")); + m_quickFilter->clear(); + + Import::Format format = static_cast<Import::Format>(format_); + bool checkURL = true; + KURL url; + switch(ImportDialog::importTarget(format)) { + case Import::File: + url = KFileDialog::getOpenURL(ImportDialog::startDir(format), ImportDialog::fileFilter(format), + this, i18n("Import File")); + break; + + case Import::Dir: + // TODO: allow remote audiofile importing + url.setPath(KFileDialog::getExistingDirectory(ImportDialog::startDir(format), + this, i18n("Import Directory"))); + break; + + case Import::None: + default: + checkURL = false; + break; + } + + if(checkURL) { + bool ok = !url.isEmpty() && url.isValid() && KIO::NetAccess::exists(url, true, this); + if(!ok) { + StatusBar::self()->clearStatus(); + return; + } + } + importFile(format, url); + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotFileExport(int format_) { + slotStatusMsg(i18n("Exporting data...")); + + Export::Format format = static_cast<Export::Format>(format_); + ExportDialog dlg(format, Data::Document::self()->collection(), this, "exportdialog"); + + if(dlg.exec() == QDialog::Rejected) { + StatusBar::self()->clearStatus(); + return; + } + + switch(ExportDialog::exportTarget(format)) { + case Export::None: + dlg.exportURL(); + break; + + case Export::Dir: + myDebug() << "MainWindow::slotFileExport() - ExportDir not implemented!" << endl; + break; + + case Export::File: + { + KFileDialog fileDlg(QString::fromLatin1(":export"), dlg.fileFilter(), this, "filedialog", true); + fileDlg.setCaption(i18n("Export As")); + fileDlg.setOperationMode(KFileDialog::Saving); + + if(fileDlg.exec() == QDialog::Rejected) { + StatusBar::self()->clearStatus(); + return; + } + + KURL url = fileDlg.selectedURL(); + if(!url.isEmpty() && url.isValid()) { + GUI::CursorSaver cs(Qt::waitCursor); + dlg.exportURL(url); + } + } + break; + } + + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotShowStringMacroDialog() { + if(Data::Document::self()->collection()->type() != Data::Collection::Bibtex) { + return; + } + + if(!m_stringMacroDlg) { + const Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data()); + m_stringMacroDlg = new StringMapDialog(c->macroList(), this, "StringMacroDialog", false); + m_stringMacroDlg->setCaption(i18n("String Macros")); + m_stringMacroDlg->setLabels(i18n("Macro"), i18n("String")); + connect(m_stringMacroDlg, SIGNAL(finished()), SLOT(slotHideStringMacroDialog())); + connect(m_stringMacroDlg, SIGNAL(okClicked()), SLOT(slotStringMacroDialogOk())); + } else { + KWin::activateWindow(m_stringMacroDlg->winId()); + } + m_stringMacroDlg->show(); +} + +void MainWindow::slotHideStringMacroDialog() { + if(m_stringMacroDlg) { + m_stringMacroDlg->delayedDestruct(); + m_stringMacroDlg = 0; + } +} + +void MainWindow::slotStringMacroDialogOk() { + // no point in checking if collection is bibtex, as dialog would never have been created + if(m_stringMacroDlg) { + static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data())->setMacroList(m_stringMacroDlg->stringMap()); + Data::Document::self()->slotSetModified(true); + } +} + +void MainWindow::slotNewEntry() { + m_toggleEntryEditor->setChecked(true); + slotToggleEntryEditor(); + m_editDialog->slotHandleNew(); +} + +void MainWindow::slotEditDialogFinished() { + m_toggleEntryEditor->setChecked(false); +} + +void MainWindow::slotShowEntryEditor() { + m_toggleEntryEditor->setChecked(true); + m_editDialog->show(); + + KWin::activateWindow(m_editDialog->winId()); +} + +void MainWindow::slotConvertToBibliography() { + // only book collections can be converted to bibtex + Data::CollPtr coll = Data::Document::self()->collection(); + if(!coll || coll->type() != Data::Collection::Book) { + return; + } + + GUI::CursorSaver cs; + + Data::CollPtr newColl = Data::BibtexCollection::convertBookCollection(coll); + if(newColl) { + m_newDocument = true; + Kernel::self()->replaceCollection(newColl); + m_fileOpenRecent->setCurrentItem(-1); + slotUpdateToolbarIcons(); + updateCollectionActions(); + } else { + kdWarning() << "MainWindow::slotConvertToBibliography() - ERROR: no bibliography created!" << endl; + } +} + +void MainWindow::slotCiteEntry(int action_) { + StatusBar::self()->setStatus(i18n("Creating citations...")); + Cite::ActionManager::self()->cite(static_cast<Cite::CiteAction>(action_), Controller::self()->selectedEntries()); + StatusBar::self()->clearStatus(); +} + +void MainWindow::slotShowFetchDialog() { + if(!m_fetchDlg) { + m_fetchDlg = new FetchDialog(this); + connect(m_fetchDlg, SIGNAL(finished()), SLOT(slotHideFetchDialog())); + connect(Controller::self(), SIGNAL(collectionAdded(int)), m_fetchDlg, SLOT(slotResetCollection())); + } else { + KWin::activateWindow(m_fetchDlg->winId()); + } + m_fetchDlg->show(); +} + +void MainWindow::slotHideFetchDialog() { + if(m_fetchDlg) { + m_fetchDlg->delayedDestruct(); + m_fetchDlg = 0; + } +} + +bool MainWindow::importFile(Import::Format format_, const KURL& url_, Import::Action action_) { + // try to open document + GUI::CursorSaver cs(Qt::waitCursor); + + bool failed = false; + Data::CollPtr coll; + if(!url_.isEmpty() && url_.isValid() && KIO::NetAccess::exists(url_, true, this)) { + coll = ImportDialog::importURL(format_, url_); + } else { + Kernel::self()->sorry(i18n(errorLoad).arg(url_.fileName())); + failed = true; + } + + if(!coll && !m_initialized) { + // special case on startup when openURL() is called with a command line argument + // and that URL can't be opened. The window still needs to be initialized + // the doc object is created with an initial book collection, continue with that + Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); + m_fileSave->setEnabled(false); + slotEnableOpenedActions(); + slotEnableModifiedActions(false); + slotEntryCount(); + m_fileOpenRecent->setCurrentItem(-1); + m_initialized = true; + failed = true; + } else if(coll) { + // this is rather dumb, but I'm too lazy to find the bug + // if the document isn't initialized, then Tellico crashes + // since Document::replaceCollection() ends up calling lots of stuff that isn't initialized + if(!m_initialized) { + Controller::self()->slotCollectionAdded(Data::Document::self()->collection()); + m_initialized = true; + } + failed = !importCollection(coll, action_); + } + + StatusBar::self()->clearStatus(); + return !failed; // return true means success +} + +bool MainWindow::exportCollection(Export::Format format_, const KURL& url_) { + if(!url_.isValid()) { + myDebug() << "MainWindow::exportCollection() - invalid URL: " << url_.url() << endl; + return false; + } + + GUI::CursorSaver cs; + const Data::CollPtr c = Data::Document::self()->collection(); + if(!c) { + return false; + } + + // only bibliographies can export to bibtex or bibtexml + bool isBibtex = (c->type() == Data::Collection::Bibtex); + if(!isBibtex && (format_ == Export::Bibtex || format_ == Export::Bibtexml)) { + return false; + } + // only books and bibliographies can export to alexandria + bool isBook = (c->type() == Data::Collection::Book); + if(!isBibtex && !isBook && format_ == Export::Alexandria) { + return false; + } + + bool success = ExportDialog::exportCollection(format_, url_); + return success; +} + +bool MainWindow::showEntry(long id) { + Data::EntryPtr entry = Data::Document::self()->collection()->entryById(id); + if(entry) { + m_viewStack->showEntry(entry); + } + return entry != 0; +} + +void MainWindow::addFilterView() { + if(m_filterView) { + return; + } + + m_filterView = new FilterView(m_viewTabs, "filterview"); + Controller::self()->addObserver(m_filterView); + m_viewTabs->insertTab(m_filterView, SmallIcon(QString::fromLatin1("filter")), i18n("Filters"), 1); + QWhatsThis::add(m_filterView, i18n("<qt>The <i>Filter View</i> shows the entries which meet certain " + "filter rules.</qt>")); + + int sortStyle = Config::filterViewSortColumn(); + m_filterView->setSortStyle(static_cast<GUI::ListView::SortStyle>(sortStyle)); + bool sortAscending = Config::filterViewSortAscending(); + m_filterView->setSortOrder(sortAscending ? Qt::Ascending : Qt::Descending); +} + +void MainWindow::addLoanView() { + if(m_loanView) { + return; + } + + m_loanView = new LoanView(m_viewTabs, "loanview"); + Controller::self()->addObserver(m_loanView); + m_viewTabs->insertTab(m_loanView, SmallIcon(QString::fromLatin1("kaddressbook")), i18n("Loans"), 2); + QWhatsThis::add(m_loanView, i18n("<qt>The <i>Loan View</i> shows a list of all the people who " + "have borrowed items from your collection.</qt>")); + + int sortStyle = Config::loanViewSortColumn(); + m_loanView->setSortStyle(static_cast<GUI::ListView::SortStyle>(sortStyle)); + bool sortAscending = Config::loanViewSortAscending(); + m_loanView->setSortOrder(sortAscending ? Qt::Ascending : Qt::Descending); +} + +void MainWindow::updateCaption(bool modified_) { + QString caption; + if(Data::Document::self()->collection()) { + caption = Data::Document::self()->collection()->title(); + } + if(!m_newDocument) { + if(!caption.isEmpty()) { + caption += QString::fromLatin1(" - "); + } + KURL u = Data::Document::self()->URL(); + if(u.isLocalFile()) { + // for new files, the path is set to /Untitled in Data::Document + if(u.path() == '/' + i18n("Untitled")) { + caption += u.fileName(); + } else { + caption += u.path(); + } + } else { + caption += u.prettyURL(); + } + } + setCaption(caption, modified_); +} + +void MainWindow::slotUpdateToolbarIcons() { + // myDebug() << "MainWindow::slotUpdateToolbarIcons() " << endl; + // first change the icon for the menu item + m_newEntry->setIconSet(UserIconSet(Kernel::self()->collectionTypeName())); + + // since the toolbar icon is probably a different size than the menu item icon + // superimpose it on the "mime_empty" icon + KToolBar* tb = toolBar("collectionToolBar"); + if(!tb) { + return; + } + + for(int i = 0; i < tb->count(); ++i) { + if(m_newEntry->isPlugged(tb, tb->idAt(i))) { + QIconSet icons; + icons.installIconFactory(new EntryIconFactory(tb->iconSize())); + tb->setButtonIconSet(tb->idAt(i), icons); + break; + } + } +} + +void MainWindow::slotGroupLabelActivated() { + // need entry grouping combo id + KToolBar* tb = toolBar("collectionToolBar"); + if(!tb) { + return; + } + + for(int i = 0; i < tb->count(); ++i) { + if(m_entryGrouping->isPlugged(tb, tb->idAt(i))) { + KComboBox* combo = tb->getCombo(tb->idAt(i)); + if(combo) { + combo->popup(); + break; + } + } + } +} + +void MainWindow::slotFilterLabelActivated() { + m_quickFilter->setFocus(); + m_quickFilter->selectAll(); +} + +void MainWindow::slotClearFilter() { + m_quickFilter->clear(); + slotQueueFilter(); +} + +void MainWindow::slotRenameCollection() { + Kernel::self()->renameCollection(); +} + +void MainWindow::updateCollectionActions() { + if(!Data::Document::self()->collection()) { + return; + } + + stateChanged(QString::fromLatin1("collection_reset")); + Data::Collection::Type type = Data::Document::self()->collection()->type(); + switch(type) { + case Data::Collection::Book: + stateChanged(QString::fromLatin1("is_book")); + break; + case Data::Collection::Bibtex: + stateChanged(QString::fromLatin1("is_bibliography")); + break; + case Data::Collection::Video: + stateChanged(QString::fromLatin1("is_video")); + break; + default: + break; + } + Controller::self()->updateActions(); + // special case when there are no available data sources + if(m_fetchActions.isEmpty() && m_updateAll) { + m_updateAll->setEnabled(false); + } +} + +void MainWindow::updateEntrySources() { + QSignalMapper* mapper = ::qt_cast<QSignalMapper*>(child("update_mapper")); + if(!mapper) { + kdWarning() << "MainWindow::updateEntrySources() - no update mapper!" << endl; + return; + } + + unplugActionList(QString::fromLatin1("update_entry_actions")); + for(QPtrListIterator<KAction> it(m_fetchActions); it.current(); ++it) { + it.current()->unplugAll(); + mapper->removeMappings(it.current()); + } + // autoDelete() all actions, which removes them from the actionCollection() + m_fetchActions.clear(); + + Fetch::FetcherVec vec = Fetch::Manager::self()->fetchers(Kernel::self()->collectionType()); + for(Fetch::FetcherVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + KAction* action = new KAction(actionCollection()); + action->setText(it->source()); + action->setToolTip(i18n("Update entry data from %1").arg(it->source())); + action->setIconSet(Fetch::Manager::fetcherIcon(it.data())); + connect(action, SIGNAL(activated()), mapper, SLOT(map())); + mapper->setMapping(action, it->source()); + m_fetchActions.append(action); + } + + plugActionList(QString::fromLatin1("update_entry_actions"), m_fetchActions); +} + +void MainWindow::importFile(Import::Format format_, const KURL::List& urls_) { + KURL::List urls = urls_; + // update as DropHandler and Importer classes are updated + if(urls_.count() > 1 && + format_ != Import::Bibtex && + format_ != Import::RIS && + format_ != Import::PDF) { + KURL u = urls_.front(); + QString url = u.isLocalFile() ? u.path() : u.prettyURL(); + Kernel::self()->sorry(i18n("Tellico can only import one file of this type at a time. " + "Only %1 will be imported.").arg(url)); + urls.clear(); + urls = u; + } + + ImportDialog dlg(format_, urls, this, "importdlg"); + if(dlg.exec() != QDialog::Accepted) { + return; + } + +// if edit dialog is saved ok and if replacing, then the doc is saved ok + if(m_editDialog->queryModified() && + (dlg.action() != Import::Replace || Data::Document::self()->saveModified())) { + GUI::CursorSaver cs(Qt::waitCursor); + Data::CollPtr coll = dlg.collection(); + if(!coll) { + if(!dlg.statusMessage().isEmpty()) { + Kernel::self()->sorry(dlg.statusMessage()); + } + return; + } + importCollection(coll, dlg.action()); + } +} + +bool MainWindow::importCollection(Data::CollPtr coll_, Import::Action action_) { + bool failed = false; + switch(action_) { + case Import::Append: + { + // only append if match, but special case importing books into bibliographies + Data::CollPtr c = Data::Document::self()->collection(); + if(c->type() == coll_->type() + || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) { + Kernel::self()->appendCollection(coll_); + slotEnableModifiedActions(true); + } else { + Kernel::self()->sorry(i18n(errorAppendType)); + failed = true; + } + } + break; + + case Import::Merge: + { + // only merge if match, but special case importing books into bibliographies + Data::CollPtr c = Data::Document::self()->collection(); + if(c->type() == coll_->type() + || (c->type() == Data::Collection::Bibtex && coll_->type() == Data::Collection::Book)) { + Kernel::self()->mergeCollection(coll_); + slotEnableModifiedActions(true); + } else { + Kernel::self()->sorry(i18n(errorMergeType)); + failed = true; + } + } + break; + + default: // replace + Kernel::self()->replaceCollection(coll_); + m_fileOpenRecent->setCurrentItem(-1); + m_newDocument = true; + slotEnableOpenedActions(); + slotEnableModifiedActions(false); + break; + } + return !failed; +} + +void MainWindow::slotURLAction(const KURL& url_) { + Q_ASSERT(url_.protocol() == Latin1Literal("tc")); + QString actionName = url_.fileName(); + KAction* action = this->action(actionName); + if(action) { + action->activate(); + } else { + myWarning() << "MainWindow::slotURLAction() - unknown action: " << actionName << endl; + } +} + +#include "mainwindow.moc" diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..96ed71a --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,534 @@ +/*************************************************************************** + 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_MAINWINDOW_H +#define TELLICO_MAINWINDOW_H + +#include <config.h> + +#include "core/dcopinterface.h" +#include "translators/translators.h" +#include "datavectors.h" + +#include <kmainwindow.h> + +class KToolBar; +class KURL; +class KAction; +class KSelectAction; +class KToggleAction; +class KRecentFilesAction; +class KActionMenu; +class KDialogBase; + +class QCloseEvent; +class QSplitter; +class QListViewItem; + +namespace Tellico { +// forward declarations + namespace GUI { + class LineEdit; + class TabControl; + } + class Controller; + class ViewStack; + class DetailedListView; + class FilterDialog; + class EntryEditDialog; + class GroupView; + class FilterView; + class LoanView; + class ConfigDialog; + class CollectionFieldsDialog; + class StringMapDialog; + class EntryItem; + class FetchDialog; + class ReportDialog; + class StatusBar; + class DropHandler; + +/** + * The base class for Tellico application windows. It sets up the main + * window and reads the config file as well as providing a menubar, toolbar + * and statusbar. Tellico reimplements the methods that KMainWindow provides + * for main window handling and supports full session management as well as + * using KActions. + * @see KMainWindow + * @see KApplication + * @see KConfig + * + * @author Robby Stephenson + */ +class MainWindow : public KMainWindow, public ApplicationInterface { +Q_OBJECT + +friend class Controller; +friend class DropHandler; + +public: + /** + * The main window constructor, calls all init functions to create the application. + */ + MainWindow(QWidget* parent=0, const char* name=0); + + /** + * Opens the initial file. + * + * @param nofile If true, even if the config option to reopen last file is set, no file is opened + */ + void initFileOpen(bool nofile); + /** + * Saves the document + * + * @return Returns @p true if successful + */ + bool fileSave(); + /** + * Saves a document by a new filename + * + * @return Returns @p true if successful + */ + bool fileSaveAs(); + /** + * @return Returns whether the current collection is still the non-saved default one + */ + bool isNewDocument() const { return m_newDocument; } + /** + * Used by main() and DCOP to import file. + * + * @param format The file format + * @param url The url + */ + virtual bool importFile(Import::Format format, const KURL& url, Import::Action action); + /** + * Used by DCOP to export to a file. + */ + virtual bool exportCollection(Export::Format format, const KURL& url); + /** + * Used by DCOP + */ + virtual void openFile(const QString& file); + virtual void setFilter(const QString& text); + virtual bool showEntry(long id); + +public slots: + /** + * Initializes some stuff after the object is created. + */ + void slotInit(); + /** + * Cleans up everything and then opens a new document. + * + * @param type Type of collection to add + */ + void slotFileNew(int type); + /** + * Opens a file and loads it into the document + */ + void slotFileOpen(); + /** + * Opens a file by URL and loads it into the document + * + * @param url The url to open + */ + void slotFileOpen(const KURL& url); + /** + * Opens a file from the recent files menu + * + * @param url The url sent by the RecentFilesAction + */ + void slotFileOpenRecent(const KURL& url); + /** + * Saves the document + */ + void slotFileSave(); + /** + * Saves a document by a new filename + */ + void slotFileSaveAs(); + /** + * Prints the current document. + */ + void slotFilePrint(); + /** + * Quits the application. + */ + void slotFileQuit(); + /** + * Puts the marked text/object into the clipboard and removes it from the document. + */ + void slotEditCut(); + /* + * Puts the marked text/object into the clipboard. + */ + void slotEditCopy(); + /** + * Pastes the clipboard into the document. + */ + void slotEditPaste(); + /** + * Selects all the entries in the collection. + */ + void slotEditSelectAll(); + /** + * Deselects all the entries in the collection. + */ + void slotEditDeselect(); + /** + * Toggles the group widget. + */ + void slotToggleGroupWidget(); + /** + * Toggles the edit widget. + */ + void slotToggleEntryEditor(); + /** + * Toggles the edit widget. + */ + void slotToggleEntryView(); + /** + * Shows the configuration dialog for the application. + */ + void slotShowConfigDialog(); + /** + * Hides the configuration dialog for the application. + */ + void slotHideConfigDialog(); + /** + * Shows the fetch dialog. + */ + void slotShowFetchDialog(); + /** + * Hides the fetch dialog. + */ + void slotHideFetchDialog(); + /** + * Changes the statusbar contents for the standard label permanently, + * used to indicate current actions being made. + * + * @param text The text that is displayed in the statusbar + */ + void slotStatusMsg(const QString& text); + void slotClearStatus(); + /** + * Shows the configuration window for the toolbars. + */ + void slotConfigToolbar(); + /** + * Updates the toolbars; + */ + void slotNewToolbarConfig(); + /** + * Shows the configuration window for the key bindgins. + */ + void slotConfigKeys(); + /** + * Updates the entry count in the status bar. + */ + void slotEntryCount(); + /** + * Handles updating everything when the configuration is changed + * via the configuration dialog. This slot is called when the OK or Apply + * button is clicked in the dialog + */ + void slotHandleConfigChange(); + /** + * Changes the grouping of the entries in the @ref GroupView. The current value + * of the combobox in the toolbar is used. + */ + void slotChangeGrouping(); + /** + * Imports data. + * + * @param format The import format + */ + void slotFileImport(int format); + /** + * Exports the current document. + * + * @param format The export format + */ + void slotFileExport(int format); + /** + * Shows the filter dialog for the application. + */ + void slotShowFilterDialog(); + /** + * Hides the filter dialog for the application. + */ + void slotHideFilterDialog(); + /** + * Shows the collection properties dialog for the application. + */ + void slotShowCollectionFieldsDialog(); + /** + * Hides the collection properties dialog for the application. + */ + void slotHideCollectionFieldsDialog(); + /** + * Shows the "Tip of the Day" dialog. + * + * @param force Whether the configuration setting should be ignored + */ + void slotShowTipOfDay(bool force=true); + /** + * Shows the string macro editor dialog for the application. + */ + void slotShowStringMacroDialog(); + /** + * Hides the string macro editor dialog for the application. + */ + void slotHideStringMacroDialog(); + /** + * Handle a url that indicates some actino should be taken + */ + void slotURLAction(const KURL& url); + +private: + /** + * Saves the general options like all toolbar positions and status as well as the + * geometry and the recent file list to the configuration file. + */ + void saveOptions(); + /** + * Reads the specific options. + */ + void readOptions(); + /** + * Initializes the KActions of the application + */ + void initActions(); + /** + * Sets up the statusbar for the main window by initializing a status label + * and inserting a progress bar and entry counter. + */ + void initStatusBar(); + /** + * Initiates the view, setting up all the dock windows and so on. + */ + void initView(); + /** + * Initiates the document. + */ + void initDocument(); + /** + * Initiates all the signal and slot connections between major objects in the view. + */ + void initConnections(); + /** + * Initiates shutdown + */ +// void closeEvent(QCloseEvent *e); + /** + * Saves the window properties for each open window during session end to the + * session config file, including saving the currently opened file by a temporary + * filename provided by KApplication. + * @see KMainWindow::saveProperties + * + * @param cfg The config class with the properties to restore + */ + void saveProperties(KConfig* cfg); + /** + * Reads the session config file and restores the application's state including + * the last opened files and documents by reading the temporary files saved by + * @ref saveProperties(). + * @see KMainWindow::readProperties + * + * @param cfg The config class with the properties to restore + */ + void readProperties(KConfig* cfg); + /** + * Called before the window is closed, either by the user or indirectely by the + * session manager. + * + * The purpose of this function is to prepare the window in a way that it is safe to close it, + * i.e. without the user losing some data. + * @see KMainWindow::queryClose + */ + bool queryClose(); + /** + * Called before the very last window is closed, either by the user + * or indirectly by the session manager. + * @see KMainWindow::queryExit + */ + bool queryExit(); + /** + * Actual method used when opening a URL. Updating for the list views is turned off + * as well as sorting, in order to more quickly load the document. + * + * @param url The url to open + */ + bool openURL(const KURL& url); + /* + * Helper method to handle the printing duties. + * + * @param html The HTML string representing the doc to print + */ + void doPrint(const QString& html); + + void XSLTError(); + /** + * Helper function to activate a slot in the edit widget. + * Primarily used for copy, cut, and paste. + * + * @param slot The slot name + */ + void activateEditSlot(const char* slot); + void addFilterView(); + void addLoanView(); + void updateCaption(bool modified); + void updateCollectionActions(); + void updateEntrySources(); + +private slots: + /** + * Updates the actions when a file is opened. + */ + void slotEnableOpenedActions(); + /** + * Updates the save action and the caption when the document is modified. + */ + void slotEnableModifiedActions(bool modified = true); + /** + * Read the options specific to a collection + * + * @param coll The collection pointer + */ + void readCollectionOptions(Tellico::Data::CollPtr coll); + /** + * Saves the options relevant for a collection. I was having problems with the collection + * being destructed before I could save info. + * + * @param coll A pointer to the collection + */ + void saveCollectionOptions(Tellico::Data::CollPtr coll); + /** + * Queue a filter update. The timer adds a 200 millisecond delay before actually + * updating the filter. + */ + void slotQueueFilter(); + /** + * Update the filter to match any field with text. If a non-word character appears, the + * text is interpreted as a regexp. + */ + void slotUpdateFilter(); + /** + * Updates the collection toolbar. + */ + void slotUpdateCollectionToolBar(Tellico::Data::CollPtr coll); + /** + * Make sure the edit dialog is visible and start a new entry. + */ + void slotNewEntry(); + /** + * Handle the entry editor dialog being closed. + */ + void slotEditDialogFinished(); + /** + * Handle the Ok button being clicked in the string macros dialog. + */ + void slotStringMacroDialogOk(); + /** + * Since I use an application icon in the toolbar, I need to change its size whenever + * the toolbar changes mode + */ + void slotUpdateToolbarIcons(); + /** + * Convert current collection to a bibliography. + */ + void slotConvertToBibliography(); + /** + * Send a citation for the selected entries + */ + void slotCiteEntry(int action); + /** + * Show the entry editor and update menu item. + */ + void slotShowEntryEditor(); + /** + * Show the report window. + */ + void slotShowReportDialog(); + void slotGroupLabelActivated(); + /** + * Show the report window. + */ + void slotHideReportDialog(); + /** + * Focus the filter + */ + void slotFilterLabelActivated(); + void slotClearFilter(); + void slotRenameCollection(); + +private: + void importFile(Import::Format format, const KURL::List& kurls); + bool importCollection(Data::CollPtr coll, Import::Action action); + + // the reason that I have to keep pointers to all these + // is because they get plugged into menus later in Controller + KRecentFilesAction* m_fileOpenRecent; + KAction* m_fileSave; + KAction* m_newEntry; + KAction* m_editEntry; + KAction* m_copyEntry; + KAction* m_deleteEntry; + KAction* m_mergeEntry; + KActionMenu* m_updateEntryMenu; + KAction* m_updateAll; + KAction* m_checkInEntry; + KAction* m_checkOutEntry; + KToggleAction* m_toggleGroupWidget; + KToggleAction* m_toggleEntryEditor; + KToggleAction* m_toggleEntryView; + + KSelectAction* m_entryGrouping; + GUI::LineEdit* m_quickFilter; + + // m_split is used between the stuff on the left and stuff on the right + QSplitter* m_split; + // m_leftSplit is used between detailed view and entry view + QSplitter* m_rightSplit; + + Tellico::StatusBar* m_statusBar; + + DetailedListView* m_detailedView; + EntryEditDialog* m_editDialog; + GUI::TabControl* m_viewTabs; + GroupView* m_groupView; + FilterView* m_filterView; + LoanView* m_loanView; + ViewStack* m_viewStack; + + ConfigDialog* m_configDlg; + FilterDialog* m_filterDlg; + CollectionFieldsDialog* m_collFieldsDlg; + StringMapDialog* m_stringMacroDlg; + FetchDialog* m_fetchDlg; + ReportDialog* m_reportDlg; + + QPtrList<KAction> m_fetchActions; + CollectionInterface m_collInterface; + + // keep track of the number of queued filter updates + uint m_queuedFilters; + + // keep track whether everything gets initialized + bool m_initialized : 1; + // need to keep track of whether the current collection has never been saved + bool m_newDocument : 1; +}; + +} // end namespace +#endif // TELLICO_MAINWINDOW_H diff --git a/src/newstuff/Makefile.am b/src/newstuff/Makefile.am new file mode 100644 index 0000000..9054dcb --- /dev/null +++ b/src/newstuff/Makefile.am @@ -0,0 +1,18 @@ +noinst_LIBRARIES = libnewstuff.a + +libnewstuff_a_METASOURCES = AUTO + +libnewstuff_a_SOURCES = manager.cpp dialog.cpp newscript.cpp providerloader.cpp + +CLEANFILES = *~ + +EXTRA_DIST = \ +manager.h manager.cpp \ +dialog.h dialog.cpp \ +newscript.h newscript.cpp \ +providerloader.h providerloader.cpp + +AM_CPPFLAGS = $(all_includes) + +KDE_OPTIONS = noautodist + diff --git a/src/newstuff/dialog.cpp b/src/newstuff/dialog.cpp new file mode 100644 index 0000000..c13a249 --- /dev/null +++ b/src/newstuff/dialog.cpp @@ -0,0 +1,477 @@ +/*************************************************************************** + copyright : (C) 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 "dialog.h" +#include "providerloader.h" +#include "../gui/listview.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" + +#include <klocale.h> +#include <kpushbutton.h> +#include <kconfig.h> +#include <kiconloader.h> +#include <kstatusbar.h> +#include <kio/job.h> +#include <kio/netaccess.h> +#include <kaccelmanager.h> +#include <knewstuff/entry.h> +#include <knewstuff/provider.h> +#include <ktempfile.h> + +#include <qlabel.h> +#include <qtextedit.h> +#include <qlayout.h> +#include <qwhatsthis.h> +#include <qregexp.h> +#include <qvbox.h> +#include <qimage.h> +#include <qtimer.h> +#include <qprogressbar.h> + +#if KDE_IS_VERSION(3,4,90) +#define ENTRYNAME(e) e->name(m_lang) +#define ENTRYSUMM(e) e->summary(m_lang) +#define ENTRYEMAIL(e) e->authorEmail() +#else +#define ENTRYNAME(e) e->name() +#define ENTRYSUMM(e) e->summary() +#define ENTRYEMAIL(e) QString() +#endif + +namespace { + static const int NEW_STUFF_MIN_WIDTH = 600; + static const int NEW_STUFF_MIN_HEIGHT = 400; + static const int PROGRESS_STATUS_ID = 0; +} + +using Tellico::NewStuff::Dialog; + +class Dialog::Item : public GUI::ListViewItem { +public: + Item(GUI::ListView* parent) : GUI::ListViewItem(parent) {} + + InstallStatus status() const { return m_status; } + void setStatus(InstallStatus status) { + m_status = status; + if(m_status == Current) { + setPixmap(0, SmallIcon(QString::fromLatin1("ok"))); + } else if(m_status == OldVersion) { + setPixmap(0, SmallIcon(QString::fromLatin1("reload"))); + } + } + + QString key(int col, bool asc) const { + if(col == 2 || col == 3) { + QString s; + s.sprintf("%08d", text(col).toInt()); + return s; + } else if(col == 4) { + QString s; + QDate date = KGlobal::locale()->readDate(text(col)); + s.sprintf("%08d", date.year() * 366 + date.dayOfYear()); + return s; + } + return GUI::ListViewItem::key(col, asc); + } + +private: + InstallStatus m_status; +}; + +Dialog::Dialog(NewStuff::DataType type_, QWidget* parent_) + : KDialogBase(KDialogBase::Plain, i18n("Get Hot New Stuff"), 0, (KDialogBase::ButtonCode)0, parent_) + , m_manager(new Manager(this)) + , m_type(type_) + , m_timer(new QTimer(this)) + , m_cursorSaver(new GUI::CursorSaver()) + , m_tempPreviewImage(0) + , m_lastPreviewItem(0) { + + m_lang = KGlobal::locale()->language(); + + QFrame* frame = plainPage(); + QBoxLayout* boxLayout = new QVBoxLayout(frame, 0, KDialog::spacingHint()); + + m_split = new QSplitter(Qt::Vertical, frame); + boxLayout->addWidget(m_split); + + m_listView = new GUI::ListView(m_split); + m_listView->setAllColumnsShowFocus(true); + m_listView->setSelectionMode(QListView::Single); + m_listView->addColumn(i18n("Name")); + m_listView->addColumn(i18n("Version")); + m_listView->addColumn(i18n("Rating")); + m_listView->addColumn(i18n("Downloads")); + m_listView->addColumn(i18n("Release Date")); + m_listView->setSorting(2, false); + m_listView->setResizeMode(QListView::AllColumns); + connect(m_listView, SIGNAL(clicked(QListViewItem*)), SLOT(slotSelected(QListViewItem*))); + QWhatsThis::add(m_listView, i18n("This is a list of all the items available for download. " + "Previously installed items have a checkmark icon, while " + "items with new version available have an update icon")); + + QWidget* widget = new QWidget(m_split); + QBoxLayout* boxLayout2 = new QVBoxLayout(widget, 0, KDialog::spacingHint()); + + m_iconLabel = new QLabel(widget); + m_iconLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); + m_iconLabel->setMargin(0); + + m_nameLabel = new QLabel(widget); + QFont font = m_nameLabel->font(); + font.setBold(true); + font.setItalic(true); + m_nameLabel->setFont(font); + QWhatsThis::add(m_nameLabel, i18n("The name and license of the selected item")); + + m_infoLabel = new QLabel(widget); + QWhatsThis::add(m_infoLabel, i18n("The author of the selected item")); + + m_install = new KPushButton(i18n("Install"), widget); + m_install->setIconSet(SmallIconSet(QString::fromLatin1("knewstuff"))); + m_install->setEnabled(false); + connect(m_install, SIGNAL(clicked()), SLOT(slotInstall())); + + // the button's text changes later + // I don't want it resizing, so figure out the maximum size and set that + m_install->polish(); + int maxWidth = m_install->sizeHint().width(); + int maxHeight = m_install->sizeHint().height(); + m_install->setGuiItem(KGuiItem(i18n("Update"), SmallIconSet(QString::fromLatin1("knewstuff")))); + maxWidth = QMAX(maxWidth, m_install->sizeHint().width()); + maxHeight = QMAX(maxHeight, m_install->sizeHint().height()); + m_install->setMinimumWidth(maxWidth); + m_install->setMinimumHeight(maxHeight); + + QPixmap pix; + if(m_type == EntryTemplate) { + pix = DesktopIcon(QString::fromLatin1("looknfeel"), KIcon::SizeLarge); + QWhatsThis::add(m_install, i18n("Download and install the selected template.")); + } else { + pix = UserIcon(QString::fromLatin1("script")); + QWhatsThis::add(m_install, i18n("Download and install the selected script. Some scripts " + "may need to be configured after being installed.")); + } + m_iconLabel->setPixmap(pix); + + QBoxLayout* boxLayout3 = new QHBoxLayout(boxLayout2); + + QBoxLayout* boxLayout4 = new QVBoxLayout(boxLayout3); + boxLayout4->addWidget(m_iconLabel); + boxLayout4->addStretch(10); + + boxLayout3->addSpacing(4); + + QBoxLayout* boxLayout5 = new QVBoxLayout(boxLayout3); + boxLayout5->addWidget(m_nameLabel); + boxLayout5->addWidget(m_infoLabel); + boxLayout5->addStretch(10); + + boxLayout3->addStretch(10); + + QBoxLayout* boxLayout6 = new QVBoxLayout(boxLayout3); + boxLayout6->addWidget(m_install); + boxLayout6->addStretch(10); + + m_descLabel = new QTextEdit(widget); + m_descLabel->setReadOnly(true); + m_descLabel->setTextFormat(Qt::RichText); + m_descLabel->setPaper(colorGroup().background()); + m_descLabel->setMinimumHeight(5 * fontMetrics().height()); + boxLayout2->addWidget(m_descLabel, 10); + QWhatsThis::add(m_descLabel, i18n("A description of the selected item is shown here.")); + + QHBox* box = new QHBox(frame, "statusbox"); + boxLayout->addWidget(box); + box->setSpacing(KDialog::spacingHint()); + + m_statusBar = new KStatusBar(box, "statusbar"); + m_statusBar->insertItem(QString::null, PROGRESS_STATUS_ID, 1, false); + m_statusBar->setItemAlignment(PROGRESS_STATUS_ID, AlignLeft | AlignVCenter); + m_progress = new QProgressBar(m_statusBar, "progress"); + m_progress->setTotalSteps(0); + m_progress->setFixedHeight(fontMetrics().height()+2); + m_statusBar->addWidget(m_progress, 0, true); + + KPushButton* closeButton = new KPushButton(KStdGuiItem::close(), box); + connect(closeButton, SIGNAL(clicked()), SLOT(slotClose())); + closeButton->setFocus(); + + connect(m_timer, SIGNAL(timeout()), SLOT(slotMoveProgress())); + + setMinimumWidth(QMAX(minimumWidth(), NEW_STUFF_MIN_WIDTH)); + setMinimumHeight(QMAX(minimumHeight(), NEW_STUFF_MIN_HEIGHT)); + resize(configDialogSize(QString::fromLatin1("NewStuff Dialog Options"))); + + KConfigGroup dialogConfig(KGlobal::config(), "NewStuff Dialog Options"); + QValueList<int> splitList = dialogConfig.readIntListEntry("Splitter Sizes"); + if(!splitList.empty()) { + m_split->setSizes(splitList); + } + + setStatus(i18n("Downloading information...")); + + ProviderLoader* loader = new Tellico::NewStuff::ProviderLoader(this); + connect(loader, SIGNAL(providersLoaded(QPtrList<KNS::Provider>*)), SLOT(slotProviders(QPtrList<KNS::Provider>*))); + connect(loader, SIGNAL(percent(KIO::Job*, unsigned long)), SLOT(slotShowPercent(KIO::Job*, unsigned long))); + connect(loader, SIGNAL(error()), SLOT(slotProviderError())); + + KConfigGroup config(KGlobal::config(), "KNewStuff"); + QString prov = config.readEntry("ProvidersUrl"); + if(prov.isEmpty()) { + if(m_type == EntryTemplate) { + prov = QString::fromLatin1("http://periapsis.org/tellico/newstuff/tellicotemplates-providers.php"); + QString alt = QString::fromLatin1("http://download.kde.org/khotnewstuff/tellicotemplates-providers.xml"); + loader->setAlternativeProvider(alt); + } else { + prov = QString::fromLatin1("http://periapsis.org/tellico/newstuff/tellicoscripts-providers.php"); + } + } + if(m_type == EntryTemplate) { + m_typeName = QString::fromLatin1("tellico/entry-template"); + } else { + m_typeName = QString::fromLatin1("tellico/data-source"); + } + loader->load(m_typeName, prov); + + KAcceleratorManager::manage(this); +} + +Dialog::~Dialog() { + delete m_cursorSaver; + m_cursorSaver = 0; + + saveDialogSize(QString::fromLatin1("NewStuff Dialog Options")); + KConfigGroup config(KGlobal::config(), "NewStuff Dialog Options"); + config.writeEntry("Splitter Sizes", m_split->sizes()); +} + +void Dialog::slotProviderError() { + if(m_listView->childCount() == 0) { + myDebug() << "NewStuff::Dialog::slotCheckError() - no available items" << endl; + setStatus(QString()); + + delete m_cursorSaver; + m_cursorSaver = 0; + } +} + +void Dialog::slotProviders(QPtrList<KNS::Provider>* list_) { + for(KNS::Provider* prov = list_->first(); prov; prov = list_->next()) { + KIO::TransferJob* job = KIO::get(prov->downloadUrl()); + m_jobs[job] = prov; + connect(job, SIGNAL(data(KIO::Job*, const QByteArray&)), + SLOT(slotData(KIO::Job*, const QByteArray&))); + connect(job, SIGNAL(result(KIO::Job*)), SLOT(slotResult(KIO::Job*))); + connect(job, SIGNAL(percent(KIO::Job*, unsigned long)), + SLOT(slotShowPercent(KIO::Job*, unsigned long))); + } +} + +void Dialog::slotData(KIO::Job* job_, const QByteArray& data_) { + QDataStream stream(m_data[job_], IO_WriteOnly | IO_Append); + stream.writeRawBytes(data_.data(), data_.size()); +} + +void Dialog::slotResult(KIO::Job* job_) { +// myDebug() << "NewStuff::Dialog::slotResult()" << endl; + QDomDocument dom; + if(!dom.setContent(m_data[job_])) { + KNS::Provider* prov = m_jobs[job_]; + KURL u = prov ? prov->downloadUrl() : KURL(); + myDebug() << "NewStuff::Dialog::slotResult() - can't load result: " << u.url() << endl; + m_jobs.remove(job_); + if(m_jobs.isEmpty()) { + setStatus(i18n("Ready.")); + delete m_cursorSaver; + m_cursorSaver = 0; + } + return; + } + + QDomElement knewstuff = dom.documentElement(); + + for(QDomNode pn = knewstuff.firstChild(); !pn.isNull(); pn = pn.nextSibling()) { + QDomElement stuff = pn.toElement(); + if(stuff.isNull()) { + continue; + } + + if(stuff.tagName() == Latin1Literal("stuff")) { + KNS::Entry* entry = new KNS::Entry(stuff); + if(!entry->type().isEmpty() && entry->type() != m_typeName) { + myLog() << "NewStuff::Dialog::slotResult() - type mismatch, skipping " << ENTRYNAME(entry) << endl; + continue; + } + + addEntry(entry); + } + } + m_jobs.remove(job_); + if(m_jobs.isEmpty()) { + setStatus(i18n("Ready.")); + delete m_cursorSaver; + m_cursorSaver = 0; + } +} + +void Dialog::addEntry(KNS::Entry* entry_) { + if(!entry_) { + return; + } + + Item* item = new Item(m_listView); + item->setText(0, ENTRYNAME(entry_)); + item->setText(1, entry_->version()); + item->setText(2, QString::number(entry_->rating())); + item->setText(3, QString::number(entry_->downloads())); + item->setText(4, KGlobal::locale()->formatDate(entry_->releaseDate(), true /*short format */)); + item->setStatus(NewStuff::Manager::installStatus(entry_)); + m_entryMap.insert(item, entry_); + + if(!m_listView->selectedItem()) { + m_listView->setSelected(item, true); + slotSelected(item); + } +} + +void Dialog::slotSelected(QListViewItem* item_) { + if(!item_) { + return; + } + + KNS::Entry* entry = m_entryMap[item_]; + if(!entry) { + return; + } + + KURL preview = entry->preview(m_lang); + if(!preview.isEmpty() && preview.isValid()) { + delete m_tempPreviewImage; + m_tempPreviewImage = new KTempFile(); + m_tempPreviewImage->setAutoDelete(true); + KURL dest; + dest.setPath(m_tempPreviewImage->name()); + KIO::FileCopyJob* job = KIO::file_copy(preview, dest, -1, true, false, false); + connect(job, SIGNAL(result(KIO::Job*)), SLOT(slotPreviewResult(KIO::Job*))); + connect(job, SIGNAL(percent(KIO::Job*, unsigned long)), + SLOT(slotShowPercent(KIO::Job*, unsigned long))); + m_lastPreviewItem = item_; + } + QPixmap pix = m_type == EntryTemplate + ? DesktopIcon(QString::fromLatin1("looknfeel"), KIcon::SizeLarge) + : UserIcon(QString::fromLatin1("script")); + m_iconLabel->setPixmap(pix); + + QString license = entry->license(); + if(!license.isEmpty()) { + license.prepend('(').append(')'); + } + QString name = QString::fromLatin1("%1 %2").arg(ENTRYNAME(entry)).arg(license); + QFont font = m_nameLabel->font(); + font.setBold(true); + font.setItalic(false); + m_nameLabel->setFont(font); + m_nameLabel->setText(name); + + m_infoLabel->setText(entry->author()); + + QString desc = entry->summary(m_lang); + desc.replace(QRegExp(QString::fromLatin1("\\n")), QString::fromLatin1("<br>")); + m_descLabel->setText(desc); + + InstallStatus installed = static_cast<Item*>(item_)->status(); + m_install->setText(installed == OldVersion ? i18n("Update Stuff", "Update") : i18n("Install")); + m_install->setEnabled(installed != Current); +} + +void Dialog::slotInstall() { + QListViewItem* item = m_listView->currentItem(); + if(!item) { + return; + } + + KNS::Entry* entry = m_entryMap[item]; + if(!entry) { + return; + } + + delete m_cursorSaver; + m_cursorSaver = new GUI::CursorSaver(); + setStatus(i18n("Installing item...")); + m_progress->show(); + m_timer->start(100); + connect(m_manager, SIGNAL(signalInstalled(KNS::Entry*)), SLOT(slotDoneInstall(KNS::Entry*))); + m_manager->install(m_type, entry); + delete m_cursorSaver; + m_cursorSaver = 0; +} + +void Dialog::slotDoneInstall(KNS::Entry* entry_) { + QMap<QListViewItem*, KNS::Entry*>::Iterator it; + for(it = m_entryMap.begin(); entry_ && it != m_entryMap.end(); ++it) { + if(it.data() == entry_) { + InstallStatus installed = Manager::installStatus(entry_); + static_cast<Item*>(it.key())->setStatus(installed); + m_install->setEnabled(installed != Current); + break; + } + } + delete m_cursorSaver; + m_cursorSaver = 0; + setStatus(i18n("Ready.")); + m_timer->stop(); + m_progress->hide(); +} + +void Dialog::slotMoveProgress() { + m_progress->setProgress(m_progress->progress()+5); +} + +void Dialog::setStatus(const QString& text_) { + m_statusBar->changeItem(QChar(' ') + text_, PROGRESS_STATUS_ID); +} + +void Dialog::slotShowPercent(KIO::Job*, unsigned long pct_) { + if(pct_ >= 100) { + m_progress->hide(); + } else { + m_progress->show(); + m_progress->setProgress(static_cast<int>(pct_), 100); + } +} + +void Dialog::slotPreviewResult(KIO::Job* job_) { + KIO::FileCopyJob* job = static_cast<KIO::FileCopyJob*>(job_); + if(job->error()) { + return; + } + QString tmpFile = job->destURL().path(); // might be different than m_tempPreviewImage->name() + QPixmap pix(tmpFile); + + if(!pix.isNull()) { + if(pix.width() > 64 || pix.height() > 64) { + pix.convertFromImage(pix.convertToImage().smoothScale(64, 64, QImage::ScaleMin)); + } + // only set label if it's still current + if(m_listView->selectedItem() == m_lastPreviewItem) { + m_iconLabel->setPixmap(pix); + } + } + delete m_tempPreviewImage; + m_tempPreviewImage = 0; +} + +#include "dialog.moc" diff --git a/src/newstuff/dialog.h b/src/newstuff/dialog.h new file mode 100644 index 0000000..f1c5e48 --- /dev/null +++ b/src/newstuff/dialog.h @@ -0,0 +1,101 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_NEWSTUFF_DIALOG_H +#define TELLICO_NEWSTUFF_DIALOG_H + +#include "manager.h" + +#include <kdialogbase.h> + +class KPushButton; +class KStatusBar; +namespace KIO { + class Job; +} +namespace KNS { + class Entry; + class Provider; +} + +class QProgressBar; +class QSplitter; +class QLabel; +class QTextEdit; + +namespace Tellico { + namespace GUI { + class ListView; + class CursorSaver; + } + + namespace NewStuff { + +class Dialog : public KDialogBase { +Q_OBJECT + +public: + Dialog(DataType type, QWidget* parent); + virtual ~Dialog(); + + QPtrList<DataSourceInfo> dataSourceInfo() const { return m_manager->dataSourceInfo(); } + +private slots: + void slotProviders(QPtrList<KNS::Provider>* list); + void slotData(KIO::Job* job, const QByteArray& data); + void slotResult(KIO::Job* job); + void slotPreviewResult(KIO::Job* job); + + void slotShowPercent(KIO::Job* job, unsigned long percent); + + void slotSelected(QListViewItem* item); + void slotInstall(); + void slotDoneInstall(KNS::Entry* entry); + + void slotProviderError(); + void slotMoveProgress(); + +private: + class Item; + + void setStatus(const QString& status); + void addEntry(KNS::Entry* entry); + + Manager* const m_manager; + DataType m_type; + QString m_lang; + QString m_typeName; + + QSplitter* m_split; + GUI::ListView* m_listView; + QLabel* m_iconLabel; + QLabel* m_nameLabel; + QLabel* m_infoLabel; + QTextEdit* m_descLabel; + KPushButton* m_install; + KStatusBar* m_statusBar; + QProgressBar* m_progress; + QTimer* m_timer; + GUI::CursorSaver* m_cursorSaver; + KTempFile* m_tempPreviewImage; + + QMap<KIO::Job*, KNS::Provider*> m_jobs; + QMap<KIO::Job*, QByteArray> m_data; + + QMap<QListViewItem*, KNS::Entry*> m_entryMap; + QListViewItem* m_lastPreviewItem; +}; + + } +} +#endif diff --git a/src/newstuff/manager.cpp b/src/newstuff/manager.cpp new file mode 100644 index 0000000..3b7efbf --- /dev/null +++ b/src/newstuff/manager.cpp @@ -0,0 +1,446 @@ +/*************************************************************************** + copyright : (C) 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 "manager.h" +#include "newscript.h" +#include "../filehandler.h" +#include "../tellico_debug.h" +#include "../tellico_utils.h" +#include "../tellico_kernel.h" +#include "../fetch/fetch.h" + +#include <kurl.h> +#include <ktar.h> +#include <kglobal.h> +#include <kio/netaccess.h> +#include <kconfig.h> +#include <ktempfile.h> +#include <kio/job.h> +#include <kfileitem.h> +#include <kdeversion.h> +#include <knewstuff/entry.h> +#include <kstandarddirs.h> + +#include <qfileinfo.h> +#include <qdir.h> +#include <qptrstack.h> +#include <qvaluestack.h> +#include <qwidget.h> + +#include <sys/types.h> +#include <sys/stat.h> + +using Tellico::NewStuff::Manager; + +Manager::Manager(QObject* parent_) : QObject(parent_), m_tempFile(0) { + m_infoList.setAutoDelete(true); +} + +Manager::~Manager() { + delete m_tempFile; + m_tempFile = 0; +} + +bool Manager::installTemplate(const KURL& url_, const QString& entryName_) { + FileHandler::FileRef* ref = FileHandler::fileRef(url_); + + QString xslFile; + QStringList allFiles; + + bool success = true; + + // is there a better way to figure out if the url points to a XSL file or a tar archive + // than just trying to open it? + KTar archive(ref->fileName()); + if(archive.open(IO_ReadOnly)) { + const KArchiveDirectory* archiveDir = archive.directory(); + archiveDir->copyTo(Tellico::saveLocation(QString::fromLatin1("entry-templates/"))); + + allFiles = archiveFiles(archiveDir); + // remember files installed for template + xslFile = findXSL(archiveDir); + } else { // assume it's an xsl file + QString name = entryName_; + if(name.isEmpty()) { + name = url_.fileName(); + } + if(!name.endsWith(QString::fromLatin1(".xsl"))) { + name += QString::fromLatin1(".xsl"); + } + KURL dest; + dest.setPath(Tellico::saveLocation(QString::fromLatin1("entry-templates/")) + name); + success = true; + if(QFile::exists(dest.path())) { + myDebug() << "Manager::installTemplate() - " << dest.path() << " exists!" << endl; + success = false; + } else if(KIO::NetAccess::file_copy(url_, dest)) { + xslFile = dest.fileName(); + allFiles += xslFile; + } + } + + if(xslFile.isEmpty()) { + success = false; + } else { + // remove ".xsl" + xslFile.truncate(xslFile.length()-4); + KConfigGroup config(KGlobal::config(), "KNewStuffFiles"); + config.writeEntry(xslFile, allFiles); + m_urlNameMap.insert(url_, xslFile); + } + + checkCommonFile(); + + delete ref; + return success; +} + +bool Manager::removeTemplate(const QString& name_) { + KConfigGroup fileGroup(KGlobal::config(), "KNewStuffFiles"); + QStringList files = fileGroup.readListEntry(name_); + // at least, delete xsl file + if(files.isEmpty()) { + kdWarning() << "Manager::deleteTemplate() no file list found for " << name_ << endl; + files += name_; + } + + bool success = true; + QString path = Tellico::saveLocation(QString::fromLatin1("entry-templates/")); + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + if((*it).endsWith(QChar('/'))) { + // ok to not delete all directories + QDir().rmdir(path + *it); + } else { + success = success && QFile(path + *it).remove(); + if(!success) { + myDebug() << "Manager::removeTemplate() - failed to remove " << (path+*it) << endl; + } + } + } + + // remove config entries even if unsuccessful + fileGroup.deleteEntry(name_); + KConfigGroup statusGroup(KGlobal::config(), "KNewStuffStatus"); + QString entryName = statusGroup.readEntry(name_); + statusGroup.deleteEntry(name_); + statusGroup.deleteEntry(entryName); + + return success; +} + +bool Manager::installScript(const KURL& url_) { + FileHandler::FileRef* ref = FileHandler::fileRef(url_); + + KTar archive(ref->fileName()); + if(!archive.open(IO_ReadOnly)) { + myDebug() << "Manager::installScript() - can't open tar file" << endl; + return false; + } + + const KArchiveDirectory* archiveDir = archive.directory(); + + QString exeFile = findEXE(archiveDir); + if(exeFile.isEmpty()) { + myDebug() << "Manager::installScript() - no exe file found" << endl; + return false; + } + + QFileInfo exeInfo(exeFile); + DataSourceInfo* info = new DataSourceInfo(); + + QString copyTarget = Tellico::saveLocation(QString::fromLatin1("data-sources/")); + QString scriptFolder; + + // package could have a top-level directory or not + // it should have a directory... + const KArchiveEntry* firstEntry = archiveDir->entry(archiveDir->entries().first()); + if(firstEntry->isFile()) { + copyTarget += exeInfo.baseName(true) + '/'; + if(QFile::exists(copyTarget)) { + KURL u; + u.setPath(scriptFolder); + myLog() << "Manager::installScript() - deleting " << scriptFolder << endl; + KIO::NetAccess::del(u, Kernel::self()->widget()); + info->isUpdate = true; + } + scriptFolder = copyTarget; + } else { + scriptFolder = copyTarget + firstEntry->name() + '/'; + if(QFile::exists(copyTarget + exeFile)) { + info->isUpdate = true; + } + } + + // overwrites stuff there + archiveDir->copyTo(copyTarget); + + info->specFile = scriptFolder + exeInfo.baseName(true) + ".spec"; + if(!QFile::exists(info->specFile)) { + myDebug() << "Manager::installScript() - no spec file for script! " << info->specFile << endl; + delete info; + return false; + } + info->sourceName = exeFile; + info->sourceExec = copyTarget + exeFile; + m_infoList.append(info); + + KURL dest; + dest.setPath(info->sourceExec); + KFileItem item(KFileItem::Unknown, KFileItem::Unknown, dest, true); + ::chmod(QFile::encodeName(dest.path()), item.permissions() | S_IXUSR); + + { + KConfig spec(info->specFile, false, false); + // update name + info->sourceName = spec.readEntry("Name", info->sourceName); + spec.writePathEntry("ExecPath", info->sourceExec); + spec.writeEntry("NewStuffName", info->sourceName); + spec.writeEntry("DeleteOnRemove", true); + } + + { + KConfigGroup config(KGlobal::config(), "KNewStuffFiles"); + config.writeEntry(info->sourceName, archiveFiles(archiveDir)); + m_urlNameMap.insert(url_, info->sourceName); + } + + +// myDebug() << "Manager::installScript() - exeFile = " << exeFile << endl; +// myDebug() << "Manager::installScript() - sourceExec = " << info->sourceExec << endl; +// myDebug() << "Manager::installScript() - sourceName = " << info->sourceName << endl; +// myDebug() << "Manager::installScript() - specFile = " << info->specFile << endl; + + delete ref; + return true; +} + +bool Manager::removeScript(const QString& name_) { + KConfigGroup fileGroup(KGlobal::config(), "KNewStuffFiles"); + QStringList files = fileGroup.readListEntry(name_); + + bool success = true; + QString path = Tellico::saveLocation(QString::fromLatin1("data-sources/")); + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + if((*it).endsWith(QChar('/'))) { + // ok to not delete all directories + QDir().rmdir(path + *it); + } else { + success = success && QFile(path + *it).remove(); + if(!success) { + myDebug() << "Manager::removeScript() - failed to remove " << (path+*it) << endl; + } + } + } + + // remove config entries even if unsuccessful + fileGroup.deleteEntry(name_); + KConfigGroup statusGroup(KGlobal::config(), "KNewStuffStatus"); + QString entryName = statusGroup.readEntry(name_); + statusGroup.deleteEntry(name_); + statusGroup.deleteEntry(entryName); + + return success; +} + +Tellico::NewStuff::InstallStatus Manager::installStatus(KNS::Entry* entry_) { + KConfigGroup config(KGlobal::config(), "KNewStuffStatus"); + QString datestring = config.readEntry(entry_->name()); + if(datestring.isEmpty()) { + return NotInstalled; + } + + QDate date = QDate::fromString(datestring, Qt::ISODate); + if(!date.isValid()) { + return NotInstalled; + } + if(date < entry_->releaseDate()) { + return OldVersion; + } + + // also check that executable files exists + KConfigGroup fileGroup(KGlobal::config(), "KNewStuffFiles"); + QStringList files = fileGroup.readListEntry(entry_->name()); + QString path = Tellico::saveLocation(QString::fromLatin1("data-sources/")); + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + if(!QFile::exists(path + *it)) { + return NotInstalled; + } + } + return Current; +} + +QStringList Manager::archiveFiles(const KArchiveDirectory* dir_, const QString& path_) { + QStringList list; + + const QStringList dirEntries = dir_->entries(); + for(QStringList::ConstIterator it = dirEntries.begin(); it != dirEntries.end(); ++it) { + const QString& entry = *it; + const KArchiveEntry* curEntry = dir_->entry(entry); + if(curEntry->isFile()) { + list += path_ + entry; + } else if(curEntry->isDirectory()) { + list += archiveFiles(static_cast<const KArchiveDirectory*>(curEntry), path_ + entry + '/'); + // add directory AFTER contents, since we delete from the top down later + list += path_ + entry + '/'; + } + } + + return list; +} + +// don't recurse, the .xsl must be in top directory +QString Manager::findXSL(const KArchiveDirectory* dir_) { + const QStringList entries = dir_->entries(); + for(QStringList::ConstIterator it = entries.begin(); it != entries.end(); ++it) { + const QString& entry = *it; + if(entry.endsWith(QString::fromLatin1(".xsl"))) { + return entry; + } + } + return QString(); +} + +QString Manager::findEXE(const KArchiveDirectory* dir_) { + QPtrStack<KArchiveDirectory> dirStack; + QValueStack<QString> dirNameStack; + + dirStack.push(dir_); + dirNameStack.push(QString()); + + do { + const QString dirName = dirNameStack.pop(); + const KArchiveDirectory* curDir = dirStack.pop(); + const QStringList entries = curDir->entries(); + for(QStringList::ConstIterator it = entries.begin(); it != entries.end(); ++it) { + const QString& entry = *it; + const KArchiveEntry* archEntry = curDir->entry(entry); + + if(archEntry->isFile() && (archEntry->permissions() & S_IEXEC)) { + return dirName + entry; + } else if(archEntry->isDirectory()) { + dirStack.push(static_cast<const KArchiveDirectory*>(archEntry)); + dirNameStack.push(dirName + entry + '/'); + } + } + } while(!dirStack.isEmpty()); + + return QString(); +} + +void Manager::install(DataType type_, KNS::Entry* entry_) { + if(m_tempFile) { + delete m_tempFile; + } + m_tempFile = new KTempFile(); + m_tempFile->setAutoDelete(true); + + KURL destination; + destination.setPath(m_tempFile->name()); + KIO::FileCopyJob* job = KIO::file_copy(entry_->payload(), destination, -1, true); + connect(job, SIGNAL(result(KIO::Job*)), SLOT(slotDownloadJobResult(KIO::Job*))); + m_jobMap.insert(job, EntryPair(entry_, type_)); +} + +void Manager::slotDownloadJobResult(KIO::Job* job_) { + KIO::FileCopyJob* job = static_cast<KIO::FileCopyJob*>(job_); + if(job->error()) { + GUI::CursorSaver cs(Qt::arrowCursor); + delete m_tempFile; + m_tempFile = 0; + job->showErrorDialog(Kernel::self()->widget()); + emit signalInstalled(0); // still need to notify dialog that install failed + return; + } + + KNS::Entry* entry = m_jobMap[job_].first; + DataType type = m_jobMap[job_].second; + + bool deleteTempFile = true; + if(type == EntryTemplate) { + installTemplate(job->destURL(), entry->name()); + } else { +#if KDE_IS_VERSION(3,3,90) + // needed so the GPG signature can be checked + NewScript* newScript = new NewScript(this, Kernel::self()->widget()); + connect(newScript, SIGNAL(installFinished()), this, SLOT(slotInstallFinished())); + // need to delete temp file if install was not a success + // if it was a success, it gets deleted later + deleteTempFile = !newScript->install(job->destURL().path()); + m_scriptEntryMap.insert(newScript, entry); +#endif + // if failed, emit empty signal now + if(deleteTempFile) { + emit signalInstalled(0); + } + } + if(deleteTempFile) { + delete m_tempFile; + m_tempFile = 0; + } +} + +void Manager::slotInstallFinished() { + const NewScript* newScript = ::qt_cast<const NewScript*>(sender()); + if(newScript && newScript->successfulInstall()) { + const QString name = m_urlNameMap[newScript->url()]; + KNS::Entry* entry = m_scriptEntryMap[newScript]; + KConfigGroup config(KGlobal::config(), "KNewStuffStatus"); + // have to keep a config entry that maps the name of the file to the name + // of the newstuff entry, so we can track which entries are installed + // name and entry-name() are probably the same for scripts, but not a problem + config.writeEntry(name, entry->name()); + config.writeEntry(entry->name(), entry->releaseDate().toString(Qt::ISODate)); + config.sync(); + emit signalInstalled(entry); + } else { + emit signalInstalled(0); + kdWarning() << "Manager::slotInstallFinished() - Failed to install" << endl; + } + delete m_tempFile; + m_tempFile = 0; +} + +bool Manager::checkCommonFile() { + // look for a file that gets installed to know the installation directory + // need to check timestamps + QString userDataDir = Tellico::saveLocation(QString::null); + QString userCommonFile = userDataDir + '/' + QString::fromLatin1("tellico-common.xsl"); + if(QFile::exists(userCommonFile)) { + // check timestamps + // pics/tellico.png is not likely to be in a user directory + QString installDir = KGlobal::dirs()->findResourceDir("appdata", QString::fromLatin1("pics/tellico.png")); + QString installCommonFile = installDir + '/' + QString::fromLatin1("tellico-common.xsl"); +#ifndef NDEBUG + if(userCommonFile == installCommonFile) { + kdWarning() << "Manager::checkCommonFile() - install location is same as user location" << endl; + } +#endif + QFileInfo installInfo(installCommonFile); + QFileInfo userInfo(userCommonFile); + if(installInfo.lastModified() > userInfo.lastModified()) { + // the installed file has been modified more recently than the user's + // remove user's tellico-common.xsl file so it gets copied again + myLog() << "Manager::checkCommonFile() - removing " << userCommonFile << endl; + myLog() << "Manager::checkCommonFile() - copying " << installCommonFile << endl; + QFile::remove(userCommonFile); + } else { + return true; + } + } + KURL src, dest; + src.setPath(KGlobal::dirs()->findResource("appdata", QString::fromLatin1("tellico-common.xsl"))); + dest.setPath(userCommonFile); + return KIO::NetAccess::file_copy(src, dest); +} + +#include "manager.moc" diff --git a/src/newstuff/manager.h b/src/newstuff/manager.h new file mode 100644 index 0000000..4102be3 --- /dev/null +++ b/src/newstuff/manager.h @@ -0,0 +1,101 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_NEWSTUFF_MANAGER_H +#define TELLICO_NEWSTUFF_MANAGER_H + +class KArchiveDirectory; +class KURL; +class KTempFile; + +#include <qobject.h> +#include <qptrlist.h> + +class QStringList; + +namespace KNS { + class Entry; +} +namespace KIO { + class Job; +} + +namespace Tellico { + namespace NewStuff { + +class NewScript; + +enum DataType { + EntryTemplate, + DataScript +}; + +enum InstallStatus { + NotInstalled, + OldVersion, + Current +}; + +struct DataSourceInfo { + DataSourceInfo() : isUpdate(false) {} + QString specFile; // full path of .spec file + QString sourceName; + QString sourceExec; // full executable path of script + bool isUpdate : 1; // whether the info is for an updated source +}; + +class Manager : public QObject { +Q_OBJECT + +public: + Manager(QObject* parent); + ~Manager(); + + void install(DataType type, KNS::Entry* entry); + QPtrList<DataSourceInfo> dataSourceInfo() const { return m_infoList; } + + bool installTemplate(const KURL& url, const QString& entryName = QString::null); + bool removeTemplate(const QString& name); + + bool installScript(const KURL& url); + bool removeScript(const QString& name); + + static InstallStatus installStatus(KNS::Entry* entry); + static bool checkCommonFile(); + +signals: + void signalInstalled(KNS::Entry* entry); + +private slots: + void slotDownloadJobResult(KIO::Job* job); + void slotInstallFinished(); + +private: + static QStringList archiveFiles(const KArchiveDirectory* dir, + const QString& path = QString::null); + + static QString findXSL(const KArchiveDirectory* dir); + static QString findEXE(const KArchiveDirectory* dir); + + typedef QPair<KNS::Entry*, DataType> EntryPair; + QMap<KIO::Job*, EntryPair> m_jobMap; + QMap<KURL, QString> m_urlNameMap; + QMap<const NewScript*, KNS::Entry*> m_scriptEntryMap; + QPtrList<DataSourceInfo> m_infoList; + KTempFile* m_tempFile; +}; + + } +} + +#endif diff --git a/src/newstuff/newscript.cpp b/src/newstuff/newscript.cpp new file mode 100644 index 0000000..045f881 --- /dev/null +++ b/src/newstuff/newscript.cpp @@ -0,0 +1,48 @@ +/*************************************************************************** + copyright : (C) 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 "newscript.h" +#include "manager.h" + +#include <kurl.h> + +#include <qwidget.h> + +using Tellico::NewStuff::NewScript; + +NewScript::NewScript(Manager* manager_, QWidget* parentWidget_) +#if KDE_IS_VERSION(3,3,90) + : KNewStuffSecure(QString::fromLatin1("tellico/data-source"), parentWidget_) +#else + : QObject(parentWidget_) +#endif + , m_manager(manager_), m_success(false) { +} + +void NewScript::installResource() { + // m_tarName is protected in superclass + KURL u; + u.setPath(m_tarName); + m_success = m_manager->installScript(u); + m_url = u; +} + +#if KDE_IS_VERSION(3,3,90) +#include <knewstuff/knewstuffsecure.h> +#define SUPERCLASS KNewStuffSecure +#else +#define SUPERCLASS QObject +#endif + +#include "newscript.moc" +#undef SUPERCLASS diff --git a/src/newstuff/newscript.h b/src/newstuff/newscript.h new file mode 100644 index 0000000..8bc3154 --- /dev/null +++ b/src/newstuff/newscript.h @@ -0,0 +1,60 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_NEWSTUFF_NEWSCRIPT_H +#define TELLICO_NEWSTUFF_NEWSCRIPT_H + +#include <kdeversion.h> +#include <kurl.h> + +#if KDE_IS_VERSION(3,3,90) +#include <knewstuff/knewstuffsecure.h> +#define SUPERCLASS KNewStuffSecure +#else +#define SUPERCLASS QObject +#endif + +#include <qobject.h> + +namespace Tellico { + namespace NewStuff { + +class Manager; + +class NewScript : public SUPERCLASS { +Q_OBJECT + +public: + NewScript(Manager* manager, QWidget* parentWidget = 0); + virtual ~NewScript() {} + + const KURL& url() const { return m_url; } + bool successfulInstall() const { return m_success; } + +private: + virtual void installResource(); + + Manager* m_manager; +#if !KDE_IS_VERSION(3,3,90) + // KNewStuffSecure has a protected variable + QString m_tarName; +#endif + KURL m_url; + bool m_success : 1; +}; + + } +} + +#undef SUPERCLASS +#endif diff --git a/src/newstuff/providerloader.cpp b/src/newstuff/providerloader.cpp new file mode 100644 index 0000000..b3f95ae --- /dev/null +++ b/src/newstuff/providerloader.cpp @@ -0,0 +1,102 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// this class is largely copied from kdelibs/knewstuff/provider.cpp +// which is Copyright (c) 2002 Cornelius Schumacher <schumacher@kde.org> +// and licensed under GPL v2, just like Tellico + +#include "providerloader.h" +#include "../tellico_debug.h" +#include "../latin1literal.h" + +#include <kio/job.h> +#include <knewstuff/provider.h> +#include <kglobal.h> +#include <kconfig.h> +#include <kmessagebox.h> +#include <klocale.h> + +#include <qdom.h> + +using Tellico::NewStuff::ProviderLoader; + +ProviderLoader::ProviderLoader( QWidget *parentWidget ) : + mParentWidget( parentWidget ), mTryAlt(true) +{ + mProviders.setAutoDelete( true ); +} + +void ProviderLoader::load( const QString &type, const QString &providersList ) +{ + mProviders.clear(); + mJobData.truncate(0); + +// myLog() << "ProviderLoader::load(): providersList: " << providersList << endl; + + KIO::TransferJob *job = KIO::get( KURL( providersList ), false, false ); + connect( job, SIGNAL( result( KIO::Job * ) ), + SLOT( slotJobResult( KIO::Job * ) ) ); + connect( job, SIGNAL( data( KIO::Job *, const QByteArray & ) ), + SLOT( slotJobData( KIO::Job *, const QByteArray & ) ) ); + connect( job, SIGNAL( percent (KIO::Job *, unsigned long) ), + SIGNAL( percent (KIO::Job *, unsigned long) ) ); + +// job->dumpObjectInfo(); +} + +void ProviderLoader::slotJobData( KIO::Job *, const QByteArray &data ) +{ + if ( data.size() == 0 ) return; + QCString str( data, data.size() + 1 ); + mJobData.append( QString::fromUtf8( str ) ); +} + +void ProviderLoader::slotJobResult( KIO::Job *job ) +{ + if ( job->error() ) { + job->showErrorDialog( mParentWidget ); + if(mTryAlt && !mAltProvider.isEmpty()) { + mTryAlt = false; + load(QString(), mAltProvider); + } else { + emit error(); + } + return; + } + + QDomDocument doc; + if ( !doc.setContent( mJobData ) ) { + myDebug() << "ProviderLoader::slotJobResult() - error parsing providers list." << endl; + if(mTryAlt && !mAltProvider.isEmpty()) { + mTryAlt = false; + load(QString(), mAltProvider); + } else { + emit error(); + } + return; + } + + QDomElement providers = doc.documentElement(); + QDomNode n; + for ( n = providers.firstChild(); !n.isNull(); n = n.nextSibling() ) { + QDomElement p = n.toElement(); + + if ( p.tagName() == Latin1Literal("provider") ) { + mProviders.append( new KNS::Provider( p ) ); + } + } + + emit providersLoaded( &mProviders ); +} + +#include "providerloader.moc" diff --git a/src/newstuff/providerloader.h b/src/newstuff/providerloader.h new file mode 100644 index 0000000..3d2968c --- /dev/null +++ b/src/newstuff/providerloader.h @@ -0,0 +1,84 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// this class is largely copied from kdelibs/knewstuff/provider.h +// which is Copyright (c) 2002 Cornelius Schumacher <schumacher@kde.org> +// and licensed under GPL v2, just like Tellico +// +// I want progress info for the download, and this was the +// easiest way to get it + +#ifndef TELLICO_NEWSTUFF_PROVIDERLOADER_H +#define TELLICO_NEWSTUFF_PROVIDERLOADER_H + +#include <qobject.h> +#include <qptrlist.h> + +namespace KIO { + class Job; +} +namespace KNS { + class Provider; +} + +namespace Tellico { + namespace NewStuff { + +class ProviderLoader : public QObject { +Q_OBJECT +public: + /** + * Constructor. + * + * @param parentWidget the parent widget + */ + ProviderLoader( QWidget *parentWidget ); + + /** + * Starts asynchronously loading the list of providers of the + * specified type. + * + * @param type data type such as 'kdesktop/wallpaper'. + * @param providerList the URl to the list of providers; if empty + * we first try the ProvidersUrl from KGlobal::config, then we + * fall back to a hardcoded value. + */ + void load( const QString &type, const QString &providerList = QString::null ); + + void setAlternativeProvider(const QString& alt) { mAltProvider = alt; } + + signals: + /** + * Indicates that the list of providers has been successfully loaded. + */ + void providersLoaded( QPtrList<KNS::Provider>* ); + void percent(KIO::Job *job, unsigned long percent); + void error(); + + protected slots: + void slotJobData( KIO::Job *, const QByteArray & ); + void slotJobResult( KIO::Job * ); + + private: + QWidget *mParentWidget; + + QString mJobData; + + QPtrList<KNS::Provider> mProviders; + QString mAltProvider; + bool mTryAlt; +}; + + } +} +#endif diff --git a/src/observer.h b/src/observer.h new file mode 100644 index 0000000..f8f7661 --- /dev/null +++ b/src/observer.h @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_OBSERVER_H +#define TELLICO_OBSERVER_H + +#include "datavectors.h" + +namespace Tellico { + class Filter; + +/** + * @author Robby Stephenson + */ +class Observer { + +public: + virtual ~Observer() {} + + virtual void addBorrower(Data::BorrowerPtr) {} + virtual void modifyBorrower(Data::BorrowerPtr) {} + // no removeBorrower() + + virtual void addEntries(Data::EntryVec) {} + virtual void modifyEntries(Data::EntryVec) {} + virtual void removeEntries(Data::EntryVec) {} + + virtual void addField(Data::CollPtr, Data::FieldPtr) {} + // coll, oldfield, newfield + virtual void modifyField(Data::CollPtr, Data::FieldPtr, Data::FieldPtr) {} + virtual void removeField(Data::CollPtr, Data::FieldPtr) {} + + virtual void addFilter(FilterPtr) {} + virtual void modifyFilter(FilterPtr) {} + virtual void removeFilter(FilterPtr) {} +}; + +} + +#endif diff --git a/src/progressmanager.cpp b/src/progressmanager.cpp new file mode 100644 index 0000000..a487710 --- /dev/null +++ b/src/progressmanager.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + copyright : (C) 2005-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 "progressmanager.h" +#include "tellico_debug.h" + +#include <qtimer.h> + +using Tellico::ProgressItem; +using Tellico::ProgressManager; +ProgressManager* ProgressManager::s_self = 0; + +ProgressItem::Done::~Done() { + ProgressManager::self()->setDone(m_object); +} + +ProgressItem::ProgressItem(const QString& label_, bool canCancel_) + : m_label(label_) + , m_canCancel(canCancel_) + , m_progress(0) + , m_total(0) + , m_cancelled(false) { +} + +ProgressItem::~ProgressItem() { +// myDebug() << "~ProgressItem() - " << m_label << endl; +} + +void ProgressItem::setProgress(uint steps_) { + m_progress = steps_; + emit signalProgress(this); + + if(m_progress >= m_total) { + setDone(); + } +} + +void ProgressItem::setTotalSteps(uint steps_) { + m_total = steps_; + emit signalTotalSteps(this); +} + +void ProgressItem::setDone() { + if(!m_cancelled) { + m_progress = m_total; + } + emit signalDone(this); + // make sure the deleting doesn't interfere with anything + QTimer::singleShot(3000, this, SLOT(deleteLater())); +} + +void ProgressItem::cancel() { +// myDebug() << "ProgressItem::cancel()" << endl; + if(!m_canCancel || m_cancelled) { + return; + } + + m_cancelled = true; + emit signalCancelled(this); +} + +ProgressManager::ProgressManager() : QObject() { +} + +void ProgressManager::setProgress(const QObject* owner_, uint steps_) { + if(!m_items.contains(owner_)) { + return; + } + + m_items[owner_] ->setProgress(steps_); +// slotUpdateTotalProgress(); // called in ProgressItem::setProgress() +// emit signalItemProgress(m_items[owner_]); +} + +void ProgressManager::setTotalSteps(const QObject* owner_, uint steps_) { + if(!m_items.contains(owner_)) { + return; + } + + m_items[owner_]->setTotalSteps(steps_); +// updateTotalProgress(); // called in ProgressItem::setTotalSteps() +} + +void ProgressManager::setDone(const QObject* owner_) { + if(!m_items.contains(owner_)) { + return; + } + setDone(m_items[owner_]); +} + +void ProgressManager::setDone(ProgressItem* item_) { + if(!item_) { + myDebug() << "ProgressManager::setDone() - null ProgressItem!" << endl; + return; + } + item_->setDone(); +// updateTotalProgress(); +} + +void ProgressManager::slotItemDone(ProgressItem* item_) { +// cancel ends up removing it from the map, so make a copy + ProgressMap map = m_items; + for(ProgressMap::Iterator it = map.begin(); it != map.end(); ++it) { + if(static_cast<ProgressItem*>(it.data()) == item_) { + m_items.remove(it.key()); + break; + } + } + slotUpdateTotalProgress(); +// emit signalItemDone(item_); +} + +ProgressItem& ProgressManager::newProgressItemImpl(const QObject* owner_, + const QString& label_, + bool canCancel_) { +// myDebug() << "ProgressManager::newProgressItem() - " << owner_->className() << ":" << label_ << endl; + if(m_items.contains(owner_)) { + return *m_items[owner_]; + } + + ProgressItem* item = new ProgressItem(label_, canCancel_); + m_items.insert(owner_, item); + + connect(item, SIGNAL(signalTotalSteps(ProgressItem*)), SLOT(slotUpdateTotalProgress())); + connect(item, SIGNAL(signalProgress(ProgressItem*)), SLOT(slotUpdateTotalProgress())); + connect(item, SIGNAL(signalDone(ProgressItem*)), SLOT(slotUpdateTotalProgress())); + connect(item, SIGNAL(signalDone(ProgressItem*)), SLOT(slotItemDone(ProgressItem*))); + +// connect(item, SIGNAL(signalProgress(ProgressItem*)), SIGNAL(signalItemProgress(ProgressItem*))); +// emit signalItemAdded(item); + return *item; +} + +void ProgressManager::slotUpdateTotalProgress() { + uint progress = 0; + uint total = 0; + + for(ProgressMap::ConstIterator it = m_items.begin(); it != m_items.end(); ++it) { + if(it.data()) { + progress += (*it)->progress(); + total += (*it)->totalSteps(); + } + } + + if(total == 0) { + emit signalTotalProgress(100); + return; + } + + emit signalTotalProgress(100*progress/total); +} + +void ProgressManager::slotCancelAll() { +// cancel ends up removing it from the map, so make a copy + ProgressMap map = m_items; + for(ProgressMap::ConstIterator it = map.begin(), end = map.end(); it != end; ++it) { + if(it.data()) { + it.data()->cancel(); + setDone(it.data()); + } + } +} + +bool ProgressManager::anyCanBeCancelled() const { + for(ProgressMap::ConstIterator it = m_items.begin(), end = m_items.end(); it != end; ++it) { + if(it.data() && it.data()->canCancel()) { + return true; + } + } + return false; +} + +#include "progressmanager.moc" diff --git a/src/progressmanager.h b/src/progressmanager.h new file mode 100644 index 0000000..5eaac3b --- /dev/null +++ b/src/progressmanager.h @@ -0,0 +1,127 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +// much of this code is adapted from libkdepim +// which is GPL licensed, Copyright (c) 2004 Till Adam + +#ifndef TELLICO_PROGRESSMANAGER_H +#define TELLICO_PROGRESSMANAGER_H + +#include <qobject.h> +#include <qmap.h> +#include <qguardedptr.h> + +namespace Tellico { + +class ProgressManager; + +/** + * @author Robby Stephenson + */ +class ProgressItem : public QObject { +Q_OBJECT + +friend class ProgressManager; + +public: + class Done { + public: + Done(const QObject* obj) : m_object(obj) {} + ~Done(); + private: + const QObject* m_object; + }; + + bool canCancel() const { return m_canCancel; } + const QString& label() const { return m_label; } + void setLabel(const QString& label); + +// uint progress() const { return m_total ? (100*m_completed/m_total) : 0; } + uint progress() const { return m_progress; } + void setProgress(uint steps); + uint totalSteps() const { return m_total; } + void setTotalSteps(uint steps); + void setDone(); + + void cancel(); + +signals: + void signalProgress(ProgressItem* item); + void signalDone(ProgressItem* item); + void signalCancelled(ProgressItem* item); + void signalTotalSteps(ProgressItem* item); + +protected: + /* Only to be used by the ProgressManager */ + ProgressItem(const QString& label, bool canCancel); + virtual ~ProgressItem(); + +private: + QString m_label; + bool m_canCancel; + uint m_progress; + uint m_total; + bool m_cancelled; +}; + +/** + * @author Robby Stephenson + */ +class ProgressManager : public QObject { +Q_OBJECT + +public: + virtual ~ProgressManager() {} + + static ProgressManager* self() { if(!s_self) s_self = new ProgressManager(); return s_self; } + + ProgressItem& newProgressItem(const QObject* owner, const QString& label, bool canCancel = false) { + return newProgressItemImpl(owner, label, canCancel); + } + + void setProgress(const QObject* owner, uint steps); + void setTotalSteps(const QObject* owner, uint steps); + void setDone(const QObject* owner); + + bool anyCanBeCancelled() const; + +signals: +// void signalItemAdded(ProgressItem* item); +// void signalItemProgress(ProgressItem* item); +// void signalItemDone(ProgressItem* item); +// void signalItemCancelled(ProgressItem* item); + void signalTotalProgress(uint progress); + +public slots: + void slotCancelAll(); + +private slots: + void slotItemDone(ProgressItem* item); + void slotUpdateTotalProgress(); + +private: + ProgressManager(); + ProgressManager(const ProgressManager&); // no copies + + ProgressItem& newProgressItemImpl(const QObject* owner, const QString& label, bool canCancel); + void setDone(ProgressItem* item); + + typedef QMap<QGuardedPtr<const QObject>, QGuardedPtr<ProgressItem> > ProgressMap; + ProgressMap m_items; + + static ProgressManager* s_self; +}; + +} // end namespace + +#endif diff --git a/src/ptrvector.h b/src/ptrvector.h new file mode 100644 index 0000000..1dd66ea --- /dev/null +++ b/src/ptrvector.h @@ -0,0 +1,322 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_PTRVECTOR_H +#define TELLICO_PTRVECTOR_H + +#include <kdebug.h> +#include <ksharedptr.h> + +#include <qvaluevector.h> + +namespace Tellico { + +template <class T> class Vector; +template <class T> class VectorIterator; +template <class T> class VectorConstIterator; +template <class T> bool operator==(const VectorIterator<T>& left, const VectorIterator<T>& right); +template <class T> bool operator!=(const VectorIterator<T>& left, const VectorIterator<T>& right); +template <class T> bool operator==(const VectorConstIterator<T>& left, const VectorConstIterator<T>& right); +template <class T> bool operator!=(const VectorConstIterator<T>& left, const VectorConstIterator<T>& right); + +template <class T> +class VectorIterator { +public: + VectorIterator() : m_vector(0), m_index(0) {} + VectorIterator(Vector<T>* vector, size_t index) : m_vector(vector), m_index(index) {} + VectorIterator(const VectorIterator<T>& other) : m_vector(other.m_vector), m_index(other.m_index) {} + +// operator T*() { return m_vector->at(m_index).data(); } + operator KSharedPtr<T>() { return m_vector->at(m_index); } + T* operator->() { return m_vector->at(m_index).data(); } + T& operator*() { return *m_vector->at(m_index); } + T* data() { return m_vector->at(m_index).data(); } + + VectorIterator& operator++() { ++m_index; return *this; } + VectorIterator& operator--() { --m_index; return *this; } + + friend bool operator==(const VectorIterator<T>& left, const VectorIterator<T>& right) + { return left.m_vector == right.m_vector && left.m_index == right.m_index; } + friend bool operator!=(const VectorIterator<T>& left, const VectorIterator<T>& right) + { return left.m_vector != right.m_vector || left.m_index != right.m_index; } + + bool nextEnd() const { return m_index == m_vector->count()-1; } + +private: + friend class Vector<T>; + Vector<T>* m_vector; + size_t m_index; +}; + +template <class T> +class VectorConstIterator { +public: + VectorConstIterator() : m_vector(0), m_index(0) {} + VectorConstIterator(const Vector<T>* vector, size_t index) : m_vector(vector), m_index(index) {} + VectorConstIterator(const VectorIterator<T>& other) : m_vector(other.m_vector), m_index(other.m_index) {} + +// operator const T*() { return m_vector->at(m_index).data(); } + operator KSharedPtr<const T>() { return m_vector->at(m_index); } + const T* operator->() const { return m_vector->at(m_index).data(); } + const T& operator*() const { return *m_vector->at(m_index); } + const T* data() const { return m_vector->at(m_index).data(); } + + VectorConstIterator& operator++() { ++m_index; return *this; } + VectorConstIterator& operator--() { --m_index; return *this; } + + friend bool operator==(const VectorConstIterator<T>& left, const VectorConstIterator<T>& right) + { return left.m_vector == right.m_vector && left.m_index == right.m_index; } + friend bool operator!=(const VectorConstIterator<T>& left, const VectorConstIterator<T>& right) + { return left.m_vector != right.m_vector || left.m_index != right.m_index; } + + bool nextEnd() const { return m_index == m_vector->count()-1; } + +private: + friend class Vector<T>; + const Vector<T>* m_vector; + size_t m_index; +}; + +template <class T> +class Vector { +public: + typedef KSharedPtr<T> Ptr; + typedef VectorIterator<T> Iterator; + typedef VectorConstIterator<T> ConstIterator; + + Vector() {} + Vector(Ptr t) { append(t); } + Vector(const Vector<T>& v) : m_baseVector(v.m_baseVector) {} + Vector& operator=(const Vector<T>& other) { + if(this != &other) { + m_baseVector = other.m_baseVector; + } + return *this; + } + + bool operator== (const Vector<T>& x) const { return x.m_baseVector == m_baseVector; } + bool isEmpty() const { return m_baseVector.empty(); } + size_t count() const { return m_baseVector.size(); } + + Ptr& operator[](size_t i) { return m_baseVector[i]; } + const Ptr& operator[](size_t i) const { return m_baseVector[i]; } + + Ptr& at(size_t i, bool* ok = 0) { return m_baseVector.at(i, ok); } + const Ptr& at(size_t i, bool* ok = 0) const { return m_baseVector.at(i, ok); } + + Iterator begin() { return Iterator(this, 0); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator constBegin() const { return ConstIterator(this, 0); } + Iterator end() { return Iterator(this, count()); } + ConstIterator end() const { return ConstIterator(this, count()); } + ConstIterator constEnd() const { return ConstIterator(this, count()); } + + Ptr& front() { return at(0); } + const Ptr& front() const { return at(0); } + Ptr& back() { return at(count()-1); } + const Ptr& back() const { return at(count()-1); } + + void clear() { m_baseVector.clear(); } + void append(Ptr t) { m_baseVector.append(t); } + void append(Vector<T> v); + + void insert(Iterator pos, Ptr t) { + m_baseVector.insert(&m_baseVector[pos.m_index], t); + } + + Iterator find(Ptr t) { + for(size_t i = 0; i < count(); ++i) { + if(m_baseVector[i].data() == t) { + return Iterator(this, i); + } + } + return end(); + } + + bool contains(Ptr t) const { return qFind(m_baseVector.begin(), m_baseVector.end(), Ptr(t)) != m_baseVector.end(); } + bool remove(const Ptr& t) { + Ptr* it = qFind(m_baseVector.begin(), m_baseVector.end(), t); + if(it == m_baseVector.end()) return false; + m_baseVector.erase(it); + return true; + } + +private: + QValueVector<Ptr> m_baseVector; +}; + +template <class T> class PtrVector; +template <class T> class PtrVectorIterator; +template <class T> class PtrVectorConstIterator; +template <class T> bool operator==(const PtrVectorIterator<T>& left, const PtrVectorIterator<T>& right); +template <class T> bool operator!=(const PtrVectorIterator<T>& left, const PtrVectorIterator<T>& right); +template <class T> bool operator==(const PtrVectorConstIterator<T>& left, const PtrVectorConstIterator<T>& right); +template <class T> bool operator!=(const PtrVectorConstIterator<T>& left, const PtrVectorConstIterator<T>& right); + +template <class T> +class PtrVectorIterator { +public: + PtrVectorIterator() : m_vector(0), m_index(0) {} + PtrVectorIterator(const PtrVector<T>* vector, size_t index) : m_vector(vector), m_index(index) {} + PtrVectorIterator(const PtrVectorIterator<T>& other) : m_vector(other.m_vector), m_index(other.m_index) {} + + T* operator->() { return &m_vector->at(m_index); } + T& operator*() { return m_vector->at(m_index); } + + T* ptr() { return &m_vector->at(m_index); } + + PtrVectorIterator& operator++() { ++m_index; return *this; } + PtrVectorIterator& operator--() { --m_index; return *this; } + + friend bool operator==(const PtrVectorIterator<T>& left, const PtrVectorIterator<T>& right) + { return left.m_vector == right.m_vector && left.m_index == right.m_index; } + friend bool operator!=(const PtrVectorIterator<T>& left, const PtrVectorIterator<T>& right) + { return left.m_vector != right.m_vector || left.m_index != right.m_index; } + +private: + const PtrVector<T>* m_vector; + size_t m_index; +}; + +template <class T> +class PtrVectorConstIterator { +public: + PtrVectorConstIterator() : m_vector(0), m_index(0) {} + PtrVectorConstIterator(const PtrVector<T>* vector, size_t index) : m_vector(vector), m_index(index) {} + PtrVectorConstIterator(const PtrVectorConstIterator<T>& other) : m_vector(other.m_vector), m_index(other.m_index) {} + + const T* operator->() const { return &m_vector->at(m_index); } + const T& operator*() const { return m_vector->at(m_index); } + + const T* ptr() const { return &m_vector->at(m_index); } + + PtrVectorConstIterator& operator++() { ++m_index; return *this; } + PtrVectorConstIterator& operator--() { --m_index; return *this; } + + friend bool operator==(const PtrVectorConstIterator<T>& left, const PtrVectorConstIterator<T>& right) + { return left.m_vector == right.m_vector && left.m_index == right.m_index; } + friend bool operator!=(const PtrVectorConstIterator<T>& left, const PtrVectorConstIterator<T>& right) + { return left.m_vector != right.m_vector || left.m_index != right.m_index; } + +private: + const PtrVector<T>* m_vector; + size_t m_index; +}; + +/** + * @author Robby Stephenson + */ +template <class T> +class PtrVector { + +public: + typedef Tellico::PtrVectorIterator<T> Iterator; + typedef Tellico::PtrVectorConstIterator<T> ConstIterator; + + PtrVector() : m_autoDelete(false) {} + PtrVector(const PtrVector<T>& other) : m_baseVector(other.m_baseVector), m_autoDelete(false) {} + PtrVector& operator=(const PtrVector<T>& other) { + if(this != &other) { + m_baseVector = other.m_baseVector; + m_autoDelete = false; + } + return *this; + } + ~PtrVector() { if(m_autoDelete) clear(); } + + size_t count() const { return m_baseVector.size(); } + bool isEmpty() const { return m_baseVector.empty(); } + bool autoDelete() const { return m_autoDelete; } + void setAutoDelete(bool b) { m_autoDelete = b; } + + T& operator[](size_t n) const { check(n); return *m_baseVector[n]; } + T& at(size_t n) const { check(n); return *m_baseVector.at(n); } + + Iterator begin() { return Iterator(this, 0); } + ConstIterator begin() const { return ConstIterator(this, 0); } + ConstIterator constBegin() const { return ConstIterator(this, 0); } + Iterator end() { return Iterator(this, count()); } + ConstIterator end() const { return ConstIterator(this, count()); } + ConstIterator constEnd() const { return ConstIterator(this, count()); } + + T* front() { return count() > 0 ? m_baseVector.at(0) : 0; } + const T* front() const { return count() > 0 ? m_baseVector.at(0) : 0; } + T* back() { return count() > 0 ? m_baseVector.at(count()-1) : 0; } + const T* back() const { return count() > 0 ? m_baseVector.at(count()-1) : 0; } + + void clear() { while(remove(begin())) { ; } } + + void push_back(T* ptr) { m_baseVector.push_back(ptr); } + bool remove(T* ptr); + bool remove(Iterator it); + bool contains(const T* ptr) const; + +private: +#ifndef NDEBUG + void check(size_t n) const { if(n >= count()) kdDebug() << "PtrVector() - bad index" << endl; } +#else + void check(size_t) const {} +#endif + + QValueVector<T*> m_baseVector; + bool m_autoDelete : 1; +}; + +} + +template <class T> +void Tellico::Vector<T>::append(Tellico::Vector<T> v) { + typename Tellico::Vector<T>::Iterator it; + for(it = v.begin(); it != v.end(); ++it) { + append(it.data()); + } +} + +template <class T> +bool Tellico::PtrVector<T>::remove(T* t) { + if(!t) { + return false; + } + T** ptr = qFind(m_baseVector.begin(), m_baseVector.end(), t); + if(ptr == m_baseVector.end()) { + return false; + } + if(m_autoDelete) { + delete *ptr; + } + m_baseVector.erase(ptr); + // in case the pointer is in the vector multiple times + while(remove(t)) { + kdDebug() << "PtrVector::remove() - pointer was duplicated in vector" << endl; + } + return true; +} + +template <class T> +bool Tellico::PtrVector<T>::remove(Iterator it) { + if(it == end()) { + return false; + } + return remove(it.ptr()); +} + +template <class T> +bool Tellico::PtrVector<T>::contains(const T* t) const { + if(!t) { + return false; + } + const T* const* ptr = qFind(m_baseVector.begin(), m_baseVector.end(), t); + return ptr != m_baseVector.end(); +} + +#endif diff --git a/src/reportdialog.cpp b/src/reportdialog.cpp new file mode 100644 index 0000000..ee879de --- /dev/null +++ b/src/reportdialog.cpp @@ -0,0 +1,220 @@ +/*************************************************************************** + copyright : (C) 2005-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 "reportdialog.h" +#include "translators/htmlexporter.h" +#include "imagefactory.h" +#include "tellico_kernel.h" +#include "collection.h" +#include "document.h" +#include "entry.h" +#include "controller.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "gui/combobox.h" +#include "core/tellico_config.h" + +#include <klocale.h> +#include <khtml_part.h> +#include <khtmlview.h> +#include <kstandarddirs.h> +#include <kdebug.h> +#include <kapplication.h> +#include <kpushbutton.h> +#include <kiconloader.h> +#include <ksortablevaluelist.h> +#include <kfiledialog.h> + +#include <qlayout.h> +#include <qfile.h> +#include <qlabel.h> +#include <qfileinfo.h> +#include <qtimer.h> + +namespace { + static const int REPORT_MIN_WIDTH = 600; + static const int REPORT_MIN_HEIGHT = 420; +} + +using Tellico::ReportDialog; + +// default button is going to be used as a print button, so it's separated +ReportDialog::ReportDialog(QWidget* parent_, const char* name_/*=0*/) + : KDialogBase(parent_, name_, false, i18n("Collection Report"), Close, Close), + m_exporter(0) { + QWidget* mainWidget = new QWidget(this, "ReportDialog mainWidget"); + setMainWidget(mainWidget); + QVBoxLayout* topLayout = new QVBoxLayout(mainWidget, 0, KDialog::spacingHint()); + + QHBoxLayout* hlay = new QHBoxLayout(topLayout); + QLabel* l = new QLabel(i18n("&Report template:"), mainWidget); + hlay->addWidget(l); + +// KStandardDirs::findAllResources(const char *type, const QString &filter, bool recursive, bool uniq) + QStringList files = KGlobal::dirs()->findAllResources("appdata", QString::fromLatin1("report-templates/*.xsl"), + false, true); + KSortableValueList<QString, QString> templates; + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + QFileInfo fi(*it); + QString file = fi.fileName(); + QString name = file.section('.', 0, -2); + name.replace('_', ' '); + QString title = i18n((name + QString::fromLatin1(" XSL Template")).utf8(), name.utf8()); + templates.insert(title, file); + } + templates.sort(); + m_templateCombo = new GUI::ComboBox(mainWidget); + for(KSortableValueList<QString, QString>::iterator it = templates.begin(); it != templates.end(); ++it) { + m_templateCombo->insertItem((*it).index(), (*it).value()); + } + hlay->addWidget(m_templateCombo); + l->setBuddy(m_templateCombo); + + KPushButton* pb1 = new KPushButton(SmallIconSet(QString::fromLatin1("exec")), i18n("&Generate"), mainWidget); + hlay->addWidget(pb1); + connect(pb1, SIGNAL(clicked()), SLOT(slotGenerate())); + + hlay->addStretch(); + + KPushButton* pb2 = new KPushButton(KStdGuiItem::saveAs(), mainWidget); + hlay->addWidget(pb2); + connect(pb2, SIGNAL(clicked()), SLOT(slotSaveAs())); + + KPushButton* pb3 = new KPushButton(KStdGuiItem::print(), mainWidget); + hlay->addWidget(pb3); + connect(pb3, SIGNAL(clicked()), SLOT(slotPrint())); + + m_HTMLPart = new KHTMLPart(mainWidget); + m_HTMLPart->setJScriptEnabled(false); + m_HTMLPart->setJavaEnabled(false); + m_HTMLPart->setMetaRefreshEnabled(false); + m_HTMLPart->setPluginsEnabled(false); + topLayout->addWidget(m_HTMLPart->view()); + + QString text = QString::fromLatin1("<html><style>p{font-weight:bold;width:50%;" + "margin:20% auto auto auto;text-align:center;" + "background:white;color:%1;}</style><body><p>").arg(contrastColor.name()) + + i18n("Select a report template and click <em>Generate</em>.") + ' ' + + i18n("Some reports may take several seconds to generate for large collections."); + + QString::fromLatin1("</p></body></html>"); + m_HTMLPart->begin(); + m_HTMLPart->write(text); + m_HTMLPart->end(); + + setMinimumWidth(QMAX(minimumWidth(), REPORT_MIN_WIDTH)); + setMinimumHeight(QMAX(minimumHeight(), REPORT_MIN_HEIGHT)); + resize(configDialogSize(QString::fromLatin1("Report Dialog Options"))); +} + +ReportDialog::~ReportDialog() { + delete m_exporter; + m_exporter = 0; + + saveDialogSize(QString::fromLatin1("Report Dialog Options")); +} + +void ReportDialog::slotGenerate() { + GUI::CursorSaver cs(Qt::waitCursor); + + QString fileName = QString::fromLatin1("report-templates/") + m_templateCombo->currentData().toString(); + QString xsltFile = locate("appdata", fileName); + if(xsltFile.isEmpty()) { + kdWarning() << "ReportDialog::setXSLTFile() - can't locate " << m_templateCombo->currentData().toString() << endl; + return; + } + // if it's the same XSL file, no need to reload the XSLTHandler, just refresh + if(xsltFile == m_xsltFile) { + slotRefresh(); + return; + } + + m_xsltFile = xsltFile; + + delete m_exporter; + m_exporter = new Export::HTMLExporter(); + m_exporter->setXSLTFile(m_xsltFile); + m_exporter->setPrintHeaders(false); // the templates should take care of this themselves + m_exporter->setPrintGrouped(true); // allow templates to take advantage of added DOM + + slotRefresh(); +} + +void ReportDialog::slotRefresh() { + if(!m_exporter) { + kdWarning() << "ReportDialog::slotRefresh() - no exporter" << endl; + return; + } + + m_exporter->setGroupBy(Controller::self()->expandedGroupBy()); + m_exporter->setSortTitles(Controller::self()->sortTitles()); + m_exporter->setColumns(Controller::self()->visibleColumns()); + // only print visible entries + m_exporter->setEntries(Controller::self()->visibleEntries()); + + long options = Export::ExportUTF8 | Export::ExportComplete | Export::ExportImages; + if(Config::autoFormat()) { + options |= Export::ExportFormatted; + } + m_exporter->setOptions(options); + + // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found + // by simply using a relative path in the xslt file + KURL u; + u.setPath(m_xsltFile); + m_HTMLPart->begin(u); + m_HTMLPart->write(m_exporter->text()); +#if 0 + QFile f(QString::fromLatin1("/tmp/test.html")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t << m_exporter->text(); + } + f.close(); +#endif + m_HTMLPart->end(); + // is this needed? +// view()->layout(); +} + +// actually the print button +void ReportDialog::slotPrint() { + m_HTMLPart->view()->print(); +} + +void ReportDialog::slotSaveAs() { + QString filter = i18n("*.html|HTML Files (*.html)") + QChar('\n') + i18n("*|All Files"); + KURL u = KFileDialog::getSaveURL(QString::null, filter, this); + if(!u.isEmpty() && u.isValid()) { + KConfigGroup config(KGlobal::config(), "ExportOptions"); + bool encode = config.readBoolEntry("EncodeUTF8", true); + int oldOpt = m_exporter->options(); + + // turn utf8 off + long options = oldOpt & ~Export::ExportUTF8; + // now turn it on if true + if(encode) { + options |= Export::ExportUTF8; + } + + KURL oldURL = m_exporter->url(); + m_exporter->setOptions(options); + m_exporter->setURL(u); + + m_exporter->exec(); + + m_exporter->setURL(oldURL); + m_exporter->setOptions(oldOpt); + } +} + +#include "reportdialog.moc" diff --git a/src/reportdialog.h b/src/reportdialog.h new file mode 100644 index 0000000..758c2d6 --- /dev/null +++ b/src/reportdialog.h @@ -0,0 +1,64 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef REPORTDIALOG_H +#define REPORTDIALOG_H + +#include <kdialogbase.h> + +class KHTMLPart; + +namespace Tellico { + namespace Export { + class HTMLExporter; + } + namespace GUI { + class ComboBox; + } + +/** + * @author Robby Stephenson + */ +class ReportDialog : public KDialogBase { +Q_OBJECT + +public: + /** + * The constructor sets up the dialog. + * + * @param parent A pointer to the parent widget + * @param name The widget name + */ + ReportDialog(QWidget* parent, const char* name=0); + virtual ~ReportDialog(); + +public slots: + /** + * Regenerate the report. + */ + void slotRefresh(); + +private slots: + void slotGenerate(); + void slotPrint(); + void slotSaveAs(); + +private: + KHTMLPart* m_HTMLPart; + GUI::ComboBox* m_templateCombo; + Export::HTMLExporter* m_exporter; + QString m_xsltFile; +}; + +} // end namespace +#endif diff --git a/src/rtf2html/Makefile.am b/src/rtf2html/Makefile.am new file mode 100644 index 0000000..ea3d39f --- /dev/null +++ b/src/rtf2html/Makefile.am @@ -0,0 +1,15 @@ +AM_CPPFLAGS = $(all_includes) + +noinst_LIBRARIES = librtf2html.a +librtf2html_a_SOURCES = fmt_opts.cpp rtf2html.cpp rtf_keyword.cpp rtf_table.cpp + +librtf2html_a_METASOURCES = AUTO + +KDE_OPTIONS = noautodist + +EXTRA_DIST = common.h dbg_iter.h fmt_opts.h fmt_opts.cpp \ +rtf2html.h rtf2html.cpp rtf_keyword.h rtf_keyword.cpp \ +rtf_table.h rtf_table.cpp rtf_tools.h + +CLEANFILES = *~ + diff --git a/src/rtf2html/common.h b/src/rtf2html/common.h new file mode 100644 index 0000000..01b1f5b --- /dev/null +++ b/src/rtf2html/common.h @@ -0,0 +1,38 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef __COMMON_H__ +#define __COMMON_H__ + +#include <string> +#include <sstream> +#include <iomanip> + +inline std::string from_int(int value) +{ + std::ostringstream buf; + buf<<value; + return buf.str(); +} + +inline std::string hex(unsigned int value) +{ + std::ostringstream buf; + buf<<std::setw(2)<<std::setfill('0')<<std::hex<<value; + return buf.str(); +} + +#endif diff --git a/src/rtf2html/dbg_iter.h b/src/rtf2html/dbg_iter.h new file mode 100644 index 0000000..dfbccf2 --- /dev/null +++ b/src/rtf2html/dbg_iter.h @@ -0,0 +1,67 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +namespace rtf { + +template <class T> +class dbg_iter_mixin : public virtual T +{ + public: + int offset; + dbg_iter_mixin(const T& t) : T(t) + {} + T& operator=(const T& t) + { + return T::operator=(t); + } + dbg_iter_mixin& operator++ () + { + ++offset; + T::operator++(); + return *this; + } + dbg_iter_mixin operator++ (int i) + { + ++offset; + return T::operator++(i); + } + char operator *() const + { + T::value_type c=T::operator*(); +// std::cerr<<offset<<":"<<c<<std::endl; + return c; + } +}; + +template <class T> +class dbg_iter : public dbg_iter_mixin<T> +{ + public: + dbg_iter(const T& t) : dbg_iter_mixin<T>(t) + {} +}; + +template<class T> +class dbg_iter<std::istreambuf_iterator<T> > : + public virtual std::istreambuf_iterator<T>, + public dbg_iter_mixin<std::istreambuf_iterator<T> > +{ + public: + dbg_iter(std::basic_streambuf<T> *buf) : std::istreambuf_iterator<T>(buf) + {} +}; + +}
\ No newline at end of file diff --git a/src/rtf2html/fmt_opts.cpp b/src/rtf2html/fmt_opts.cpp new file mode 100644 index 0000000..25a2f24 --- /dev/null +++ b/src/rtf2html/fmt_opts.cpp @@ -0,0 +1,221 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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 "fmt_opts.h" + +using namespace rtf; + +std::string formatting_options::get_par_str() const +{ + std::string style; + switch (papAlign) + { + case formatting_options::align_right: + style+="text-align:right;"; + break; + case formatting_options::align_center: + style+="text-align:center;"; + break; + case formatting_options::align_justify: + style+="text-align:justify;"; + default: break; + } + if (papFirst!=0) + { + style+="text-indent:"; + style+=from_int(papFirst); + style+="pt;"; + } + if (papLeft!=0) + { + style+="margin-left:"; + style+=from_int(papLeft); + style+="pt;"; + } + if (papRight!=0) + { + style+="margin-right:"; + style+=from_int(papRight); + style+="pt;"; + } + if (papBefore!=0) + { + style+="margin-top:"; + style+=from_int(papBefore); + style+="pt;"; + } + if (papAfter!=0) + { + style+="margin-bottom:"; + style+=from_int(papAfter); + style+="pt;"; + } + if (style.empty()) + return std::string("<p>"); + else + { + style.insert(0, "<p style=\""); + return style+"\">"; + } +} + +std::string formatter::format(const formatting_options &_opt) +{ + formatting_options last_opt, opt(_opt); + std::string result; + if (!opt_stack.empty()) + { + int cnt=0; + fo_deque::reverse_iterator i; + for (i=opt_stack.rbegin(); i!=opt_stack.rend(); ++i) + { + if (*i==opt) + break; + ++cnt; + } + if (cnt==0) + return ""; + if (i!=opt_stack.rend()) + { + while (cnt--) + { + result+="</span>"; + opt_stack.pop_back(); + } + return result; + } + last_opt=opt_stack.back(); + } + if (last_opt.chpVAlign!=formatting_options::va_normal + && last_opt.chpVAlign!=opt.chpVAlign) + { + int cnt=0; + fo_deque::reverse_iterator i; + for (i=opt_stack.rbegin(); i!=opt_stack.rend(); ++i) + { + if (i->chpVAlign==formatting_options::va_normal) + break; + ++cnt; + } + while (cnt--) + { + result+="</span>"; + opt_stack.pop_back(); + } + last_opt=opt_stack.empty()?formatting_options():opt_stack.back(); + } + std::string style; + if (opt.chpBold!=last_opt.chpBold) + { + style+="font-weight:"; + style+=opt.chpBold?"bold":"normal"; + style+=";"; + } + if (opt.chpItalic!=last_opt.chpItalic) + { + style+="font-style:"; + style+=opt.chpItalic?"italic":"normal"; + style+=";"; + } + if (opt.chpUnderline!=last_opt.chpUnderline) + { + style+="text-decoration:"; + style+=opt.chpUnderline?"underline":"none"; + style+=";"; + } + if (opt.chpVAlign!=formatting_options::va_normal) + opt.chpFontSize=(int)(0.7*(opt.chpFontSize?opt.chpFontSize:24)); + if (opt.chpFontSize!=last_opt.chpFontSize) + { + style+="font-size:"; + style+=from_int(opt.chpFontSize/2); + style+="pt;"; + } + if (opt.chpVAlign!=last_opt.chpVAlign) + { + style+="vertical-align:"; + style+=opt.chpVAlign==formatting_options::va_sub?"sub":"super"; + style+=";"; + } + if (opt.chpFColor!=last_opt.chpFColor) + { + style+="color:"; + style+=opt.chpFColor.r>0?"#"+hex(opt.chpFColor.r&0xFF) + +hex(opt.chpFColor.g&0xFF) + +hex(opt.chpFColor.b&0xFF) + :"WindowText"; + style+=";"; + } + if (opt.chpBColor!=last_opt.chpBColor) + { + style+="background-color:"; + style+=opt.chpBColor.r>0?"#"+hex(opt.chpBColor.r&0xFF) + +hex(opt.chpBColor.g&0xFF) + +hex(opt.chpBColor.b&0xFF) + :"Window"; + style+=";"; + } + if (opt.chpHighlight!=last_opt.chpHighlight) + { + style+="background-color:"; + switch (opt.chpHighlight) + { + case 0: style+="Window"; break; + case 1: style+="black"; break; + case 2: style+="blue"; break; + case 3: style+="aqua"; break; + case 4: style+="lime"; break; + case 5: style+="fuchsia"; break; + case 6: style+="red"; break; + case 7: style+="yellow"; break; + case 9: style+="navy"; break; + case 10: style+="teal"; break; + case 11: style+="green"; break; + case 12: style+="purple"; break; + case 13: style+="maroon"; break; + case 14: style+="olive"; break; + case 15: style+="gray"; break; + case 16: style+="silver"; break; + } + style+=";"; + } + if (opt.chpFont!=last_opt.chpFont) + { + style+="font-family:'"; + style+=opt.chpFont.name.empty()?"serif":opt.chpFont.name; + style+="'"; + switch (opt.chpFont.family) + { + case font::ff_serif: style+=", serif"; break; + case font::ff_sans_serif: style+=", sans-serif"; break; + case font::ff_cursive: style+=", cursive"; break; + case font::ff_fantasy: style+=", fantasy"; break; + case font::ff_monospace: style+=", monospace"; break; + default: break; + } + style+=";"; + } + opt_stack.push_back(opt); + return result+"<span style=\""+style+"\">"; +} + +std::string formatter::close() +{ + std::string result; + for (fo_deque::iterator i=opt_stack.begin(); i!=opt_stack.end(); ++i) + result+="</span>"; + return result; +} diff --git a/src/rtf2html/fmt_opts.h b/src/rtf2html/fmt_opts.h new file mode 100644 index 0000000..6845d60 --- /dev/null +++ b/src/rtf2html/fmt_opts.h @@ -0,0 +1,154 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef __FMT_OPTS_H__ +#define __FMT_OPTS_H__ + +#include "common.h" +#include <stack> +#include <vector> +#include <deque> +#include <map> + +namespace rtf { + +struct color { + int r, g, b; + color() : r(-1), g(-1), b(-1) {} + bool operator==(const color &clr) + { + return r==clr.r && g==clr.g && b==clr.b; + } + bool operator!=(const color &clr) + { + return !(*this==clr); + } + color &operator=(const color &clr) + { + r=clr.r; g=clr.g; b=clr.b; + return *this; + } +}; + +typedef std::vector<color> colorvect; + +struct font { + enum font_family {ff_none, ff_serif, ff_sans_serif, ff_cursive, + ff_fantasy, ff_monospace}; + font_family family; + std::string name; + int pitch; + int charset; + font() : family(ff_none), name(), pitch(0), charset(0) {} + bool operator==(const font &f) + { + return family==f.family && name==f.name; + } + bool operator!=(const font &f) + { + return !(*this==f); + } + font &operator=(const font &f) + { + family=f.family; name=f.name; pitch=f.pitch; charset=f.charset; + return *this; + } +}; + +typedef std::map<int, font> fontmap; + +struct formatting_options +{ + enum halign {align_left, align_right, align_center, align_justify, align_error}; + enum valign {va_normal, va_sub, va_sup}; + bool chpBold, chpItalic, chpUnderline; + valign chpVAlign; + int chpFontSize, chpHighlight; + color chpFColor, chpBColor; + font chpFont; + int papLeft, papRight, papFirst; + int papBefore, papAfter; + halign papAlign; + bool papInTbl; + formatting_options() + { + chpBold=chpItalic=chpUnderline=false; + chpVAlign=va_normal; + chpFontSize=chpHighlight=0; + papLeft=papRight=papFirst=papBefore=papAfter=0; + papAlign=align_left; + papInTbl=false; + } + bool operator==(const formatting_options &opt) // tests only for character options + { + return chpBold==opt.chpBold && chpItalic==opt.chpItalic + && chpUnderline==opt.chpUnderline && chpVAlign==opt.chpVAlign + && chpFontSize==opt.chpFontSize + && chpFColor==opt.chpFColor && chpBColor==opt.chpBColor + && chpHighlight==opt.chpHighlight && chpFont==opt.chpFont; + } + bool operator!=(const formatting_options &opt) // tests only for character options + { + return !(*this==opt); + } + formatting_options &operator=(const formatting_options &opt) + { + chpBold=opt.chpBold; chpItalic=opt.chpItalic; + chpUnderline=opt.chpUnderline; chpVAlign=opt.chpVAlign; + chpFontSize=opt.chpFontSize; + chpFColor=opt.chpFColor; chpBColor=opt.chpBColor; + chpHighlight=opt.chpHighlight; chpFont=opt.chpFont; + papLeft=opt.papLeft; papRight=opt.papRight; + papFirst=opt.papFirst; papBefore=opt.papBefore; papAfter=opt.papAfter; + papAlign=opt.papAlign; papInTbl=opt.papInTbl; + return *this; + } + std::string get_par_str() const; +}; + +typedef std::stack<formatting_options> fo_stack; + +typedef std::deque<formatting_options> fo_deque; + +class formatter { + private: + fo_deque opt_stack; + public: + std::string format(const formatting_options &opt); + std::string close(); + void clear() { opt_stack.clear(); } +}; + +class html_text { + private: + const formatting_options &opt; + formatter fmt; + std::string text; + public: + html_text(const formatting_options &_opt) : opt(_opt) {} + const std::string &str() { return text; } + template <class T> void write(T s) + { + text+=fmt.format(opt)+s; + } + std::string close() { return fmt.close(); } +// void write(char c) { write(std::string()+c); } + void clear() { text.clear(); fmt.clear(); } +}; + +} +#endif + diff --git a/src/rtf2html/rtf2html.cpp b/src/rtf2html/rtf2html.cpp new file mode 100644 index 0000000..4f29fe7 --- /dev/null +++ b/src/rtf2html/rtf2html.cpp @@ -0,0 +1,531 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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 "rtf2html.h" +#include "rtf_table.h" +#include "rtf_tools.h" +#include "rtf_keyword.h" +#include "fmt_opts.h" + +#include <cstdlib> +#include <stdexcept> +#include <fstream> +#include <iostream> +#include <string> + +using Tellico::RTF2HTML; +using namespace rtf; + +RTF2HTML::RTF2HTML(const QString& text) : m_text(text) { +} + +QString RTF2HTML::toHTML() const { + std::string str_in = m_text; + + std::string::iterator buf_in=str_in.begin(), buf_in_end=str_in.end(); + colorvect colortbl; + fontmap fonttbl; + std::string title; + + bool bAsterisk=false; + fo_stack foStack; + formatting_options cur_options; + std::string html; + html_text par_html(cur_options); + + /* CellDefs in rtf are really queer. We'll keep a list of them in main() + and will give an iterator into this list to a row */ + table_cell_defs_list CellDefsList; + table_cell_defs_list::iterator CurCellDefs; + table_cell_def* tcdCurCellDef=new table_cell_def; + table_cell* tcCurCell=new table_cell; + table_row* trCurRow=new table_row; + table* tblCurTable=new table; + int iLastRowLeft=0, iLastRowHeight=0; + std::string t_str; + + bool bInTable=false; + int iDocWidth=12240; + int iMarginLeft=1800; + while(buf_in!=buf_in_end) + { + switch (*buf_in) + { + case '\\': + { + rtf_keyword kw(++buf_in); + if (kw.is_control_char()) + switch (kw.control_char()) + { + case '\\': case '{': case '}': + par_html.write(kw.control_char()); + break; + case '\'': + { + std::string stmp(1,*buf_in++); + stmp+=*buf_in++; + int code=std::strtol(stmp.c_str(), NULL, 16); + switch (code) + { + case 167: + par_html.write("•"); + break; + case 188: + par_html.write("…"); + break; + default: + par_html.write((char)code); + } + break; + } + case '*': + bAsterisk=true; + break; + case '~': + par_html.write(" "); + break; + case '\n': + par_html.write("<br><br>"); + break; + } + else //kw.is_control_char + if (bAsterisk) + { + bAsterisk=false; + skip_group(buf_in); + } + else + { + switch (kw.keyword()) + { + case rtf_keyword::rkw_filetbl: + case rtf_keyword::rkw_stylesheet: + case rtf_keyword::rkw_header: + case rtf_keyword::rkw_footer: case rtf_keyword::rkw_headerf: + case rtf_keyword::rkw_footerf: case rtf_keyword::rkw_pict: + case rtf_keyword::rkw_object: + // we'll skip such groups + skip_group(buf_in); + break; + // document title + case rtf_keyword::rkw_info: + { + int depth=1; + bool in_title=false; + while (depth>0) + { +// std::cout<<std::string(buf_in).substr(0,20)<<"\t"<<depth<<std::endl; + switch (*buf_in) + { + case '\\': + { + rtf_keyword kw(++buf_in); + if (kw.keyword()==rtf_keyword::rkw_title) + in_title=true; + break; + } + case '{': ++depth; ++buf_in; break; + case '}': --depth; ++buf_in; in_title=false; break; + default: if (in_title) title+=*buf_in; ++buf_in; break; + } + } + break; + } + // color table + case rtf_keyword::rkw_colortbl: + { + color clr; + while (*buf_in!='}') + { + switch (*buf_in) + { + case '\\': + { + rtf_keyword kw(++buf_in); + switch (kw.keyword()) + { + case rtf_keyword::rkw_red: + clr.r=kw.parameter(); + break; + case rtf_keyword::rkw_green: + clr.g=kw.parameter(); + break; + case rtf_keyword::rkw_blue: + clr.b=kw.parameter(); + break; + default: break; + } + break; + } + case ';': + colortbl.push_back(clr); + ++buf_in; + break; + default: + ++buf_in; + break; + } + } + ++buf_in; + break; + } + // font table + case rtf_keyword::rkw_fonttbl: + { + font fnt; + int font_num; + bool full_name=false; + bool in_font=false; + while (! (*buf_in=='}' && !in_font)) + { + switch (*buf_in) + { + case '\\': + { + rtf_keyword kw(++buf_in); + if (kw.is_control_char() && kw.control_char()=='*') + skip_group(buf_in); + else + switch (kw.keyword()) + { + case rtf_keyword::rkw_f: + font_num=kw.parameter(); + break; + case rtf_keyword::rkw_fprq: + fnt.pitch=kw.parameter(); + break; + case rtf_keyword::rkw_fcharset: + fnt.charset=kw.parameter(); + break; + case rtf_keyword::rkw_fnil: + fnt.family=font::ff_none; + break; + case rtf_keyword::rkw_froman: + fnt.family=font::ff_serif; + break; + case rtf_keyword::rkw_fswiss: + fnt.family=font::ff_sans_serif; + break; + case rtf_keyword::rkw_fmodern: + fnt.family=font::ff_monospace; + break; + case rtf_keyword::rkw_fscript: + fnt.family=font::ff_cursive; + break; + case rtf_keyword::rkw_fdecor: + fnt.family=font::ff_fantasy; + break; + default: break; + } + break; + } + case '{': + in_font=true; + ++buf_in; + break; + case '}': + in_font=false; + fonttbl.insert(std::make_pair(font_num, fnt)); + fnt=font(); + full_name=false; + ++buf_in; + break; + case ';': + full_name=true; + ++buf_in; + break; + default: + if (!full_name && in_font) + fnt.name+=*buf_in; + ++buf_in; + break; + } + } + ++buf_in; + break; + } + // special characters + case rtf_keyword::rkw_line: case rtf_keyword::rkw_softline: + par_html.write("<br>"); + break; + case rtf_keyword::rkw_tab: + par_html.write(" "); // maybe, this can be done better + break; + case rtf_keyword::rkw_enspace: case rtf_keyword::rkw_emspace: + par_html.write(" "); + break; + case rtf_keyword::rkw_qmspace: + par_html.write(" "); + break; + case rtf_keyword::rkw_endash: + par_html.write("–"); + break; + case rtf_keyword::rkw_emdash: + par_html.write("—"); + break; + case rtf_keyword::rkw_bullet: + par_html.write("•"); + break; + case rtf_keyword::rkw_lquote: + par_html.write("‘"); + break; + case rtf_keyword::rkw_rquote: + par_html.write("’"); + break; + case rtf_keyword::rkw_ldblquote: + par_html.write("“"); + break; + case rtf_keyword::rkw_rdblquote: + par_html.write("”"); + break; + // paragraph formatting + case rtf_keyword::rkw_ql: + cur_options.papAlign=formatting_options::align_left; + break; + case rtf_keyword::rkw_qr: + cur_options.papAlign=formatting_options::align_right; + break; + case rtf_keyword::rkw_qc: + cur_options.papAlign=formatting_options::align_center; + break; + case rtf_keyword::rkw_qj: + cur_options.papAlign=formatting_options::align_justify; + break; + case rtf_keyword::rkw_fi: + cur_options.papFirst=(int)rint(kw.parameter()/20); + break; + case rtf_keyword::rkw_li: + cur_options.papLeft=(int)rint(kw.parameter()/20); + break; + case rtf_keyword::rkw_ri: + cur_options.papRight=(int)rint(kw.parameter()/20); + break; + case rtf_keyword::rkw_sb: + cur_options.papBefore=(int)rint(kw.parameter()/20); + break; + case rtf_keyword::rkw_sa: + cur_options.papAfter=(int)rint(kw.parameter()/20); + break; + case rtf_keyword::rkw_pard: + cur_options.papBefore=cur_options.papAfter=0; + cur_options.papLeft=cur_options.papRight=0; + cur_options.papFirst=0; + cur_options.papAlign=formatting_options::align_left; + cur_options.papInTbl=false; + break; + case rtf_keyword::rkw_par: + case rtf_keyword::rkw_sect: + t_str=cur_options.get_par_str()+par_html.str() + +" "+par_html.close()+"</p>\n"; + if (!bInTable) + { + html+=t_str; + } + else + { + if (cur_options.papInTbl) + { + tcCurCell->Text+=t_str; + } + else + { + html+=tblCurTable->make()+t_str; + bInTable=false; + tblCurTable=new table; + } + } + par_html.clear(); + break; + // character formatting + case rtf_keyword::rkw_super: + cur_options.chpVAlign= + kw.parameter()==0?formatting_options::va_normal + :formatting_options::va_sup; + break; + case rtf_keyword::rkw_sub: + cur_options.chpVAlign= + kw.parameter()==0?formatting_options::va_normal + :formatting_options::va_sub; + break; + case rtf_keyword::rkw_b: + cur_options.chpBold=!(kw.parameter()==0); + break; + case rtf_keyword::rkw_i: + cur_options.chpItalic=!(kw.parameter()==0); + break; + case rtf_keyword::rkw_ul: + cur_options.chpUnderline=!(kw.parameter()==0); + break; + case rtf_keyword::rkw_ulnone: + cur_options.chpUnderline=false; + break; + case rtf_keyword::rkw_fs: + cur_options.chpFontSize=kw.parameter(); + break; + case rtf_keyword::rkw_cf: + cur_options.chpFColor=colortbl[kw.parameter()]; + break; + case rtf_keyword::rkw_cb: + cur_options.chpBColor=colortbl[kw.parameter()]; + break; + case rtf_keyword::rkw_highlight: + cur_options.chpHighlight=kw.parameter(); + break; + case rtf_keyword::rkw_f: + cur_options.chpFont=fonttbl[kw.parameter()]; + break; + case rtf_keyword::rkw_plain: + cur_options.chpBold=cur_options.chpItalic + =cur_options.chpUnderline=false; + cur_options.chpVAlign=formatting_options::va_normal; + cur_options.chpFontSize=cur_options.chpHighlight=0; + cur_options.chpFColor=cur_options.chpBColor=color(); + cur_options.chpFont=font(); + break; + // table formatting + case rtf_keyword::rkw_intbl: + cur_options.papInTbl=true; + break; + case rtf_keyword::rkw_trowd: + CurCellDefs=CellDefsList.insert(CellDefsList.end(), + table_cell_defs()); + case rtf_keyword::rkw_row: + if (!trCurRow->Cells.empty()) + { + trCurRow->CellDefs=CurCellDefs; + if (trCurRow->Left==-1000) + trCurRow->Left=iLastRowLeft; + if (trCurRow->Height==-1000) + trCurRow->Height=iLastRowHeight; + tblCurTable->push_back(trCurRow); + trCurRow=new table_row; + } + bInTable=true; + break; + case rtf_keyword::rkw_cell: + t_str=cur_options.get_par_str()+par_html.str() + +" "+par_html.close()+"</p>\n"; + tcCurCell->Text+=t_str; + par_html.clear(); + trCurRow->Cells.push_back(tcCurCell); + tcCurCell=new table_cell; + break; + case rtf_keyword::rkw_cellx: + tcdCurCellDef->Right=kw.parameter(); + CurCellDefs->push_back(tcdCurCellDef); + tcdCurCellDef=new table_cell_def; + break; + case rtf_keyword::rkw_trleft: + trCurRow->Left=kw.parameter(); + iLastRowLeft=kw.parameter(); + break; + case rtf_keyword::rkw_trrh: + trCurRow->Height=kw.parameter(); + iLastRowHeight=kw.parameter(); + break; + case rtf_keyword::rkw_clvmgf: + tcdCurCellDef->FirstMerged=true; + break; + case rtf_keyword::rkw_clvmrg: + tcdCurCellDef->Merged=true; + break; + case rtf_keyword::rkw_clbrdrb: + tcdCurCellDef->BorderBottom=true; + tcdCurCellDef->ActiveBorder=&(tcdCurCellDef->BorderBottom); + break; + case rtf_keyword::rkw_clbrdrt: + tcdCurCellDef->BorderTop=true; + tcdCurCellDef->ActiveBorder=&(tcdCurCellDef->BorderTop); + break; + case rtf_keyword::rkw_clbrdrl: + tcdCurCellDef->BorderLeft=true; + tcdCurCellDef->ActiveBorder=&(tcdCurCellDef->BorderLeft); + break; + case rtf_keyword::rkw_clbrdrr: + tcdCurCellDef->BorderRight=true; + tcdCurCellDef->ActiveBorder=&(tcdCurCellDef->BorderRight); + break; + case rtf_keyword::rkw_brdrnone: + if (tcdCurCellDef->ActiveBorder!=NULL) + { + *(tcdCurCellDef->ActiveBorder)=false; + } + break; + case rtf_keyword::rkw_clvertalt: + tcdCurCellDef->VAlign=table_cell_def::valign_top; + break; + case rtf_keyword::rkw_clvertalc: + tcdCurCellDef->VAlign=table_cell_def::valign_center; + break; + case rtf_keyword::rkw_clvertalb: + tcdCurCellDef->VAlign=table_cell_def::valign_bottom; + break; + // page formatting + case rtf_keyword::rkw_paperw: + iDocWidth=kw.parameter(); + break; + case rtf_keyword::rkw_margl: + iMarginLeft=kw.parameter(); + break; + default: break; + } + } + break; + } + case '{': + // perform group opening actions here + foStack.push(cur_options); + ++buf_in; + break; + case '}': + // perform group closing actions here + cur_options=foStack.top(); + foStack.pop(); + ++buf_in; + break; + case 13: + case 10: + ++buf_in; + break; + case '<': + par_html.write("<"); + ++buf_in; + break; + case '>': + par_html.write(">"); + ++buf_in; + break; +/* case ' ': + par_html.write(" "); + ++buf_in; + break;*/ + default: + par_html.write(*buf_in++); + } + } + + t_str=cur_options.get_par_str()+par_html.str() + +" "+par_html.close()+"</p>\n"; + html+=t_str; + + delete tcCurCell; + delete trCurRow; + delete tblCurTable; + delete tcdCurCellDef; + + return html; +} + diff --git a/src/rtf2html/rtf2html.h b/src/rtf2html/rtf2html.h new file mode 100644 index 0000000..a4e1d2f --- /dev/null +++ b/src/rtf2html/rtf2html.h @@ -0,0 +1,28 @@ +/*************************************************************************** + copyright : (C) 2007 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 <qstring.h> + +namespace Tellico { + +class RTF2HTML { +public: + RTF2HTML(const QString& text); + + QString toHTML() const; + +private: + QString m_text; +}; + +} diff --git a/src/rtf2html/rtf_keyword.cpp b/src/rtf2html/rtf_keyword.cpp new file mode 100644 index 0000000..ee4774c --- /dev/null +++ b/src/rtf2html/rtf_keyword.cpp @@ -0,0 +1,107 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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 "rtf_keyword.h" + +using namespace rtf; + +rtf_keyword::keyword_map::keyword_map() : base_class() +{ + insert(value_type("b", rkw_b)); + insert(value_type("bin", rkw_bin)); + insert(value_type("blue", rkw_blue)); + insert(value_type("brdrnone", rkw_brdrnone)); + insert(value_type("bullet", rkw_bullet)); + insert(value_type("cb", rkw_cb)); + insert(value_type("cell", rkw_cell)); + insert(value_type("cellx", rkw_cellx)); + insert(value_type("cf", rkw_cf)); + insert(value_type("clbrdrb", rkw_clbrdrb)); + insert(value_type("clbrdrl", rkw_clbrdrl)); + insert(value_type("clbrdrr", rkw_clbrdrr)); + insert(value_type("clbrdrt", rkw_clbrdrt)); + insert(value_type("clvertalb", rkw_clvertalb)); + insert(value_type("clvertalc", rkw_clvertalc)); + insert(value_type("clvertalt", rkw_clvertalt)); + insert(value_type("clvmgf", rkw_clvmgf)); + insert(value_type("clvmrg", rkw_clvmrg)); + insert(value_type("colortbl", rkw_colortbl)); + insert(value_type("emdash", rkw_emdash)); + insert(value_type("emspace", rkw_emspace)); + insert(value_type("endash", rkw_endash)); + insert(value_type("enspace", rkw_enspace)); + insert(value_type("f", rkw_f)); + insert(value_type("fprq", rkw_fprq)); + insert(value_type("fcharset", rkw_fcharset)); + insert(value_type("fnil", rkw_fnil)); + insert(value_type("froman", rkw_froman)); + insert(value_type("fswiss", rkw_fswiss)); + insert(value_type("fmodern", rkw_fmodern)); + insert(value_type("fscript", rkw_fscript)); + insert(value_type("fdecor", rkw_fdecor)); + insert(value_type("ftech", rkw_ftech)); + insert(value_type("fbidi", rkw_fbidi)); + insert(value_type("field", rkw_field)); + insert(value_type("filetbl", rkw_filetbl)); + insert(value_type("fldrslt", rkw_fldrslt)); + insert(value_type("fonttbl", rkw_fonttbl)); + insert(value_type("footer", rkw_footer)); + insert(value_type("footerf", rkw_footerf)); + insert(value_type("fs", rkw_fs)); + insert(value_type("green", rkw_green)); + insert(value_type("header", rkw_header)); + insert(value_type("headerf", rkw_headerf)); + insert(value_type("highlight", rkw_highlight)); + insert(value_type("i", rkw_i)); + insert(value_type("info", rkw_info)); + insert(value_type("intbl", rkw_intbl)); + insert(value_type("ldblquote", rkw_ldblquote)); + insert(value_type("li", rkw_li)); + insert(value_type("line", rkw_line)); + insert(value_type("lquote", rkw_lquote)); + insert(value_type("margl", rkw_margl)); + insert(value_type("object", rkw_object)); + insert(value_type("paperw", rkw_paperw)); + insert(value_type("par", rkw_par)); + insert(value_type("pard", rkw_pard)); + insert(value_type("pict", rkw_pict)); + insert(value_type("plain", rkw_plain)); + insert(value_type("qc", rkw_qc)); + insert(value_type("qj", rkw_qj)); + insert(value_type("ql", rkw_ql)); + insert(value_type("qr", rkw_qr)); + insert(value_type("rdblquote", rkw_rdblquote)); + insert(value_type("red", rkw_red)); + insert(value_type("ri", rkw_ri)); + insert(value_type("row", rkw_row)); + insert(value_type("rquote", rkw_rquote)); + insert(value_type("sa", rkw_sa)); + insert(value_type("sb", rkw_sb)); + insert(value_type("sect", rkw_sect)); + insert(value_type("softline", rkw_softline)); + insert(value_type("stylesheet", rkw_stylesheet)); + insert(value_type("sub", rkw_sub)); + insert(value_type("super", rkw_super)); + insert(value_type("tab", rkw_tab)); + insert(value_type("title", rkw_title)); + insert(value_type("trleft", rkw_trleft)); + insert(value_type("trowd", rkw_trowd)); + insert(value_type("trrh", rkw_trrh)); + insert(value_type("ul", rkw_ul)); + insert(value_type("ulnone", rkw_ulnone)); +} + +rtf_keyword::keyword_map rtf_keyword::keymap; diff --git a/src/rtf2html/rtf_keyword.h b/src/rtf2html/rtf_keyword.h new file mode 100644 index 0000000..c510ea0 --- /dev/null +++ b/src/rtf2html/rtf_keyword.h @@ -0,0 +1 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally.
Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net
available at http://rtf2html.sf.net
Original available under the terms of the GNU LGPL2, and according
to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */
/***************************************************************************
* *
* 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; *
* *
***************************************************************************/
#ifndef __RTF_KEYWORD_H__
#define __RTF_KEYWORD_H__
#include <string>
#include <map>
#include <ctype.h>
#include <cstdlib>
namespace rtf {
class rtf_keyword{
public:
enum keyword_type {rkw_unknown,
rkw_b, rkw_bin, rkw_blue, rkw_brdrnone, rkw_bullet,
rkw_cb, rkw_cell, rkw_cellx, rkw_cf, rkw_clbrdrb, rkw_clbrdrl,
rkw_clbrdrr, rkw_clbrdrt, rkw_clvertalb, rkw_clvertalc,
rkw_clvertalt, rkw_clvmgf, rkw_clvmrg, rkw_colortbl,
rkw_emdash, rkw_emspace, rkw_endash, rkw_enspace,
rkw_fi, rkw_field, rkw_filetbl,
rkw_f, rkw_fprq, rkw_fcharset,
rkw_fnil, rkw_froman, rkw_fswiss, rkw_fmodern,
rkw_fscript, rkw_fdecor, rkw_ftech, rkw_fbidi,
rkw_fldrslt, rkw_fonttbl, rkw_footer, rkw_footerf, rkw_fs,
rkw_green,
rkw_header, rkw_headerf, rkw_highlight,
rkw_i, rkw_info, rkw_intbl,
rkw_ldblquote, rkw_li, rkw_line, rkw_lquote,
rkw_margl,
rkw_object,
rkw_paperw, rkw_par, rkw_pard, rkw_pict, rkw_plain,
rkw_qc, rkw_qj, rkw_ql, rkw_qmspace, rkw_qr,
rkw_rdblquote, rkw_red, rkw_ri, rkw_row, rkw_rquote,
rkw_sa, rkw_sb, rkw_sect, rkw_softline, rkw_stylesheet,
rkw_sub, rkw_super,
rkw_tab, rkw_title, rkw_trleft, rkw_trowd, rkw_trrh,
rkw_ul, rkw_ulnone
};
private:
class keyword_map : public std::map<std::string, keyword_type>
{
private:
typedef std::map<std::string, keyword_type> base_class;
public:
keyword_map();
};
private:
static keyword_map keymap;
std::string s_keyword;
keyword_type e_keyword;
int param;
char ctrl_chr;
bool is_ctrl_chr;
public:
// iter must point after the backslash starting the keyword. We don't check it.
// after construction, iter points at the char following the keyword
template <class InputIter> explicit rtf_keyword(InputIter &iter);
bool is_control_char() const
{ return is_ctrl_chr; }
const std::string &keyword_str() const
{ return s_keyword; }
keyword_type keyword() const
{ return e_keyword; }
int parameter() const
{ return param; }
char control_char() const
{ return ctrl_chr; }
};
template <class InputIter>
rtf_keyword::rtf_keyword(InputIter &iter)
{
char curchar=*iter;
is_ctrl_chr=!isalpha(curchar);
if (is_ctrl_chr)
{
ctrl_chr=curchar;
++iter;
}
else
{
do
s_keyword+=curchar;
while (isalpha(curchar=*++iter));
std::string param_str;
while (isdigit(curchar)||curchar=='-')
{
param_str+=curchar;
curchar=*++iter;
}
if (param_str.empty())
param=-1;
else
param=std::atoi(param_str.c_str());
if (curchar==' ')
++iter;
keyword_map::iterator kw_pos=keymap.find(s_keyword);
if (kw_pos==keymap.end())
e_keyword=rkw_unknown;
else
e_keyword=kw_pos->second;
}
}
}
#endif
\ No newline at end of file diff --git a/src/rtf2html/rtf_table.cpp b/src/rtf2html/rtf_table.cpp new file mode 100644 index 0000000..b5cdf7b --- /dev/null +++ b/src/rtf2html/rtf_table.cpp @@ -0,0 +1,235 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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 "rtf_table.h" +#include <set> +#include <ostream> +#include <iostream> +#include <stdexcept> +#include <functional> +#include <algorithm> + +using namespace rtf; + +typedef std::set<int> intset; + +template <class T, class C> +std::basic_ostream<C>& operator<<(std::basic_ostream<C> &dest, std::set<T> &s) +{ + for (typename std::set<T>::iterator i=s.begin(); i!=s.end(); ++i) + dest<<*i<<" "; + return dest; +} + +std::string table::make() +{ + std::string result; + intset pts; + iterator row, span_row, row2; + table_cell_defs::iterator cell_def, prev_cell_def, cell_def_2; + table_cells::iterator cell; + intset::iterator pt, ptp; + int left, right, colspan; + bool btop, bbottom, bleft, bright; + std::string style; + for (row=begin(); row!=end();) + { + if ((*row)->Cells.empty()) + { + delete *row; + row=erase(row); + } + else + { + pts.insert((*row)->Left); + for (cell_def=(*row)->CellDefs->begin(); cell_def!=(*row)->CellDefs->end(); ++cell_def) + { + pts.insert((*cell_def)->Right); + } + ++row; + } + } + if (pts.empty()) + { +// throw std::logic_error("No CellDefs!"); + } + pt=pts.begin(); + ptp=pts.end(); + ptp--; + result="<table border=0 width="; + result+=from_int((int)rint((*ptp-*pt)/15)); + result+=" style=\"margin-left:"; + result+=from_int((int)rint(*pt/15)); + result+=";border-collapse: collapse;\">"; + result+="<tr height=0>"; + for (ptp=pt++=pts.begin(); pt!=pts.end(); ptp=pt++) + { + result+="<td width="; + result+=from_int((int)rint((*pt-*ptp)/15)); + result+="></td>"; + //coefficient may be different + } + result+="</tr>\n"; + + // first, we'll determine all the rowspans and leftsides + for (row=begin(); row!=end(); ++row) + { + if ((*row)->CellDefs->size()!=(*row)->Cells.size()) +// throw std::logic_error("Number of Cells and number of CellDefs are unequal!"); + for (cell_def=(*row)->CellDefs->begin(), cell=(*row)->Cells.begin(); + cell!=(*row)->Cells.end(); + ++cell, prev_cell_def=cell_def++ + ) + { + if (cell_def==(*row)->CellDefs->begin()) + (*cell_def)->Left=(*row)->Left; + else + (*cell_def)->Left=(*prev_cell_def)->Right; + if ((*cell_def)->FirstMerged) + { + for (span_row=row, ++span_row; span_row!=end(); + ++span_row) + { + cell_def_2= + std::find_if((*span_row)->CellDefs->begin(), + (*span_row)->CellDefs->end(), + std::bind2nd( + std::mem_fun(&table_cell_def::right_equals), + (*cell_def)->Right)); + if (cell_def_2==(*span_row)->CellDefs->end()) + break; + if (!(*cell_def_2)->Merged) + break; + } + (*cell)->Rowspan=span_row-row; + } + } + } + + for (row=begin(); row!=end(); ++row) + { + result+="<tr>"; + pt=pts.find((*row)->Left); + if (pt==pts.end()) +// throw std::logic_error("No row.left point!"); + if (pt!=pts.begin()) + { + result+="<td colspan="; + result+=from_int(std::distance(pts.begin(), pt)); + result+="></td>"; + } + for (cell_def=(*row)->CellDefs->begin(), cell=(*row)->Cells.begin(); + cell!=(*row)->Cells.end(); ++cell, ++cell_def) + { + ptp=pts.find((*cell_def)->Right); + if (ptp==pts.end()) +// throw std::logic_error("No celldef.right point!"); + colspan=std::distance(pt, ptp); + pt=ptp; + if (!(*cell_def)->Merged) + { + result+="<td"; + // analyzing borders + left=(*cell_def)->Left; + right=(*cell_def)->Right; + bbottom=(*cell_def)->BorderBottom; + btop=(*cell_def)->BorderTop; + bleft=(*cell_def)->BorderLeft; + bright=(*cell_def)->BorderRight; + span_row=row; + if ((*cell_def)->FirstMerged) + std::advance(span_row, (*cell)->Rowspan-1); + for (row2=row; row2!=span_row; ++row2) + { + cell_def_2= + std::find_if((*row2)->CellDefs->begin(), + (*row2)->CellDefs->end(), + std::bind2nd( + std::mem_fun(&table_cell_def::right_equals), + left)); + if (cell_def_2!=(*row2)->CellDefs->end()) + { + bleft=bleft && (*cell_def_2)->BorderRight; + } + cell_def_2= + std::find_if((*row2)->CellDefs->begin(), + (*row2)->CellDefs->end(), + std::bind2nd( + std::mem_fun(&table_cell_def::left_equals), + right)); + if (cell_def_2!=(*row2)->CellDefs->end()) + { + bright=bright && (*cell_def_2)->BorderLeft; + } + } + + if (bbottom && btop && bleft && bright) + { + style="border:1px solid black;"; + } + else + { + style=""; + if (bbottom) + style+="border-bottom:1px solid black;"; + if (btop) + style+="border-top:1px solid black;"; + if (bleft) + style+="border-left:1px solid black;"; + if (bright) + style+="border-right:1px solid black;"; + } + if (!style.empty()) + { + result+=" style=\""; + result+=style; + result+="\""; + } + if (colspan>1) + { + result+=" colspan="; + result+=from_int(colspan); + } + if ((*cell_def)->FirstMerged) + { + result+=" rowspan="; + result+=from_int((*cell)->Rowspan); + } + + switch ((*cell_def)->VAlign) + { + case table_cell_def::valign_top: + result+=" valign=top"; + break; + case table_cell_def::valign_bottom: + result+=" valign=bottom"; + break; + default: break; + } + + result+=">"; + if ((*cell)->Text[0]>0) + result+=(*cell)->Text; + else + result+=" "; + result+="</td>"; + } + } + result+="</tr>"; + } + result+="</table>"; + return result; +} diff --git a/src/rtf2html/rtf_table.h b/src/rtf2html/rtf_table.h new file mode 100644 index 0000000..927cc40 --- /dev/null +++ b/src/rtf2html/rtf_table.h @@ -0,0 +1,90 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef __RTF_H__ +#define __RTF_H__ + +#include "common.h" +#include <vector> +#include <cmath> +#include <list> +#include <cstdlib> + +namespace rtf { + +struct table_cell +{ + int Rowspan; + std::string Text; + table_cell() : Rowspan(0) {} +}; + +struct table_cell_def +{ + enum valign {valign_top, valign_bottom, valign_center}; + bool BorderTop, BorderBottom, BorderLeft, BorderRight; + bool *ActiveBorder; + int Right, Left; + bool Merged, FirstMerged; + valign VAlign; + table_cell_def() + { + BorderTop=BorderBottom=BorderLeft=BorderRight=Merged=FirstMerged=false; + ActiveBorder=NULL; + Right=Left=0; + VAlign=valign_top; + } + bool right_equals(int x) { return x==Right; } + bool left_equals(int x) { return x==Left; } +}; + +template <class T> +class killing_ptr_vector : public std::vector<T*> +{ + public: + ~killing_ptr_vector() + { + for (typename killing_ptr_vector<T>::iterator i=this->begin(); i!=this->end(); ++i) + delete *i; + } +}; + +typedef killing_ptr_vector<table_cell> table_cells; +typedef killing_ptr_vector<table_cell_def> table_cell_defs; + +typedef std::list<table_cell_defs> table_cell_defs_list; + +struct table_row +{ + table_cells Cells; + table_cell_defs_list::iterator CellDefs; + int Height; + int Left; + table_row() : Height(-1000), Left(-1000) {} +}; + +class table : public killing_ptr_vector<table_row> +{ + private: + typedef killing_ptr_vector<table_row> base_class; + public: + table() : base_class() {} + std::string make(); +}; + +} + +#endif diff --git a/src/rtf2html/rtf_tools.h b/src/rtf2html/rtf_tools.h new file mode 100644 index 0000000..528549e --- /dev/null +++ b/src/rtf2html/rtf_tools.h @@ -0,0 +1,65 @@ +/* This is RTF to HTML converter, implemented as a text filter, generally. + Copyright (C) 2003 Valentin Lavrinenko, vlavrinenko@users.sourceforge.net + + available at http://rtf2html.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#ifndef __RTF_TOOLS_H__ +#define __RTF_TOOLS_H__ + +#include "common.h" +#include "rtf_keyword.h" + +namespace rtf { + +template <class InputIter> +void skip_group(InputIter &iter); + + +/**************************************** +function assumes that file pointer points AFTER the opening brace +and that the group is really closed. cs is caller's curchar. +Returns the character that comes after the enclosing brace. +*****************************************/ + +template <class InputIter> +void skip_group(InputIter &iter) +{ + int cnt=1; + while (cnt) + { + switch (*iter++) + { + case '{': + cnt++; + break; + case '}': + cnt--; + break; + case '\\': + { + rtf_keyword kw(iter); + if (!kw.is_control_char() && kw.keyword()==rtf_keyword::rkw_bin + && kw.parameter()>0) + { + std::advance(iter, kw.parameter()); + } + break; + } + } + } +} + +} + +#endif diff --git a/src/statusbar.cpp b/src/statusbar.cpp new file mode 100644 index 0000000..a21baca --- /dev/null +++ b/src/statusbar.cpp @@ -0,0 +1,122 @@ +/*************************************************************************** + copyright : (C) 2005-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 "statusbar.h" +#include "tellico_debug.h" +#include "progressmanager.h" +#include "tellico_debug.h" +#include "gui/progress.h" + +#include <klocale.h> +#include <kapplication.h> +#include <kpushbutton.h> +#include <kiconloader.h> + +#include <qobjectlist.h> +#include <qpainter.h> +#include <qstyle.h> +#include <qtimer.h> +#include <qtooltip.h> + +using Tellico::StatusBar; +StatusBar* StatusBar::s_self = 0; + +StatusBar::StatusBar(QWidget* parent_) : KStatusBar(parent_) { + s_self = this; + + // don't care about text and id + m_mainLabel = new KStatusBarLabel(QString(), 0, this); + m_mainLabel->setIndent(4); + m_mainLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + addWidget(m_mainLabel, 3 /*stretch*/, true /*permanent*/); + + m_countLabel = new KStatusBarLabel(QString(), 1, this); + m_countLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_countLabel->setIndent(4); + addWidget(m_countLabel, 0, true); + + m_progress = new GUI::Progress(100, this); + addWidget(m_progress, 1, true); + m_cancelButton = new KPushButton(SmallIcon(QString::fromLatin1("cancel")), QString::null, this); + QToolTip::add(m_cancelButton, i18n("Cancel")); + addWidget(m_cancelButton, 0, true); + m_progress->hide(); + m_cancelButton->hide(); + + ProgressManager* pm = ProgressManager::self(); + connect(pm, SIGNAL(signalTotalProgress(uint)), SLOT(slotProgress(uint))); + connect(m_cancelButton, SIGNAL(clicked()), pm, SLOT(slotCancelAll())); +} + +void StatusBar::polish() { + KStatusBar::polish(); + + int h = 0; + QObjectList* list = queryList("QWidget", 0, false, false); + for(QObject* o = list->first(); o; o = list->next()) { + int _h = static_cast<QWidget*>(o)->minimumSizeHint().height(); + if(_h > h) { + h = _h; + } + } + + h -= 4; // hint from amarok, it's too big usually + + for(QObject* o = list->first(); o; o = list->next()) { + static_cast<QWidget*>(o)->setFixedHeight(h); + } + + delete list; +} + +void StatusBar::clearStatus() { + setStatus(i18n("Ready.")); +} + +void StatusBar::setStatus(const QString& status_) { + // always add a space for asthetics + m_mainLabel->setText(status_ + ' '); +} + +void StatusBar::setCount(const QString& count_) { + m_countLabel->setText(count_ + ' '); +} + +void StatusBar::slotProgress(uint progress_) { + m_progress->setProgress(progress_); + if(m_progress->isDone()) { + m_progress->hide(); + m_cancelButton->hide(); + } else if(m_progress->isHidden()) { + m_progress->show(); + if(ProgressManager::self()->anyCanBeCancelled()) { + m_cancelButton->show(); + } + kapp->processEvents(); // needed so the window gets updated ??? + } +} + +void StatusBar::slotUpdate() { +/* + myDebug() << "StatusBar::slotUpdate() - " << m_progress->isShown() << endl; + if(m_progressBox->isEmpty()) { + QTimer::singleShot(0, m_progress, SLOT(hide())); +// m_progressBox->hide(); + } else { + QTimer::singleShot(0, m_progress, SLOT(show())); +// m_progressBox->show(); + } +*/ +} + +#include "statusbar.moc" diff --git a/src/statusbar.h b/src/statusbar.h new file mode 100644 index 0000000..1ce3c39 --- /dev/null +++ b/src/statusbar.h @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +// much of this code is adapted from amarok +// which is GPL licensed, Copyright (C) 2005 by Max Howell + +#ifndef TELLICO_STATUSBAR_H +#define TELLICO_STATUSBAR_H + +#include <kstatusbar.h> + +namespace Tellico { + namespace GUI { + class Progress; + } + class MainWindow; + +/** + * @author Robby Stephenson + */ +class StatusBar : public KStatusBar { +Q_OBJECT + +public: + void clearStatus(); + void setStatus(const QString& status); + void setCount(const QString& count); + + static StatusBar* self() { return s_self; } + +protected: + virtual void polish(); + +private slots: + void slotProgress(uint progress); + void slotUpdate(); + +private: + static StatusBar* s_self; + + friend class MainWindow; + + StatusBar(QWidget* parent); + + KStatusBarLabel* m_mainLabel; + KStatusBarLabel* m_countLabel; + GUI::Progress* m_progress; + QWidget* m_cancelButton; +}; + +} + +#endif diff --git a/src/stringset.h b/src/stringset.h new file mode 100644 index 0000000..47667a0 --- /dev/null +++ b/src/stringset.h @@ -0,0 +1,58 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_STRINGSET_H +#define TELLICO_STRINGSET_H + +#include <qdict.h> +#include <qstringlist.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class StringSet { + +public: + StringSet(int size = 17) : m_dict(size) {} + + // replace instead of insert, to ensure unique keys + void add(const QString& val) { if(!val.isEmpty()) m_dict.replace(val, reinterpret_cast<const int *>(1)); } + void add(const QStringList& vals) { + for(QStringList::ConstIterator it = vals.begin(), end = vals.end(); it != end; ++it) { + add(*it); + } + } + bool remove(const QString& val) { return !val.isEmpty() && m_dict.remove(val); } + void clear() { m_dict.clear(); } + bool has(const QString& val) const { return !val.isEmpty() && (m_dict.find(val) != 0); } + bool isEmpty() const { return m_dict.isEmpty(); } + uint count() const { return m_dict.count(); } + + QStringList toList() const { + QStringList list; + for(QDictIterator<int> it(m_dict); it.current(); ++it) { + list << it.currentKey(); + } + return list; + } + +private: + // use a dict for fast random access to keep track of the values + QDict<int> m_dict; +}; + +} + +#endif diff --git a/src/tellico_debug.h b/src/tellico_debug.h new file mode 100644 index 0000000..ea65518 --- /dev/null +++ b/src/tellico_debug.h @@ -0,0 +1,156 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_DEBUG_H +#define TELLICO_DEBUG_H + +// most of this is borrowed from amarok/src/debug.h +// which is copyright Max Howell <max.howell@methylblue.com> +// amarok is licensed under the GPL + +#include <kdebug.h> +// std::clock_t +#include <ctime> + +// linux has __GNUC_PREREQ, NetBSD has __GNUC_PREQ__ +#if defined(__GNUC_PREREQ) && !defined(__GNUC_PREREQ__) +#define __GNUC_PREREQ__ __GNUC_PREREQ +#endif + +#if !defined(__GNUC_PREREQ__) +#if defined __GNUC__ +#define __GNUC_PREREQ__(x, y) \ + ((__GNUC__ == (x) && __GNUC_MINOR__ >= (y)) || \ + (__GNUC__ > (x))) +#else +#define __GNUC_PREREQ__(x, y) 0 +#endif +#endif + +# if defined __cplusplus ? __GNUC_PREREQ__ (2, 6) : __GNUC_PREREQ__ (2, 4) +# define MY_FUNCTION __PRETTY_FUNCTION__ +# else +# if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L +# define MY_FUNCTION __func__ +# else +# define MY_FUNCTION __FILE__ ":" __LINE__ +# endif +# endif + +// some logging +#ifndef NDEBUG +#define TELLICO_LOG +#endif + +#ifndef NDEBUG +#define TELLICO_DEBUG +#endif + +namespace Debug { + typedef kndbgstream NoDebugStream; +#ifndef TELLICO_DEBUG + typedef kndbgstream DebugStream; + static inline DebugStream log() { return DebugStream(); } + static inline DebugStream debug() { return DebugStream(); } + static inline DebugStream warning() { return DebugStream(); } + static inline DebugStream error() { return DebugStream(); } + static inline DebugStream fatal() { return DebugStream(); } + +#else + #ifndef DEBUG_PREFIX + #define FUNC_PREFIX "" + #else + #define FUNC_PREFIX "[" DEBUG_PREFIX "] " + #endif + +//from kdebug.h +/* + enum DebugLevels { + KDEBUG_INFO = 0, + KDEBUG_WARN = 1, + KDEBUG_ERROR = 2, + KDEBUG_FATAL = 3 + }; +*/ + + typedef kdbgstream DebugStream; +#ifdef TELLICO_LOG + static inline DebugStream log() { return kdDebug(); } +#else + static inline kndbgstream log() { return NoDebugStream(); } +#endif + static inline DebugStream debug() { return kdDebug() << FUNC_PREFIX; } + static inline DebugStream warning() { return kdWarning() << FUNC_PREFIX << "[WARNING!] "; } + static inline DebugStream error() { return kdError() << FUNC_PREFIX << "[ERROR!] "; } + static inline DebugStream fatal() { return kdFatal() << FUNC_PREFIX; } + + #undef FUNC_PREFIX +#endif + +class Block { + +public: + Block(const char* label) : m_start(std::clock()), m_label(label) { + Debug::debug() << "BEGIN: " << label << endl; + } + + ~Block() { + std::clock_t finish = std::clock(); + const double duration = (double) (finish - m_start) / CLOCKS_PER_SEC; + Debug::debug() << " END: " << m_label << " - duration = " << duration << endl; + } + +private : + std::clock_t m_start; + const char* m_label; +}; + +} + +#define myDebug() Debug::debug() +#define myWarning() Debug::warning() +#define myLog() Debug::log() + +/// Standard function announcer +#define DEBUG_FUNC_INFO myDebug() << k_funcinfo << endl; + +/// Announce a line +#define DEBUG_LINE_INFO myDebug() << k_funcinfo << "Line: " << __LINE__ << endl; + +/// Convenience macro for making a standard Debug::Block +#define DEBUG_BLOCK Debug::Block uniquelyNamedStackAllocatedStandardBlock( __func__ ); + +#ifdef TELLICO_LOG +// see http://www.gnome.org/~federico/news-2006-03.html#timeline-tools +#define MARK do { \ + char str[128]; \ + ::snprintf(str, 128, "MARK: %s: %s (%d)", className(), MY_FUNCTION, __LINE__); \ + ::access (str, F_OK); \ + } while(false) +#define MARK_MSG(s) do { \ + char str[128]; \ + ::snprintf(str, 128, "MARK: %s: %s (%d)", className(), s, __LINE__); \ + ::access (str, F_OK); \ + } while(false) +#define MARK_LINE do { \ + char str[128]; \ + ::snprintf(str, 128, "MARK: tellico: %s (%d)", __FILE__, __LINE__); \ + ::access (str, F_OK); \ + } while(false) +#else +#define MARK +#define MARK_MSG(s) +#define MARK_LINE +#endif + +#endif diff --git a/src/tellico_kernel.cpp b/src/tellico_kernel.cpp new file mode 100644 index 0000000..e0e42b5 --- /dev/null +++ b/src/tellico_kernel.cpp @@ -0,0 +1,407 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellico_kernel.h" +#include "mainwindow.h" +#include "document.h" +#include "collection.h" +#include "entryitem.h" +#include "controller.h" +#include "filter.h" +#include "filterdialog.h" +#include "loandialog.h" +#include "calendarhandler.h" +#include "tellico_utils.h" +#include "tellico_debug.h" +#include "commands/group.h" +#include "commands/collectioncommand.h" +#include "commands/fieldcommand.h" +#include "commands/filtercommand.h" +#include "commands/addentries.h" +#include "commands/modifyentries.h" +#include "commands/updateentries.h" +#include "commands/removeentries.h" +#include "commands/removeloans.h" +#include "commands/reorderfields.h" +#include "commands/renamecollection.h" +#include "collectionfactory.h" +#include "stringset.h" + +#include <kmessagebox.h> +#include <kglobal.h> +#include <kiconloader.h> +#include <kinputdialog.h> +#include <klocale.h> + +using Tellico::Kernel; +Kernel* Kernel::s_self = 0; + +Kernel::Kernel(MainWindow* parent) : m_widget(parent) + , m_commandHistory(parent->actionCollection()) + , m_commandGroup(0) { +} + +const KURL& Kernel::URL() const { + return Data::Document::self()->URL(); +} + +const QStringList& Kernel::fieldTitles() const { + return Data::Document::self()->collection()->fieldTitles(); +} + +QString Kernel::fieldNameByTitle(const QString& title_) const { + return Data::Document::self()->collection()->fieldNameByTitle(title_); +} + +QString Kernel::fieldTitleByName(const QString& name_) const { + return Data::Document::self()->collection()->fieldTitleByName(name_); +} + +QStringList Kernel::valuesByFieldName(const QString& name_) const { + return Data::Document::self()->collection()->valuesByFieldName(name_); +} + +int Kernel::collectionType() const { + return Data::Document::self()->collection()->type(); +} + +QString Kernel::collectionTypeName() const { + return Data::Document::self()->collection()->typeName(); +} + +void Kernel::sorry(const QString& text_, QWidget* widget_/* =0 */) { + if(text_.isEmpty()) { + return; + } + GUI::CursorSaver cs(Qt::arrowCursor); + KMessageBox::sorry(widget_ ? widget_ : m_widget, text_); +} + +void Kernel::beginCommandGroup(const QString& name_) { + if(m_commandGroup) { + myDebug() << "Kernel::beginCommandGroup() - there's an uncommitted group!" << endl; + delete m_commandGroup; + } + m_commandGroup = new Command::Group(name_); +} + +void Kernel::endCommandGroup() { + if(!m_commandGroup) { + myDebug() << "Kernel::endCommandGroup() - beginCommandGroup() must be called first!" << endl; + return; + } + if(m_commandGroup->isEmpty()) { + delete m_commandGroup; + } else { + m_commandHistory.addCommand(m_commandGroup); + Data::Document::self()->slotSetModified(true); + } + m_commandGroup = 0; +} + +void Kernel::resetHistory() { + m_commandHistory.clear(); + m_commandHistory.documentSaved(); +} + +bool Kernel::addField(Data::FieldPtr field_) { + if(!field_) { + return false; + } + doCommand(new Command::FieldCommand(Command::FieldCommand::FieldAdd, + Data::Document::self()->collection(), + field_)); + return true; +} + +bool Kernel::modifyField(Data::FieldPtr field_) { + if(!field_) { + return false; + } + Data::FieldPtr oldField = Data::Document::self()->collection()->fieldByName(field_->name()); + if(!oldField) { + return false; + } + doCommand(new Command::FieldCommand(Command::FieldCommand::FieldModify, + Data::Document::self()->collection(), + field_, + oldField)); + return true; +} + +bool Kernel::removeField(Data::FieldPtr field_) { + if(!field_) { + return false; + } + doCommand(new Command::FieldCommand(Command::FieldCommand::FieldRemove, + Data::Document::self()->collection(), + field_)); + return true; +} + +void Kernel::addEntries(Data::EntryVec entries_, bool checkFields_) { + if(entries_.isEmpty()) { + return; + } + + KCommand* cmd = new Command::AddEntries(Data::Document::self()->collection(), entries_); + if(checkFields_) { + beginCommandGroup(cmd->name()); + + // this is the same as in Command::UpdateEntries::execute() + Data::CollPtr c = Data::Document::self()->collection(); + Data::FieldVec fields = entries_[0]->collection()->fields(); + + QPair<Data::FieldVec, Data::FieldVec> p = mergeFields(c, fields, entries_); + Data::FieldVec modifiedFields = p.first; + Data::FieldVec addedFields = p.second; + + for(Data::FieldVec::Iterator field = modifiedFields.begin(); field != modifiedFields.end(); ++field) { + if(c->hasField(field->name())) { + doCommand(new Command::FieldCommand(Command::FieldCommand::FieldModify, c, + field, c->fieldByName(field->name()))); + } + } + + for(Data::FieldVec::Iterator field = addedFields.begin(); field != addedFields.end(); ++field) { + doCommand(new Command::FieldCommand(Command::FieldCommand::FieldAdd, c, field)); + } + } + doCommand(cmd); + if(checkFields_) { + endCommandGroup(); + } +} + +void Kernel::modifyEntries(Data::EntryVec oldEntries_, Data::EntryVec newEntries_) { + if(newEntries_.isEmpty()) { + return; + } + + doCommand(new Command::ModifyEntries(Data::Document::self()->collection(), oldEntries_, newEntries_)); +} + +void Kernel::updateEntry(Data::EntryPtr oldEntry_, Data::EntryPtr newEntry_, bool overWrite_) { + if(!newEntry_) { + return; + } + + doCommand(new Command::UpdateEntries(Data::Document::self()->collection(), oldEntry_, newEntry_, overWrite_)); +} + +void Kernel::removeEntries(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return; + } + + doCommand(new Command::RemoveEntries(Data::Document::self()->collection(), entries_)); +} + +bool Kernel::addLoans(Data::EntryVec entries_) { + if(entries_.isEmpty()) { + return false; + } + + LoanDialog dlg(entries_, m_widget); + if(dlg.exec() != QDialog::Accepted) { + return false; + } + + KCommand* cmd = dlg.createCommand(); + if(!cmd) { + return false; + } + doCommand(cmd); + return true; +} + +bool Kernel::modifyLoan(Data::LoanPtr loan_) { + if(!loan_) { + return false; + } + + LoanDialog dlg(loan_, m_widget); + if(dlg.exec() != QDialog::Accepted) { + return false; + } + + KCommand* cmd = dlg.createCommand(); + if(!cmd) { + return false; + } + doCommand(cmd); + return true; +} + +bool Kernel::removeLoans(Data::LoanVec loans_) { + if(loans_.isEmpty()) { + return true; + } + + doCommand(new Command::RemoveLoans(loans_)); + return true; +} + +void Kernel::addFilter(FilterPtr filter_) { + if(!filter_) { + return; + } + + doCommand(new Command::FilterCommand(Command::FilterCommand::FilterAdd, filter_)); +} + +bool Kernel::modifyFilter(FilterPtr filter_) { + if(!filter_) { + return false; + } + + FilterDialog filterDlg(FilterDialog::ModifyFilter, m_widget); + // need to create a new filter object + FilterPtr newFilter = new Filter(*filter_); + filterDlg.setFilter(newFilter); + if(filterDlg.exec() != QDialog::Accepted) { + return false; + } + + doCommand(new Command::FilterCommand(Command::FilterCommand::FilterModify, newFilter, filter_)); + return true; +} + +bool Kernel::removeFilter(FilterPtr filter_) { + if(!filter_) { + return false; + } + + QString str = i18n("Do you really want to delete this filter?"); + QString dontAsk = QString::fromLatin1("DeleteFilter"); + int ret = KMessageBox::questionYesNo(m_widget, str, i18n("Delete Filter?"), + KStdGuiItem::yes(), KStdGuiItem::no(), dontAsk); + if(ret != KMessageBox::Yes) { + return false; + } + + doCommand(new Command::FilterCommand(Command::FilterCommand::FilterRemove, filter_)); + return true; +} + +void Kernel::reorderFields(const Data::FieldVec& fields_) { + doCommand(new Command::ReorderFields(Data::Document::self()->collection(), + Data::Document::self()->collection()->fields(), + fields_)); +} + +void Kernel::appendCollection(Data::CollPtr coll_) { + doCommand(new Command::CollectionCommand(Command::CollectionCommand::Append, + Data::Document::self()->collection(), + coll_)); +} + +void Kernel::mergeCollection(Data::CollPtr coll_) { + doCommand(new Command::CollectionCommand(Command::CollectionCommand::Merge, + Data::Document::self()->collection(), + coll_)); +} + +void Kernel::replaceCollection(Data::CollPtr coll_) { + doCommand(new Command::CollectionCommand(Command::CollectionCommand::Replace, + Data::Document::self()->collection(), + coll_)); +} + +void Kernel::renameCollection() { + bool ok; + QString newTitle = KInputDialog::getText(i18n("Rename Collection"), i18n("New collection name:"), + Data::Document::self()->collection()->title(), &ok, m_widget); + if(ok) { + doCommand(new Command::RenameCollection(Data::Document::self()->collection(), newTitle)); + } +} + +void Kernel::doCommand(KCommand* command_) { + if(m_commandGroup) { + m_commandGroup->addCommand(command_); + } else { + m_commandHistory.addCommand(command_); + Data::Document::self()->slotSetModified(true); + } +} + +QPair<Tellico::Data::FieldVec, Tellico::Data::FieldVec> Kernel::mergeFields(Data::CollPtr coll_, + Data::FieldVec fields_, + Data::EntryVec entries_) { + Data::FieldVec modified, created; + for(Data::FieldVec::Iterator field = fields_.begin(); field != fields_.end(); ++field) { + // don't add a field if it's a default field and not in the current collection + if(coll_->hasField(field->name()) || CollectionFactory::isDefaultField(coll_->type(), field->name())) { + // special case for choice fields, since we might want to add a value + if(field->type() == Data::Field::Choice && coll_->hasField(field->name())) { + QStringList a1 = field->allowed(); + QStringList a2 = coll_->fieldByName(field->name())->allowed(); + if(a1 != a2) { + StringSet a; + a.add(a1); + a.add(a2); + Data::FieldPtr f = new Data::Field(*coll_->fieldByName(field->name())); + f->setAllowed(a.toList()); + modified.append(f); + } + } + continue; + } + // add field if any values are not empty + for(Data::EntryVec::Iterator entry = entries_.begin(); entry != entries_.end(); ++entry) { + if(!entry->field(field).isEmpty()) { + created.append(new Data::Field(*field)); + break; + } + } + } + return qMakePair(modified, created); +} + +int Kernel::askAndMerge(Data::EntryPtr entry1_, Data::EntryPtr entry2_, Data::FieldPtr field_, + QString value1_, QString value2_) { + QString title1 = entry1_->field(QString::fromLatin1("title")); + QString title2 = entry2_->field(QString::fromLatin1("title")); + if(title1 == title2) { + title1 = i18n("Entry 1"); + title2 = i18n("Entry 2"); + } + if(value1_.isEmpty()) { + value1_ = entry1_->field(field_); + } + if(value2_.isEmpty()) { + value2_ = entry2_->field(field_); + } + QString text = QString::fromLatin1("<qt>") + + i18n("Conflicting values for %1 were found while merging entries.").arg(field_->title()) + + QString::fromLatin1("<br/><center><table><tr>" + "<th>%1</th>" + "<th>%2</th></tr>").arg(title1, title2) + + QString::fromLatin1("<tr><td><em>%1</em></td>").arg(value1_) + + QString::fromLatin1("<td><em>%1</em></td></tr></table></center>").arg(value2_) + + i18n("Please choose which value to keep.") + + QString::fromLatin1("</qt>"); + + int ret = KMessageBox::warningYesNoCancel(Kernel::self()->widget(), + text, + i18n("Merge Entries"), + i18n("Select value from %1").arg(title1), + i18n("Select value from %1").arg(title2)); + switch(ret) { + case KMessageBox::Cancel: return 0; + case KMessageBox::Yes: return -1; // keep original value + case KMessageBox::No: return 1; // use newer value + } + return 0; +} diff --git a/src/tellico_kernel.h b/src/tellico_kernel.h new file mode 100644 index 0000000..ed801d6 --- /dev/null +++ b/src/tellico_kernel.h @@ -0,0 +1,145 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_KERNEL_H +#define TELLICO_KERNEL_H + +#include "datavectors.h" +#include "borrower.h" + +#include <kcommand.h> + +class KURL; + +class QWidget; +class QString; +class QStringList; + +namespace Tellico { + class MainWindow; + class Filter; + namespace Command { + class Group; + } + namespace Data { + class Collection; + } + +/** + * @author Robby Stephenson + */ +class Kernel { + +public: + static Kernel* self() { return s_self; } + /** + * Initializes the singleton. Should just be called once, from Tellico::MainWindow + */ + static void init(MainWindow* parent) { if(!s_self) s_self = new Kernel(parent); } + + /** + * Returns a pointer to the parent widget. This is mainly used for error dialogs and the like. + * + * @return The widget pointer + */ + QWidget* widget() { return m_widget; } + + /** + * Returns the url of the current document. + * + * @return The URL + */ + const KURL& URL() const; + /** + * Returns a list of the field titles, wraps the call to the collection itself. + * + * @return the field titles + */ + const QStringList& fieldTitles() const; + /** + * Returns the name of an field, given its title. Wraps the call to the collection itself. + * + * @param title The field title + * @return The field name + */ + QString fieldNameByTitle(const QString& title) const; + /** + * Returns the title of an field, given its name. Wraps the call to the collection itself. + * + * @param name The field name + * @return The field title + */ + QString fieldTitleByName(const QString& name) const; + QStringList valuesByFieldName(const QString& name) const; + + int collectionType() const; + QString collectionTypeName() const; + + void sorry(const QString& text, QWidget* widget=0); + + void beginCommandGroup(const QString& name); + void endCommandGroup(); + void resetHistory(); + + bool addField(Data::FieldPtr field); + bool modifyField(Data::FieldPtr field); + bool removeField(Data::FieldPtr field); + + void addEntries(Data::EntryVec entries, bool checkFields); + void modifyEntries(Data::EntryVec oldEntries, Data::EntryVec newEntries); + void updateEntry(Data::EntryPtr oldEntry, Data::EntryPtr newEntry, bool overWrite); + void removeEntries(Data::EntryVec entries); + + bool addLoans(Data::EntryVec entries); + bool modifyLoan(Data::LoanPtr loan); + bool removeLoans(Data::LoanVec loans); + + void addFilter(FilterPtr filter); + bool modifyFilter(FilterPtr filter); + bool removeFilter(FilterPtr filter); + + void reorderFields(const Data::FieldVec& fields); + + void appendCollection(Data::CollPtr coll); + void mergeCollection(Data::CollPtr coll); + void replaceCollection(Data::CollPtr coll); + + // adds new fields into collection if any values in entries are not empty + // first object is modified fields, second is new fields + QPair<Data::FieldVec, Data::FieldVec> mergeFields(Data::CollPtr coll, + Data::FieldVec fields, + Data::EntryVec entries); + + void renameCollection(); + const KCommandHistory* commandHistory() { return &m_commandHistory; } + + int askAndMerge(Data::EntryPtr entry1, Data::EntryPtr entry2, Data::FieldPtr field, + QString value1 = QString(), QString value2 = QString()); + +private: + static Kernel* s_self; + + // all constructors are private + Kernel(MainWindow* parent); + Kernel(const Kernel&); + Kernel& operator=(const Kernel&); + + void doCommand(KCommand* command); + + QWidget* m_widget; + KCommandHistory m_commandHistory; + Command::Group* m_commandGroup; +}; + +} // end namespace +#endif diff --git a/src/tellico_map.h b/src/tellico_map.h new file mode 100644 index 0000000..75ceb32 --- /dev/null +++ b/src/tellico_map.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_MAP_H +#define TELLICO_MAP_H + +#include <qmap.h> + +/** + * This file contains some template functions for maps + * + * @author Robby Stephenson + */ +namespace Tellico { + +/** + * Reverse a map's keys and values + */ +template<class M> +QMap<typename M::T, typename M::Key> flipMap(const M& map_) { + QMap<typename M::T, typename M::Key> map; + typename M::ConstIterator it = map_.begin(), end = map_.end(); + for( ; it != end; ++it) { + map.insert(it.data(), it.key()); + } + return map; +} + +} + +#endif diff --git a/src/tellico_strings.cpp b/src/tellico_strings.cpp new file mode 100644 index 0000000..e80cbd7 --- /dev/null +++ b/src/tellico_strings.cpp @@ -0,0 +1,27 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellico_strings.h" + +#include <klocale.h> + +const char* Tellico::errorLoad = I18N_NOOP("Tellico is unable to load the file - %1."); +const char* Tellico::errorWrite = I18N_NOOP("Tellico is unable to write the file - %1."); +const char* Tellico::errorUpload = I18N_NOOP("Tellico is unable to upload the file - %1."); +const char* Tellico::errorAppendType = I18N_NOOP("Only collections with the same type of entries as " + "the current one can be appended. No changes are being " + "made to the current collection."); +const char* Tellico::errorMergeType = I18N_NOOP("Only collections with the same type of entries as " + "the current one can be merged. No changes are being " + "made to the current collection."); +const char* Tellico::errorImageLoad = I18N_NOOP("Tellico is unable to load an image from the file - %1."); diff --git a/src/tellico_strings.h b/src/tellico_strings.h new file mode 100644 index 0000000..5db5c60 --- /dev/null +++ b/src/tellico_strings.h @@ -0,0 +1,26 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_STRINGS_H +#define TELLICO_STRINGS_H + +namespace Tellico { + extern const char* errorLoad; + extern const char* errorWrite; + extern const char* errorUpload; + extern const char* errorAppendType; + extern const char* errorMergeType; + extern const char* errorImageLoad; +} + +#endif diff --git a/src/tellico_utils.cpp b/src/tellico_utils.cpp new file mode 100644 index 0000000..6af6706 --- /dev/null +++ b/src/tellico_utils.cpp @@ -0,0 +1,218 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellico_utils.h" +#include "tellico_kernel.h" +#include "latin1literal.h" +#include "tellico_debug.h" + +#include <kapplication.h> +#include <klocale.h> +#include <kglobal.h> +#include <klibloader.h> +#include <kstandarddirs.h> +#include <kcharsets.h> + +#include <qregexp.h> +#include <qdir.h> +#include <qcursor.h> +#include <qscrollview.h> + +namespace { + static const int STRING_STORE_SIZE = 997; // too big, too small? +} + +QColor Tellico::contrastColor; + +QString Tellico::decodeHTML(QString text) { + QRegExp rx(QString::fromLatin1("&(.+);")); + rx.setMinimal(true); + int pos = rx.search(text); + while(pos > -1) { + QChar c = KCharsets::fromEntity(rx.cap(1)); + if(!c.isNull()) { + text.replace(pos, rx.matchedLength(), c); + } + pos = rx.search(text, pos+1); + } + return text; +} + +QString Tellico::uid(int l, bool prefix) { + QString uid; + if(prefix) { + uid = QString::fromLatin1("Tellico"); + } + uid.append(kapp->randomString(QMAX(l - uid.length(), 0))); + return uid; +} + +uint Tellico::toUInt(const QString& s, bool* ok) { + if(s.isEmpty()) { + if(ok) { + *ok = false; + } + return 0; + } + + uint idx = 0; + while(s[idx].isDigit()) { + ++idx; + } + if(idx == 0) { + if(ok) { + *ok = false; + } + return 0; + } + return s.left(idx).toUInt(ok); +} + +QString Tellico::i18nReplace(QString text) { + // Because QDomDocument sticks in random newlines, go ahead and grab them too + static QRegExp rx(QString::fromLatin1("(?:\\n+ *)*<i18n>([^<]*)</i18n>(?: *\\n+)*")); + int pos = rx.search(text); + while(pos > -1) { + text.replace(pos, rx.matchedLength(), i18n(rx.cap(1).utf8())); + pos = rx.search(text, pos+rx.matchedLength()); + } + return text; +} + +QStringList Tellico::findAllSubDirs(const QString& dir_) { + if(dir_.isEmpty()) { + return QStringList(); + } + + // TODO: build in symlink checking, for now, prohibit + QDir dir(dir_, QString::null, QDir::Name | QDir::IgnoreCase, QDir::Dirs | QDir::Readable | QDir::NoSymLinks); + + QStringList allSubdirs; // the whole list + + // find immediate sub directories + const QStringList subdirs = dir.entryList(); + for(QStringList::ConstIterator subdir = subdirs.begin(); subdir != subdirs.end(); ++subdir) { + if((*subdir).isEmpty() || *subdir == Latin1Literal(".") || *subdir == Latin1Literal("..")) { + continue; + } + QString absSubdir = dir.absFilePath(*subdir); + allSubdirs += findAllSubDirs(absSubdir); + allSubdirs += absSubdir; + } + return allSubdirs; +} + +// Based on QGDict's hash functions, Copyright (C) 1992-2000 Trolltech AS +// and used from Juk, Copyright (C) 2003 - 2004 by Scott Wheeler +int Tellico::stringHash(const QString& str) { + uint h = 0; + uint g = 0; + for(uint i = 0; i < str.length(); ++i) { + h = (h << 4) + str.unicode()[i].cell(); + if((g = h & 0xf0000000)) { + h ^= g >> 24; + } + h &= ~g; + } + + int index = h; + return index < 0 ? -index : index; +} + +QString Tellico::shareString(const QString& str) { + static QString stringStore[STRING_STORE_SIZE]; + + const int hash = stringHash(str) % STRING_STORE_SIZE; + if(stringStore[hash] != str) { + stringStore[hash] = str; + } + return stringStore[hash]; +} + +void Tellico::updateContrastColor(const QColorGroup& cg_) { + // if the value difference between background and highlight is more than ??? + // use highlight, else go lighter or darker + int h1, s1, v1, h2, s2, v2; + cg_.background().getHsv(&h1, &s1, &v1); + + QColor hl = cg_.highlight(); + hl.getHsv(&h2, &s2, &v2); + h2 += 120; + s2 = 255; + hl.setHsv(h2, s2, v2); + + if(KABS(v2-v1) < 48) { + if(v1 < 128) { + contrastColor = hl.light(); + } else { + contrastColor = hl.dark(); + } + } else { + contrastColor = hl; + } +} + +KLibrary* Tellico::openLibrary(const QString& libName_) { + QString path = KLibLoader::findLibrary(QFile::encodeName(libName_)); + if(path.isEmpty()) { + kdWarning() << "Tellico::openLibrary() - Could not find library '" << libName_ << "'" << endl; + kdWarning() << "ERROR: " << KLibLoader::self()->lastErrorMessage() << endl; + return 0; + } + + KLibrary* library = KLibLoader::self()->library(QFile::encodeName(path)); + if(!library) { + kdWarning() << "Tellico::openLibrary() - Could not load library '" << libName_ << "'" << endl; + kdWarning() << " PATH: " << path << endl; + kdWarning() << "ERROR: " << KLibLoader::self()->lastErrorMessage() << endl; + return 0; + } + + return library; +} + +QColor Tellico::blendColors(const QColor& color1, const QColor& color2, int percent) { + const double factor2 = percent/100.0; + const double factor1 = 1.0 - factor2; + + const int r = static_cast<int>(color1.red() * factor1 + color2.red() * factor2); + const int g = static_cast<int>(color1.green() * factor1 + color2.green() * factor2); + const int b = static_cast<int>(color1.blue() * factor1 + color2.blue() * factor2); + + return QColor(r, g, b); +} + +QString Tellico::minutes(int seconds) { + int min = seconds / 60; + seconds = seconds % 60; + return QString::number(min) + ':' + QString::number(seconds).rightJustify(2, '0'); +} + +QString Tellico::saveLocation(const QString& dir_) { + return KGlobal::dirs()->saveLocation("appdata", dir_, true); +} + +Tellico::GUI::CursorSaver::CursorSaver(const QCursor& cursor_) : m_restored(false) { + kapp->setOverrideCursor(cursor_); +} + +Tellico::GUI::CursorSaver::~CursorSaver() { + if(!m_restored) { + kapp->restoreOverrideCursor(); + } +} + +void Tellico::GUI::CursorSaver::restore() { + kapp->restoreOverrideCursor(); + m_restored = true; +} diff --git a/src/tellico_utils.h b/src/tellico_utils.h new file mode 100644 index 0000000..13250e6 --- /dev/null +++ b/src/tellico_utils.h @@ -0,0 +1,80 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_UTILS_H +#define TELLICO_UTILS_H + +#include <qnamespace.h> + +class KLibrary; + +class QColor; +class QColorGroup; +class QCursor; +class QString; +class QStringList; +class QScrollView; + +/** + * This file contains utility functions. + * + * @author Robby Stephenson + */ +namespace Tellico { + /** + * Decode HTML entities. Only numeric entities are handled currently. + */ + QString decodeHTML(QString text); + /** + * Return a random, and almost certainly unique UID. + * + * @param length The UID starts with "Tellico" and adds enough letters to be @p length long. + */ + QString uid(int length=20, bool prefix=true); + uint toUInt(const QString& string, bool* ok); + /** + * Replace all occurrences of <i18n>text</i18n> with i18n("text") + */ + QString i18nReplace(QString text); + /** + * Returns a list of the subdirectories in @param dir + * Symbolic links are ignored + */ + QStringList findAllSubDirs(const QString& dir); + int stringHash(const QString& str); + /** take advantage string collisions to reduce memory + */ + QString shareString(const QString& str); + + extern QColor contrastColor; + void updateContrastColor(const QColorGroup& cg); + QColor blendColors(const QColor& color1, const QColor& color2, int percent); + QString minutes(int seconds); + QString saveLocation(const QString& dir); + + KLibrary* openLibrary(const QString& libName); + +namespace GUI { + class CursorSaver { + public: + CursorSaver(const QCursor& cursor = Qt::waitCursor); + ~CursorSaver(); + void restore(); + private: + bool m_restored : 1; + }; +} + +} + +#endif diff --git a/src/tellicorc b/src/tellicorc new file mode 100644 index 0000000..209e5d1 --- /dev/null +++ b/src/tellicorc @@ -0,0 +1,3 @@ +[KDE Action Restrictions] +shell_access=false +run_desktop_files=false; diff --git a/src/tellicoui.rc b/src/tellicoui.rc new file mode 100644 index 0000000..44e9691 --- /dev/null +++ b/src/tellicoui.rc @@ -0,0 +1,187 @@ +<?xml version = '1.0'?> +<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd"> +<kpartgui version="24" name="tellico"> + <MenuBar> + <Menu name="file"> + <text>&File</text> + <Menu append="new_merge" name="file_new_collection" > + <text>&New</text> + <Action name="new_book_collection"/> + <Action name="new_bibtex_collection"/> + <Action name="new_comic_book_collection"/> + <Action name="new_video_collection"/> + <Action name="new_music_collection"/> + <Action name="new_coin_collection"/> + <Action name="new_stamp_collection"/> + <Action name="new_card_collection"/> + <Action name="new_wine_collection"/> + <Action name="new_game_collection"/> + <Action name="new_boardgame_collection"/> + <Action name="new_file_catalog"/> + <Action name="new_custom_collection"/> + </Menu> + <Menu name="file_import"> + <text>&Import</text> + <Action name="file_import_tellico"/> + <Action name="file_import_csv"/> + <Separator/> + <Action name="file_import_alexandria"/> + <Action name="file_import_bibtex"/> + <Action name="file_import_bibtexml"/> + <Action name="file_import_delicious"/> + <Action name="file_import_mods"/> + <Action name="file_import_pdf"/> + <Action name="file_import_referencer"/> + <Action name="file_import_ris"/> + <Separator/> + <Action name="file_import_freedb"/> + <Action name="file_import_audiofile"/> + <Separator/> + <Action name="file_import_amc"/> + <Action name="file_import_gcfilms"/> + <Action name="file_import_griffith"/> + <Separator/> + <Action name="file_import_filelisting"/> + <Separator/> + <Action name="file_import_xslt"/> + </Menu> + <Menu name="file_export"> + <text>&Export</text> + <Action name="file_export_xml"/> + <Action name="file_export_zip"/> + <Action name="file_export_html"/> + <Action name="file_export_csv"/> + <Action name="file_export_pilotdb"/> + <Separator/> + <Action name="file_export_alexandria"/> + <Action name="file_export_bibtex"/> + <Action name="file_export_bibtexml"/> + <Action name="file_export_onix"/> + <Separator/> + <Action name="file_export_gcfilms"/> + <Separator/> + <Action name="file_export_xslt"/> + </Menu> + </Menu> + <Menu name="edit"> + <Action name="edit_search_internet"/> + <Action name="filter_dialog"/> + </Menu> + <Menu name="collection"> + <text>&Collection</text> + <Action name="coll_new_entry"/> + <Action name="coll_edit_entry"/> + <Action name="coll_copy_entry"/> + <Action name="coll_delete_entry"/> + <Action name="coll_merge_entry"/> + <Menu name="coll_update_entry"> + <text>&Update Entry</text> + <Action name="update_entry_all"/> + <Separator/> + <ActionList name="update_entry_actions"/> + </Menu> + <Separator/> + <Action name="coll_checkout"/> + <Action name="coll_checkin"/> + <Separator/> + <Action name="coll_rename_collection"/> + <Action name="coll_fields"/> + <Action name="coll_reports"/> + <Separator/> + <Action name="coll_convert_bibliography"/> + <Action name="coll_string_macros"/> + <Action name="cite_clipboard"/> + <Action name="cite_lyxpipe"/> + <Action name="cite_openoffice"/> + </Menu> + <Menu name="settings"> + <Action append="show_merge" name="toggle_collection_bar"/> + <Action append="show_merge" name="toggle_group_widget"/> + <Action append="show_merge" name="toggle_edit_widget"/> + <Action append="show_merge" name="toggle_entry_view"/> + <Action append="show_merge" name="change_entry_grouping"/> + </Menu> + <Menu name="help"> + <Action name="tipOfDay"/> + </Menu> + </MenuBar> + <ToolBar noMerge="1" name="mainToolBar"> + <text>Main Toolbar</text> + <Action name="file_new_collection"/> + <Action name="file_open"/> + <Action name="file_save"/> + <Action name="file_print"/> + <Separator/> + <Action name="edit_search_internet"/> + </ToolBar> + <ToolBar name="collectionToolBar"> + <text>Collection Toolbar</text> + <Action name="coll_new_entry"/> + <Action name="coll_fields"/> + <Action name="coll_reports"/> + <Action name="change_entry_grouping"/> + <Separator lineSeparator="true"/> + <Action name="quick_filter_clear"/> + <Action name="quick_filter"/> + <Action name="filter_dialog"/> + </ToolBar> + +<State name="collection_reset"> + <Disable> + <Action name="file_export_alexandria"/> + <Action name="file_export_onix"/> + <Action name="file_export_bibtex"/> + <Action name="file_export_bibtexml"/> + <Action name="file_export_gcfilms"/> + <Action name="file_export_amc"/> + <Action name="coll_convert_bibliography"/> + <Action name="coll_string_macros"/> + <Action name="cite_clipboard"/> + <Action name="cite_lyxpipe"/> + <Action name="cite_openoffice"/> + </Disable> +</State> + +<State name="is_book"> + <Enable> + <Action name="file_export_alexandria"/> + <Action name="file_export_onix"/> + <Action name="coll_convert_bibliography"/> + </Enable> +</State> + +<State name="is_bibliography"> + <Enable> + <Action name="file_export_alexandria"/> + <Action name="file_export_onix"/> + <Action name="file_export_bibtex"/> + <Action name="file_export_bibtexml"/> + <Action name="coll_string_macros"/> +<!-- cite actions taken care in selection code + <Action name="cite_clipboard"/> + <Action name="cite_lyxpipe"/> + <Action name="cite_openoffice"/> + --> + </Enable> +</State> + +<State name="is_video"> + <Enable> + <Action name="file_export_gcfilms"/> + <Action name="file_export_amc"/> + </Enable> +</State> + +<State name="empty_selection"> + <Disable> + <Action name="coll_edit_entry"/> + <Action name="coll_copy_entry"/> + <Action name="coll_delete_entry"/> + <Action name="coll_update_entry"/> + <Menu name="coll_update_entry"/> + <Action name="update_entry_all"/> + <Action name="coll_checkout"/> + </Disable> +</State> + +</kpartgui> diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am new file mode 100644 index 0000000..75c0313 --- /dev/null +++ b/src/tests/Makefile.am @@ -0,0 +1,28 @@ +AM_CPPFLAGS = -I$(srcdir)/.. $(all_includes) + +KDE_OPTIONS = noautodist + +AM_LDFLAGS = $(QT_LDFLAGS) $(KDE_LDFLAGS) $(X_LDFLAGS) + +check_PROGRAMS = isbntest latin1test entitytest + +check: isbntest latin1test entitytest + ./isbntest + ./latin1test + ./entitytest + +METASOURCES = AUTO + +DISTCLEANFILES = *~ *.Po $(CLEANFILES) + +isbntest_SOURCES = isbntest.cpp +isbntest_LDADD = ../isbnvalidator.o $(LIB_QT) $(LIB_KDECORE) $(LIB_KDEUI) + +latin1test_SOURCES = latin1test.cpp +latin1test_LDADD = $(LIB_QT) $(LIB_KDECORE) + +entitytest_SOURCES = entitytest.cpp +entitytest_LDADD = ../tellico_utils.o $(LIB_QT) $(LIB_KDECORE) + +#formattest_SOURCES = formattest.cpp +#formattest_LDADD = ../core/tellico_config.o ../core/tellico_config_addons.o ../field.o ../tellico_utils.o $(LIB_QT) $(LIB_KDECORE) diff --git a/src/tests/entitytest.cpp b/src/tests/entitytest.cpp new file mode 100644 index 0000000..215c377 --- /dev/null +++ b/src/tests/entitytest.cpp @@ -0,0 +1,19 @@ +#ifdef QT_NO_CAST_ASCII +#undef QT_NO_CAST_ASCII +#endif + +#include "tellico_utils.h" +#include <kdebug.h> +#include <assert.h> + +int main(int, char**) { + kdDebug() << "\n*****************************************************" << endl; + + assert(Tellico::decodeHTML("robby") == "robby"); + assert(Tellico::decodeHTML("&fake;") == "&fake;"); + assert(Tellico::decodeHTML("0") == "0"); + assert(Tellico::decodeHTML("robby0robby") == "robby0robby"); + + kdDebug() << "\ndecodeHTML Test OK !" << endl; + kdDebug() << "\n*****************************************************" << endl; +} diff --git a/src/tests/isbntest.cpp b/src/tests/isbntest.cpp new file mode 100644 index 0000000..b4c5fee --- /dev/null +++ b/src/tests/isbntest.cpp @@ -0,0 +1,73 @@ +#ifdef QT_NO_CAST_ASCII +#undef QT_NO_CAST_ASCII +#endif + +#include "isbnvalidator.h" + +#include <kdebug.h> + +#include <stdlib.h> + +bool check(QString a, QString b) { + static const Tellico::ISBNValidator val(0); + val.fixup(a); + if(a == b) { + kdDebug() << "checking '" << a << "' against expected value '" << b << "'... " << "ok" << endl; + } else { + kdDebug() << "checking '" << a << "' against expected value '" << b << "'... " << "KO!" << endl; + exit(1); + } + return true; +} + +int main(int, char**) { + kdDebug() << "\n*****************************************************" << endl; + + // initial checks + check("0-446-60098-9", "0-446-60098-9"); + // check sum value + check("0-446-60098", "0-446-60098-9"); + + check(Tellico::ISBNValidator::isbn10("978-0-06-087298-4"), "0-06-087298-5"); + check(Tellico::ISBNValidator::isbn13("0-06-087298-5"), "978-0-06-087298-4"); + + // check EAN-13 + check("9780940016750", "978-0-940016-75-0"); + check("978-0940016750", "978-0-940016-75-0"); + check("978-0-940016-75-0", "978-0-940016-75-0"); + check("978286274486", "978-2-86274-486-5"); + check("9788186119130", "978-81-86-11913-6"); + check("9788186119137", "978-81-86-11913-6"); + check("97881-8611-9-13-0", "978-81-86-11913-6"); + check("97881-8611-9-13-7", "978-81-86-11913-6"); + + // don't add checksum for EAN that start with 978 or 979 and are less than 13 in length + check("978059600", "978-059600"); + check("978-0596000", "978-059600-0"); + + // normal english-language hyphenation + check("0-596-00053", "0-596-00053-7"); + check("044660098", "0-446-60098-9"); + check("0446600989", "0-446-60098-9"); + + // check french hyphenation + check("2862744867", "2-86274-486-7"); + + // check german hyphenation + check("3423071516", "3-423-07151-6"); + + // check dutch hyphenation + check("9065442979", "90-6544-297-9"); + + // check keeping middle hyphens + check("6-18611913-0", "6-18611913-0"); + check("6-186119-13-0", "6-186119-13-0"); + check("6-18611-9-13-0", "6-18611-913-0"); + + // check garbage + check("My name is robby", ""); + check("http://www.abclinuxu.cz/clanky/show/63080", "6-3080"); + + kdDebug() << "\n ISBN Validator Test OK !" << endl; + kdDebug() << "\n*****************************************************" << endl; +} diff --git a/src/tests/latin1test.cpp b/src/tests/latin1test.cpp new file mode 100644 index 0000000..636c33f --- /dev/null +++ b/src/tests/latin1test.cpp @@ -0,0 +1,25 @@ +#ifdef QT_NO_CAST_ASCII +#undef QT_NO_CAST_ASCII +#endif + +#include "latin1literal.h" +#include <qstring.h> +#include <kdebug.h> +#include <assert.h> + +int main(int, char**) { + kdDebug() << "\n*****************************************************" << endl; + + assert(QString::null == Latin1Literal(0)); + assert(QString::null != Latin1Literal("")); + assert(QString::fromLatin1("") == Latin1Literal("")); + assert(QString::fromLatin1("") != Latin1Literal(0)); + assert(QString::fromLatin1("x") != Latin1Literal("")); + assert(QString::fromLatin1("a") == Latin1Literal("a")); + assert(QString::fromLatin1("a") != Latin1Literal("b")); + assert(QString::fromLatin1("\xe4") == Latin1Literal("\xe4")); + assert(QString::fromUtf8("\xe2\x82\xac") != Latin1Literal("?")); + + kdDebug() << "\nLatin1Literal Test OK !" << endl; + kdDebug() << "\n*****************************************************" << endl; +} diff --git a/src/translators/Makefile.am b/src/translators/Makefile.am new file mode 100644 index 0000000..68ee4cc --- /dev/null +++ b/src/translators/Makefile.am @@ -0,0 +1,70 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = libtranslators.a + +## AM_CPPFLAGS were found outside kdevelop specific part + +libtranslators_a_METASOURCES = AUTO + +libtranslators_a_SOURCES = alexandriaexporter.cpp alexandriaimporter.cpp \ + amcimporter.cpp audiofileimporter.cpp bibtexexporter.cpp bibtexhandler.cpp \ + bibteximporter.cpp bibtexmlexporter.cpp bibtexmlimporter.cpp csvexporter.cpp \ + csvimporter.cpp dcimporter.cpp deliciousimporter.cpp exporter.cpp \ + filelistingimporter.cpp freedb_util.cpp freedbimporter.cpp gcfilmsexporter.cpp \ + gcfilmsimporter.cpp griffithimporter.cpp grs1importer.cpp htmlexporter.cpp libcsv.c \ + onixexporter.cpp pdfimporter.cpp pilotdbexporter.cpp referencerimporter.cpp \ + risimporter.cpp tellico_xml.cpp tellicoimporter.cpp tellicoxmlexporter.cpp \ + tellicozipexporter.cpp textimporter.cpp xmlimporter.cpp xsltexporter.cpp xslthandler.cpp \ + xsltimporter.cpp + +if !USE_LIBBTPARSE + SUBDIR_LIBBTPARSE = btparse +endif + +SUBDIRS = pilotdb $(SUBDIR_LIBBTPARSE) + +CLEANFILES = *~ + +EXTRA_DIST = bibtex-translation.xml \ +bibtexexporter.cpp tellicoxmlexporter.h pilotdbexporter.cpp \ +bibtexexporter.h tellicozipexporter.cpp pilotdbexporter.h \ +bibtexhandler.cpp tellicozipexporter.h \ +bibtexhandler.h csvexporter.cpp textimporter.cpp \ +bibteximporter.cpp csvexporter.h textimporter.h \ +bibteximporter.h csvimporter.cpp xmlimporter.cpp \ +bibtexmlexporter.cpp csvimporter.h xmlimporter.h \ +bibtexmlexporter.h xsltexporter.cpp \ +bibtexmlimporter.cpp dataimporter.h xsltexporter.h \ +bibtexmlimporter.h exporter.h xslthandler.cpp \ +tellicoimporter.cpp htmlexporter.cpp xslthandler.h \ +tellicoimporter.h htmlexporter.h xsltimporter.cpp \ +tellicoxmlexporter.cpp importer.h xsltimporter.h \ +audiofileimporter.h audiofileimporter.cpp alexandriaimporter.h \ +alexandriaimporter.cpp alexandriaexporter.h alexandriaexporter.cpp \ +freedbimporter.h freedbimporter.cpp freedb_util.cpp \ +risimporter.h risimporter.cpp tellico_xml.h \ +tellico_xml.cpp translators.h exporter.cpp \ +onixexporter.h onixexporter.cpp gcfilmsimporter.h \ +gcfilmsimporter.cpp gcfilmsexporter.h gcfilmsexporter.cpp \ +filelistingimporter.h filelistingimporter.cpp grs1importer.h \ +grs1importer.cpp amcimporter.h amcimporter.cpp \ +dcimporter.h dcimporter.cpp griffithimporter.h \ +griffithimporter.cpp griffith2tellico.py pdfimporter.h \ +pdfimporter.cpp referencerimporter.h referencerimporter.cpp \ +libcsv.h libcsv.c \ +deliciousimporter.h deliciousimporter.cpp + +####### kdevelop will overwrite this part!!! (end)############ + +AM_CPPFLAGS = \ + $(LIBXML_CFLAGS) \ + $(LIBXSLT_CFLAGS) \ + $(TAGLIB_CFLAGS) \ + $(KCDDB_CFLAGS) \ + $(all_includes) \ + $(POPPLER_CFLAGS) + +KDE_OPTIONS = noautodist + +appdir = $(kde_datadir)/tellico +app_DATA = bibtex-translation.xml +app_SCRIPTS = griffith2tellico.py diff --git a/src/translators/alexandriaexporter.cpp b/src/translators/alexandriaexporter.cpp new file mode 100644 index 0000000..186b866 --- /dev/null +++ b/src/translators/alexandriaexporter.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + copyright : (C) 2003-2007 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 "alexandriaexporter.h" +#include "../document.h" +#include "../collection.h" +#include "../tellico_kernel.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" +#include "../progressmanager.h" + +#include <klocale.h> +#include <kmessagebox.h> +#include <kapplication.h> + +#include <qdir.h> + +namespace { + static const int ALEXANDRIA_MAX_SIZE_SMALL = 60; + static const int ALEXANDRIA_MAX_SIZE_MEDIUM = 140; +} + +using Tellico::Export::AlexandriaExporter; + +QString& AlexandriaExporter::escapeText(QString& str_) { + str_.replace('"', QString::fromLatin1("\\\"")); + return str_; +} + +QString AlexandriaExporter::formatString() const { + return i18n("Alexandria"); +} + +bool AlexandriaExporter::exec() { + Data::CollPtr coll = collection(); + if(!coll || (coll->type() != Data::Collection::Book && coll->type() != Data::Collection::Bibtex)) { + myLog() << "AlexandriaExporter::exec() - bad collection" << endl; + return false; + } + + const QString alexDirName = QString::fromLatin1(".alexandria"); + + // create if necessary + QDir libraryDir = QDir::home(); + if(!libraryDir.cd(alexDirName)) { + if(!libraryDir.mkdir(alexDirName) || !libraryDir.cd(alexDirName)) { + myLog() << "AlexandriaExporter::exec() - can't locate directory" << endl; + return false; + } + } + + // the collection title is the name of the directory to create + if(libraryDir.cd(coll->title())) { + int ret = KMessageBox::warningContinueCancel(Kernel::self()->widget(), + i18n("<qt>An Alexandria library called <i>%1</i> already exists. " + "Any existing books in that library could be overwritten.</qt>") + .arg(coll->title())); + if(ret == KMessageBox::Cancel) { + return false; + } + } else if(!libraryDir.mkdir(coll->title()) || !libraryDir.cd(coll->title())) { + return false; // could not create and cd to the dir + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, QString::null, false); + item.setTotalSteps(entries().count()); + ProgressItem::Done done(this); + const uint stepSize = QMIN(1, entries().count()/100); + const bool showProgress = options() & ExportProgress; + + GUI::CursorSaver cs; + bool success = true; + uint j = 0; + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt, ++j) { + success &= writeFile(libraryDir, entryIt.data()); + if(showProgress && j%stepSize == 0) { + item.setProgress(j); + kapp->processEvents(); + } + } + return success; +} + +// this isn't true YAML export, of course +// everything is put between quotes except for the rating, just to be sure it's interpreted as a string +bool AlexandriaExporter::writeFile(const QDir& dir_, Data::ConstEntryPtr entry_) { + // the filename is the isbn without dashes, followed by .yaml + QString isbn = entry_->field(QString::fromLatin1("isbn")); + if(isbn.isEmpty()) { + return false; // can't write it since Alexandria uses isbn as name of file + } + isbn.remove('-'); // remove dashes + + QFile file(dir_.absPath() + QDir::separator() + isbn + QString::fromLatin1(".yaml")); + if(!file.open(IO_WriteOnly)) { + return false; + } + + // do we format? + bool format = options() & Export::ExportFormatted; + + QTextStream ts(&file); + // alexandria uses utf-8 all the time + ts.setEncoding(QTextStream::UnicodeUTF8); + ts << "--- !ruby/object:Alexandria::Book\n"; + ts << "authors:\n"; + QStringList authors = entry_->fields(QString::fromLatin1("author"), format); + for(QStringList::Iterator it = authors.begin(); it != authors.end(); ++it) { + ts << " - " << escapeText(*it) << "\n"; + } + // Alexandria crashes when no authors, and uses n/a when none + if(authors.count() == 0) { + ts << " - n/a\n"; + } + + QString tmp = entry_->field(QString::fromLatin1("title"), format); + ts << "title: \"" << escapeText(tmp) << "\"\n"; + + // Alexandria refers to the binding as the edition + tmp = entry_->field(QString::fromLatin1("binding"), format); + ts << "edition: \"" << escapeText(tmp) << "\"\n"; + + // sometimes Alexandria interprets the isbn as a number instead of a string + // I have no idea how to debug ruby, so err on safe side and add quotes + ts << "isbn: \"" << isbn << "\"\n"; + + tmp = entry_->field(QString::fromLatin1("comments"), format); + ts << "notes: \"" << escapeText(tmp) << "\"\n"; + + tmp = entry_->field(QString::fromLatin1("publisher"), format); + // publisher uses n/a when empty + ts << "publisher: \"" << (tmp.isEmpty() ? QString::fromLatin1("n/a") : escapeText(tmp)) << "\"\n"; + + tmp = entry_->field(QString::fromLatin1("pub_year"), format); + if(!tmp.isEmpty()) { + ts << "publishing_year: \"" << escapeText(tmp) << "\"\n"; + } + + tmp = entry_->field(QString::fromLatin1("rating")); + bool ok; + int rating = Tellico::toUInt(tmp, &ok); + if(ok) { + ts << "rating: " << rating << "\n"; + } + + file.close(); + + QString cover = entry_->field(QString::fromLatin1("cover")); + if(cover.isEmpty() || !(options() & Export::ExportImages)) { + return true; // all done + } + + QImage img1(ImageFactory::imageById(cover)); + QImage img2; + QString filename = dir_.absPath() + QDir::separator() + isbn; + if(img1.height() > ALEXANDRIA_MAX_SIZE_SMALL) { + if(img1.height() > ALEXANDRIA_MAX_SIZE_MEDIUM) { // limit maximum size + img1 = img1.scale(ALEXANDRIA_MAX_SIZE_MEDIUM, ALEXANDRIA_MAX_SIZE_MEDIUM, QImage::ScaleMin); + } + img2 = img1.scale(ALEXANDRIA_MAX_SIZE_SMALL, ALEXANDRIA_MAX_SIZE_SMALL, QImage::ScaleMin); + } else { + img2 = img1.smoothScale(ALEXANDRIA_MAX_SIZE_MEDIUM, ALEXANDRIA_MAX_SIZE_MEDIUM, QImage::ScaleMin); // scale up + } + if(!img1.save(filename + QString::fromLatin1("_medium.jpg"), "JPEG") + || !img2.save(filename + QString::fromLatin1("_small.jpg"), "JPEG")) { + return false; + } + return true; +} + +#include "alexandriaexporter.moc" diff --git a/src/translators/alexandriaexporter.h b/src/translators/alexandriaexporter.h new file mode 100644 index 0000000..033bb14 --- /dev/null +++ b/src/translators/alexandriaexporter.h @@ -0,0 +1,51 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef ALEXANDRIAEXPORTER_H +#define ALEXANDRIAEXPORTER_H + +class QDir; + +#include "exporter.h" + +namespace Tellico { + namespace Data { + class Entry; + } + namespace Export { + +/** + * @author Robby Stephenson + */ +class AlexandriaExporter : public Exporter { +Q_OBJECT + +public: + AlexandriaExporter() : Exporter() {} + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const { return QString::null; } // no need for this + + // no config options + virtual QWidget* widget(QWidget*, const char*) { return 0; } + +private: + static QString& escapeText(QString& str); + + bool writeFile(const QDir& dir, Data::ConstEntryPtr entry); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/alexandriaimporter.cpp b/src/translators/alexandriaimporter.cpp new file mode 100644 index 0000000..5e49e86 --- /dev/null +++ b/src/translators/alexandriaimporter.cpp @@ -0,0 +1,255 @@ +/*************************************************************************** + copyright : (C) 2003-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 "alexandriaimporter.h" +#include "../collections/bookcollection.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../isbnvalidator.h" +#include "../imagefactory.h" +#include "../progressmanager.h" +#include "../tellico_debug.h" + +#include <kcombobox.h> +#include <kapplication.h> +#include <kstringhandler.h> + +#include <qlayout.h> +#include <qlabel.h> +#include <qgroupbox.h> + +using Tellico::Import::AlexandriaImporter; + +bool AlexandriaImporter::canImport(int type) const { + return type == Data::Collection::Book; +} + +Tellico::Data::CollPtr AlexandriaImporter::collection() { + if(!m_widget || m_library->count() == 0) { + return 0; + } + + m_coll = new Data::BookCollection(true); + + QDir dataDir = m_libraryDir; + dataDir.cd(m_library->currentText()); + dataDir.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks); + + const QString title = QString::fromLatin1("title"); + const QString author = QString::fromLatin1("author"); + const QString year = QString::fromLatin1("pub_year"); + const QString binding = QString::fromLatin1("binding"); + const QString isbn = QString::fromLatin1("isbn"); + const QString pub = QString::fromLatin1("publisher"); + const QString rating = QString::fromLatin1("rating"); + const QString cover = QString::fromLatin1("cover"); + const QString comments = QString::fromLatin1("comments"); + + // start with yaml files + dataDir.setNameFilter(QString::fromLatin1("*.yaml")); + const QStringList files = dataDir.entryList(); + const uint numFiles = files.count(); + const uint stepSize = QMAX(s_stepSize, numFiles/100); + const bool showProgress = options() & ImportProgress; + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(numFiles); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + QStringList covers; + covers << QString::fromLatin1(".cover") + << QString::fromLatin1("_medium.jpg") + << QString::fromLatin1("_small.jpg"); + + QTextStream ts; + ts.setEncoding(QTextStream::UnicodeUTF8); // YAML is always utf8? + uint j = 0; + for(QStringList::ConstIterator it = files.begin(); !m_cancelled && it != files.end(); ++it, ++j) { + QFile file(dataDir.absFilePath(*it)); + if(!file.open(IO_ReadOnly)) { + continue; + } + + Data::EntryPtr entry = new Data::Entry(m_coll); + + bool readNextLine = true; + ts.unsetDevice(); + ts.setDevice(&file); + QString line; + while(!ts.atEnd()) { + if(readNextLine) { + line = ts.readLine(); + } else { + readNextLine = true; + } + // skip the line that starts with --- + if(line.isEmpty() || line.startsWith(QString::fromLatin1("---"))) { + continue; + } + if(line.endsWith(QChar('\\'))) { + line.truncate(line.length()-1); // remove last character + line += ts.readLine(); + } + + cleanLine(line); + QString alexField = line.section(':', 0, 0); + QString alexValue = line.section(':', 1).stripWhiteSpace(); + clean(alexValue); + + // Alexandria uses "n/a for empty values, and it is translated + // only thing we can do is check for english value and continue + if(alexValue == Latin1Literal("n/a")) { + continue; + } + + if(alexField == Latin1Literal("authors")) { + QStringList authors; + line = ts.readLine(); + QRegExp begin(QString::fromLatin1("^\\s*-\\s+")); + while(!line.isNull() && line.find(begin) > -1) { + line.remove(begin); + authors += clean(line); + line = ts.readLine(); + } + entry->setField(author, authors.join(QString::fromLatin1("; "))); + // the next line has already been read + readNextLine = false; + + // Alexandria calls the edition the binding + } else if(alexField == Latin1Literal("edition")) { + // special case if it's "Hardcover" + if(alexValue.lower() == Latin1Literal("hardcover")) { + alexValue = i18n("Hardback"); + } + entry->setField(binding, alexValue); + + } else if(alexField == Latin1Literal("publishing_year")) { + entry->setField(year, alexValue); + + } else if(alexField == Latin1Literal("isbn")) { + const ISBNValidator val(0); + val.fixup(alexValue); + entry->setField(isbn, alexValue); + + // now find cover image + KURL u; + alexValue.remove('-'); + for(QStringList::Iterator ext = covers.begin(); ext != covers.end(); ++ext) { + u.setPath(dataDir.absFilePath(alexValue + *ext)); + if(!QFile::exists(u.path())) { + continue; + } + QString id = ImageFactory::addImage(u, true); + if(!id.isEmpty()) { + entry->setField(cover, id); + break; + } + } + } else if(alexField == Latin1Literal("notes")) { + entry->setField(comments, alexValue); + + // now try by name then title + } else if(m_coll->fieldByName(alexField)) { + entry->setField(alexField, alexValue); + + } else if(m_coll->fieldByTitle(alexField)) { + entry->setField(m_coll->fieldByTitle(alexField), alexValue); + } + } + m_coll->addEntries(entry); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } + + return m_coll; +} + +QWidget* AlexandriaImporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget) { + return m_widget; + } + + m_libraryDir = QDir::home(); + m_libraryDir.setFilter(QDir::Dirs | QDir::Readable | QDir::NoSymLinks); + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(2, Qt::Horizontal, i18n("Alexandria Options"), m_widget); + QLabel* label = new QLabel(i18n("&Library:"), box); + m_library = new KComboBox(box); + label->setBuddy(m_library); + + // .alexandria might not exist + if(m_libraryDir.cd(QString::fromLatin1(".alexandria"))) { + QStringList dirs = m_libraryDir.entryList(); + dirs.remove(QString::fromLatin1(".")); // why can't I tell QDir not to include these? QDir::Hidden doesn't work + dirs.remove(QString::fromLatin1("..")); + m_library->insertStringList(dirs); + } + + l->addWidget(box); + l->addStretch(1); + return m_widget; +} + +QString& AlexandriaImporter::cleanLine(QString& str_) { + static QRegExp escRx(QString::fromLatin1("\\\\x(\\w\\w)"), false); + str_.remove(QString::fromLatin1("\\r")); + str_.replace(QString::fromLatin1("\\n"), QString::fromLatin1("\n")); + str_.replace(QString::fromLatin1("\\t"), QString::fromLatin1("\t")); + + // YAML uses escape sequences like \xC3 + int pos = escRx.search(str_); + int origPos = pos; + QCString bytes; + while(pos > -1) { + bool ok; + char c = escRx.cap(1).toInt(&ok, 16); + if(ok) { + bytes += c; + } else { + bytes = QCString(); + break; + } + pos = escRx.search(str_, pos+1); + } + if(!bytes.isEmpty()) { + str_.replace(origPos, bytes.length()*4, QString::fromUtf8(bytes.data())); + } + return str_; +} + +QString& AlexandriaImporter::clean(QString& str_) { + const QRegExp quote(QString::fromLatin1("\\\\\"")); // equals \" + if(str_.startsWith(QChar('\'')) || str_.startsWith(QChar('"'))) { + str_.remove(0, 1); + } + if(str_.endsWith(QChar('\'')) || str_.endsWith(QChar('"'))) { + str_.truncate(str_.length()-1); + } + // we ignore YAML tags, this is not actually a good parser, but will do for now + str_.remove(QRegExp(QString::fromLatin1("^![^\\s]*\\s+"))); + return str_.replace(quote, QChar('"')); +} + +void AlexandriaImporter::slotCancel() { + m_cancelled = true; +} + +#include "alexandriaimporter.moc" diff --git a/src/translators/alexandriaimporter.h b/src/translators/alexandriaimporter.h new file mode 100644 index 0000000..2c12923 --- /dev/null +++ b/src/translators/alexandriaimporter.h @@ -0,0 +1,72 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef ALEXANDRIAIMPORTER_H +#define ALEXANDRIAIMPORTER_H + +class KComboBox; + +#include "importer.h" +#include "../datavectors.h" + +#include <qdir.h> + +namespace Tellico { + namespace Import { + +/** + * An importer for importing collections used by Alexandria, the Gnome book collection manager. + * + * The libraries are assumed to be in $HOME/.alexandria. The file format is YAML, but instead + * using a real YAML reader, the file is parsed line-by-line, so it's very crude. When Alexandria + * adds new fields or types, this will have to be updated. + * + * @author Robby Stephenson + */ +class AlexandriaImporter : public Importer { +Q_OBJECT + +public: + /** + */ + AlexandriaImporter() : Importer(), m_coll(0), m_widget(0), m_cancelled(false) {} + /** + */ + virtual ~AlexandriaImporter() {} + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + static QString& cleanLine(QString& str); + static QString& clean(QString& str); + + Data::CollPtr m_coll; + QWidget* m_widget; + KComboBox* m_library; + + QDir m_libraryDir; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/amcimporter.cpp b/src/translators/amcimporter.cpp new file mode 100644 index 0000000..8e45cb7 --- /dev/null +++ b/src/translators/amcimporter.cpp @@ -0,0 +1,294 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +// The information about the AMC file format was taken from the source code for +// GCfilms, (GPL) (c) 2005 Tian +// Monotheka, (GPL) (c) 2004, 2005 Michael Dominic K. +// 2005 Aurelien Mino + +#include "amcimporter.h" +#include "../collections/videocollection.h" +#include "../imagefactory.h" +#include "../latin1literal.h" +#include "../progressmanager.h" +#include "../tellico_debug.h" + +#include <kapplication.h> + +#include <qfile.h> +#include <qimage.h> + +#include <limits.h> + +namespace { + static const QCString AMC_FILE_ID = " AMC_X.Y Ant Movie Catalog 3.5.x www.buypin.com www.antp.be "; +} + +using Tellico::Import::AMCImporter; + +AMCImporter::AMCImporter(const KURL& url_) : DataImporter(url_), m_coll(0), m_cancelled(false) { +} + +AMCImporter::~AMCImporter() { +} + +bool AMCImporter::canImport(int type) const { + return type == Data::Collection::Video; +} + +Tellico::Data::CollPtr AMCImporter::collection() { + if(m_coll) { + return m_coll; + } + + if(!fileRef().open()) { + return 0; + } + + QIODevice* f = fileRef().file(); + m_ds.setDevice(f); + // AMC is always little-endian? can't confirm + m_ds.setByteOrder(QDataStream::LittleEndian); + + const uint l = AMC_FILE_ID.length(); + QMemArray<char> buffer(l+1); + m_ds.readRawBytes(buffer.data(), l); + QString version = QString::fromLocal8Bit(buffer, l); + QRegExp versionRx(QString::fromLatin1(".+AMC_(\\d+)\\.(\\d+).+")); + if(version.find(versionRx) == -1) { + myDebug() << "AMCImporter::collection() - no file id match" << endl; + return 0; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(f->size()); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + m_coll = new Data::VideoCollection(true); + + m_majVersion = versionRx.cap(1).toInt(); + m_minVersion = versionRx.cap(2).toInt(); +// myDebug() << m_majVersion << "::" << m_minVersion << endl; + + readString(); // name + readString(); // email + if(m_majVersion <= 3 && m_minVersion < 5) { + readString(); // icq + } + readString(); // webpage + readString(); // description + + const bool showProgress = options() & ImportProgress; + + while(!m_cancelled && !f->atEnd()) { + readEntry(); + if(showProgress) { + ProgressManager::self()->setProgress(this, f->at()); + kapp->processEvents(); + } + } + + return m_coll; +} + +bool AMCImporter::readBool() { + Q_UINT8 b; + m_ds >> b; + return b; +} + +Q_UINT32 AMCImporter::readInt() { + Q_UINT32 i; + m_ds >> i; + if(i >= UINT_MAX) { + i = 0; + } + return i; +} + +QString AMCImporter::readString() { + // The serialization format is a length specifier first, then l bytes of data + uint l = readInt(); + if(l == 0) { + return QString(); + } + QMemArray<char> buffer(l+1); + m_ds.readRawBytes(buffer.data(), l); + QString s = QString::fromLocal8Bit(buffer, l); +// myDebug() << "string: " << s << endl; + return s; +} + +QString AMCImporter::readImage(const QString& format_) { + uint l = readInt(); + if(l == 0) { + return QString(); + } + QMemArray<char> buffer(l+1); + m_ds.readRawBytes(buffer.data(), l); + QByteArray bytes; + bytes.setRawData(buffer.data(), l); + QImage img(bytes); + bytes.resetRawData(buffer.data(), l); + if(img.isNull()) { + myDebug() << "AMCImporter::readImage() - null image" << endl; + return QString(); + } + QString format = QString::fromLatin1("PNG"); + if(format_ == Latin1Literal(".jpg")) { + format = QString::fromLatin1("JPEG"); + } else if(format_ == Latin1Literal(".gif")) { + format = QString::fromLatin1("GIF"); + } + return ImageFactory::addImage(img, format); +} + +void AMCImporter::readEntry() { + Data::EntryPtr e = new Data::Entry(m_coll); + + int id = readInt(); + if(id > 0) { + e->setId(id); + } + readInt(); // add date + + int rating = readInt(); + if(m_majVersion >= 3 && m_minVersion >= 5) { + rating /= 10; + } + e->setField(QString::fromLatin1("rating"), QString::number(rating)); + int year = readInt(); + if(year > 0) { + e->setField(QString::fromLatin1("year"), QString::number(year)); + } + int time = readInt(); + if(time > 0) { + e->setField(QString::fromLatin1("running-time"), QString::number(time)); + } + + readInt(); // video bitrate + readInt(); // audio bitrate + readInt(); // number of files + readBool(); // checked + readString(); // media label + e->setField(QString::fromLatin1("medium"), readString()); + readString(); // source + readString(); // borrower + QString s = readString(); // title + if(!s.isEmpty()) { + e->setField(QString::fromLatin1("title"), s); + } + QString s2 = readString(); // translated title + if(s.isEmpty()) { + e->setField(QString::fromLatin1("title"), s2); + } + + e->setField(QString::fromLatin1("director"), readString()); + s = readString(); + QRegExp roleRx(QString::fromLatin1("(.+) \\(([^(]+)\\)")); + roleRx.setMinimal(true); + if(s.find(roleRx) > -1) { + QString role = roleRx.cap(2).lower(); + if(role == Latin1Literal("story") || role == Latin1Literal("written by")) { + e->setField(QString::fromLatin1("writer"), roleRx.cap(1)); + } else { + e->setField(QString::fromLatin1("producer"), s); + } + } else { + e->setField(QString::fromLatin1("producer"), s); + } + e->setField(QString::fromLatin1("nationality"), readString()); + e->setField(QString::fromLatin1("genre"), readString().replace(QString::fromLatin1(", "), QString::fromLatin1("; "))); + + e->setField(QString::fromLatin1("cast"), parseCast(readString()).join(QString::fromLatin1("; "))); + + readString(); // url + e->setField(QString::fromLatin1("plot"), readString()); + e->setField(QString::fromLatin1("comments"), readString()); + s = readString(); // video format + QRegExp regionRx(QString::fromLatin1("Region \\d")); + if(s.find(regionRx) > -1) { + e->setField(QString::fromLatin1("region"), regionRx.cap(0)); + } + e->setField(QString::fromLatin1("audio-track"), readString()); // audio format + readString(); // resolution + readString(); // frame rate + e->setField(QString::fromLatin1("language"), readString()); // audio language + e->setField(QString::fromLatin1("subtitle"), readString()); // subtitle + readString(); // file size + s = readString(); // picture extension + s = readImage(s); // picture + if(!s.isEmpty()) { + e->setField(QString::fromLatin1("cover"), s); + } + + m_coll->addEntries(e); +} + +QStringList AMCImporter::parseCast(const QString& text_) { + QStringList cast; + int nPar = 0; + QRegExp castRx(QString::fromLatin1("[,()]")); + QString person, role; + int oldPos = 0; + for(int pos = text_.find(castRx); pos > -1; pos = text_.find(castRx, pos+1)) { + if(text_.at(pos) == ',' && nPar%2 == 0) { + // we're done with this one + person += text_.mid(oldPos, pos-oldPos).stripWhiteSpace(); + QString all = person; + if(!role.isEmpty()) { + if(role.startsWith(QString::fromLatin1("as "))) { + role = role.mid(3); + } + all += "::" + role; + } + cast << all; + person.truncate(0); + role.truncate(0); + oldPos = pos+1; // add one to go past comma + } else if(text_.at(pos) == '(') { + if(nPar == 0) { + person = text_.mid(oldPos, pos-oldPos).stripWhiteSpace(); + oldPos = pos+1; // add one to go past parenthesis + } + ++nPar; + } else if(text_.at(pos) == ')') { + --nPar; + if(nPar == 0) { + role = text_.mid(oldPos, pos-oldPos).stripWhiteSpace(); + oldPos = pos+1; // add one to go past parenthesis + } + } + } + // grab the last one + if(nPar%2 == 0) { + int pos = text_.length(); + person += text_.mid(oldPos, pos-oldPos).stripWhiteSpace(); + QString all = person; + if(!role.isEmpty()) { + if(role.startsWith(QString::fromLatin1("as "))) { + role = role.mid(3); + } + all += "::" + role; + } + cast << all; + } + return cast; +} + +void AMCImporter::slotCancel() { + m_cancelled = true; +} + +#include "amcimporter.moc" diff --git a/src/translators/amcimporter.h b/src/translators/amcimporter.h new file mode 100644 index 0000000..d1b9d1a --- /dev/null +++ b/src/translators/amcimporter.h @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_AMCIMPORTER_H +#define TELLICO_IMPORT_AMCIMPORTER_H + +#include "dataimporter.h" + +namespace Tellico { + namespace Import { + +/** + @author Robby Stephenson + */ +class AMCImporter : public DataImporter { +Q_OBJECT +public: + AMCImporter(const KURL& url); + virtual ~AMCImporter(); + + virtual Data::CollPtr collection(); + bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + bool readBool(); + Q_UINT32 readInt(); + QString readString(); + QString readImage(const QString& format); + void readEntry(); + QStringList parseCast(const QString& text); + + Data::CollPtr m_coll; + bool m_cancelled : 1; + QDataStream m_ds; + int m_majVersion; + int m_minVersion; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/translators/audiofileimporter.cpp b/src/translators/audiofileimporter.cpp new file mode 100644 index 0000000..f825964 --- /dev/null +++ b/src/translators/audiofileimporter.cpp @@ -0,0 +1,424 @@ +/*************************************************************************** + copyright : (C) 2004-2007 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 <config.h> + +#include "audiofileimporter.h" +#include "../collections/musiccollection.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../imagefactory.h" +#include "../tellico_utils.h" +#include "../tellico_kernel.h" +#include "../progressmanager.h" +#include "../tellico_debug.h" + +#ifdef HAVE_TAGLIB +#include <taglib/fileref.h> +#include <taglib/tag.h> +#include <taglib/id3v2tag.h> +#include <taglib/mpegfile.h> +#include <taglib/vorbisfile.h> +#include <taglib/flacfile.h> +#include <taglib/audioproperties.h> +#endif + +#include <klocale.h> +#include <kapplication.h> + +#include <qlabel.h> +#include <qlayout.h> +#include <qvgroupbox.h> +#include <qcheckbox.h> +#include <qdir.h> +#include <qwhatsthis.h> + +using Tellico::Import::AudioFileImporter; + +AudioFileImporter::AudioFileImporter(const KURL& url_) : Tellico::Import::Importer(url_) + , m_coll(0) + , m_widget(0) + , m_cancelled(false) { +} + +bool AudioFileImporter::canImport(int type) const { + return type == Data::Collection::Album; +} + +Tellico::Data::CollPtr AudioFileImporter::collection() { +#ifndef HAVE_TAGLIB + return 0; +#else + + if(m_coll) { + return m_coll; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Scanning audio files..."), true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + // TODO: allow remote audio file importing + QStringList dirs = url().path(); + if(m_recursive->isChecked()) { + dirs += Tellico::findAllSubDirs(dirs[0]); + } + + if(m_cancelled) { + return 0; + } + + const bool showProgress = options() & ImportProgress; + + QStringList files; + for(QStringList::ConstIterator it = dirs.begin(); !m_cancelled && it != dirs.end(); ++it) { + if((*it).isEmpty()) { + continue; + } + + QDir dir(*it); + dir.setFilter(QDir::Files | QDir::Readable | QDir::Hidden); // hidden since I want directory files + const QStringList list = dir.entryList(); + for(QStringList::ConstIterator it2 = list.begin(); it2 != list.end(); ++it2) { + files += dir.absFilePath(*it2); + } +// kapp->processEvents(); not needed ? + } + + if(m_cancelled) { + return 0; + } + item.setTotalSteps(files.count()); + + const QString title = QString::fromLatin1("title"); + const QString artist = QString::fromLatin1("artist"); + const QString year = QString::fromLatin1("year"); + const QString genre = QString::fromLatin1("genre"); + const QString track = QString::fromLatin1("track"); + const QString comments = QString::fromLatin1("comments"); + const QString file = QString::fromLatin1("file"); + + m_coll = new Data::MusicCollection(true); + + const bool addFile = m_addFilePath->isChecked(); + const bool addBitrate = m_addBitrate->isChecked(); + + Data::FieldPtr f; + if(addFile) { + f = m_coll->fieldByName(file); + if(!f) { + f = new Data::Field(file, i18n("Files"), Data::Field::Table); + m_coll->addField(f); + } + f->setProperty(QString::fromLatin1("column1"), i18n("Files")); + if(addBitrate) { + f->setProperty(QString::fromLatin1("columns"), QChar('2')); + f->setProperty(QString::fromLatin1("column2"), i18n("Bitrate")); + } else { + f->setProperty(QString::fromLatin1("columns"), QChar('1')); + } + } + + QMap<QString, Data::EntryPtr> albumMap; + + QStringList directoryFiles; + const uint stepSize = QMAX(static_cast<size_t>(1), files.count() / 100); + + bool changeTrackTitle = true; + uint j = 0; + for(QStringList::ConstIterator it = files.begin(); !m_cancelled && it != files.end(); ++it, ++j) { + TagLib::FileRef f(QFile::encodeName(*it)); + if(f.isNull() || !f.tag()) { + if((*it).endsWith(QString::fromLatin1("/.directory"))) { + directoryFiles += *it; + } + continue; + } + + TagLib::Tag* tag = f.tag(); + QString album = TStringToQString(tag->album()).stripWhiteSpace(); + if(album.isEmpty()) { + // can't do anything since tellico entries are by album + kdWarning() << "Skipping: no album listed for " << *it << endl; + continue; + } + int disc = discNumber(f); + if(disc > 1 && !m_coll->hasField(QString::fromLatin1("track%1").arg(disc))) { + Data::FieldPtr f2 = new Data::Field(QString::fromLatin1("track%1").arg(disc), + i18n("Tracks (Disc %1)").arg(disc), + Data::Field::Table); + f2->setFormatFlag(Data::Field::FormatTitle); + f2->setProperty(QString::fromLatin1("columns"), QChar('3')); + f2->setProperty(QString::fromLatin1("column1"), i18n("Title")); + f2->setProperty(QString::fromLatin1("column2"), i18n("Artist")); + f2->setProperty(QString::fromLatin1("column3"), i18n("Length")); + m_coll->addField(f2); + if(changeTrackTitle) { + Data::FieldPtr newTrack = new Data::Field(*m_coll->fieldByName(track)); + newTrack->setTitle(i18n("Tracks (Disc %1)").arg(1)); + m_coll->modifyField(newTrack); + changeTrackTitle = false; + } + } + bool various = false; + bool exists = true; + Data::EntryPtr entry = 0; + if(!(entry = albumMap[album.lower()])) { + entry = new Data::Entry(m_coll); + albumMap.insert(album.lower(), entry); + exists = false; + } + // album entries use the album name as the title + entry->setField(title, album); + QString a = TStringToQString(tag->artist()).stripWhiteSpace(); + if(!a.isEmpty()) { + if(exists && entry->field(artist).lower() != a.lower()) { + various = true; + entry->setField(artist, i18n("(Various)")); + } else { + entry->setField(artist, a); + } + } + if(tag->year() > 0) { + entry->setField(year, QString::number(tag->year())); + } + if(!tag->genre().isEmpty()) { + entry->setField(genre, TStringToQString(tag->genre()).stripWhiteSpace()); + } + + if(!tag->title().isEmpty()) { + int trackNum = tag->track(); + if(trackNum <= 0) { // try to figure out track number from file name + QFileInfo f(*it); + QString fileName = f.baseName(); + QString numString; + int i = 0; + const int len = fileName.length(); + while(fileName[i].isNumber() && i < len) { + i++; + } + if(i == 0) { // does not start with a number + i = len - 1; + while(i >= 0 && fileName[i].isNumber()) { + i--; + } + // file name ends with a number + if(i != len - 1) { + numString = fileName.mid(i + 1); + } + } else { + numString = fileName.mid(0, i); + } + bool ok; + int number = numString.toInt(&ok); + if(ok) { + trackNum = number; + } + } + if(trackNum > 0) { + QString t = TStringToQString(tag->title()).stripWhiteSpace(); + t += "::" + a; + const int len = f.audioProperties()->length(); + if(len > 0) { + t += "::" + Tellico::minutes(len); + } + QString realTrack = disc > 1 ? track + QString::number(disc) : track; + entry->setField(realTrack, insertValue(entry->field(realTrack), t, trackNum)); + if(addFile) { + QString fileValue = *it; + if(addBitrate) { + fileValue += "::" + QString::number(f.audioProperties()->bitrate()); + } + entry->setField(file, insertValue(entry->field(file), fileValue, trackNum)); + } + } else { + myDebug() << *it << " contains no track number and track number cannot be determined, so the track is not imported." << endl; + } + } else { + myDebug() << *it << " has an empty title, so the track is not imported." << endl; + } + if(!tag->comment().stripWhiteSpace().isEmpty()) { + QString c = entry->field(comments); + if(!c.isEmpty()) { + c += QString::fromLatin1("<br/>"); + } + if(!tag->title().isEmpty()) { + c += QString::fromLatin1("<em>") + TStringToQString(tag->title()).stripWhiteSpace() + QString::fromLatin1("</em> - "); + } + c += TStringToQString(tag->comment().stripWhiteSpace()).stripWhiteSpace(); + entry->setField(comments, c); + } + + if(!exists) { + m_coll->addEntries(entry); + } + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setTotalSteps(this, files.count() + directoryFiles.count()); + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + +/* kdDebug() << "-- TAG --" << endl; + kdDebug() << "title - \"" << tag->title().to8Bit() << "\"" << endl; + kdDebug() << "artist - \"" << tag->artist().to8Bit() << "\"" << endl; + kdDebug() << "album - \"" << tag->album().to8Bit() << "\"" << endl; + kdDebug() << "year - \"" << tag->year() << "\"" << endl; + kdDebug() << "comment - \"" << tag->comment().to8Bit() << "\"" << endl; + kdDebug() << "track - \"" << tag->track() << "\"" << endl; + kdDebug() << "genre - \"" << tag->genre().to8Bit() << "\"" << endl;*/ + } + + if(m_cancelled) { + m_coll = 0; + return 0; + } + + QTextStream ts; + QRegExp iconRx(QString::fromLatin1("Icon\\s*=\\s*(.*)")); + for(QStringList::ConstIterator it = directoryFiles.begin(); !m_cancelled && it != directoryFiles.end(); ++it, ++j) { + QFile file(*it); + if(!file.open(IO_ReadOnly)) { + continue; + } + ts.unsetDevice(); + ts.setDevice(&file); + for(QString line = ts.readLine(); !line.isNull(); line = ts.readLine()) { + if(!iconRx.exactMatch(line)) { + continue; + } + QDir thisDir(*it); + thisDir.cdUp(); + QFileInfo fi(thisDir, iconRx.cap(1)); + Data::EntryPtr entry = albumMap[thisDir.dirName()]; + if(!entry) { + continue; + } + KURL u; + u.setPath(fi.absFilePath()); + QString id = ImageFactory::addImage(u, true); + if(!id.isEmpty()) { + entry->setField(QString::fromLatin1("cover"), id); + } + break; + } + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } + + if(m_cancelled) { + m_coll = 0; + return 0; + } + + return m_coll; +#endif +} + +QWidget* AudioFileImporter::widget(QWidget* parent_, const char* name_) { + if(m_widget) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QVGroupBox* box = new QVGroupBox(i18n("Audio File Options"), m_widget); + + m_recursive = new QCheckBox(i18n("Recursive &folder search"), box); + QWhatsThis::add(m_recursive, i18n("If checked, folders are recursively searched for audio files.")); + // by default, make it checked + m_recursive->setChecked(true); + + m_addFilePath = new QCheckBox(i18n("Include file &location"), box); + QWhatsThis::add(m_addFilePath, i18n("If checked, the file names for each track are added to the entries.")); + m_addFilePath->setChecked(false); + connect(m_addFilePath, SIGNAL(toggled(bool)), SLOT(slotAddFileToggled(bool))); + + m_addBitrate = new QCheckBox(i18n("Include &bitrate"), box); + QWhatsThis::add(m_addBitrate, i18n("If checked, the bitrate for each track is added to the entries.")); + m_addBitrate->setChecked(false); + m_addBitrate->setEnabled(false); + + l->addWidget(box); + l->addStretch(1); + return m_widget; +} + +// pos_ is NOT zero-indexed! +QString AudioFileImporter::insertValue(const QString& str_, const QString& value_, uint pos_) { + QStringList list = Data::Field::split(str_, true); + for(uint i = list.count(); i < pos_; ++i) { + list += QString::null; + } + if(!list[pos_-1].isNull()) { + myDebug() << "AudioFileImporter::insertValue() - overwriting track " << pos_ << endl; + myDebug() << "*** Old value: " << list[pos_-1] << endl; + myDebug() << "*** New value: " << value_ << endl; + } + list[pos_-1] = value_; + return list.join(QString::fromLatin1("; ")); +} + +void AudioFileImporter::slotCancel() { + m_cancelled = true; +} + +void AudioFileImporter::slotAddFileToggled(bool on_) { + m_addBitrate->setEnabled(on_); + if(!on_) { + m_addBitrate->setChecked(false); + } +} + +int AudioFileImporter::discNumber(const TagLib::FileRef& ref_) const { + // default to 1 unless otherwise + int num = 1; +#ifdef HAVE_TAGLIB + QString disc; + if(TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref_.file())) { + if(file->ID3v2Tag() && !file->ID3v2Tag()->frameListMap()["TPOS"].isEmpty()) { + disc = TStringToQString(file->ID3v2Tag()->frameListMap()["TPOS"].front()->toString()).stripWhiteSpace(); + } + } else if(TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(ref_.file())) { + if(file->tag() && !file->tag()->fieldListMap()["DISCNUMBER"].isEmpty()) { + disc = TStringToQString(file->tag()->fieldListMap()["DISCNUMBER"].front()).stripWhiteSpace(); + } + } else if(TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(ref_.file())) { + if(file->xiphComment() && !file->xiphComment()->fieldListMap()["DISCNUMBER"].isEmpty()) { + disc = TStringToQString(file->xiphComment()->fieldListMap()["DISCNUMBER"].front()).stripWhiteSpace(); + } + } + + if(!disc.isEmpty()) { + int pos = disc.find('/'); + int n; + bool ok; + if(pos == -1) { + n = disc.toInt(&ok); + } else { + n = disc.left(pos).toInt(&ok); + } + if(ok && n > 0) { + num = n; + } + } +#endif + return num; +} + +#include "audiofileimporter.moc" diff --git a/src/translators/audiofileimporter.h b/src/translators/audiofileimporter.h new file mode 100644 index 0000000..d9c0c9a --- /dev/null +++ b/src/translators/audiofileimporter.h @@ -0,0 +1,69 @@ +/*************************************************************************** + copyright : (C) 2004-2007 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; * + * * + ***************************************************************************/ + +#ifndef AUDIOFILEIMPORTER_H +#define AUDIOFILEIMPORTER_H + +class QCheckBox; + +#include "importer.h" +#include "../datavectors.h" + +namespace TagLib { + class FileRef; +} + +namespace Tellico { + namespace Import { + +/** + * The AudioFileImporter class takes care of importing audio files. + * + * @author Robby Stephenson + */ +class AudioFileImporter : public Importer { +Q_OBJECT + +public: + /** + */ + AudioFileImporter(const KURL& url); + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + void slotAddFileToggled(bool on); + +private: + static QString insertValue(const QString& str, const QString& value, uint pos); + + int discNumber(const TagLib::FileRef& file) const; + + Data::CollPtr m_coll; + QWidget* m_widget; + QCheckBox* m_recursive; + QCheckBox* m_addFilePath; + QCheckBox* m_addBitrate; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/bibtex-translation.xml b/src/translators/bibtex-translation.xml new file mode 100644 index 0000000..0c1bf03 --- /dev/null +++ b/src/translators/bibtex-translation.xml @@ -0,0 +1,298 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE keymap> +<!-- Some bibtex file may incorrectly place the braces. --> +<!-- The first string element should be the correct representation --> +<!-- Most of these key mappings were taken, with permission, from the --> +<!-- CharacterConversion.plist file in the Bibdesk program by Michael McCracken --> +<keymap version="1.0"> + <key char="Å"> + <string>{\AA}</string> + </key> + <key char="À"> + <string>{\`A}</string> + <string>\`{A}</string> + </key> + <key char="Â"> + <string>{\^A}</string> + <string>\^{A}</string> + </key> + <key char="Á"> + <string>{\'A}</string> + <string>\'{A}</string> + </key> + <key char="Ã"> + <string>{\~A}</string> + <string>\~{A}</string> + </key> + <key char="Ä"> + <string>{\"A}</string> + <string>\"{A}</string> + </key> + <key char="Æ"> + <string>{\AE}</string> + </key> + <key char="Ø"> + <string>{\O}</string> + </key> + <key char="à"> + <string>{\`a}</string> + <string>\`{a}</string> + </key> + <key char="á"> + <string>{\'a}</string> + <string>\'{a}</string> + </key> + <key char="â"> + <string>{\^a}</string> + <string>\^{a}</string> + </key> + <key char="ã"> + <string>{\~a}</string> + <string>\~{a}</string> + </key> + <key char="ä"> + <string>{\"a}</string> + <string>\"{a}</string> + </key> + <key char="å"> + <string>{\aa}</string> + </key> + <key char="æ"> + <string>{\ae}</string> + </key> + <key char="Ç"> + <string>{\c C}</string> + <string>\c{C}</string> + </key> + <key char="Č"> + <string>{\u C}</string> + <string>\u{C}</string> + </key> + <key char="Č"> + <string>{\v C}</string> + <string>\v{C}</string> + </key> + <key char="ç"> + <string>{\c c}</string> + <string>\c{c}</string> + </key> + <key char="ć"> + <string>{\'c}</string> + <string>\'{c}</string> + </key> + <key char="č"> + <string>{\v c}</string> + <string>\v{c}</string> + </key> + <key char="È"> + <string>{\`E}</string> + <string>\`{E}</string> + </key> + <key char="Ê"> + <string>{\^E}</string> + <string>\^{E}</string> + </key> + <key char="É"> + <string>{\'E}</string> + <string>\'{E}</string> + </key> + <key char="Ë"> + <string>{\"E}</string> + <string>\"{E}</string> + </key> + <key char="è"> + <string>{\`e}</string> + <string>\`{e}</string> + </key> + <key char="é"> + <string>{\'e}</string> + <string>\'{e}</string> + </key> + <key char="ê"> + <string>{\^e}</string> + <string>\^{e}</string> + </key> + <key char="ë"> + <string>{\"e}</string> + <string>\"{e}</string> + </key> + <key char="Î"> + <string>{\^I}</string> + <string>\^{I}</string> + </key> + <key char="Í"> + <string>{\'I}</string> + <string>\'{I}</string> + </key> + <key char="Ï"> + <string>{\"I}</string> + <string>\"{I}</string> + </key> + <key char="ì"> + <string>{\`{\i}}</string> + <string>\`{\i}</string> + </key> + <key char="í"> + <string>{\'{\i}}</string> + <string>\'{\i}</string> + </key> + <key char="î"> + <string>{\^{\i}}</string> + <string>\^{\i}</string> + </key> + <key char="ï"> + <string>{\"{\i}}</string> + <string>\"{\i}</string> + </key> + <key char="Ñ"> + <string>{\~N}</string> + <string>\~{N}</string> + </key> + <key char="ñ"> + <string>{\~n}</string> + <string>\~{n}</string> + </key> + <key char="Ó"> + <string>{\'O}</string> + <string>\'{O}</string> + </key> + <key char="Ô"> + <string>{\^O}</string> + <string>\^{O}</string> + </key> + <key char="Ø"> + <string>{\O}</string> + </key> + <key char="Ö"> + <string>{\"O}</string> + <string>\"{O}</string> + </key> + <key char="Œ"> + <string>{\OE}</string> + </key> + <key char="ò"> + <string>{\`o}</string> + <string>\`{o}</string> + </key> + <key char="ó"> + <string>{\'o}</string> + <string>\'{o}</string> + </key> + <key char="ô"> + <string>{\^o}</string> + <string>\^{o}</string> + </key> + <key char="õ"> + <string>{\~o}</string> + <string>\~{o}</string> + </key> + <key char="ö"> + <string>{\"o}</string> + <string>\"{o}</string> + </key> + <key char="œ"> + <string>{\oe}</string> + </key> + <key char="ø"> + <string>{\o}</string> + </key> + <key char="ş"> + <string>{\c s}</string> + <string>\c{s}</string> + </key> + <key char="š"> + <string>{\v s}</string> + <string>\v{s}</string> + </key> + <key char="Ţ"> + <string>{\c T}</string> + <string>\c{T}</string> + </key> + <key char="ţ"> + <string>{\c t}</string> + <string>\c{t}</string> + </key> + <key char="Ú"> + <string>{\'U}</string> + <string>\'{U}</string> + </key> + <key char="Û"> + <string>{\^U}</string> + <string>\^{U}</string> + </key> + <key char="Ü"> + <string>{\"U}</string> + <string>\"{U}</string> + </key> + <key char="ù"> + <string>{\`u}</string> + <string>\`{u}</string> + </key> + <key char="ú"> + <string>{\'u}</string> + <string>\'{u}</string> + </key> + <key char="û"> + <string>{\^u}</string> + <string>\^{u}</string> + </key> + <key char="ü"> + <string>{\"u}</string> + <string>\"{u}</string> + </key> + <key char="Ÿ"> + <string>{\"Y}</string> + <string>\"{Y}</string> + </key> + <key char="ÿ"> + <string>{\"y}</string> + <string>\"{y}</string> + </key> + <key char="Ž"> + <string>{\v Z}</string> + <string>\v{Z}</string> + </key> + <key char="ž"> + <string>{\v z}</string> + <string>\v{z}</string> + </key> + <key char="ß"> + <string>{\ss}</string> + </key> + <key char="£"> + <string>\pounds</string> + </key> + <key char="±"> + <string>$\pm$</string> + </key> + <key char="–"> + <string>--</string> + </key> + <key char="—"> + <string>---</string> + </key> + <key char="•"> + <string>*</string> + </key> + <key char="…"> + <string>{\ldots}</string> + </key> + <key char="§"> + <string>{\S}</string> + </key> + <key char="©"> + <string>{\copyright}</string> + </key> + <key char="®"> + <string>{\textregistered}</string> + </key> + <key char="™"> + <string>{\texttrademark}</string> + </key> + <key char="°"> + <string>$^{\circ}$</string> + </key> + <key char="%"> + <string>\%</string> + </key> +</keymap> diff --git a/src/translators/bibtexexporter.cpp b/src/translators/bibtexexporter.cpp new file mode 100644 index 0000000..2706ac8 --- /dev/null +++ b/src/translators/bibtexexporter.cpp @@ -0,0 +1,326 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bibtexexporter.h" +#include "bibtexhandler.h" +#include "../document.h" +#include "../collections/bibtexcollection.h" +#include "../latin1literal.h" +#include "../filehandler.h" +#include "../stringset.h" +#include "../tellico_debug.h" + +#include <config.h> + +#include <klocale.h> +#include <kdebug.h> +#include <kconfig.h> +#include <kcombobox.h> + +#include <qregexp.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qgroupbox.h> +#include <qwhatsthis.h> +#include <qlabel.h> +#include <qhbox.h> + +using Tellico::Export::BibtexExporter; + +BibtexExporter::BibtexExporter() : Tellico::Export::Exporter(), + m_expandMacros(false), + m_packageURL(true), + m_skipEmptyKeys(false), + m_widget(0) { +} + +QString BibtexExporter::formatString() const { + return i18n("Bibtex"); +} + +QString BibtexExporter::fileFilter() const { + return i18n("*.bib|Bibtex Files (*.bib)") + QChar('\n') + i18n("*|All Files"); +} + +bool BibtexExporter::exec() { + Data::CollPtr c = collection(); + if(!c || c->type() != Data::Collection::Bibtex) { + return false; + } + const Data::BibtexCollection* coll = static_cast<const Data::BibtexCollection*>(c.data()); + +// there are some special attributes +// the entry-type specifies the entry type - book, inproceedings, whatever + QString typeField; +// the key specifies the cite-key + QString keyField; +// the crossref bibtex field can reference another entry + QString crossRefField; + bool hasCrossRefs = false; + + const QString bibtex = QString::fromLatin1("bibtex"); +// keep a list of all the 'ordinary' fields to iterate through later + Data::FieldVec fields; + Data::FieldVec vec = coll->fields(); + for(Data::FieldVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + QString bibtexField = it->property(bibtex); + if(bibtexField == Latin1Literal("entry-type")) { + typeField = it->name(); + } else if(bibtexField == Latin1Literal("key")) { + keyField = it->name(); + } else if(bibtexField == Latin1Literal("crossref")) { + fields.append(it); // still output crossref field + crossRefField = it->name(); + hasCrossRefs = true; + } else if(!bibtexField.isEmpty()) { + fields.append(it); + } + } + + if(typeField.isEmpty() || keyField.isEmpty()) { + kdWarning() << "BibtexExporter::exec() - the collection must have fields defining " + "the entry-type and the key of the entry" << endl; + return false; + } + if(fields.isEmpty()) { + kdWarning() << "BibtexExporter::exec() - no bibtex field mapping exists in the collection." << endl; + return false; + } + + QString text = QString::fromLatin1("@comment{Generated by Tellico ") + + QString::fromLatin1(VERSION) + + QString::fromLatin1("}\n\n"); + + if(!coll->preamble().isEmpty()) { + text += QString::fromLatin1("@preamble{") + coll->preamble() + QString::fromLatin1("}\n\n"); + } + + const QStringList macros = coll->macroList().keys(); + if(!m_expandMacros) { + QMap<QString, QString>::ConstIterator macroIt; + for(macroIt = coll->macroList().constBegin(); macroIt != coll->macroList().constEnd(); ++macroIt) { + if(!macroIt.data().isEmpty()) { + text += QString::fromLatin1("@string{") + + macroIt.key() + + QString::fromLatin1("=") + + BibtexHandler::exportText(macroIt.data(), macros) + + QString::fromLatin1("}\n\n"); + } + } + } + + // if anything is crossref'd, we have to do an initial scan through the + // whole collection first + StringSet crossRefKeys; + if(hasCrossRefs) { + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + crossRefKeys.add(entryIt->field(crossRefField)); + } + } + + + StringSet usedKeys; + Data::ConstEntryVec crossRefs; + QString type, key, newKey, value; + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + type = entryIt->field(typeField); + if(type.isEmpty()) { + kdWarning() << "BibtexExporter::text() - the entry for '" << entryIt->title() + << "' has no entry-type, skipping it!" << endl; + continue; + } + + key = entryIt->field(keyField); + if(key.isEmpty()) { + if(m_skipEmptyKeys) { + continue; + } + key = BibtexHandler::bibtexKey(entryIt.data()); + } else { + // check crossrefs, only counts for non-empty keys + // if this entry is crossref'd, add it to the list, and skip it + if(hasCrossRefs && crossRefKeys.has(key)) { + crossRefs.append(entryIt.data()); + continue; + } + } + + newKey = key; + char c = 'a'; + while(usedKeys.has(newKey)) { + // duplicate found! + newKey = key + c; + ++c; + } + key = newKey; + usedKeys.add(key); + + writeEntryText(text, fields, *entryIt, type, key); + } + + // now write out crossrefs + for(Data::ConstEntryVec::Iterator entryIt = crossRefs.begin(); entryIt != crossRefs.end(); ++entryIt) { + // no need to check type + + key = entryIt->field(keyField); + newKey = key; + char c = 'a'; + while(usedKeys.has(newKey)) { + // duplicate found! + newKey = key + c; + ++c; + } + key = newKey; + usedKeys.add(key); + + writeEntryText(text, fields, *entryIt, entryIt->field(typeField), key); + } + + return FileHandler::writeTextURL(url(), text, options() & ExportUTF8, options() & Export::ExportForce); +} + +QWidget* BibtexExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("Bibtex Options"), m_widget); + l->addWidget(box); + + m_checkExpandMacros = new QCheckBox(i18n("Expand string macros"), box); + m_checkExpandMacros->setChecked(m_expandMacros); + QWhatsThis::add(m_checkExpandMacros, i18n("If checked, the string macros will be expanded and no " + "@string{} entries will be written.")); + + m_checkPackageURL = new QCheckBox(i18n("Use URL package"), box); + m_checkPackageURL->setChecked(m_packageURL); + QWhatsThis::add(m_checkPackageURL, i18n("If checked, any URL fields will be wrapped in a " + "\\url declaration.")); + + m_checkSkipEmpty = new QCheckBox(i18n("Skip entries with empty citation keys"), box); + m_checkSkipEmpty->setChecked(m_skipEmptyKeys); + QWhatsThis::add(m_checkSkipEmpty, i18n("If checked, any entries without a bibtex citation key " + "will be skipped.")); + + QHBox* hbox = new QHBox(box); + QLabel* l1 = new QLabel(i18n("Bibtex quotation style:") + ' ', hbox); // add a space for astheticss + m_cbBibtexStyle = new KComboBox(hbox); + m_cbBibtexStyle->insertItem(i18n("Braces")); + m_cbBibtexStyle->insertItem(i18n("Quotes")); + QString whats = i18n("<qt>The quotation style used when exporting bibtex. All field values will " + " be escaped with either braces or quotation marks.</qt>"); + QWhatsThis::add(l1, whats); + QWhatsThis::add(m_cbBibtexStyle, whats); + if(BibtexHandler::s_quoteStyle == BibtexHandler::BRACES) { + m_cbBibtexStyle->setCurrentItem(i18n("Braces")); + } else { + m_cbBibtexStyle->setCurrentItem(i18n("Quotes")); + } + + l->addStretch(1); + return m_widget; +} + +void BibtexExporter::readOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_expandMacros = group.readBoolEntry("Expand Macros", m_expandMacros); + m_packageURL = group.readBoolEntry("URL Package", m_packageURL); + m_skipEmptyKeys = group.readBoolEntry("Skip Empty Keys", m_skipEmptyKeys); + + if(group.readBoolEntry("Use Braces", true)) { + BibtexHandler::s_quoteStyle = BibtexHandler::BRACES; + } else { + BibtexHandler::s_quoteStyle = BibtexHandler::QUOTES; + } +} + +void BibtexExporter::saveOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_expandMacros = m_checkExpandMacros->isChecked(); + group.writeEntry("Expand Macros", m_expandMacros); + m_packageURL = m_checkPackageURL->isChecked(); + group.writeEntry("URL Package", m_packageURL); + m_skipEmptyKeys = m_checkSkipEmpty->isChecked(); + group.writeEntry("Skip Empty Keys", m_skipEmptyKeys); + + bool useBraces = m_cbBibtexStyle->currentText() == i18n("Braces"); + group.writeEntry("Use Braces", useBraces); + if(useBraces) { + BibtexHandler::s_quoteStyle = BibtexHandler::BRACES; + } else { + BibtexHandler::s_quoteStyle = BibtexHandler::QUOTES; + } +} + +void BibtexExporter::writeEntryText(QString& text_, const Data::FieldVec& fields_, const Data::Entry& entry_, + const QString& type_, const QString& key_) { + const QStringList macros = static_cast<const Data::BibtexCollection*>(Data::Document::self()->collection().data())->macroList().keys(); + const QString bibtex = QString::fromLatin1("bibtex"); + const QString bibtexSep = QString::fromLatin1("bibtex-separator"); + + text_ += '@' + type_ + '{' + key_; + + QString value; + Data::FieldVec::ConstIterator fIt, end = fields_.constEnd(); + bool format = options() & Export::ExportFormatted; + for(fIt = fields_.constBegin(); fIt != end; ++fIt) { + value = entry_.field(fIt->name(), format); + if(value.isEmpty()) { + continue; + } + + // If the entry is formatted as a name and allows multiple values + // insert "and" in between them (e.g. author and editor) + if(fIt->formatFlag() == Data::Field::FormatName + && fIt->flags() & Data::Field::AllowMultiple) { + value.replace(Data::Field::delimiter(), QString::fromLatin1(" and ")); + } else if(fIt->flags() & Data::Field::AllowMultiple) { + QString bibsep = fIt->property(bibtexSep); + if(!bibsep.isEmpty()) { + value.replace(Data::Field::delimiter(), bibsep); + } + } else if(fIt->type() == Data::Field::Para) { + // strip HTML from bibtex export + QRegExp stripHTML(QString::fromLatin1("<.*>"), true); + stripHTML.setMinimal(true); + value.remove(stripHTML); + } else if(fIt->property(bibtex) == Latin1Literal("pages")) { + QRegExp rx(QString::fromLatin1("(\\d)-(\\d)")); + for(int pos = rx.search(value); pos > -1; pos = rx.search(value, pos+2)) { + value.replace(pos, 3, rx.cap(1)+"--"+rx.cap(2)); + } + } + + if(m_packageURL && fIt->type() == Data::Field::URL) { + bool b = BibtexHandler::s_quoteStyle == BibtexHandler::BRACES; + value = (b ? QChar('{') : QChar('"')) + + QString::fromLatin1("\\url{") + BibtexHandler::exportText(value, macros) + QChar('}') + + (b ? QChar('}') : QChar('"')); + } else if(fIt->type() != Data::Field::Number) { + // numbers aren't escaped, nor will they have macros + // if m_expandMacros is true, then macros is empty, so this is ok even then + value = BibtexHandler::exportText(value, macros); + } + text_ += QString::fromLatin1(",\n ") + + fIt->property(bibtex) + + QString::fromLatin1(" = ") + + value; + } + text_ += QString::fromLatin1("\n}\n\n"); +} + +#include "bibtexexporter.moc" diff --git a/src/translators/bibtexexporter.h b/src/translators/bibtexexporter.h new file mode 100644 index 0000000..dccfde8 --- /dev/null +++ b/src/translators/bibtexexporter.h @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXEXPORTER_H +#define BIBTEXEXPORTER_H + +class QCheckBox; +class KComboBox; + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * The Bibtex exporter shows a list of possible Bibtex fields next to a combobox of all + * the current attributes in the collection. I had thought about the reverse - having a list + * of all the attributes, with comboboxes for each Bibtex field, but I think this way is more obvious. + * + * @author Robby Stephenson + */ +class BibtexExporter : public Exporter { +Q_OBJECT + +public: + BibtexExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual void readOptions(KConfig*); + virtual void saveOptions(KConfig*); + +private: + void writeEntryText(QString& text, const Data::FieldVec& field, const Data::Entry& entry, + const QString& type, const QString& key); + + bool m_expandMacros; + bool m_packageURL; + bool m_skipEmptyKeys; + + QWidget* m_widget; + QCheckBox* m_checkExpandMacros; + QCheckBox* m_checkPackageURL; + QCheckBox* m_checkSkipEmpty; + KComboBox* m_cbBibtexStyle; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/bibtexhandler.cpp b/src/translators/bibtexhandler.cpp new file mode 100644 index 0000000..8c88e43 --- /dev/null +++ b/src/translators/bibtexhandler.cpp @@ -0,0 +1,319 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bibtexhandler.h" +#include "../collections/bibtexcollection.h" +#include "../entry.h" +#include "../field.h" +#include "../collection.h" +#include "../document.h" +#include "../filehandler.h" +#include "../latin1literal.h" +#include "../tellico_debug.h" + +#include <kstandarddirs.h> +#include <kurl.h> +#include <kstringhandler.h> +#include <klocale.h> + +#include <qstring.h> +#include <qstringlist.h> +#include <qregexp.h> +#include <qdom.h> + +// don't add braces around capital letters by default +#define TELLICO_BIBTEX_BRACES 0 + +using Tellico::BibtexHandler; + +BibtexHandler::StringListMap* BibtexHandler::s_utf8LatexMap = 0; +BibtexHandler::QuoteStyle BibtexHandler::s_quoteStyle = BibtexHandler::BRACES; +const QRegExp BibtexHandler::s_badKeyChars(QString::fromLatin1("[^0-9a-zA-Z-]")); + +QStringList BibtexHandler::bibtexKeys(const Data::EntryVec& entries_) { + QStringList keys; + for(Data::EntryVec::ConstIterator it = entries_.begin(); it != entries_.end(); ++it) { + QString s = bibtexKey(it.data()); + if(!s.isEmpty()) { + keys << s; + } + } + return keys; +} + +QString BibtexHandler::bibtexKey(Data::ConstEntryPtr entry_) { + if(!entry_ || !entry_->collection() || entry_->collection()->type() != Data::Collection::Bibtex) { + return QString::null; + } + + const Data::BibtexCollection* c = static_cast<const Data::BibtexCollection*>(entry_->collection().data()); + Data::FieldPtr f = c->fieldByBibtexName(QString::fromLatin1("key")); + if(f) { + QString key = entry_->field(f->name()); + if(!key.isEmpty()) { + return key; + } + } + + QString author; + Data::FieldPtr authorField = c->fieldByBibtexName(QString::fromLatin1("author")); + if(authorField) { + if(authorField->flags() & Data::Field::AllowMultiple) { + // grab first author only; + QString tmp = entry_->field(authorField->name()); + author = tmp.section(';', 0, 0); + } else { + author = entry_->field(authorField->name()); + } + } + + Data::FieldPtr titleField = c->fieldByBibtexName(QString::fromLatin1("title")); + QString title; + if(titleField) { + title = entry_->field(titleField->name()); + } + + Data::FieldPtr yearField = c->fieldByBibtexName(QString::fromLatin1("year")); + QString year; + if(yearField) { + year = entry_->field(yearField->name()); + } + if(year.isEmpty()) { + year = entry_->field(QString::fromLatin1("pub_year")); + if(year.isEmpty()) { + year = entry_->field(QString::fromLatin1("cr_year")); + } + } + year = year.section(';', 0, 0); + + return bibtexKey(author, title, year); +} + +QString BibtexHandler::bibtexKey(const QString& author_, const QString& title_, const QString& year_) { + QString key; + // if no comma, take the last word + if(!author_.isEmpty()) { + if(author_.find(',') == -1) { + key += author_.section(' ', -1).lower() + '-'; + } else { + // if there is a comma, take the string up to the first comma + key += author_.section(',', 0, 0).lower() + '-'; + } + } + QStringList words = QStringList::split(' ', title_); + for(QStringList::ConstIterator it = words.begin(); it != words.end(); ++it) { + key += (*it).left(1).lower(); + } + key += year_; + // bibtex key may only contain [0-9a-zA-Z-] + return key.replace(s_badKeyChars, QString::null); +} + +void BibtexHandler::loadTranslationMaps() { + QString mapfile = locate("appdata", QString::fromLatin1("bibtex-translation.xml")); + if(mapfile.isEmpty()) { + return; + } + + s_utf8LatexMap = new StringListMap(); + + KURL u; + u.setPath(mapfile); + // no namespace processing + QDomDocument dom = FileHandler::readXMLFile(u, false); + + QDomNodeList keyList = dom.elementsByTagName(QString::fromLatin1("key")); + + for(unsigned i = 0; i < keyList.count(); ++i) { + QDomNodeList strList = keyList.item(i).toElement().elementsByTagName(QString::fromLatin1("string")); + // the strList might have more than one node since there are multiple ways + // to represent a character in LaTex. + QString s = keyList.item(i).toElement().attribute(QString::fromLatin1("char")); + for(unsigned j = 0; j < strList.count(); ++j) { + (*s_utf8LatexMap)[s].append(strList.item(j).toElement().text()); +// kdDebug() << "BibtexHandler::loadTranslationMaps - " +// << s << " = " << strList.item(j).toElement().text() << endl; + } + } +} + +QString BibtexHandler::importText(char* text_) { + if(!s_utf8LatexMap) { + loadTranslationMaps(); + } + + QString str = QString::fromUtf8(text_); + for(StringListMap::Iterator it = s_utf8LatexMap->begin(); it != s_utf8LatexMap->end(); ++it) { + for(QStringList::Iterator sit = it.data().begin(); sit != it.data().end(); ++sit) { + str.replace(*sit, it.key()); + } + } + + // now replace capitalized letters, such as {X} + // but since we don't want to turn "... X" into "... {X}" later when exporting + // we need to lower-case any capitalized text after the first letter that is + // NOT contained in braces + + QRegExp rx(QString::fromLatin1("\\{([A-Z]+)\\}")); + rx.setMinimal(true); + str.replace(rx, QString::fromLatin1("\\1")); + + return str; +} + +QString BibtexHandler::exportText(const QString& text_, const QStringList& macros_) { + if(!s_utf8LatexMap) { + loadTranslationMaps(); + } + + QChar lquote, rquote; + switch(s_quoteStyle) { + case BRACES: + lquote = '{'; + rquote = '}'; + break; + case QUOTES: + lquote = '"'; + rquote = '"'; + break; + } + + QString text = text_; + + for(StringListMap::Iterator it = s_utf8LatexMap->begin(); it != s_utf8LatexMap->end(); ++it) { + text.replace(it.key(), it.data()[0]); + } + + if(macros_.isEmpty()) { + return lquote + addBraces(text) + rquote; + } + +// Now, split the text by the character '#', and examine each token to see if it is in +// the macro list. If it is not, then add left-quote and right-quote around it. If it is, don't +// change it. Then, in case '#' occurs in a non-macro string, replace any occurrences of '}#{' with '#' + +// list of new tokens + QStringList list; + +// first, split the text + QStringList tokens = QStringList::split('#', text, true); + for(QStringList::Iterator it = tokens.begin(); it != tokens.end(); ++it) { + // check to see if token is a macro + if(macros_.findIndex((*it).stripWhiteSpace()) == -1) { + // the token is NOT a macro, add braces around whole words and also around capitals + list << lquote + addBraces(*it) + rquote; + } else { + list << *it; + } + } + + const QChar octo = '#'; + text = list.join(octo); + text.replace(QString(rquote)+octo+lquote, octo); + + return text; +} + +bool BibtexHandler::setFieldValue(Data::EntryPtr entry_, const QString& bibtexField_, const QString& value_) { + Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(entry_->collection().data()); + Data::FieldPtr field = c->fieldByBibtexName(bibtexField_); + if(!field) { + // it was the case that the default bibliography did not have a bibtex property for keywords + // so a "keywords" field would get created in the imported collection + // but the existing collection had a field "keyword" so the values would not get imported + // here, check to see if the current collection has a field with the same bibtex name and + // use it instead of creating a new one + Data::BibtexCollection* existingColl = Data::Document::self()->collection()->type() == Data::Collection::Bibtex + ? static_cast<Data::BibtexCollection*>(Data::Document::self()->collection().data()) + : 0; + Data::FieldPtr existingField = existingColl ? existingColl->fieldByBibtexName(bibtexField_) : 0; + if(existingField) { + field = new Data::Field(*existingField); + } else if(value_.length() < 100) { + // arbitrarily say if the value has more than 100 chars, then it's a paragraph + QString vlower = value_.lower(); + // special case, try to detect URLs + // In qt 3.1, QString::startsWith() is always case-sensitive + if(bibtexField_ == Latin1Literal("url") + || vlower.startsWith(QString::fromLatin1("http")) // may also be https + || vlower.startsWith(QString::fromLatin1("ftp:/")) + || vlower.startsWith(QString::fromLatin1("file:/")) + || vlower.startsWith(QString::fromLatin1("/"))) { // assume this indicates a local path + myDebug() << "BibtexHandler::setFieldValue() - creating a URL field for " << bibtexField_ << endl; + field = new Data::Field(bibtexField_, KStringHandler::capwords(bibtexField_), Data::Field::URL); + } else { + field = new Data::Field(bibtexField_, KStringHandler::capwords(bibtexField_), Data::Field::Line); + } + field->setCategory(i18n("Unknown")); + } else { + field = new Data::Field(bibtexField_, KStringHandler::capwords(bibtexField_), Data::Field::Para); + } + field->setProperty(QString::fromLatin1("bibtex"), bibtexField_); + c->addField(field); + } + // special case keywords, replace commas with semi-colons so they get separated + QString value = value_; + if(field->property(QString::fromLatin1("bibtex")).startsWith(QString::fromLatin1("keyword"))) { + value.replace(',', ';'); + // special case refbase bibtex export, with multiple keywords fields + QString oValue = entry_->field(field); + if(!oValue.isEmpty()) { + value = oValue + "; " + value; + } + } + return entry_->setField(field, value); +} + +QString& BibtexHandler::cleanText(QString& text_) { + // FIXME: need to improve this for removing all Latex entities +// QRegExp rx(QString::fromLatin1("(?=[^\\\\])\\\\.+\\{")); + QRegExp rx(QString::fromLatin1("\\\\.+\\{")); + rx.setMinimal(true); + text_.replace(rx, QString::null); + text_.replace(QRegExp(QString::fromLatin1("[{}]")), QString::null); + text_.replace('~', ' '); + return text_; +} + +// add braces around capital letters +QString& BibtexHandler::addBraces(QString& text) { +#if !TELLICO_BIBTEX_BRACES + return text; +#else + int inside = 0; + uint l = text.length(); + // start at first letter, but skip if only the first is capitalized + for(uint i = 0; i < l; ++i) { + const QChar c = text.at(i); + if(inside == 0 && c >= 'A' && c <= 'Z') { + uint j = i+1; + while(text.at(j) >= 'A' && text.at(j) <= 'Z' && j < l) { + ++j; + } + if(i == 0 && j == 1) { + continue; // no need to do anything to first letter + } + text.insert(i, '{'); + // now j should be incremented + text.insert(j+1, '}'); + i = j+1; + l += 2; // the length changed + } else if(c == '{') { + ++inside; + } else if(c == '}') { + --inside; + } + } + return text; +#endif +} diff --git a/src/translators/bibtexhandler.h b/src/translators/bibtexhandler.h new file mode 100644 index 0000000..87d8bf0 --- /dev/null +++ b/src/translators/bibtexhandler.h @@ -0,0 +1,60 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXHANDLER_H +#define BIBTEXHANDLER_H + +class QString; +class QStringList; +class QRegExp; + +#include "../datavectors.h" + +#include <qmap.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class BibtexHandler { +public: + enum QuoteStyle { BRACES=0, QUOTES=1 }; + static QStringList bibtexKeys(const Data::EntryVec& entries); + static QString bibtexKey(Data::ConstEntryPtr entry); + static QString importText(char* text); + static QString exportText(const QString& text, const QStringList& macros); + static bool setFieldValue(Data::EntryPtr entry, const QString& bibtexField, const QString& value); + /** + * Strips the text of all vestiges of LaTeX. + * + * @param text A reference to the text + * @return A reference to the text + */ + static QString& cleanText(QString& text); + + static QuoteStyle s_quoteStyle; + +private: + typedef QMap<QString, QStringList> StringListMap; + + static QString bibtexKey(const QString& author, const QString& title, const QString& year); + static void loadTranslationMaps(); + static QString& addBraces(QString& string); + + static StringListMap* s_utf8LatexMap; + static const QRegExp s_badKeyChars; +}; + +} // end namespace +#endif diff --git a/src/translators/bibteximporter.cpp b/src/translators/bibteximporter.cpp new file mode 100644 index 0000000..2e514d3 --- /dev/null +++ b/src/translators/bibteximporter.cpp @@ -0,0 +1,312 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bibteximporter.h" +#include "bibtexhandler.h" +#include "../collections/bibtexcollection.h" +#include "../entry.h" +#include "../latin1literal.h" +#include "../progressmanager.h" +#include "../filehandler.h" +#include "../tellico_debug.h" + +#include <kapplication.h> +#include <kconfig.h> + +#include <qptrlist.h> +#include <qregexp.h> +#include <qlayout.h> +#include <qvbuttongroup.h> +#include <qradiobutton.h> +#include <qwhatsthis.h> +#include <qtextcodec.h> + +using Tellico::Import::BibtexImporter; + +BibtexImporter::BibtexImporter(const KURL::List& urls_) : Importer(urls_) + , m_coll(0), m_widget(0), m_readUTF8(0), m_readLocale(0), m_cancelled(false) { + bt_initialize(); +} + +BibtexImporter::BibtexImporter(const QString& text_) : Importer(text_) + , m_coll(0), m_widget(0), m_readUTF8(0), m_readLocale(0), m_cancelled(false) { + bt_initialize(); +} + +BibtexImporter::~BibtexImporter() { + bt_cleanup(); + if(m_readUTF8) { + KConfigGroup config(kapp->config(), "Import Options"); + config.writeEntry("Bibtex UTF8", m_readUTF8->isChecked()); + } +} + +bool BibtexImporter::canImport(int type) const { + return type == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr BibtexImporter::collection() { + if(m_coll) { + return m_coll; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(urls().count() * 100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + bool useUTF8 = m_widget && m_readUTF8->isChecked(); + + m_coll = new Data::BibtexCollection(true); + + int count = 0; + // might be importing text only + if(!text().isEmpty()) { + QString text = this->text(); + Data::CollPtr coll = readCollection(text, count); + if(!coll || coll->entryCount() == 0) { + setStatusMessage(i18n("No valid bibtex entries were found")); + } else { + m_coll->addEntries(coll->entries()); + } + } + + KURL::List urls = this->urls(); + for(KURL::List::ConstIterator it = urls.begin(); it != urls.end(); ++it, ++count) { + if(m_cancelled) { + return 0; + } + if(!(*it).isValid()) { + continue; + } + QString text = FileHandler::readTextFile(*it, false, useUTF8); + if(text.isEmpty()) { + continue; + } + Data::CollPtr coll = readCollection(text, count); + if(!coll || coll->entryCount() == 0) { + setStatusMessage(i18n("No valid bibtex entries were found in file - %1").arg(url().fileName())); + continue; + } + m_coll->addEntries(coll->entries()); + } + + if(m_cancelled) { + return 0; + } + + return m_coll; +} + +Tellico::Data::CollPtr BibtexImporter::readCollection(const QString& text, int n) { + if(text.isEmpty()) { + myDebug() << "BibtexImporter::readCollection() - no text" << endl; + return 0; + } + Data::CollPtr ptr = new Data::BibtexCollection(true); + Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(ptr.data()); + + parseText(text); // populates m_nodes + if(m_cancelled) { + return 0; + } + + if(m_nodes.isEmpty()) { + return 0; + } + + QString str; + const uint count = m_nodes.count(); + const uint stepSize = QMAX(s_stepSize, count/100); + const bool showProgress = options() & ImportProgress; + + uint j = 0; + for(ASTListIterator it(m_nodes); !m_cancelled && it.current(); ++it, ++j) { + // if we're parsing a macro string, comment or preamble, skip it for now + if(bt_entry_metatype(it.current()) == BTE_PREAMBLE) { + char* preamble = bt_get_text(it.current()); + if(preamble) { + c->setPreamble(QString::fromUtf8(preamble)); + } + continue; + } + + if(bt_entry_metatype(it.current()) == BTE_MACRODEF) { + char* macro; + (void) bt_next_field(it.current(), 0, ¯o); + // FIXME: replace macros within macro definitions! + // lookup lowercase macro in map + c->addMacro(m_macros[QString::fromUtf8(macro)], QString::fromUtf8(bt_macro_text(macro, 0, 0))); + continue; + } + + if(bt_entry_metatype(it.current()) == BTE_COMMENT) { + continue; + } + + // now we're parsing a regular entry + Data::EntryPtr entry = new Data::Entry(ptr); + + str = QString::fromUtf8(bt_entry_type(it.current())); +// kdDebug() << "entry type: " << str << endl; + // text is automatically put into lower-case by btparse + BibtexHandler::setFieldValue(entry, QString::fromLatin1("entry-type"), str); + + str = QString::fromUtf8(bt_entry_key(it.current())); +// kdDebug() << "entry key: " << str << endl; + BibtexHandler::setFieldValue(entry, QString::fromLatin1("key"), str); + + char* name; + AST* field = 0; + while((field = bt_next_field(it.current(), field, &name))) { +// kdDebug() << "\tfound: " << name << endl; +// str = QString::fromLatin1(bt_get_text(field)); + str.truncate(0); + AST* value = 0; + bt_nodetype type; + char* svalue; + bool end_macro = false; + while((value = bt_next_value(field, value, &type, &svalue))) { + switch(type) { + case BTAST_STRING: + case BTAST_NUMBER: + str += BibtexHandler::importText(svalue).simplifyWhiteSpace(); + end_macro = false; + break; + case BTAST_MACRO: + str += QString::fromUtf8(svalue) + '#'; + end_macro = true; + break; + default: + break; + } + } + if(end_macro) { + // remove last character '#' + str.truncate(str.length() - 1); + } + QString fieldName = QString::fromUtf8(name); + if(fieldName == Latin1Literal("author") || fieldName == Latin1Literal("editor")) { + str.replace(QRegExp(QString::fromLatin1("\\sand\\s")), QString::fromLatin1("; ")); + } + BibtexHandler::setFieldValue(entry, fieldName, str); + } + + ptr->addEntries(entry); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, n*100 + 100*j/count); + kapp->processEvents(); + } + } + + if(m_cancelled) { + ptr = 0; + } + + // clean-up + for(ASTListIterator it(m_nodes); it.current(); ++it) { + bt_free_ast(it.current()); + } + + return ptr; +} + +void BibtexImporter::parseText(const QString& text) { + m_nodes.clear(); + m_macros.clear(); + + ushort bt_options = 0; // ushort is defined in btparse.h + boolean ok; // boolean is defined in btparse.h as an int + + // for regular nodes (entries), do NOT convert numbers to strings, do NOT expand macros + bt_set_stringopts(BTE_REGULAR, 0); + bt_set_stringopts(BTE_MACRODEF, 0); +// bt_set_stringopts(BTE_PREAMBLE, BTO_CONVERT | BTO_EXPAND); + + QString entry; + QRegExp rx(QString::fromLatin1("[{}]")); + QRegExp macroName(QString::fromLatin1("@string\\s*\\{\\s*(.*)="), false /*case sensitive*/); + macroName.setMinimal(true); + + bool needsCleanup = false; + int brace = 0; + int startpos = 0; + int pos = text.find(rx, 0); + while(pos > 0 && !m_cancelled) { + if(text[pos] == '{') { + ++brace; + } else if(text[pos] == '}' && brace > 0) { + --brace; + } + if(brace == 0) { + entry = text.mid(startpos, pos-startpos+1).stripWhiteSpace(); + // All the downstream text processing on the AST node will assume utf-8 + AST* node = bt_parse_entry_s(const_cast<char*>(entry.utf8().data()), + const_cast<char*>(url().fileName().local8Bit().data()), + 0, bt_options, &ok); + if(ok && node) { + if(bt_entry_metatype(node) == BTE_MACRODEF && macroName.search(entry) > -1) { + char* macro; + (void) bt_next_field(node, 0, ¯o); + m_macros.insert(QString::fromUtf8(macro), macroName.cap(1).stripWhiteSpace()); + } + m_nodes.append(node); + needsCleanup = true; + } + startpos = pos+1; + } + pos = text.find(rx, pos+1); + } + if(needsCleanup) { + // clean up some structures + bt_parse_entry_s(0, 0, 1, 0, 0); + } +} + +void BibtexImporter::slotCancel() { + m_cancelled = true; +} + +QWidget* BibtexImporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QButtonGroup* box = new QVButtonGroup(i18n("Bibtex Options"), m_widget); + m_readUTF8 = new QRadioButton(i18n("Use Unicode (UTF-8) encoding"), box); + QWhatsThis::add(m_readUTF8, i18n("Read the imported file in Unicode (UTF-8).")); + QString localStr = i18n("Use user locale (%1) encoding").arg( + QString::fromLatin1(QTextCodec::codecForLocale()->name())); + m_readLocale = new QRadioButton(localStr, box); + m_readLocale->setChecked(true); + QWhatsThis::add(m_readLocale, i18n("Read the imported file in the local encoding.")); + + KConfigGroup config(kapp->config(), "Import Options"); + bool useUTF8 = config.readBoolEntry("Bibtex UTF8", false); + if(useUTF8) { + m_readUTF8->setChecked(true); + } else { + m_readLocale->setChecked(true); + } + + l->addWidget(box); + l->addStretch(1); + return m_widget; +} + + +#include "bibteximporter.moc" diff --git a/src/translators/bibteximporter.h b/src/translators/bibteximporter.h new file mode 100644 index 0000000..c17195b --- /dev/null +++ b/src/translators/bibteximporter.h @@ -0,0 +1,90 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXIMPORTER_H +#define BIBTEXIMPORTER_H + +#include <config.h> +#include "importer.h" +#include "../datavectors.h" + +extern "C" { +#ifdef HAVE_LIBBTPARSE +#include <btparse.h> +#else +#include "btparse/btparse.h" +} +#endif + +#include <qptrlist.h> +#include <qmap.h> + +class QRadioButton; + +namespace Tellico { + namespace Import { + +/** + * Bibtex files are used for bibliographies within LaTex. The btparse library is used to + * parse the text and generate a @ref BibtexCollection. + * + * @author Robby Stephenson + */ +class BibtexImporter : public Importer { +Q_OBJECT + +public: + /** + * Initializes the btparse library + * + * @param url The url of the bibtex file + */ + BibtexImporter(const KURL::List& urls); + BibtexImporter(const QString& text); + /* + * Some cleanup is done for the btparse library + */ + virtual ~BibtexImporter(); + + /** + * Returns a pointer to a @ref BibtexCollection created on the stack. All entries + * in the bibtex file are added, including any preamble, all macro strings, and each entry. + * + * @return A pointer to a @ref BibtexCollection, or 0 if none can be created. + */ + virtual Data::CollPtr collection(); + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + Data::CollPtr readCollection(const QString& text, int n); + void parseText(const QString& text); + + typedef QPtrList<AST> ASTList; + typedef QPtrListIterator<AST> ASTListIterator; + ASTList m_nodes; + QMap<QString, QString> m_macros; + + Data::CollPtr m_coll; + QWidget* m_widget; + QRadioButton* m_readUTF8; + QRadioButton* m_readLocale; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/bibtexmlexporter.cpp b/src/translators/bibtexmlexporter.cpp new file mode 100644 index 0000000..4a0a4d3 --- /dev/null +++ b/src/translators/bibtexmlexporter.cpp @@ -0,0 +1,182 @@ +/************************************************************************* + copyright : (C) 2003-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 <config.h> + +#include "bibtexmlexporter.h" +#include "bibtexhandler.h" +#include "../document.h" +#include "../collections/bibtexcollection.h" +#include "../latin1literal.h" +#include "../filehandler.h" +#include "tellico_xml.h" +#include "../stringset.h" + +#include <klocale.h> +#include <kdebug.h> + +#include <qvbox.h> +#include <qdom.h> +#include <qregexp.h> +#include <qtextcodec.h> + +using Tellico::Export::BibtexmlExporter; + +QString BibtexmlExporter::formatString() const { + return i18n("Bibtexml"); +} + +QString BibtexmlExporter::fileFilter() const { + return i18n("*.xml|Bibtexml Files (*.xml)") + QChar('\n') + i18n("*|All Files"); +} + +bool BibtexmlExporter::exec() { + Data::CollPtr c = collection(); + if(!c || c->type() != Data::Collection::Bibtex) { + return false; + } + const Data::BibtexCollection* coll = static_cast<const Data::BibtexCollection*>(c.data()); + +// there are some special fields +// the entry-type specifies the entry type - book, inproceedings, whatever + QString typeField; +// the key specifies the cite-key + QString keyField; + + const QString bibtex = QString::fromLatin1("bibtex"); +// keep a list of all the 'ordinary' fields to iterate through later + Data::FieldVec fields; + Data::FieldVec vec = coll->fields(); + for(Data::FieldVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + QString bibtexField = it->property(bibtex); + if(bibtexField == Latin1Literal("entry-type")) { + typeField = it->name(); + } else if(bibtexField == Latin1Literal("key")) { + keyField = it->name(); + } else if(!bibtexField.isEmpty()) { + fields.append(it); + } + } + + QDomImplementation impl; + QDomDocumentType doctype = impl.createDocumentType(QString::fromLatin1("file"), + QString::null, + XML::dtdBibtexml); + //default namespace + const QString& ns = XML::nsBibtexml; + + QDomDocument dom = impl.createDocument(ns, QString::fromLatin1("file"), doctype); + + // root element + QDomElement root = dom.documentElement(); + + QString encodeStr = QString::fromLatin1("version=\"1.0\" encoding=\""); + if(options() & Export::ExportUTF8) { + encodeStr += QString::fromLatin1("UTF-8"); + } else { + encodeStr += QString::fromLatin1(QTextCodec::codecForLocale()->mimeName()); + } + encodeStr += '"'; + + // createDocument creates a root node, insert the processing instruction before it + dom.insertBefore(dom.createProcessingInstruction(QString::fromLatin1("xml"), encodeStr), root); + QString comment = QString::fromLatin1("Generated by Tellico ") + QString::fromLatin1(VERSION); + dom.insertBefore(dom.createComment(comment), root); + + Data::ConstFieldPtr field; + Data::FieldVec::ConstIterator fIt, end = fields.constEnd(); + bool format = options() & Export::ExportFormatted; + + StringSet usedKeys; + QString type, key, newKey, value, elemName, parElemName; + QDomElement btElem, entryElem, parentElem, fieldElem; + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + key = entryIt->field(keyField); + if(key.isEmpty()) { + key = BibtexHandler::bibtexKey(entryIt.data()); + } + QString newKey = key; + char c = 'a'; + while(usedKeys.has(newKey)) { + // duplicate found! + newKey = key + c; + ++c; + } + key = newKey; + usedKeys.add(key); + + btElem = dom.createElement(QString::fromLatin1("entry")); + btElem.setAttribute(QString::fromLatin1("id"), key); + root.appendChild(btElem); + + type = entryIt->field(typeField); + if(type.isEmpty()) { + kdWarning() << "BibtexmlExporter::exec() - the entry for '" << entryIt->title() + << "' has no entry-type, skipping it!" << endl; + continue; + } + + entryElem = dom.createElement(type); + btElem.appendChild(entryElem); + + // now iterate over attributes + for(fIt = fields.constBegin(); fIt != end; ++fIt) { + field = fIt.data(); + value = entryIt->field(field->name(), format); + if(value.isEmpty()) { + continue; + } + +/* Bibtexml has special container elements for titles, authors, editors, and keywords + I'm going to ignore the titlelist element for right now. All authors are contained in + an authorlist element, editors in an editorlist element, and keywords are in a + keywords element, and themselves as a keyword. Also, Bibtexml can format names + similar to docbook, with first, middle, last, etc elements. I'm going to ignore that + for now, too.*/ + elemName = field->property(bibtex); + // split text for author, editor, and keywords + if(elemName == Latin1Literal("author") || + elemName == Latin1Literal("editor") || + elemName == Latin1Literal("keywords")) { + if(elemName == Latin1Literal("author")) { + parElemName = QString::fromLatin1("authorlist"); + } else if(elemName == Latin1Literal("editor")) { + parElemName = QString::fromLatin1("editorlist"); + } else { // keywords + parElemName = QString::fromLatin1("keywords"); + elemName = QString::fromLatin1("keyword"); + } + + parentElem = dom.createElement(parElemName); + const QStringList values = entryIt->fields(field->name(), false); + for(QStringList::ConstIterator it = values.begin(); it != values.end(); ++it) { + fieldElem = dom.createElement(elemName); + fieldElem.appendChild(dom.createTextNode(*it)); + parentElem.appendChild(fieldElem); + } + if(parentElem.hasChildNodes()) { + entryElem.appendChild(parentElem); + } + } else { + fieldElem = dom.createElement(elemName); + fieldElem.appendChild(dom.createTextNode(value)); + entryElem.appendChild(fieldElem); + } + } + } + + return FileHandler::writeTextURL(url(), dom.toString(), + options() & ExportUTF8, options() & Export::ExportForce); +} + +#include "bibtexmlexporter.moc" diff --git a/src/translators/bibtexmlexporter.h b/src/translators/bibtexmlexporter.h new file mode 100644 index 0000000..8f63a55 --- /dev/null +++ b/src/translators/bibtexmlexporter.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXMLEXPORTER_H +#define BIBTEXMLEXPORTER_H + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class BibtexmlExporter : public Exporter { +Q_OBJECT + +public: + BibtexmlExporter() : Exporter() {} + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + // no options + virtual QWidget* widget(QWidget*, const char*) { return 0; } +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/bibtexmlimporter.cpp b/src/translators/bibtexmlimporter.cpp new file mode 100644 index 0000000..2feb2f2 --- /dev/null +++ b/src/translators/bibtexmlimporter.cpp @@ -0,0 +1,163 @@ +/*************************************************************************** + copyright : (C) 2003-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 "bibtexmlimporter.h" +#include "tellico_xml.h" +#include "bibtexhandler.h" +#include "../collections/bibtexcollection.h" +#include "../field.h" +#include "../entry.h" +#include "../latin1literal.h" +#include "../tellico_strings.h" +#include "../progressmanager.h" +#include "../tellico_debug.h" + +#include <kapplication.h> + +using Tellico::Import::BibtexmlImporter; + +bool BibtexmlImporter::canImport(int type) const { + return type == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr BibtexmlImporter::collection() { + if(!m_coll) { + loadDomDocument(); + } + return m_coll; +} + +void BibtexmlImporter::loadDomDocument() { + QDomElement root = domDocument().documentElement(); + if(root.isNull() || root.localName() != Latin1Literal("file")) { + setStatusMessage(i18n(errorLoad).arg(url().fileName())); + return; + } + + const QString& ns = XML::nsBibtexml; + m_coll = new Data::BibtexCollection(true); + + QDomNodeList entryelems = root.elementsByTagNameNS(ns, QString::fromLatin1("entry")); +// kdDebug() << "BibtexmlImporter::loadDomDocument - found " << entryelems.count() << " entries" << endl; + + const uint count = entryelems.count(); + const uint stepSize = QMAX(s_stepSize, count/100); + const bool showProgress = options() & ImportProgress; + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(count); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + for(uint j = 0; !m_cancelled && j < entryelems.count(); ++j) { + readEntry(entryelems.item(j)); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } // end entry loop +} + +void BibtexmlImporter::readEntry(const QDomNode& entryNode_) { + QDomNode node = const_cast<QDomNode&>(entryNode_); + + Data::EntryPtr entry = new Data::Entry(m_coll); + +/* The Bibtexml format looks like + <entry id="..."> + <book> + <authorlist> + <author>...</author> + </authorlist> + <publisher>...</publisher> */ + + QString type = node.firstChild().toElement().tagName(); + entry->setField(QString::fromLatin1("entry-type"), type); + QString id = node.toElement().attribute(QString::fromLatin1("id")); + entry->setField(QString::fromLatin1("bibtex-key"), id); + + QString name, value; + // field values are first child of first child of entry node + for(QDomNode n = node.firstChild().firstChild(); !n.isNull(); n = n.nextSibling()) { + // n could be something like authorlist, with multiple authors, or just + // a plain element with a single text child... + // second case first + if(n.firstChild().isText()) { + name = n.toElement().tagName(); + value = n.toElement().text(); + } else { + // is either titlelist, authorlist, editorlist, or keywords + QString parName = n.toElement().tagName(); + if(parName == Latin1Literal("titlelist")) { + for(QDomNode n2 = node.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { + name = n2.toElement().tagName(); + value = n2.toElement().text(); + if(!name.isEmpty() && !value.isEmpty()) { + BibtexHandler::setFieldValue(entry, name, value.simplifyWhiteSpace()); + } + } + name.truncate(0); + value.truncate(0); + } else { + name = n.firstChild().toElement().tagName(); + if(name == Latin1Literal("keyword")) { + name = QString::fromLatin1("keywords"); + } + value.truncate(0); + for(QDomNode n2 = n.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { + // n2 could have first, middle, lastname elements... + if(name == Latin1Literal("person")) { + QStringList names; + names << QString::fromLatin1("initials") << QString::fromLatin1("first") + << QString::fromLatin1("middle") << QString::fromLatin1("prelast") + << QString::fromLatin1("last") << QString::fromLatin1("lineage"); + for(QStringList::ConstIterator it = names.begin(); it != names.end(); ++it) { + QDomNodeList list = n2.toElement().elementsByTagName(*it); + if(list.count() > 1) { + value += list.item(0).toElement().text(); + } + if(*it != names.last()) { + value += QString::fromLatin1(" "); + } + } + } + for(QDomNode n3 = n2.firstChild(); !n3.isNull(); n3 = n3.nextSibling()) { + if(n3.isElement()) { + value += n3.toElement().text(); + } else if(n3.isText()) { + value += n3.toText().data(); + } + if(n3 != n2.lastChild()) { + value += QString::fromLatin1(" "); + } + } + if(n2 != n.lastChild()) { + value += QString::fromLatin1("; "); + } + } + } + } + if(!name.isEmpty() && !value.isEmpty()) { + BibtexHandler::setFieldValue(entry, name, value.simplifyWhiteSpace()); + } + } + + m_coll->addEntries(entry); +} + +void BibtexmlImporter::slotCancel() { + m_cancelled = true; +} + +#include "bibtexmlimporter.moc" diff --git a/src/translators/bibtexmlimporter.h b/src/translators/bibtexmlimporter.h new file mode 100644 index 0000000..826ea30 --- /dev/null +++ b/src/translators/bibtexmlimporter.h @@ -0,0 +1,54 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef BIBTEXMLIMPORTER_H +#define BIBTEXMLIMPORTER_H + +#include "xmlimporter.h" +#include "../datavectors.h" + +class QDomNode; + +namespace Tellico { + namespace Import { + +/** + *@author Robby Stephenson + */ +class BibtexmlImporter : public XMLImporter { +Q_OBJECT + +public: + /** + */ + BibtexmlImporter(const KURL& url) : Import::XMLImporter(url), m_coll(0), m_cancelled(false) {} + + /** + */ + virtual Data::CollPtr collection(); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + void loadDomDocument(); + void readEntry(const QDomNode& entryNode); + + Data::CollPtr m_coll; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/btparse/Makefile.am b/src/translators/btparse/Makefile.am new file mode 100644 index 0000000..84af63b --- /dev/null +++ b/src/translators/btparse/Makefile.am @@ -0,0 +1,18 @@ +####### kdevelop will overwrite this part!!! (begin)########## +if !USE_LIBBTPARSE + +noinst_LIBRARIES = libbtparse.a + +AM_CPPFLAGS = $(all_includes) + +libbtparse_a_METASOURCES = AUTO + +libbtparse_a_SOURCES = bibtex_ast.c bibtex.c err.c ast.c scan.c util.c lex_auxiliary.c parse_auxiliary.c format_name.c string_util.c tex_tree.c names.c modify.c traversal.c sym.c macros.c error.c postprocess.c input.c init.c + +endif + +EXTRA_DIST = btparse.h init.c stdpccts.h attrib.h lex_auxiliary.h error.h parse_auxiliary.h prototypes.h tokens.h mode.h input.c postprocess.c error.c macros.c sym.h sym.c bt_debug.h traversal.c modify.c names.c my_alloca.h tex_tree.c string_util.c format_name.c antlr.h ast.h btconfig.h dlgdef.h parse_auxiliary.c lex_auxiliary.c util.c scan.c dlgauto.h ast.c err.h err.c bibtex.c bibtex_ast.c + +####### kdevelop will overwrite this part!!! (end)############ + +KDE_OPTIONS = noautodist diff --git a/src/translators/btparse/antlr.h b/src/translators/btparse/antlr.h new file mode 100644 index 0000000..f52aba6 --- /dev/null +++ b/src/translators/btparse/antlr.h @@ -0,0 +1,561 @@ +/* antlr.h + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * ANTLR 1.33 + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ +#ifndef ANTLR_H +#define ANTLR_H + +#include "btconfig.h" + +/* + * Define all of the stack setup and manipulation of $i, #i variables. + * + * Notes: + * The type 'Attrib' must be defined before entry into this .h file. + */ + +#include <stdlib.h> +#include <string.h> + +typedef int ANTLRTokenType; +typedef unsigned char SetWordType; + +typedef char ANTLRChar; + + /* G u e s s S t u f f */ + +#ifdef ZZCAN_GUESS +#ifndef ZZINF_LOOK +#define ZZINF_LOOK +#endif +#endif + +#ifdef ZZCAN_GUESS +typedef struct _zzjmp_buf { + jmp_buf state; + } zzjmp_buf; +#endif + + +/* can make this a power of 2 for more efficient lookup */ +#ifndef ZZLEXBUFSIZE +#define ZZLEXBUFSIZE 2000 +#endif + +#define zzOvfChk \ + if ( zzasp <= 0 ) \ + { \ + fprintf(stderr, zzStackOvfMsg, __FILE__, __LINE__); \ + exit(PCCTS_EXIT_FAILURE); \ + } + +#ifndef ZZA_STACKSIZE +#define ZZA_STACKSIZE 400 +#endif +#ifndef ZZAST_STACKSIZE +#define ZZAST_STACKSIZE 400 +#endif + +#ifndef zzfailed_pred +#define zzfailed_pred(_p) \ + fprintf(stderr, "semantic error; failed predicate: '%s'\n",_p) +#endif + +#ifdef LL_K +#define LOOKAHEAD \ + int zztokenLA[LL_K]; \ + char zztextLA[LL_K][ZZLEXBUFSIZE]; \ + int zzlap = 0, zzlabase=0; /* labase only used for DEMAND_LOOK */ +#else +#define LOOKAHEAD \ + int zztoken; +#endif + +#ifndef zzcr_ast +#define zzcr_ast(ast,attr,tok,text) +#endif + +#ifdef DEMAND_LOOK +#define DemandLookData int zzdirty=1; +#else +#define DemandLookData +#endif + + /* S t a t e S t u f f */ + +#ifdef ZZCAN_GUESS +#define zzGUESS_BLOCK zzantlr_state zzst; int zzrv; +#define zzGUESS zzsave_antlr_state(&zzst); \ + zzguessing = 1; \ + zzrv = setjmp(zzguess_start.state); +#define zzGUESS_FAIL longjmp(zzguess_start.state, 1) +#define zzGUESS_DONE zzrestore_antlr_state(&zzst); +#define zzNON_GUESS_MODE if ( !zzguessing ) +#define zzGuessData \ + zzjmp_buf zzguess_start; \ + int zzguessing; +#else +#define zzGUESS_BLOCK +#define zzGUESS +#define zzGUESS_FAIL +#define zzGUESS_DONE +#define zzNON_GUESS_MODE +#define zzGuessData +#endif + +typedef struct _zzantlr_state { +#ifdef ZZCAN_GUESS + zzjmp_buf guess_start; + int guessing; +#endif + int asp; + int ast_sp; +#ifdef ZZINF_LOOK + int inf_lap; /* not sure we need to save this one */ + int inf_labase; + int inf_last; +#endif +#ifdef DEMAND_LOOK + int dirty; +#endif + +#ifdef LL_K + int tokenLA[LL_K]; + char textLA[LL_K][ZZLEXBUFSIZE]; + int lap; + int labase; +#else + int token; + char text[ZZLEXBUFSIZE]; +#endif + } zzantlr_state; + + + /* I n f i n i t e L o o k a h e a d */ + + +#ifdef ZZINF_LOOK +#define InfLookData \ + int *zzinf_tokens; \ + char **zzinf_text; \ + char *zzinf_text_buffer; \ + int *zzinf_line; \ + int zzinf_labase; \ + int zzinf_last; +#else +#define InfLookData +#endif + +#ifdef ZZINF_LOOK + +#ifndef ZZINF_DEF_TEXT_BUFFER_SIZE +#define ZZINF_DEF_TEXT_BUFFER_SIZE 20000 +#endif +#ifndef ZZINF_DEF_TOKEN_BUFFER_SIZE +#define ZZINF_DEF_TOKEN_BUFFER_SIZE 2000 +#endif +/* WARNING!!!!!! + * ZZINF_BUFFER_TEXT_CHUNK_SIZE must be > sizeof(text) largest possible token. + */ +#ifndef ZZINF_BUFFER_TEXT_CHUNK_SIZE +#define ZZINF_BUFFER_TEXT_CHUNK_SIZE 5000 +#endif +#ifndef ZZINF_BUFFER_TOKEN_CHUNK_SIZE +#define ZZINF_BUFFER_TOKEN_CHUNK_SIZE 1000 +#endif + +#if ZZLEXBUFSIZE > ZZINF_BUFFER_TEXT_CHUNK_SIZE +#define ZZINF_BUFFER_TEXT_CHUNK_SIZE ZZLEXBUFSIZE+5 +#endif + +/* make inf_look user-access macros */ +#ifdef LL_K +#define ZZINF_LA_VALID(i) (((zzinf_labase+i-1)-LL_K+1) <= zzinf_last) +#define ZZINF_LA(i) zzinf_tokens[(zzinf_labase+i-1)-LL_K+1] +#define ZZINF_LATEXT(i) zzinf_text[(zzinf_labase+i-1)-LL_K+1] +/* #define ZZINF_LINE(i) zzinf_line[(zzinf_labase+i-1)-LL_K+1]*/ +#else +#define ZZINF_LA_VALID(i) (((zzinf_labase+i-1)) <= zzinf_last) +#define ZZINF_LA(i) zzinf_tokens[(zzinf_labase+i-1)] +#define ZZINF_LATEXT(i) zzinf_text[(zzinf_labase+i-1)] +#endif + +#define inf_zzgettok _inf_zzgettok() +extern void _inf_zzgettok(); + +#endif /* ZZINF_LOOK */ + + +#ifdef LL_K + +#define ANTLR_INFO \ + Attrib zzempty_attr(void) {static Attrib a; return a;} \ + Attrib zzconstr_attr(int _tok, char *_text)\ + {Attrib a; zzcr_attr((&a),_tok,_text); return a;} \ + int zzasp=ZZA_STACKSIZE; \ + char zzStackOvfMsg[]="fatal: attrib/AST stack overflow %s(%d)!\n"; \ + Attrib zzaStack[ZZA_STACKSIZE]; DemandLookData \ + InfLookData \ + zzGuessData + +#else + +#define ANTLR_INFO \ + Attrib zzempty_attr(void) {static Attrib a; return a;} \ + Attrib zzconstr_attr(int _tok, char *_text)\ + {Attrib a; zzcr_attr((&a),_tok,_text); return a;} \ + int zzasp=ZZA_STACKSIZE; \ + char zzStackOvfMsg[]="fatal: attrib/AST stack overflow %s(%d)!\n"; \ + Attrib zzaStack[ZZA_STACKSIZE]; DemandLookData \ + InfLookData \ + zzGuessData + +#endif /* LL_k */ + + +#ifdef ZZINF_LOOK + +#ifdef LL_K +#ifdef DEMAND_LOOK +#define zzPrimeLookAhead {zzdirty=LL_K; zzlap = zzlabase = 0;} +#else +#define zzPrimeLookAhead {zzlap = zzlabase = 0; zzfill_inf_look();\ + {int _i; for(_i=1;_i<=LL_K; _i++) \ + {zzCONSUME;} zzlap = zzlabase = 0;}} +#endif + +#else /* LL_K */ + +#ifdef DEMAND_LOOK +#define zzPrimeLookAhead zzfill_inf_look(); zzdirty=1 +#else +#define zzPrimeLookAhead zzfill_inf_look(); inf_zzgettok + +#endif +#endif /* LL_K */ + +#else /* ZZINF_LOOK */ + +#ifdef LL_K +#ifdef DEMAND_LOOK +#define zzPrimeLookAhead {zzdirty=LL_K; zzlap = zzlabase = 0;} +#else +#define zzPrimeLookAhead {int _i; zzlap = 0; for(_i=1;_i<=LL_K; _i++) \ + {zzCONSUME;} zzlap = 0;} +#endif + +#else + +#ifdef DEMAND_LOOK +#define zzPrimeLookAhead zzdirty=1 +#else +#define zzPrimeLookAhead zzgettok() +#endif +#endif /* LL_K */ + +#endif /* ZZINF_LOOK */ + + +#ifdef LL_K +#define zzenterANTLRs(s) \ + zzlextext = &(zztextLA[0][0]); zzrdstr( s ); zzPrimeLookAhead; +#define zzenterANTLRf(f) \ + zzlextext = &(zztextLA[0][0]); zzrdfunc( f ); zzPrimeLookAhead; +#define zzenterANTLR(f) \ + zzlextext = &(zztextLA[0][0]); zzrdstream( f ); zzPrimeLookAhead; +#ifdef ZZINF_LOOK +#define zzleaveANTLR(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#define zzleaveANTLRf(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#define zzleaveANTLRs(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#else +#define zzleaveANTLR(f) +#define zzleaveANTLRf(f) +#define zzleaveANTLRs(f) +#endif + +#else + +#define zzenterANTLRs(s) \ + {static char zztoktext[ZZLEXBUFSIZE]; \ + zzlextext = zztoktext; zzrdstr( s ); zzPrimeLookAhead;} +#define zzenterANTLRf(f) \ + {static char zztoktext[ZZLEXBUFSIZE]; \ + zzlextext = zztoktext; zzrdfunc( f ); zzPrimeLookAhead;} +#define zzenterANTLR(f) \ + {static char zztoktext[ZZLEXBUFSIZE]; \ + zzlextext = zztoktext; zzrdstream( f ); zzPrimeLookAhead;} +#ifdef ZZINF_LOOK +#define zzleaveANTLR(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#define zzleaveANTLRf(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#define zzleaveANTLRs(f) free(zzinf_text_buffer); free(zzinf_text); free(zzinf_tokens); free(zzinf_line); +#else +#define zzleaveANTLR(f) +#define zzleaveANTLRf(f) +#define zzleaveANTLRs(f) +#endif + +#endif + +#define ANTLR(st, f) zzbufsize = ZZLEXBUFSIZE; \ + zzenterANTLR(f); \ + st; ++zzasp; \ + zzleaveANTLR(f); + +#define ANTLRm(st, f, _m) zzbufsize = ZZLEXBUFSIZE; \ + zzmode(_m); \ + zzenterANTLR(f); \ + st; ++zzasp; \ + zzleaveANTLR(f); + +#define ANTLRf(st, f) zzbufsize = ZZLEXBUFSIZE; \ + zzenterANTLRf(f); \ + st; ++zzasp; \ + zzleaveANTLRf(f); + +#define ANTLRs(st, s) zzbufsize = ZZLEXBUFSIZE; \ + zzenterANTLRs(s); \ + st; ++zzasp; \ + zzleaveANTLRs(s); + +#ifdef LL_K +#define zztext (&(zztextLA[zzlap][0])) +#else +#define zztext zzlextext +#endif + + + /* A r g u m e n t A c c e s s */ + +#define zzaCur (zzaStack[zzasp]) +#define zzaRet (*zzaRetPtr) +#define zzaArg(v,n) zzaStack[v-n] +#define zzMakeAttr { zzNON_GUESS_MODE {zzOvfChk; --zzasp; zzcr_attr(&(zzaStack[zzasp]),LA(1),LATEXT(1));}} +#ifdef zzdef0 +#define zzMake0 { zzOvfChk; --zzasp; zzdef0(&(zzaStack[zzasp]));} +#else +#define zzMake0 { zzOvfChk; --zzasp;} +#endif +#define zzaPush(_v) { zzOvfChk; zzaStack[--zzasp] = _v;} +#ifndef zzd_attr +#define zzREL(t) zzasp=(t); /* Restore state of stack */ +#else +#define zzREL(t) for (; zzasp<(t); zzasp++) \ + { zzd_attr(&(zzaStack[zzasp])); } +#endif + +#define zzsetmatch(_es) \ + if ( !_zzsetmatch(_es, &zzBadText, &zzMissText, &zzMissTok, &zzBadTok, &zzMissSet) ) goto fail; +#define zzsetmatch_wsig(_es, handler) \ + if ( !_zzsetmatch_wsig(_es) ) {_signal=MismatchedToken; goto handler;} + +extern int _zzsetmatch(SetWordType *, char **, char **, int *, int *, SetWordType **); +extern int _zzsetmatch_wsig(SetWordType *); + +#define zzmatch(_t) \ + if ( !_zzmatch(_t, &zzBadText, &zzMissText, &zzMissTok, &zzBadTok, &zzMissSet) ) goto fail; +#define zzmatch_wsig(_t,handler) \ + if ( !_zzmatch_wsig(_t) ) {_signal=MismatchedToken; goto handler;} + +extern int _zzmatch(int, const char **, const char **, int *, int *, SetWordType **); +extern int _zzmatch_wsig(int); + +#define zzmatch_wdfltsig(_t,_f) \ + if ( !_zzmatch_wdfltsig(_t,_f) ) _signal=MismatchedToken; +#define zzsetmatch_wdfltsig(tw,tt,wf) \ + if ( !_zzsetmatch_wdfltsig(tw,tt,wf) ) _signal=MismatchedToken; + +extern int _zzmatch_wdfltsig(int, SetWordType *); +extern int _zzsetmatch_wdfltsig(SetWordType *tokensWanted, + int tokenTypeOfSet, + SetWordType *whatFollows); + +#ifdef GENAST +#define zzRULE Attrib *zzaRetPtr = &(zzaStack[zzasp-1]); \ + SetWordType *zzMissSet=NULL; int zzMissTok=0; \ + int zzBadTok=0; const char *zzBadText=""; \ + int zzErrk=1; \ + const char *zzMissText=""; zzASTVars +#else +#define zzRULE Attrib *zzaRetPtr = &(zzaStack[zzasp-1]); \ + int zzBadTok=0; const char *zzBadText=""; \ + int zzErrk=1; \ + SetWordType *zzMissSet=NULL; int zzMissTok=0; const char *zzMissText="" +#endif + +#ifdef GENAST +#define zzBLOCK(i) int i = zzasp - 1; int zztsp = zzast_sp +#define zzEXIT(i) zzREL(i); zzastREL; zzNON_GUESS_MODE { zzastPush(*_root); } +#define zzLOOP(i) zzREL(i); zzastREL +#else +#define zzBLOCK(i) int i = zzasp - 1 +#define zzEXIT(i) zzREL(i) +#define zzLOOP(i) zzREL(i) +#endif + +#ifdef LL_K + +#ifdef DEMAND_LOOK +#define LOOK(_k) {int i,stop=_k-(LL_K-zzdirty); for (i=1; i<=stop; i++) \ + zzCONSUME;} +#define zzCONSUME {zzgettok(); zzdirty--; \ + zzlap = (zzlap+1)&(LL_K-1); \ + zzlextext = &(zztextLA[zzlap][0]);} +#else +#ifdef ZZINF_LOOK +#define zzCONSUME {inf_zzgettok; \ + zzlap = (zzlap+1)&(LL_K-1); \ + zzlextext = &(zztextLA[zzlap][0]); \ + } +#else +#define zzCONSUME {zzgettok(); \ + zzlap = (zzlap+1)&(LL_K-1); \ + zzlextext = &(zztextLA[zzlap][0]);} +#endif /* ZZINF_LOOK */ +#endif /* DEMAND_LOOK */ + +#else /* LL_K */ + +#ifdef DEMAND_LOOK +#define LOOK(_k) if ( zzdirty) zzCONSUME; +#ifdef ZZINF_LOOK +#define zzCONSUME inf_zzgettok; zzdirty=0; +#else +#define zzCONSUME zzgettok(); zzdirty=0; +#endif /* ZZINF_LOOK */ + +#else /* DEMAND_LOOK */ + +#ifdef ZZINF_LOOK +#define zzCONSUME inf_zzgettok +#else +#define zzCONSUME zzgettok(); +#endif + +#endif /* DEMAND_LOOK */ + +#endif /* LL_K */ + +#ifdef LL_K +#define NLA zztokenLA[zzlap&(LL_K-1)] /* --> next LA */ +#define NLATEXT zztextLA[zzlap&(LL_K-1)] /* --> next text of LA */ +#ifdef DEMAND_LOOK +#define LA(i) zztokenLA[(zzlabase+(i)-1)&(LL_K-1)] +#define LATEXT(i) (&(zztextLA[(zzlabase+(i)-1)&(LL_K-1)][0])) +#else +#define LA(i) zztokenLA[(zzlap+(i)-1)&(LL_K-1)] +#define LATEXT(i) (&(zztextLA[(zzlap+(i)-1)&(LL_K-1)][0])) +#endif +#else +#define NLA zztoken +#define NLATEXT zztext +#define LA(i) zztoken +#define LATEXT(i) zztext +#endif + + + /* S t a n d a r d S i g n a l s */ + +#define NoSignal 0 +#define MismatchedToken 1 +#define NoViableAlt 2 +#define NoSemViableAlt 3 + + + /* F u n c t i o n T r a c i n g */ + +#ifndef zzTRACEIN +#define zzTRACEIN(r) fprintf(stderr, "enter rule \"%s\"\n", r); +#endif +#ifndef zzTRACEOUT +#define zzTRACEOUT(r) fprintf(stderr, "exit rule \"%s\"\n", r); +#endif + +#ifdef ZZWCHAR_T +#define zzchar_t unsigned wchar_t +#else +#define zzchar_t unsigned char +#endif + + /* E x t e r n D e f s */ + +extern Attrib zzempty_attr(void); +extern Attrib zzconstr_attr(int, char *); +extern void zzsyn(const char *, int, char *, SetWordType *, int, int, const char *); +extern int zzset_el(unsigned, SetWordType *); +extern int zzset_deg(SetWordType *); +extern void zzedecode(SetWordType *); +extern void zzFAIL(int k, ...); +extern void zzresynch(SetWordType *, SetWordType); +extern void zzsave_antlr_state(zzantlr_state *); +extern void zzrestore_antlr_state(zzantlr_state *); +extern void zzfill_inf_look(void); +#ifdef EXCEPTION_HANDLING +extern void zzdflthandlers(int, int *); +#endif + + /* G l o b a l V a r i a b l e s */ + +/* Define a parser; user should do a "#parser myname" in their grammar file */ +/*extern struct pccts_parser zzparser;*/ + +extern const char *zztokens[]; +#ifdef LL_K +extern int zztokenLA[]; +extern char zztextLA[][ZZLEXBUFSIZE]; +extern int zzlap; +extern int zzlabase; +#else +extern int zztoken; +#endif + +extern char zzStackOvfMsg[]; +extern int zzasp; +extern Attrib zzaStack[]; +#ifdef ZZINF_LOOK +extern int *zzinf_tokens; +extern char **zzinf_text; +extern char *zzinf_text_buffer; +extern int *zzinf_line; +extern int zzinf_labase; +extern int zzinf_last; +#endif +#ifdef DEMAND_LOOK +extern int zzdirty; +#endif +#ifdef ZZCAN_GUESS +extern int zzguessing; +extern zzjmp_buf zzguess_start; +#endif + +/* Define global veriables that refer to values exported by the scanner. + * These declarations duplicate those in dlgdef.h, but are needed + * if ANTLR is not to generate a .dlg file (-gx); PS, this is a hack. + */ +extern zzchar_t *zzlextext; /* text of most recently matched token */ +extern int zzbufsize; /* how long zzlextext is */ + +#endif diff --git a/src/translators/btparse/ast.c b/src/translators/btparse/ast.c new file mode 100644 index 0000000..d433f79 --- /dev/null +++ b/src/translators/btparse/ast.c @@ -0,0 +1,227 @@ +/* Abstract syntax tree manipulation functions + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * ANTLR 1.33 + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ +#include <stdarg.h> +#include <stdio.h> + +#include "ast.h" +#include "attrib.h" +#include "antlr.h" + +/* ensure that tree manipulation variables are current after a rule + * reference + */ +void +zzlink(AST **_root, AST **_sibling, AST **_tail) +{ + if ( *_sibling == NULL ) return; + if ( *_root == NULL ) *_root = *_sibling; + else if ( *_root != *_sibling ) (*_root)->down = *_sibling; + if ( *_tail==NULL ) *_tail = *_sibling; + while ( (*_tail)->right != NULL ) *_tail = (*_tail)->right; +} + +AST * +zzastnew(void) +{ + AST *p = (AST *) calloc(1, sizeof(AST)); + if ( p == NULL ) fprintf(stderr,"%s(%d): cannot allocate AST node\n",__FILE__,__LINE__); + return p; +} + +/* add a child node to the current sibling list */ +void +zzsubchild(AST **_root, AST **_sibling, AST **_tail) +{ + AST *n; + zzNON_GUESS_MODE { + n = zzastnew(); +#ifdef DEMAND_LOOK + zzcr_ast(n, &(zzaCur), LA(0), LATEXT(0)); +#else + zzcr_ast(n, &(zzaCur), LA(1), LATEXT(1)); +#endif + zzastPush( n ); + if ( *_tail != NULL ) (*_tail)->right = n; + else { + *_sibling = n; + if ( *_root != NULL ) (*_root)->down = *_sibling; + } + *_tail = n; + if ( *_root == NULL ) *_root = *_sibling; + } +} + +/* make a new AST node. Make the newly-created + * node the root for the current sibling list. If a root node already + * exists, make the newly-created node the root of the current root. + */ +void +zzsubroot(AST **_root, AST **_sibling, AST **_tail) +{ + AST *n; + zzNON_GUESS_MODE { + n = zzastnew(); +#ifdef DEMAND_LOOK + zzcr_ast(n, &(zzaCur), LA(0), LATEXT(0)); +#else + zzcr_ast(n, &(zzaCur), LA(1), LATEXT(1)); +#endif + zzastPush( n ); + if ( *_root != NULL ) + if ( (*_root)->down == *_sibling ) *_sibling = *_tail = *_root; + *_root = n; + (*_root)->down = *_sibling; + } +} + +/* Apply function to root then each sibling + * example: print tree in child-sibling LISP-format (AST has token field) + * + * void show(tree) + * AST *tree; + * { + * if ( tree == NULL ) return; + * printf(" %s", zztokens[tree->token]); + * } + * + * void before() { printf(" ("); } + * void after() { printf(" )"); } + * + * LISPdump() { zzpre_ast(tree, show, before, after); } + * + */ +void +zzpre_ast( + AST *tree, + void (*func)(AST *), /* apply this to each tree node */ + void (*before)(AST *), /* apply this to root of subtree before preordering it */ + void (*after)(AST *)) /* apply this to root of subtree after preordering it */ +{ + while ( tree!= NULL ) + { + if ( tree->down != NULL ) (*before)(tree); + (*func)(tree); + zzpre_ast(tree->down, func, before, after); + if ( tree->down != NULL ) (*after)(tree); + tree = tree->right; + } +} + +/* free all AST nodes in tree; apply func to each before freeing */ +void +zzfree_ast(AST *tree) +{ + if ( tree == NULL ) return; + zzfree_ast( tree->down ); + zzfree_ast( tree->right ); + zztfree( tree ); +} + +/* build a tree (root child1 child2 ... NULL) + * If root is NULL, simply make the children siblings and return ptr + * to 1st sibling (child1). If root is not single node, return NULL. + * + * Siblings that are actually siblins lists themselves are handled + * correctly. For example #( NULL, #( NULL, A, B, C), D) results + * in the tree ( NULL A B C D ). + * + * Requires at least two parameters with the last one being NULL. If + * both are NULL, return NULL. + */ +AST *zztmake(AST *rt, ...) +{ + va_list ap; + register AST *child, *sibling=NULL, *tail, *w; + AST *root; + + va_start(ap, rt); + root = rt; + + if ( root != NULL ) + if ( root->down != NULL ) return NULL; + child = va_arg(ap, AST *); + while ( child != NULL ) + { + for (w=child; w->right!=NULL; w=w->right) {;} /* find end of child */ + if ( sibling == NULL ) {sibling = child; tail = w;} + else {tail->right = child; tail = w;} + child = va_arg(ap, AST *); + } + if ( root==NULL ) root = sibling; + else root->down = sibling; + va_end(ap); + return root; +} + +/* tree duplicate */ +AST * +zzdup_ast(AST *t) +{ + AST *u; + + if ( t == NULL ) return NULL; + u = zzastnew(); + *u = *t; +#ifdef zzAST_DOUBLE + u->up = NULL; /* set by calling invocation */ + u->left = NULL; +#endif + u->right = zzdup_ast(t->right); + u->down = zzdup_ast(t->down); +#ifdef zzAST_DOUBLE + if ( u->right!=NULL ) u->right->left = u; + if ( u->down!=NULL ) u->down->up = u; +#endif + return u; +} + +void +zztfree(AST *t) +{ +#ifdef zzd_ast + zzd_ast( t ); +#endif + free( t ); +} + +#ifdef zzAST_DOUBLE +/* + * Set the 'up', and 'left' pointers of all nodes in 't'. + * Initial call is double_link(your_tree, NULL, NULL). + */ +void +zzdouble_link(AST *t, AST *left, AST *up) +{ + if ( t==NULL ) return; + t->left = left; + t->up = up; + zzdouble_link(t->down, NULL, t); + zzdouble_link(t->right, t, up); +} +#endif diff --git a/src/translators/btparse/ast.h b/src/translators/btparse/ast.h new file mode 100644 index 0000000..59622ec --- /dev/null +++ b/src/translators/btparse/ast.h @@ -0,0 +1,99 @@ +/* Abstract syntax tree + * + * Macros, definitions + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * ANTLR 1.33 + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ + +#ifndef ZZAST_H +#define ZZAST_H + +#define zzastOvfChk \ + if ( zzast_sp <= 0 ) \ + { \ + fprintf(stderr, zzStackOvfMsg, __FILE__, __LINE__); \ + exit(PCCTS_EXIT_FAILURE); \ + } + +#ifndef USER_DEFINED_AST +#ifndef AST_FIELDS +#define AST_FIELDS +#endif + +typedef struct _ast { + struct _ast *right, *down; +#ifdef zzAST_DOUBLE + struct _ast *left, *up; +#endif + AST_FIELDS +} AST; + +#else + +#ifdef zzAST_DOUBLE +#define AST_REQUIRED_FIELDS struct _ast *right, *down, *left, *up; +#else +#define AST_REQUIRED_FIELDS struct _ast *right, *down; +#endif + +#endif + + +/* N o d e a c c e s s m a c r o s */ +#define zzchild(t) (((t)==NULL)?NULL:(t->down)) +#define zzsibling(t) (((t)==NULL)?NULL:(t->right)) + + +/* define global variables needed by #i stack */ +#define zzASTgvars \ + AST *zzastStack[ZZAST_STACKSIZE]; \ + int zzast_sp = ZZAST_STACKSIZE; + +#define zzASTVars AST *_ast = NULL, *_sibling = NULL, *_tail = NULL +#define zzSTR ( (_tail==NULL)?(&_sibling):(&(_tail->right)) ) +#define zzastCur (zzastStack[zzast_sp]) +#define zzastArg(i) (zzastStack[zztsp-i]) +#define zzastPush(p) zzastOvfChk; zzastStack[--zzast_sp] = p; +#define zzastDPush --zzast_sp +#define zzastMARK zztsp=zzast_sp; /* Save state of stack */ +#define zzastREL zzast_sp=zztsp; /* Return state of stack */ +#define zzrm_ast {zzfree_ast(*_root); _tail = _sibling = (*_root)=NULL;} + +extern int zzast_sp; +extern AST *zzastStack[]; + +void zzlink(AST **, AST **, AST **); +void zzsubchild(AST **, AST **, AST **); +void zzsubroot(AST **, AST **, AST **); +void zzpre_ast(AST *, void (*)(), void (*)(), void (*)()); +void zzfree_ast(AST *); +AST *zztmake(AST *, ...); +AST *zzdup_ast(AST *); +void zztfree(AST *); +void zzdouble_link(AST *, AST *, AST *); +AST *zzastnew(void); +#endif diff --git a/src/translators/btparse/attrib.h b/src/translators/btparse/attrib.h new file mode 100644 index 0000000..6a3cecf --- /dev/null +++ b/src/translators/btparse/attrib.h @@ -0,0 +1,35 @@ +/* ------------------------------------------------------------------------ +@NAME : attrib.h +@DESCRIPTION: Definition of the Attrib type needed by the PCCTS- + generated parser. +@CREATED : Summer 1996, Greg Ward +@MODIFIED : +@VERSION : $Id: attrib.h,v 1.3 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef ATTRIB_H +#define ATTRIB_H + +/* + * Defining Attrib this way (as opposed to making it a pointer to a struct) + * avoid the expense of allocating/deallocating a structure for each token; + * this way, PCCTS statically allocates the whole stack once and that's + * it. (Of course, the stack is four times bigger than it would have been + * otherwise.) + */ + +typedef struct { + int line; + int offset; + int token; + char *text; +} Attrib; + +#endif /* ATTRIB_H */ diff --git a/src/translators/btparse/bibtex.c b/src/translators/btparse/bibtex.c new file mode 100644 index 0000000..c922803 --- /dev/null +++ b/src/translators/btparse/bibtex.c @@ -0,0 +1,312 @@ +/* + * A n t l r T r a n s l a t i o n H e a d e r + * + * Terence Parr, Will Cohen, and Hank Dietz: 1989-1994 + * Purdue University Electrical Engineering + * With AHPCRC, University of Minnesota + * ANTLR Version 1.33 + */ +#include <stdio.h> +#define ANTLR_VERSION 133 + +#define ZZCOL +#define USER_ZZSYN + +#include "btconfig.h" +#include "btparse.h" +#include "attrib.h" +#include "lex_auxiliary.h" +#include "error.h" +#include "parse_auxiliary.h" +/*#include "my_dmalloc.h"*/ + +extern char * InputFilename; /* for zzcr_ast call in pccts/ast.c */ +#define GENAST + +#include "ast.h" + +#define zzSET_SIZE 4 +#include "antlr.h" +#include "tokens.h" +#include "dlgdef.h" +#include "mode.h" +#ifndef PURIFY +#define PURIFY(r,s) +#endif +#include "ast.c" +zzASTgvars + +ANTLR_INFO + +void +bibfile(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + AST *last; (*_root) = NULL; + { + zzBLOCK(zztasp2); + zzMake0; + { + while ( (LA(1)==AT) ) { + _ast = NULL; entry(&_ast); + /* a little creative forestry... */ + if ((*_root) == NULL) + (*_root) = zzastArg(1); + else + last->right = zzastArg(1); + last = zzastArg(1); + zzLOOP(zztasp2); + } + zzEXIT(zztasp2); + } + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd1, 0x1); + } +} + +void +entry(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + bt_metatype metatype; + zzmatch(AT); zzCONSUME; + zzmatch(NAME); zzsubroot(_root, &_sibling, &_tail); + + metatype = entry_metatype(); + zzastArg(1)->nodetype = BTAST_ENTRY; + zzastArg(1)->metatype = metatype; + zzCONSUME; + + body(zzSTR, metatype ); zzlink(_root, &_sibling, &_tail); + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd1, 0x2); + } +} + +void +body(AST**_root, bt_metatype metatype ) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + if ( (LA(1)==STRING) ) { + if (!(metatype == BTE_COMMENT )) {zzfailed_pred(" metatype == BTE_COMMENT ");} + zzmatch(STRING); zzsubchild(_root, &_sibling, &_tail); + zzastArg(1)->nodetype = BTAST_STRING; + zzCONSUME; + + } + else { + if ( (LA(1)==ENTRY_OPEN) ) { + zzmatch(ENTRY_OPEN); zzCONSUME; + contents(zzSTR, metatype ); zzlink(_root, &_sibling, &_tail); + zzmatch(ENTRY_CLOSE); zzCONSUME; + } + else {zzFAIL(1,zzerr1,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText,&zzErrk); goto fail;} + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd1, 0x4); + } +} + +void +contents(AST**_root, bt_metatype metatype ) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + if ( (setwd1[LA(1)]&0x8)&&(metatype == BTE_REGULAR /* || metatype == BTE_MODIFY */ ) ) { + if (!(metatype == BTE_REGULAR /* || metatype == BTE_MODIFY */ )) {zzfailed_pred(" metatype == BTE_REGULAR /* || metatype == BTE_MODIFY */ ");} + { + zzBLOCK(zztasp2); + zzMake0; + { + if ( (LA(1)==NAME) ) { + zzmatch(NAME); zzsubchild(_root, &_sibling, &_tail); zzCONSUME; + } + else { + if ( (LA(1)==NUMBER) ) { + zzmatch(NUMBER); zzsubchild(_root, &_sibling, &_tail); zzCONSUME; + } + else {zzFAIL(1,zzerr2,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText,&zzErrk); goto fail;} + } + zzEXIT(zztasp2); + } + } + zzastArg(1)->nodetype = BTAST_KEY; + zzmatch(COMMA); zzCONSUME; + fields(zzSTR); zzlink(_root, &_sibling, &_tail); + } + else { + if ( (setwd1[LA(1)]&0x10)&&(metatype == BTE_MACRODEF ) ) { + if (!(metatype == BTE_MACRODEF )) {zzfailed_pred(" metatype == BTE_MACRODEF ");} + fields(zzSTR); zzlink(_root, &_sibling, &_tail); + } + else { + if ( (setwd1[LA(1)]&0x20)&&(metatype == BTE_PREAMBLE ) ) { + if (!(metatype == BTE_PREAMBLE )) {zzfailed_pred(" metatype == BTE_PREAMBLE ");} + value(zzSTR); zzlink(_root, &_sibling, &_tail); + } + else {zzFAIL(1,zzerr3,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText,&zzErrk); goto fail;} + } + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd1, 0x40); + } +} + +void +fields(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + if ( (LA(1)==NAME) ) { + field(zzSTR); zzlink(_root, &_sibling, &_tail); + { + zzBLOCK(zztasp2); + zzMake0; + { + if ( (LA(1)==COMMA) ) { + zzmatch(COMMA); zzCONSUME; + fields(zzSTR); zzlink(_root, &_sibling, &_tail); + } + zzEXIT(zztasp2); + } + } + } + else { + if ( (LA(1)==ENTRY_CLOSE) ) { + } + else {zzFAIL(1,zzerr4,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText,&zzErrk); goto fail;} + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd1, 0x80); + } +} + +void +field(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + zzmatch(NAME); zzsubroot(_root, &_sibling, &_tail); + zzastArg(1)->nodetype = BTAST_FIELD; check_field_name (zzastArg(1)); + zzCONSUME; + + zzmatch(EQUALS); zzCONSUME; + value(zzSTR); zzlink(_root, &_sibling, &_tail); + +#if DEBUG > 1 + printf ("field: fieldname = %p (%s)\n" + " first val = %p (%s)\n", + zzastArg(1)->text, zzastArg(1)->text, zzastArg(2)->text, zzastArg(2)->text); +#endif + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd2, 0x1); + } +} + +void +value(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + simple_value(zzSTR); zzlink(_root, &_sibling, &_tail); + { + zzBLOCK(zztasp2); + zzMake0; + { + while ( (LA(1)==HASH) ) { + zzmatch(HASH); zzCONSUME; + simple_value(zzSTR); zzlink(_root, &_sibling, &_tail); + zzLOOP(zztasp2); + } + zzEXIT(zztasp2); + } + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd2, 0x2); + } +} + +void +simple_value(AST**_root) +{ + zzRULE; + zzBLOCK(zztasp1); + zzMake0; + { + if ( (LA(1)==STRING) ) { + zzmatch(STRING); zzsubchild(_root, &_sibling, &_tail); + zzastArg(1)->nodetype = BTAST_STRING; + zzCONSUME; + + } + else { + if ( (LA(1)==NUMBER) ) { + zzmatch(NUMBER); zzsubchild(_root, &_sibling, &_tail); + zzastArg(1)->nodetype = BTAST_NUMBER; + zzCONSUME; + + } + else { + if ( (LA(1)==NAME) ) { + zzmatch(NAME); zzsubchild(_root, &_sibling, &_tail); + zzastArg(1)->nodetype = BTAST_MACRO; + zzCONSUME; + + } + else {zzFAIL(1,zzerr5,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText,&zzErrk); goto fail;} + } + } + zzEXIT(zztasp1); + return; +fail: + zzEXIT(zztasp1); + zzsyn(zzMissText, zzBadTok, (ANTLRChar *)"", zzMissSet, zzMissTok, zzErrk, zzBadText); + zzresynch(setwd2, 0x4); + } +} diff --git a/src/translators/btparse/bibtex_ast.c b/src/translators/btparse/bibtex_ast.c new file mode 100644 index 0000000..354cefb --- /dev/null +++ b/src/translators/btparse/bibtex_ast.c @@ -0,0 +1,63 @@ +/* ------------------------------------------------------------------------ +@NAME : bibtex_ast.c +@DESCRIPTION: Data and functions for internal display/manipulation of AST + nodes. (Stuff for external consumption, and for processing + whole trees, is to be found in traversal.c.) +@GLOBALS : +@CREATED : 1997/08/12, Greg Ward +@MODIFIED : +@VERSION : $Id: bibtex_ast.c,v 1.6 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include "btparse.h" +#include "prototypes.h" +/*#include "my_dmalloc.h"*/ + + +const char *nodetype_names[] = +{ + "bogus", "entry", "key", "field", "string", "number", "macro" +}; + + +static void dump (AST *root, int depth) +{ + AST *cur; + + if (root == NULL) + { + printf ("[empty]\n"); + return; + } + + cur = root; + while (cur != NULL) + { + printf ("%*s[%s]: ", 2*depth, "", nodetype_names[cur->nodetype]); + if (cur->text != NULL) + printf ("(%s)\n", cur->text); + else + printf ("(null)\n"); + + if (cur->down != NULL) + dump (cur->down, depth+1); + cur = cur->right; + } +} + + +void dump_ast (char *msg, AST *root) +{ + if (msg != NULL) + printf (msg); + dump (root, 0); + printf ("\n"); +} diff --git a/src/translators/btparse/bt_debug.h b/src/translators/btparse/bt_debug.h new file mode 100644 index 0000000..913ae1a --- /dev/null +++ b/src/translators/btparse/bt_debug.h @@ -0,0 +1,38 @@ +/* ------------------------------------------------------------------------ +@NAME : bt_debug.h +@DESCRIPTION: Defines various macros needed for compile-time selection + of debugging code. +@GLOBALS : +@CREATED : +@MODIFIED : +@VERSION : $Id: bt_debug.h,v 1.2 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef BT_DEBUG_H +#define BT_DEBUG_H + +/* + * DEBUG is the debug level -- an integer, defaults to 0 + * DBG_ACTION is a macro to conditionally execute a bit of code -- + * must have compiled with DEBUG true, and the debug level + * must be >= `level' (the macro argument) + */ + +#ifndef DEBUG +# define DEBUG 0 +#endif + +#if DEBUG +# define DBG_ACTION(level,action) if (DEBUG >= level) { action; } +#else +# define DBG_ACTION(level,action) +#endif + +#endif /* BT_DEBUG_H */ diff --git a/src/translators/btparse/btconfig.h b/src/translators/btparse/btconfig.h new file mode 100644 index 0000000..7405825 --- /dev/null +++ b/src/translators/btparse/btconfig.h @@ -0,0 +1,220 @@ +#ifndef BTPARSE_CONFIG_H +#define BTPARSE_CONFIG_H +/* + * config.h (for ANTLR, DLG, and SORCERER) + * + * This is a simple configuration file that doesn't have config stuff + * in it, but it's a start. + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * Used by PCCTS 1.33 (SORCERER 1.00B11 and up) + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ + +/* This file knows about the following ``environments'' + UNIX (default) + DOS (use #define PC) + MAC (use #define MPW; has a few things for THINK C, Metrowerks) + */ + +/* +* Define PC32 if in a 32-bit PC environment (e.g. extended DOS or Win32). +* The macros tested here are defined by Watcom, Microsoft, Borland, +* and djgpp, respectively, when they are used as 32-bit compilers. +* Users of these compilers *must* be sure to define PC in their +* makefiles for this to work correctly. +*/ +#ifdef PC +# if (defined(__WATCOM__) || defined(_WIN32) || defined(__WIN32__) || \ + defined(__GNUC__) || defined(__GNUG__)) +# ifndef PC32 +# define PC32 +# endif +# endif +#endif + +#ifdef PC +#define ATOKEN_H "AToken.h" +#define ATOKPTR_H "ATokPtr.h" +#define ATOKPTR_C "ATokPtr.cpp" +#define ATOKENBUFFER_H "ATokBuf.h" +#define ATOKENBUFFER_C "ATokBuf.cpp" +#define ATOKENSTREAM_H "ATokStr.h" +#define APARSER_H "AParser.h" +#define APARSER_C "AParser.cpp" +#define ASTBASE_H "ASTBase.h" +#define ASTBASE_C "ASTBase.cpp" +#define PCCTSAST_C "PCCTSAST.cpp" +#define LIST_C "List.cpp" +#define DLEXERBASE_H "DLexBase.h" +#define DLEXERBASE_C "DLexBase.cpp" +#define DLEXER_C "DLexer.cpp" +#define STREESUPPORT_C "STreeSup.C" +#else +#define ATOKEN_H "AToken.h" +#define ATOKPTR_H "ATokPtr.h" +#define ATOKPTR_C "ATokPtr.cpp" +#define ATOKENBUFFER_H "ATokenBuffer.h" +#define ATOKENBUFFER_C "ATokenBuffer.cpp" +#define ATOKENSTREAM_H "ATokenStream.h" +#define APARSER_H "AParser.h" +#define APARSER_C "AParser.cpp" +#define ASTBASE_H "ASTBase.h" +#define ASTBASE_C "ASTBase.cpp" +#define PCCTSAST_C "PCCTSAST.cpp" +#define LIST_C "List.cpp" +#define DLEXERBASE_H "DLexerBase.h" +#define DLEXERBASE_C "DLexerBase.cpp" +#define DLEXER_C "DLexer.cpp" +#define STREESUPPORT_C "STreeSupport.cpp" +#endif + +/* SORCERER Stuff */ +#ifdef PC +#define STPARSER_H "STreePar.h" +#define STPARSER_C "STreePar.C" +#else +#define STPARSER_H "STreeParser.h" +#define STPARSER_C "STreeParser.cpp" +#endif + +#ifdef MPW +#define CPP_FILE_SUFFIX ".cp" +#define CPP_FILE_SUFFIX_NO_DOT "cp" +#define OBJ_FILE_SUFFIX ".o" +#else +#ifdef PC +#define CPP_FILE_SUFFIX ".cpp" +#define CPP_FILE_SUFFIX_NO_DOT "cpp" +#define OBJ_FILE_SUFFIX ".obj" +#else +#define CPP_FILE_SUFFIX ".cpp" +#define CPP_FILE_SUFFIX_NO_DOT "cpp" +#define OBJ_FILE_SUFFIX ".o" +#endif +#endif + +/* User may redefine how line information looks */ +#define LineInfoFormatStr "# %d \"%s\"\n" + +#ifdef MPW /* Macintosh Programmer's Workshop */ +#define ErrHdr "File \"%s\"; Line %d #" +#else +#define ErrHdr "%s, line %d:" +#endif + + +/* must assume old K&R cpp here, can't use #if defined(..)... */ + +#ifdef MPW +#define TopDirectory ":" +#define DirectorySymbol ":" +#define OutputDirectoryOption "Directory where all output files should go (default=\":\")" +#else +#ifdef PC +#define TopDirectory "." +#define DirectorySymbol "\\" +#define OutputDirectoryOption "Directory where all output files should go (default=\".\")" +#else +#define TopDirectory "." +#define DirectorySymbol "/" +#define OutputDirectoryOption "Directory where all output files should go (default=\".\")" +#endif +#endif + +#ifdef MPW + +/* Make sure we have prototypes for all functions under MPW */ + +#include <string.h> +#include <stdlib.h> +#include <CursorCtl.h> +#ifdef __cplusplus +extern "C" { +#endif +extern void fsetfileinfo (char *filename, unsigned long newcreator, unsigned long newtype); +#ifdef __cplusplus +} +#endif + +/* File creators for various popular development environments */ + +#define MAC_FILE_CREATOR 'MPS ' /* MPW Text files */ +#if 0 +#define MAC_FILE_CREATOR 'KAHL' /* THINK C/Symantec C++ Text files */ +#endif +#if 0 +#define MAC_FILE_CREATOR 'MMCC' /* Metrowerks C/C++ Text files */ +#endif + +#endif + +#ifdef MPW +#define DAWDLE SpinCursor(1) +#else +#define DAWDLE +#endif + + +/* + * useless definitions of special_inits() and special_fopen_actions() + * deleted -- GPW 1997/09/06 + */ + +/* Define usable bits for set.c stuff */ +#define BytesPerWord sizeof(unsigned) +#define WORDSIZE (sizeof(unsigned)*8) +#define LogWordSize (WORDSIZE==16?4:5) + +#ifndef TRUE +#define TRUE 1 +#endif +#ifndef FALSE +#define FALSE 0 +#endif + +#ifdef VAXC +#define PCCTS_EXIT_SUCCESS 1 +#define PCCTS_EXIT_FAILURE 0 +#define zzDIE return 0; +#define zzDONE return 1; + +#else /* !VAXC */ + +#define PCCTS_EXIT_SUCCESS 0 +#define PCCTS_EXIT_FAILURE 1 +#define zzDIE return 1; +#define zzDONE return 0; + +#endif + +#ifdef USER_ZZMODE_STACK +# ifndef ZZSTACK_MAX_MODE +# define ZZSTACK_MAX_MODE 32 +# endif +# define ZZMAXSTK (ZZSTACK_MAX_MODE * 2) +#endif + +#endif diff --git a/src/translators/btparse/btparse.h b/src/translators/btparse/btparse.h new file mode 100644 index 0000000..841d3ee --- /dev/null +++ b/src/translators/btparse/btparse.h @@ -0,0 +1,378 @@ +/* ------------------------------------------------------------------------ +@NAME : btparse.h +@DESCRIPTION: Declarations and types for users of the btparse library. + + (Actually, btparse.h is generated from btparse.h.in by + the `configure' script, in order to automatically determine + the appropriate values of HAVE_USHORT and HAVE_BOOLEAN.) +@GLOBALS : +@CALLS : +@CREATED : 1997/01/19, Greg Ward +@MODIFIED : +@VERSION : $Id: btparse.h.in,v 1.35 1999/12/28 18:23:17 greg Exp $ +@COPYRIGHT : Copyright (c) 1996-97 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +#ifndef BTPARSE_H +#define BTPARSE_H + +#include <sys/types.h> /* probably supplies 'ushort' */ +#include <stdio.h> + +#include "config.h" /* not btparse's config.h but Tellico's */ + +/* + * Here we attempt to define HAVE_USHORT if a typdef for `ushort' appears + * in <sys/types.h>. The detective work is actually done by the + * `configure' script, so if compilation fails because of duplicate + * definitions of `ushort', that's a bug in `configure' -- please tell me + * about it! + */ + +#ifndef HAVE_USHORT +# define HAVE_USHORT 0 +#endif + +#if ! HAVE_USHORT /* needed for various bitmaps */ +typedef unsigned short ushort; +#endif + + +/* Likewise for boolean. */ + +#ifndef HAVE_BOOLEAN +# define HAVE_BOOLEAN 0 +#endif + +#if ! HAVE_BOOLEAN +typedef int boolean; +#endif + +#ifndef TRUE +# define TRUE 1 +# define FALSE 0 +#endif + +#ifndef HAVE_STRLWR +# define HAVE_STRLWR 0 +#endif + +#ifndef HAVE_STRUPR +# define HAVE_STRUPR 0 +#endif + + +/* Parsing (and post-processing) options */ + +#define BTO_CONVERT 1 /* convert numbers to strings? */ +#define BTO_EXPAND 2 /* expand macros? */ +#define BTO_PASTE 4 /* paste substrings together? */ +#define BTO_COLLAPSE 8 /* collapse whitespace? */ + +#define BTO_NOSTORE 16 + +#define BTO_FULL (BTO_CONVERT | BTO_EXPAND | BTO_PASTE | BTO_COLLAPSE) +#define BTO_MACRO (BTO_CONVERT | BTO_EXPAND | BTO_PASTE) +#define BTO_MINIMAL 0 + +#define BTO_STRINGMASK (BTO_CONVERT | BTO_EXPAND | BTO_PASTE | BTO_COLLAPSE) + +#define BT_VALID_NAMEPARTS "fvlj" +#define BT_MAX_NAMEPARTS 4 + +typedef enum +{ + BTE_UNKNOWN, + BTE_REGULAR, + BTE_COMMENT, + BTE_PREAMBLE, + BTE_MACRODEF +/* + BTE_ALIAS, + BTE_MODIFY +*/ +} bt_metatype; + +#define NUM_METATYPES ((int) BTE_MACRODEF + 1) + +typedef enum +{ + BTAST_BOGUS, /* to detect uninitialized nodes */ + BTAST_ENTRY, + BTAST_KEY, + BTAST_FIELD, + BTAST_STRING, + BTAST_NUMBER, + BTAST_MACRO +} bt_nodetype; + +typedef enum +{ + BTN_FIRST, BTN_VON, BTN_LAST, BTN_JR, BTN_NONE +} bt_namepart; + +typedef enum +{ + BTJ_MAYTIE, /* "discretionary" tie between words */ + BTJ_SPACE, /* force a space between words */ + BTJ_FORCETIE, /* force a tie (~ in TeX) */ + BTJ_NOTHING /* nothing between words */ +} bt_joinmethod; + + +#define USER_DEFINED_AST 1 + +#define zzcr_ast(ast,attr,tok,txt) \ +{ \ + (ast)->filename = InputFilename; \ + (ast)->line = (attr)->line; \ + (ast)->offset = (attr)->offset; \ + (ast)->text = strdup ((attr)->text); \ +} + +#define zzd_ast(ast) \ +/* printf ("zzd_ast: free'ing ast node with string %p (%s)\n", \ + (ast)->text, (ast)->text); */ \ + if ((ast)->text != NULL) free ((ast)->text); + + +#ifdef USER_DEFINED_AST +typedef struct _ast +{ + struct _ast *right, *down; + char * filename; + int line; + int offset; + bt_nodetype nodetype; + bt_metatype metatype; + char * text; +} AST; +#endif /* USER_DEFINED_AST */ + + +typedef struct +{ + /* + * `string' is the string that has been split; items[0] ... + * items[num_items-1] are pointers into `string', or NULL for empty + * substrings. Note that `string' is actually a copy of the string + * passed in to bt_split_list() with NULs inserted between substrings. + */ + + char * string; + int num_items; + char ** items; +} bt_stringlist; + + +typedef struct +{ + bt_stringlist * tokens; /* flat list of all tokens in name */ + char ** parts[BT_MAX_NAMEPARTS]; /* each elt. is list of pointers */ + /* into `tokens->string' */ + int part_len[BT_MAX_NAMEPARTS]; /* length in tokens */ +} bt_name; + + +typedef struct tex_tree_s +{ + char * start; + int len; + struct tex_tree_s + * child, + * next; +} bt_tex_tree; + + +typedef struct +{ + /* These determine the order (and presence) of parts in the name. */ + int num_parts; + bt_namepart parts[BT_MAX_NAMEPARTS]; + + /* + * These lists are always in the order of the bt_namepart enum -- *not* + * dependent on the particular order of parts the user specified! (This + * will make it a bit harder if I ever allow more than one occurrence of + * a part in a format; since I don't allow that, I'm not [yet] worried + * about it!) + */ + const char * pre_part[BT_MAX_NAMEPARTS]; + char * post_part[BT_MAX_NAMEPARTS]; + char * pre_token[BT_MAX_NAMEPARTS]; + const char * post_token[BT_MAX_NAMEPARTS]; + boolean abbrev[BT_MAX_NAMEPARTS]; + bt_joinmethod join_tokens[BT_MAX_NAMEPARTS]; + bt_joinmethod join_part[BT_MAX_NAMEPARTS]; +} bt_name_format; + + +typedef enum +{ + BTERR_NOTIFY, /* notification about next action */ + BTERR_CONTENT, /* warning about the content of a record */ + BTERR_LEXWARN, /* warning in lexical analysis */ + BTERR_USAGEWARN, /* warning about library usage */ + BTERR_LEXERR, /* error in lexical analysis */ + BTERR_SYNTAX, /* error in parser */ + BTERR_USAGEERR, /* fatal error in library usage */ + BTERR_INTERNAL /* my fault */ +} bt_errclass; + +typedef enum +{ + BTACT_NONE, /* do nothing on error */ + BTACT_CRASH, /* call exit(1) */ + BTACT_ABORT /* call abort() */ +} bt_erraction; + +typedef struct +{ + bt_errclass errclass; + char * filename; + int line; + const char * item_desc; + int item; + char * message; +} bt_error; + +typedef void (*bt_err_handler) (bt_error *); + + +#if defined(__cplusplus__) || defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/* Function prototypes */ + +/* + * First, we might need a prototype for strdup() (because the zzcr_ast + * macro uses it, and that macro is used in pccts/ast.c -- which I don't + * want to modify if I can help it, because it's someone else's code). + * This is to accomodate AIX, where including <string.h> apparently doesn't + * declare strdup() (reported by Reiner Schlotte + * <schlotte@geo.palmod.uni-bremen.de>), and compiling bibtex.c (which + * includes pccts/ast.c) crashes because of this (yes, yes, I know it + * should just be a warning -- I don't know what's going on there!). + * + * Unfortunately, this duplicates code in bt_config.h -- I can't include + * bt_config.h here, because this header must be freestanding; I don't want + * to include bt_config.h in pccts/ast.c, because I don't want to touch the + * PCCTS code if I can help it; but I don't want every source file that + * uses strdup() to have to include btparse.h. Hence the duplication. + * Yuck. + */ +#ifndef HAVE_STRDUP_DECL +# define HAVE_STRDUP_DECL 0 +#endif +#if !HAVE_STRDUP_DECL +extern char *strdup (const char *s); +#endif + + +/* init.c */ +void bt_initialize (void); +void bt_free_ast (AST *ast); +void bt_cleanup (void); + +/* input.c */ +void bt_set_stringopts (bt_metatype metatype, ushort options); +AST * bt_parse_entry_s (char * entry_text, + char * filename, + int line, + ushort options, + boolean * status); +AST * bt_parse_entry (FILE * infile, + char * filename, + ushort options, + boolean * status); +AST * bt_parse_file (char * filename, + ushort options, + boolean * overall_status); + +/* postprocess.c */ +void bt_postprocess_string (char * s, ushort options); +char * bt_postprocess_value (AST * value, ushort options, boolean replace); +char * bt_postprocess_field (AST * field, ushort options, boolean replace); +void bt_postprocess_entry (AST * entry, ushort options); + +/* error.c */ +void bt_reset_error_counts (void); +int bt_get_error_count (bt_errclass errclass); +int * bt_get_error_counts (int *counts); +ushort bt_error_status (int *saved_counts); + +/* macros.c */ +void bt_add_macro_value (AST *assignment, ushort options); +void bt_add_macro_text (char * macro, char * text, char * filename, int line); +void bt_delete_macro (char * macro); +void bt_delete_all_macros (void); +int bt_macro_length (char *macro); +char * bt_macro_text (char * macro, char * filename, int line); + +/* traversal.c */ +AST *bt_next_entry (AST *entry_list, AST *prev_entry); +bt_metatype bt_entry_metatype (AST *entry); +char *bt_entry_type (AST *entry); +char *bt_entry_key (AST *entry); +AST *bt_next_field (AST *entry, AST *prev, char **name); +AST *bt_next_macro (AST *entry, AST *prev, char **name); +AST *bt_next_value (AST *head, + AST *prev, + bt_nodetype *nodetype, + char **text); +char *bt_get_text (AST *node); + +/* modify.c */ +void bt_set_text (AST * node, char * new_text); +void bt_entry_set_key (AST * entry, char * new_key); + +/* names.c */ +bt_stringlist * bt_split_list (char * string, + char * delim, + char * filename, + int line, + char * description); +void bt_free_list (bt_stringlist *list); +bt_name * bt_split_name (char * name, + char * filename, + int line, + int name_num); +void bt_free_name (bt_name * name); + +/* tex_tree.c */ +bt_tex_tree * bt_build_tex_tree (char * string); +void bt_free_tex_tree (bt_tex_tree **top); +void bt_dump_tex_tree (bt_tex_tree *node, int depth, FILE *stream); +char * bt_flatten_tex_tree (bt_tex_tree *top); + +/* string_util.c */ +void bt_purify_string (char * string, ushort options); +void bt_change_case (char transform, char * string, ushort options); + +/* format_name.c */ +bt_name_format * bt_create_name_format (char * parts, boolean abbrev_first); +void bt_free_name_format (bt_name_format * format); +void bt_set_format_text (bt_name_format * format, + bt_namepart part, + char * pre_part, + char * post_part, + char * pre_token, + char * post_token); +void bt_set_format_options (bt_name_format * format, + bt_namepart part, + boolean abbrev, + bt_joinmethod join_tokens, + bt_joinmethod join_part); +char * bt_format_name (bt_name * name, bt_name_format * format); + +#if defined(__cplusplus__) || defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* BTPARSE_H */ diff --git a/src/translators/btparse/dlgauto.h b/src/translators/btparse/dlgauto.h new file mode 100644 index 0000000..efcc3b2 --- /dev/null +++ b/src/translators/btparse/dlgauto.h @@ -0,0 +1,408 @@ +/* dlgauto.h automaton + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * ANTLR 1.33 + * Will Cohen and Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ + +#ifndef ZZDEFAUTO_H +#define ZZDEFAUTO_H + +zzchar_t *zzlextext; /* text of most recently matched token */ +zzchar_t *zzbegexpr; /* beginning of last reg expr recogn. */ +zzchar_t *zzendexpr; /* beginning of last reg expr recogn. */ +int zzbufsize; /* number of characters in zzlextext */ +int zzbegcol = 0; /* column that first character of token is in*/ +int zzendcol = 0; /* column that last character of token is in */ +int zzline = 1; /* line current token is on */ +int zzreal_line=1; /* line of 1st portion of token that is not skipped */ +int zzchar; /* character to determine next state */ +int zzbufovf; /* indicates that buffer too small for text */ +int zzcharfull = 0; +static zzchar_t *zznextpos;/* points to next available position in zzlextext*/ +static int zzclass; + +void zzerrstd(const char *); +void (*zzerr)(const char *)=zzerrstd;/* pointer to error reporting function */ +extern int zzerr_in(void); + +static FILE *zzstream_in=0; +static int (*zzfunc_in)() = zzerr_in; +static zzchar_t *zzstr_in=0; + +#ifdef USER_ZZMODE_STACK +int zzauto = 0; +#else +static int zzauto = 0; +#endif +static int zzadd_erase; +static char zzebuf[70]; + +#ifdef ZZCOL +#define ZZINC (++zzendcol) +#else +#define ZZINC +#endif + + +#define ZZGETC_STREAM {zzchar = getc(zzstream_in); zzclass = ZZSHIFT(zzchar);} +#define ZZGETC_FUNC {zzchar = (*zzfunc_in)(); zzclass = ZZSHIFT(zzchar);} +#define ZZGETC_STR { \ + if (*zzstr_in){ \ + zzchar = *zzstr_in; \ + ++zzstr_in; \ + }else{ \ + zzchar = EOF; \ + } \ + zzclass = ZZSHIFT(zzchar); \ +} + +#define ZZNEWSTATE (newstate = dfa[state][zzclass]) + +#ifndef ZZCOPY +#define ZZCOPY \ + /* Truncate matching buffer to size (not an error) */ \ + if (zznextpos < lastpos){ \ + *(zznextpos++) = zzchar; \ + }else{ \ + zzbufovf = 1; \ + } +#endif + +void +zzrdstream( FILE *f ) +{ + /* make sure that it is really set to something, otherwise just + leave it be. + */ + if (f){ + /* make sure that there is always someplace to get input + before closing zzstream_in + */ + zzline = 1; + zzstream_in = f; + zzfunc_in = NULL; + zzstr_in = 0; + zzcharfull = 0; + } +} + +void +zzrdfunc( int (*f)() ) +{ + /* make sure that it is really set to something, otherwise just + leave it be. + */ + if (f){ + /* make sure that there is always someplace to get input + before closing zzstream_in + */ + zzline = 1; + zzstream_in = NULL; + zzfunc_in = f; + zzstr_in = 0; + zzcharfull = 0; + } +} + + +void +zzrdstr( zzchar_t *s ) +{ + /* make sure that it is really set to something, otherwise just + leave it be. + */ + if (s){ + /* make sure that there is always someplace to get input + before closing zzstream_in + */ + zzline = 1; + zzstream_in = NULL; + zzfunc_in = 0; + zzstr_in = s; + zzcharfull = 0; + } +} + + +void +zzclose_stream() +{ +} + +/* saves dlg state, but not what feeds dlg (such as file position) */ +void +zzsave_dlg_state(struct zzdlg_state *state) +{ + state->stream = zzstream_in; + state->func_ptr = zzfunc_in; + state->str = zzstr_in; + state->auto_num = zzauto; + state->add_erase = zzadd_erase; + state->lookc = zzchar; + state->char_full = zzcharfull; + state->begcol = zzbegcol; + state->endcol = zzendcol; + state->line = zzline; + state->lextext = zzlextext; + state->begexpr = zzbegexpr; + state->endexpr = zzendexpr; + state->bufsize = zzbufsize; + state->bufovf = zzbufovf; + state->nextpos = zznextpos; + state->class_num = zzclass; +} + +void +zzrestore_dlg_state(struct zzdlg_state *state) +{ + zzstream_in = state->stream; + zzfunc_in = state->func_ptr; + zzstr_in = state->str; + zzauto = state->auto_num; + zzadd_erase = state->add_erase; + zzchar = state->lookc; + zzcharfull = state->char_full; + zzbegcol = state->begcol; + zzendcol = state->endcol; + zzline = state->line; + zzlextext = state->lextext; + zzbegexpr = state->begexpr; + zzendexpr = state->endexpr; + zzbufsize = state->bufsize; + zzbufovf = state->bufovf; + zznextpos = state->nextpos; + zzclass = state->class_num; +} + +void +zzmode( int m ) +{ + /* points to base of dfa table */ + if (m<MAX_MODE){ + zzauto = m; + /* have to redo class since using different compression */ + zzclass = ZZSHIFT(zzchar); + }else{ + sprintf(zzebuf,"Invalid automaton mode = %d ",m); + zzerr(zzebuf); + } +} + +/* erase what is currently in the buffer, and get a new reg. expr */ +void +zzskip() +{ + zzadd_erase = 1; +} + +/* don't erase what is in the zzlextext buffer, add on to it */ +void +zzmore() +{ + zzadd_erase = 2; +} + +/* substitute c for the reg. expr last matched and is in the buffer */ +void +zzreplchar(zzchar_t c) +{ + /* can't allow overwriting null at end of string */ + if (zzbegexpr < &zzlextext[zzbufsize-1]){ + *zzbegexpr = c; + *(zzbegexpr+1) = '\0'; + } + zzendexpr = zzbegexpr; + zznextpos = zzbegexpr + 1; +} + +/* replace the string s for the reg. expr last matched and in the buffer */ +void +zzreplstr(register zzchar_t *s) +{ + register zzchar_t *l= &zzlextext[zzbufsize -1]; + + zznextpos = zzbegexpr; + if (s){ + while ((zznextpos <= l) && (*(zznextpos++) = *(s++))!=0){ + /* empty */ + } + /* correct for NULL at end of string */ + zznextpos--; + } + if ((zznextpos <= l) && (*(--s) == 0)){ + zzbufovf = 0; + }else{ + zzbufovf = 1; + } + *(zznextpos) = '\0'; + zzendexpr = zznextpos - 1; +} + +void +zzgettok() +{ + register int state, newstate; + /* last space reserved for the null char */ + zzchar_t *lastpos; /* GPW 1997/09/05 (removed 'register' */ + +skip: + zzreal_line = zzline; + zzbufovf = 0; + lastpos = &zzlextext[zzbufsize-1]; + zznextpos = zzlextext; + zzbegcol = zzendcol+1; +more: + zzbegexpr = zznextpos; +#ifdef ZZINTERACTIVE + /* interactive version of automaton */ + /* if there is something in zzchar, process it */ + state = newstate = dfa_base[zzauto]; + if (zzcharfull){ + ZZINC; + ZZCOPY; + ZZNEWSTATE; + } + if (zzstr_in) + while (zzalternatives[newstate]){ + state = newstate; + ZZGETC_STR; + ZZINC; + ZZCOPY; + ZZNEWSTATE; + } + else if (zzstream_in) + while (zzalternatives[newstate]){ + state = newstate; + ZZGETC_STREAM; + ZZINC; + ZZCOPY; + ZZNEWSTATE; + } + else if (zzfunc_in) + while (zzalternatives[newstate]){ + state = newstate; + ZZGETC_FUNC; + ZZINC; + ZZCOPY; + ZZNEWSTATE; + } + /* figure out if last character really part of token */ + if ((state != dfa_base[zzauto]) && (newstate == DfaStates)){ + zzcharfull = 1; + --zznextpos; + }else{ + zzcharfull = 0; + state = newstate; + } + *(zznextpos) = '\0'; + /* Able to transition out of start state to some non err state?*/ + if ( state == dfa_base[zzauto] ){ + /* make sure doesn't get stuck */ + zzadvance(); + } +#else + /* non-interactive version of automaton */ + if (!zzcharfull) + zzadvance(); + else + ZZINC; + state = dfa_base[zzauto]; + if (zzstr_in) + while (ZZNEWSTATE != DfaStates){ + state = newstate; + ZZCOPY; + ZZGETC_STR; + ZZINC; + } + else if (zzstream_in) + while (ZZNEWSTATE != DfaStates){ + state = newstate; + ZZCOPY; + ZZGETC_STREAM; + ZZINC; + } + else if (zzfunc_in) + while (ZZNEWSTATE != DfaStates){ + state = newstate; + ZZCOPY; + ZZGETC_FUNC; + ZZINC; + } + zzcharfull = 1; + if ( state == dfa_base[zzauto] ){ + if (zznextpos < lastpos){ + *(zznextpos++) = zzchar; + }else{ + zzbufovf = 1; + } + *zznextpos = '\0'; + /* make sure doesn't get stuck */ + zzadvance(); + }else{ + *zznextpos = '\0'; + } +#endif +#ifdef ZZCOL + zzendcol -= zzcharfull; +#endif + zzendexpr = zznextpos -1; + zzadd_erase = 0; + (*actions[accepts[state]])(); + switch (zzadd_erase) { + case 1: goto skip; + case 2: goto more; + } +} + +void +zzadvance() +{ + if (zzstream_in) { ZZGETC_STREAM; zzcharfull = 1; ZZINC;} + if (zzfunc_in) { ZZGETC_FUNC; zzcharfull = 1; ZZINC;} + if (zzstr_in) { ZZGETC_STR; zzcharfull = 1; ZZINC;} + if (!(zzstream_in || zzfunc_in || zzstr_in)){ + zzerr_in(); + } +} + +void +zzerrstd(const char *s) +{ + fprintf(stderr, + "%s near line %d (text was '%s')\n", + ((s == NULL) ? "Lexical error" : s), + zzline,zzlextext); +} + +int +zzerr_in() +{ + fprintf(stderr,"No input stream, function, or string\n"); + /* return eof to get out gracefully */ + return EOF; +} + +#endif diff --git a/src/translators/btparse/dlgdef.h b/src/translators/btparse/dlgdef.h new file mode 100644 index 0000000..ded2c31 --- /dev/null +++ b/src/translators/btparse/dlgdef.h @@ -0,0 +1,97 @@ +/* dlgdef.h + * Things in scanner produced by dlg that should be visible to the outside + * world + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * ANTLR 1.33 + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ + +#ifndef ZZDLGDEF_H +#define ZZDLGDEF_H + +#include "btconfig.h" + +#ifndef zzchar_t +#ifdef ZZWCHAR_T +#define zzchar_t unsigned wchar_t +#else +#define zzchar_t unsigned char +#endif +#endif + +struct zzdlg_state { + FILE *stream; + int (*func_ptr)(); + zzchar_t *str; + int auto_num; + int add_erase; + int lookc; + int char_full; + int begcol, endcol; + int line; + zzchar_t *lextext, *begexpr, *endexpr; + int bufsize; + int bufovf; + zzchar_t *nextpos; + int class_num; +}; + +extern zzchar_t *zzlextext; /* text of most recently matched token */ +extern zzchar_t *zzbegexpr; /* beginning of last reg expr recogn. */ +extern zzchar_t *zzendexpr; /* beginning of last reg expr recogn. */ +extern int zzbufsize; /* how long zzlextext is */ +extern int zzbegcol; /* column that first character of token is in*/ +extern int zzendcol; /* column that last character of token is in */ +extern int zzline; /* line current token is on */ +extern int zzreal_line; /* line of 1st portion of token that is not skipped */ +extern int zzchar; /* character to determine next state */ +extern int zzbufovf; /* indicates that buffer too small for text */ +extern void (*zzerr)(const char *);/* pointer to error reporting function */ + +#ifdef USER_ZZMODE_STACK +extern int zzauto; +#endif + +extern void zzadvance(void); +extern void zzskip(void); /* erase zzlextext, look for antoher token */ +extern void zzmore(void); /* keep zzlextext, look for another token */ +extern void zzmode(int k); /* switch to automaton 'k' */ +extern void zzrdstream(FILE *);/* what stream to read from */ +extern void zzclose_stream(void);/* close the current input stream */ +extern void zzrdfunc(int (*)());/* what function to get char from */ +extern void zzrdstr( zzchar_t * ); +extern void zzgettok(void); /* get next token */ +extern void zzreplchar(zzchar_t c);/* replace last recognized reg. expr. with + a character */ +extern void zzreplstr(zzchar_t *s);/* replace last recognized reg. expr. with + a string */ +extern void zzsave_dlg_state(struct zzdlg_state *); +extern void zzrestore_dlg_state(struct zzdlg_state *); +extern int zzerr_in(void); +extern void zzerrstd(const char *); +extern void zzerraction(); + +#endif diff --git a/src/translators/btparse/err.c b/src/translators/btparse/err.c new file mode 100644 index 0000000..f143048 --- /dev/null +++ b/src/translators/btparse/err.c @@ -0,0 +1,75 @@ +/* + * A n t l r S e t s / E r r o r F i l e H e a d e r + * + * Generated from: bibtex.g + * + * Terence Parr, Russell Quong, Will Cohen, and Hank Dietz: 1989-1995 + * Parr Research Corporation + * with Purdue University Electrical Engineering + * With AHPCRC, University of Minnesota + * ANTLR Version 1.33 + */ + +#include <stdio.h> +#define ANTLR_VERSION 133 + +#define ZZCOL +#define USER_ZZSYN + +#include "btconfig.h" +#include "btparse.h" +#include "attrib.h" +#include "lex_auxiliary.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + +extern char * InputFilename; /* for zzcr_ast call in pccts/ast.c */ +#define zzSET_SIZE 4 +#include "antlr.h" +#include "ast.h" +#include "tokens.h" +#include "dlgdef.h" +#include "err.h" + +const ANTLRChar *zztokens[27]={ + /* 00 */ "Invalid", + /* 01 */ "@", + /* 02 */ "AT", + /* 03 */ "\\n", + /* 04 */ "COMMENT", + /* 05 */ "[\\ \\r\\t]+", + /* 06 */ "~[\\@\\n\\ \\r\\t]+", + /* 07 */ "\\n", + /* 08 */ "[\\ \\r\\t]+", + /* 09 */ "NUMBER", + /* 10 */ "NAME", + /* 11 */ "LBRACE", + /* 12 */ "RBRACE", + /* 13 */ "ENTRY_OPEN", + /* 14 */ "ENTRY_CLOSE", + /* 15 */ "EQUALS", + /* 16 */ "HASH", + /* 17 */ "COMMA", + /* 18 */ "\"", + /* 19 */ "\\n~[\\n\\{\\}\\(\\)\"\\]*", + /* 20 */ "[\\r\\t]", + /* 21 */ "\\{", + /* 22 */ "\\}", + /* 23 */ "\\(", + /* 24 */ "\\)", + /* 25 */ "STRING", + /* 26 */ "~[\\n\\{\\}\\(\\)\"]+" +}; +SetWordType zzerr1[4] = {0x0,0x20,0x0,0x2}; +SetWordType zzerr2[4] = {0x0,0x6,0x0,0x0}; +SetWordType zzerr3[4] = {0x0,0x46,0x0,0x2}; +SetWordType zzerr4[4] = {0x0,0x44,0x0,0x0}; +SetWordType setwd1[27] = {0x0,0x7,0x6,0x0,0x0,0x0,0x0, + 0x0,0x0,0x28,0x38,0x0,0x0,0x0,0xd0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x20,0x0}; +SetWordType zzerr5[4] = {0x0,0x6,0x0,0x2}; +SetWordType setwd2[27] = {0x0,0x0,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x7, + 0x0,0x4,0x7,0x0,0x0,0x0,0x0,0x0, + 0x0,0x0,0x0,0x0}; diff --git a/src/translators/btparse/err.h b/src/translators/btparse/err.h new file mode 100644 index 0000000..d16615d --- /dev/null +++ b/src/translators/btparse/err.h @@ -0,0 +1,700 @@ +/* + * err.h + * + * Standard error handling mechanism + * + * SOFTWARE RIGHTS + * + * We reserve no LEGAL rights to the Purdue Compiler Construction Tool + * Set (PCCTS) -- PCCTS is in the public domain. An individual or + * company may do whatever they wish with source code distributed with + * PCCTS or the code generated by PCCTS, including the incorporation of + * PCCTS, or its output, into commerical software. + * + * We encourage users to develop software with PCCTS. However, we do ask + * that credit is given to us for developing PCCTS. By "credit", + * we mean that if you incorporate our source code into one of your + * programs (commercial product, research project, or otherwise) that you + * acknowledge this fact somewhere in the documentation, research report, + * etc... If you like PCCTS and have developed a nice tool with the + * output, please mention that you developed it using PCCTS. In + * addition, we ask that this header remain intact in our source code. + * As long as these guidelines are kept, we expect to continue enhancing + * this system and expect to make other tools available as they are + * completed. + * + * Has grown to hold all kinds of stuff (err.h is increasingly misnamed) + * + * ANTLR 1.33 + * Terence Parr + * Parr Research Corporation + * with Purdue University and AHPCRC, University of Minnesota + * 1989-1995 + */ + +#ifndef ERR_H +#define ERR_H + +#include "btconfig.h" + +#include <string.h> +#include <stdarg.h> + +#ifdef DUM +/* Define usable bits per unsigned int word (used for set stuff) */ +#ifdef PC +#define BSETWORDSIZE 16 +#define BSETLOGWORDSIZE 4 +#else +#define BSETWORDSIZE 32 +#define BSETLOGWORDSIZE 5 +#endif +#endif + +#define BSETWORDSIZE 8 +#define BSETLOGWORDSIZE 3 /* SetWordType is 8bits */ + +#define BSETMODWORD(x) ((x) & (BSETWORDSIZE-1)) /* x % BSETWORDSIZE */ +#define BSETDIVWORD(x) ((x) >> BSETLOGWORDSIZE) /* x / BSETWORDSIZE */ + +/* This is not put into the global pccts_parser structure because it is + * hidden and does not need to be saved during a "save state" operation + */ +/* maximum of 32 bits/unsigned int and must be 8 bits/byte */ +static SetWordType bitmask[] = { + 0x00000001, 0x00000002, 0x00000004, 0x00000008, + 0x00000010, 0x00000020, 0x00000040, 0x00000080 +}; + +void +zzresynch(SetWordType *wd,SetWordType mask) +{ + static int consumed = 1; + + /* if you enter here without having consumed a token from last resynch + * force a token consumption. + */ + if ( !consumed ) {zzCONSUME; return;} + + /* if current token is in resynch set, we've got what we wanted */ + if ( wd[LA(1)]&mask || LA(1) == zzEOF_TOKEN ) {consumed=0; return;} + + /* scan until we find something in the resynch set */ + while ( !(wd[LA(1)]&mask) && LA(1) != zzEOF_TOKEN ) {zzCONSUME;} + consumed=1; +} + +void +zzconsumeUntil(SetWordType *st) +{ + while ( !zzset_el(LA(1), st) ) { zzCONSUME; } +} + +void +zzconsumeUntilToken(int t) +{ + while ( LA(1)!=t ) { zzCONSUME; } +} + +/* input looks like: + * zzFAIL(k, e1, e2, ...,&zzMissSet,&zzMissText,&zzBadTok,&zzBadText) + * where the zzMiss stuff is set here to the token that did not match + * (and which set wasn't it a member of). + */ +void +zzFAIL(int k, ...) +{ +#ifdef LL_K + static char text[LL_K*ZZLEXBUFSIZE+1]; + SetWordType *f[LL_K]; +#else + static char text[ZZLEXBUFSIZE+1]; + SetWordType *f[1]; +#endif + SetWordType **miss_set; + char **miss_text; + int *bad_tok; + char **bad_text; + int *err_k; + int i; + va_list ap; +/* Removed because it shadows a parameter. gcc 3.4 complains. + I think removing it preserves the behavior of gcc 3.3 and previous. + int k; +*/ + va_start(ap, k); + text[0] = '\0'; + for (i=1; i<=k; i++) /* collect all lookahead sets */ + { + f[i-1] = va_arg(ap, SetWordType *); + } + for (i=1; i<=k; i++) /* look for offending token */ + { + if ( i>1 ) strcat(text, " "); + strcat(text, LATEXT(i)); + if ( !zzset_el((unsigned)LA(i), f[i-1]) ) break; + } + miss_set = va_arg(ap, SetWordType **); + miss_text = va_arg(ap, char **); + bad_tok = va_arg(ap, int *); + bad_text = va_arg(ap, char **); + err_k = va_arg(ap, int *); + if ( i>k ) + { + /* bad; lookahead is permutation that cannot be matched, + * but, the ith token of lookahead is valid at the ith position + * (The old LL sub 1 (k) versus LL(k) parsing technique) + */ + *miss_set = NULL; + *miss_text = zzlextext; + *bad_tok = LA(1); + *bad_text = LATEXT(1); + *err_k = k; + return; + } +/* fprintf(stderr, "%s not in %dth set\n", zztokens[LA(i)], i);*/ + *miss_set = f[i-1]; + *miss_text = text; + *bad_tok = LA(i); + *bad_text = LATEXT(i); + if ( i==1 ) *err_k = 1; + else *err_k = k; +} + +void +zzsave_antlr_state(zzantlr_state *buf) +{ +#ifdef LL_K + int i; +#endif + +#ifdef ZZCAN_GUESS + buf->guess_start = zzguess_start; + buf->guessing = zzguessing; +#endif + buf->asp = zzasp; +#ifdef GENAST + buf->ast_sp = zzast_sp; +#endif +#ifdef ZZINF_LOOK + buf->inf_labase = zzinf_labase; + buf->inf_last = zzinf_last; +#endif +#ifdef DEMAND_LOOK + buf->dirty = zzdirty; +#endif +#ifdef LL_K + for (i=0; i<LL_K; i++) buf->tokenLA[i] = zztokenLA[i]; + for (i=0; i<LL_K; i++) strcpy(buf->textLA[i], zztextLA[i]); + buf->lap = zzlap; + buf->labase = zzlabase; +#else + buf->token = zztoken; + strcpy(buf->text, zzlextext); +#endif +} + +void +zzrestore_antlr_state(zzantlr_state *buf) +{ +#ifdef LL_K + int i; +#endif + +#ifdef ZZCAN_GUESS + zzguess_start = buf->guess_start; + zzguessing = buf->guessing; +#endif + zzasp = buf->asp; +#ifdef GENAST + zzast_sp = buf->ast_sp; +#endif +#ifdef ZZINF_LOOK + zzinf_labase = buf->inf_labase; + zzinf_last = buf->inf_last; +#endif +#ifdef DEMAND_LOOK + zzdirty = buf->dirty; +#endif +#ifdef LL_K + for (i=0; i<LL_K; i++) zztokenLA[i] = buf->tokenLA[i]; + for (i=0; i<LL_K; i++) strcpy(zztextLA[i], buf->textLA[i]); + zzlap = buf->lap; + zzlabase = buf->labase; +#else + zztoken = buf->token; + strcpy(zzlextext, buf->text); +#endif +} + +void +zzedecode(SetWordType *a) +{ + register SetWordType *p = a; + register SetWordType *endp = &(p[zzSET_SIZE]); + register unsigned e = 0; + + if ( zzset_deg(a)>1 ) fprintf(stderr, " {"); + do { + register SetWordType t = *p; + register SetWordType *b = &(bitmask[0]); + do { + if ( t & *b ) fprintf(stderr, " %s", zztokens[e]); + e++; + } while (++b < &(bitmask[sizeof(SetWordType)*8])); + } while (++p < endp); + if ( zzset_deg(a)>1 ) fprintf(stderr, " }"); +} + +#ifndef USER_ZZSYN +/* standard error reporting function */ +void +zzsyn(char *text, int tok, char *egroup, SetWordType *eset, int etok, int k, char *bad_text) +{ + + fprintf(stderr, "line %d: syntax error at \"%s\"", zzline, (tok==zzEOF_TOKEN)?"EOF":bad_text); + if ( !etok && !eset ) {fprintf(stderr, "\n"); return;} + if ( k==1 ) fprintf(stderr, " missing"); + else + { + fprintf(stderr, "; \"%s\" not", bad_text); + if ( zzset_deg(eset)>1 ) fprintf(stderr, " in"); + } + if ( zzset_deg(eset)>0 ) zzedecode(eset); + else fprintf(stderr, " %s", zztokens[etok]); + if ( strlen(egroup) > 0 ) fprintf(stderr, " in %s", egroup); + fprintf(stderr, "\n"); +} +#endif + +/* is b an element of set p? */ +int +zzset_el(unsigned b, SetWordType *p) +{ + return( p[BSETDIVWORD(b)] & bitmask[BSETMODWORD(b)] ); +} + +int +zzset_deg(SetWordType *a) +{ + /* Fast compute degree of a set... the number + of elements present in the set. Assumes + that all word bits are used in the set + */ + register SetWordType *p = a; + register SetWordType *endp = &(a[zzSET_SIZE]); + register int degree = 0; + + if ( a == NULL ) return 0; + while ( p < endp ) + { + register SetWordType t = *p; + register SetWordType *b = &(bitmask[0]); + do { + if (t & *b) ++degree; + } while (++b < &(bitmask[sizeof(SetWordType)*8])); + p++; + } + + return(degree); +} + +#ifdef DEMAND_LOOK + +#ifdef LL_K +int +_zzmatch(int _t, char **zzBadText, char **zzMissText, + int *zzMissTok, int *zzBadTok, + SetWordType **zzMissSet) +{ + if ( zzdirty==LL_K ) { + zzCONSUME; + } + if ( LA(1)!=_t ) { + *zzBadText = *zzMissText=LATEXT(1); + *zzMissTok= _t; *zzBadTok=LA(1); + *zzMissSet=NULL; + return 0; + } + zzMakeAttr + zzdirty++; + zzlabase++; + return 1; +} + +int +_zzmatch_wsig(int _t) +{ + if ( zzdirty==LL_K ) { + zzCONSUME; + } + if ( LA(1)!=_t ) { + return 0; + } + zzMakeAttr + zzdirty++; + zzlabase++; + return 1; +} + +#else + +int +_zzmatch(int _t, char **zzBadText, char **zzMissText, + int *zzMissTok, int *zzBadTok, SetWordType **zzMissSet) +{ + if ( zzdirty ) {zzCONSUME;} + if ( LA(1)!=_t ) { + *zzBadText = *zzMissText=LATEXT(1); + *zzMissTok= _t; *zzBadTok=LA(1); + *zzMissSet=NULL; + return 0; + } + zzdirty = 1; + zzMakeAttr + return 1; +} + +int +_zzmatch_wsig(int _t) +{ + if ( zzdirty ) {zzCONSUME;} + if ( LA(1)!=_t ) { + return 0; + } + zzdirty = 1; + zzMakeAttr + return 1; +} + +#endif /*LL_K*/ + +#else + +int +_zzmatch(int _t, const char **zzBadText, const char **zzMissText, + int *zzMissTok, int *zzBadTok, + SetWordType **zzMissSet) +{ + if ( LA(1)!=_t ) { + *zzBadText = *zzMissText=LATEXT(1); + *zzMissTok= _t; *zzBadTok=LA(1); + *zzMissSet=NULL; + return 0; + } + zzMakeAttr + return 1; +} + +int +_zzmatch_wsig(int _t) +{ + if ( LA(1)!=_t ) return 0; + zzMakeAttr + return 1; +} + +#endif /*DEMAND_LOOK*/ + +#ifdef ZZINF_LOOK +void +_inf_zzgettok(void) +{ + if ( zzinf_labase >= zzinf_last ) + {NLA = zzEOF_TOKEN; strcpy(NLATEXT, "");} + else { + NLA = zzinf_tokens[zzinf_labase]; + zzline = zzinf_line[zzinf_labase]; /* wrong in 1.21 */ + strcpy(NLATEXT, zzinf_text[zzinf_labase]); + zzinf_labase++; + } +} +#endif + +#ifdef ZZINF_LOOK +/* allocate default size text,token and line arrays; + * then, read all of the input reallocing the arrays as needed. + * Once the number of total tokens is known, the LATEXT(i) array (zzinf_text) + * is allocated and it's pointers are set to the tokens in zzinf_text_buffer. + */ +void +zzfill_inf_look(void) +{ + int tok, line; + int zzinf_token_buffer_size = ZZINF_DEF_TOKEN_BUFFER_SIZE; + int zzinf_text_buffer_size = ZZINF_DEF_TEXT_BUFFER_SIZE; + int zzinf_text_buffer_index = 0; + int zzinf_lap = 0; + + /* allocate text/token buffers */ + zzinf_text_buffer = (char *) malloc(zzinf_text_buffer_size); + if ( zzinf_text_buffer == NULL ) + { + fprintf(stderr, "cannot allocate lookahead text buffer (%d bytes)\n", + zzinf_text_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + zzinf_tokens = (int *) calloc(zzinf_token_buffer_size,sizeof(int)); + if ( zzinf_tokens == NULL ) + { + fprintf(stderr, "cannot allocate token buffer (%d tokens)\n", + zzinf_token_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + zzinf_line = (int *) calloc(zzinf_token_buffer_size,sizeof(int)); + if ( zzinf_line == NULL ) + { + fprintf(stderr, "cannot allocate line buffer (%d ints)\n", + zzinf_token_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + + /* get tokens, copying text to text buffer */ + zzinf_text_buffer_index = 0; + do { + zzgettok(); + line = zzreal_line; + while ( zzinf_lap>=zzinf_token_buffer_size ) + { + zzinf_token_buffer_size += ZZINF_BUFFER_TOKEN_CHUNK_SIZE; + zzinf_tokens = (int *) realloc(zzinf_tokens, + zzinf_token_buffer_size*sizeof(int)); + if ( zzinf_tokens == NULL ) + { + fprintf(stderr, "cannot allocate lookahead token buffer (%d tokens)\n", + zzinf_token_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + zzinf_line = (int *) realloc(zzinf_line, + zzinf_token_buffer_size*sizeof(int)); + if ( zzinf_line == NULL ) + { + fprintf(stderr, "cannot allocate lookahead line buffer (%d ints)\n", + zzinf_token_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + + } + while ( (zzinf_text_buffer_index+strlen(NLATEXT)+1) >= zzinf_text_buffer_size ) + { + zzinf_text_buffer_size += ZZINF_BUFFER_TEXT_CHUNK_SIZE; + zzinf_text_buffer = (char *) realloc(zzinf_text_buffer, + zzinf_text_buffer_size); + if ( zzinf_text_buffer == NULL ) + { + fprintf(stderr, "cannot allocate lookahead text buffer (%d bytes)\n", + zzinf_text_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + } + /* record token and text and line of input symbol */ + tok = zzinf_tokens[zzinf_lap] = NLA; + strcpy(&zzinf_text_buffer[zzinf_text_buffer_index], NLATEXT); + zzinf_text_buffer_index += strlen(NLATEXT)+1; + zzinf_line[zzinf_lap] = line; + zzinf_lap++; + } while (tok!=zzEOF_TOKEN); + zzinf_labase = 0; + zzinf_last = zzinf_lap-1; + + /* allocate ptrs to text of ith token */ + zzinf_text = (char **) calloc(zzinf_last+1,sizeof(char *)); + if ( zzinf_text == NULL ) + { + fprintf(stderr, "cannot allocate lookahead text buffer (%d)\n", + zzinf_text_buffer_size); + exit(PCCTS_EXIT_FAILURE); + } + zzinf_text_buffer_index = 0; + zzinf_lap = 0; + /* set ptrs so that zzinf_text[i] is the text of the ith token found on input */ + while (zzinf_lap<=zzinf_last) + { + zzinf_text[zzinf_lap++] = &zzinf_text_buffer[zzinf_text_buffer_index]; + zzinf_text_buffer_index += strlen(&zzinf_text_buffer[zzinf_text_buffer_index])+1; + } +} +#endif + +int +_zzsetmatch(SetWordType *e, char **zzBadText, char **zzMissText, + int *zzMissTok, int *zzBadTok, + SetWordType **zzMissSet) +{ +#ifdef DEMAND_LOOK +#ifdef LL_K + if ( zzdirty==LL_K ) {zzCONSUME;} +#else + if ( zzdirty ) {zzCONSUME;} +#endif +#endif + if ( !zzset_el((unsigned)LA(1), e) ) { + *zzBadText = LATEXT(1); *zzMissText=NULL; + *zzMissTok= 0; *zzBadTok=LA(1); + *zzMissSet=e; + return 0; + } +#ifdef DEMAND_LOOK +#ifdef LL_K + zzdirty++; +#else + zzdirty = 1; +#endif +#endif + zzMakeAttr + return 1; +} + +int +_zzmatch_wdfltsig(int tokenWanted, SetWordType *whatFollows) +{ +#ifdef DEMAND_LOOK +#ifdef LL_K + if ( zzdirty==LL_K ) { + zzCONSUME; + } +#else + if ( zzdirty ) {zzCONSUME;} +#endif +#endif + + if ( LA(1)!=tokenWanted ) + { + fprintf(stderr, + "line %d: syntax error at \"%s\" missing %s\n", + zzline, + (LA(1)==zzEOF_TOKEN)?"<eof>":(char*)LATEXT(1), + zztokens[tokenWanted]); + zzconsumeUntil( whatFollows ); + return 0; + } + else { + zzMakeAttr +#ifdef DEMAND_LOOK +#ifdef LL_K + zzdirty++; + zzlabase++; +#else + zzdirty = 1; +#endif +#else +/* zzCONSUME; consume if not demand lookahead */ +#endif + return 1; + } +} + +int +_zzsetmatch_wdfltsig(SetWordType *tokensWanted, + int tokenTypeOfSet, + SetWordType *whatFollows) +{ +#ifdef DEMAND_LOOK +#ifdef LL_K + if ( zzdirty==LL_K ) {zzCONSUME;} +#else + if ( zzdirty ) {zzCONSUME;} +#endif +#endif + if ( !zzset_el((unsigned)LA(1), tokensWanted) ) + { + fprintf(stderr, + "line %d: syntax error at \"%s\" missing %s\n", + zzline, + (LA(1)==zzEOF_TOKEN)?"<eof>":(char*)LATEXT(1), + zztokens[tokenTypeOfSet]); + zzconsumeUntil( whatFollows ); + return 0; + } + else { + zzMakeAttr +#ifdef DEMAND_LOOK +#ifdef LL_K + zzdirty++; + zzlabase++; +#else + zzdirty = 1; +#endif +#else +/* zzCONSUME; consume if not demand lookahead */ +#endif + return 1; + } +} + +int +_zzsetmatch_wsig(SetWordType *e) +{ +#ifdef DEMAND_LOOK +#ifdef LL_K + if ( zzdirty==LL_K ) {zzCONSUME;} +#else + if ( zzdirty ) {zzCONSUME;} +#endif +#endif + if ( !zzset_el((unsigned)LA(1), e) ) return 0; +#ifdef DEMAND_LOOK +#ifdef LL_K + zzdirty++; +#else + zzdirty = 1; +#endif +#endif + zzMakeAttr + return 1; +} + +#ifdef USER_ZZMODE_STACK +static int zzmstk[ZZMAXSTK] = { -1 }; +static int zzmdep = 0; +static char zzmbuf[70]; + +void +zzmpush( int m ) +{ + if(zzmdep == ZZMAXSTK - 1) { + sprintf(zzmbuf, "Mode stack overflow "); + zzerr(zzmbuf); + } else { + zzmstk[zzmdep++] = zzauto; + zzmode(m); + } +} + +void +zzmpop( void ) +{ + if(zzmdep == 0) + { sprintf(zzmbuf, "Mode stack underflow "); + zzerr(zzmbuf); + } + else + { zzmdep--; + zzmode(zzmstk[zzmdep]); + } +} + +void +zzsave_mode_stack( int modeStack[], int *modeLevel ) +{ + int i; + memcpy(modeStack, zzmstk, sizeof(zzmstk)); + *modeLevel = zzmdep; + zzmdep = 0; + + return; +} + +void +zzrestore_mode_stack( int modeStack[], int *modeLevel ) +{ + int i; + + memcpy(zzmstk, modeStack, sizeof(zzmstk)); + zzmdep = *modeLevel; + + return; +} +#endif /* USER_ZZMODE_STACK */ + +#endif /* ERR_H */ diff --git a/src/translators/btparse/error.c b/src/translators/btparse/error.c new file mode 100644 index 0000000..26f2fb2 --- /dev/null +++ b/src/translators/btparse/error.c @@ -0,0 +1,348 @@ +/* ------------------------------------------------------------------------ +@NAME : error.c +@DESCRIPTION: Anything relating to reporting or recording errors and + warnings. +@GLOBALS : errclass_names + err_actions + err_handlers + errclass_counts + error_buf +@CALLS : +@CREATED : 1996/08/28, Greg Ward +@MODIFIED : +@VERSION : $Id: error.c,v 2.5 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include "btparse.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + + +#define NUM_ERRCLASSES ((int) BTERR_INTERNAL + 1) + + +static const char *errclass_names[NUM_ERRCLASSES] = +{ + NULL, /* BTERR_NOTIFY */ + "warning", /* BTERR_CONTENT */ + "warning", /* BTERR_LEXWARN */ + "warning", /* BTERR_USAGEWARN */ + "error", /* BTERR_LEXERR */ + "syntax error", /* BTERR_SYNTAX */ + "fatal error", /* BTERR_USAGEERR */ + "internal error" /* BTERR_INTERNAL */ +}; + +static const bt_erraction err_actions[NUM_ERRCLASSES] = +{ + BTACT_NONE, /* BTERR_NOTIFY */ + BTACT_NONE, /* BTERR_CONTENT */ + BTACT_NONE, /* BTERR_LEXWARN */ + BTACT_NONE, /* BTERR_USAGEWARN */ + BTACT_NONE, /* BTERR_LEXERR */ + BTACT_NONE, /* BTERR_SYNTAX */ + BTACT_CRASH, /* BTERR_USAGEERR */ + BTACT_ABORT /* BTERR_INTERNAL */ +}; + +void print_error (bt_error *err); + +static bt_err_handler err_handlers[NUM_ERRCLASSES] = +{ + print_error, + print_error, + print_error, + print_error, + print_error, + print_error, + print_error, + print_error +}; + +static int errclass_counts[NUM_ERRCLASSES] = { 0, 0, 0, 0, 0, 0, 0, 0 }; +static char error_buf[MAX_ERROR+1]; + + +/* ---------------------------------------------------------------------- + * Error-handling functions. + */ + +void print_error (bt_error *err) +{ + const char * name; + boolean something_printed; + + something_printed = FALSE; + + if (err->filename) + { + fprintf (stderr, err->filename); + something_printed = TRUE; + } + if (err->line > 0) /* going to print a line number? */ + { + if (something_printed) + fprintf (stderr, ", "); + fprintf (stderr, "line %d", err->line); + something_printed = TRUE; + } + if (err->item_desc && err->item > 0) /* going to print an item number? */ + { + if (something_printed) + fprintf (stderr, ", "); + fprintf (stderr, "%s %d", err->item_desc, err->item); + something_printed = TRUE; + } + + name = errclass_names[(int) err->errclass]; + if (name) + { + if (something_printed) + fprintf (stderr, ", "); + fprintf (stderr, name); + something_printed = TRUE; + } + + if (something_printed) + fprintf (stderr, ": "); + + fprintf (stderr, "%s\n", err->message); + +} /* print_error() */ + + + +/* ---------------------------------------------------------------------- + * Error-reporting functions: these are called anywhere in the library + * when we encounter an error. + */ + +void +report_error (bt_errclass errclass, + char * filename, + int line, + const char * item_desc, + int item, + const char * fmt, + va_list arglist) +{ + bt_error err; +#if !HAVE_VSNPRINTF + int msg_len; +#endif + + err.errclass = errclass; + err.filename = filename; + err.line = line; + err.item_desc = item_desc; + err.item = item; + + errclass_counts[(int) errclass]++; + + + /* + * Blech -- we're writing to a static buffer because there's no easy + * way to know how long the error message is going to be. (Short of + * reimplementing printf(), or maybe printf()'ing to a dummy file + * and using the return value -- ugh!) The GNU C library conveniently + * supplies vsnprintf(), which neatly solves this problem by truncating + * the output string if it gets too long. (I could check for this + * truncation if I wanted to, but I don't think it's necessary given the + * ample size of the message buffer.) For non-GNU systems, though, + * we're stuck with using vsprintf()'s return value. This can't be + * trusted on all systems -- thus there's a check for it in configure. + * Also, this won't necessarily trigger the internal_error() if we + * do overflow; it's conceivable that vsprintf() itself would crash. + * At least doing it this way we avoid the possibility of vsprintf() + * silently corrupting some memory, and crashing unpredictably at some + * later point. + */ + +#if HAVE_VSNPRINTF + vsnprintf (error_buf, MAX_ERROR, fmt, arglist); +#else + msg_len = vsprintf (error_buf, fmt, arglist); + if (msg_len > MAX_ERROR) + internal_error ("static error message buffer overflowed"); +#endif + + err.message = error_buf; + if (err_handlers[errclass]) + (*err_handlers[errclass]) (&err); + + switch (err_actions[errclass]) + { + case BTACT_NONE: return; + case BTACT_CRASH: exit (1); + case BTACT_ABORT: abort (); + default: internal_error ("invalid error action %d for class %d (%s)", + (int) err_actions[errclass], + (int) errclass, errclass_names[errclass]); + } + +} /* report_error() */ + + +GEN_ERRFUNC (general_error, + (bt_errclass errclass, + char * filename, + int line, + const char * item_desc, + int item, + char * fmt, + ...), + errclass, filename, line, item_desc, item, fmt) + +GEN_ERRFUNC (error, + (bt_errclass errclass, + char * filename, + int line, + char * fmt, + ...), + errclass, filename, line, NULL, -1, fmt) + +GEN_ERRFUNC (ast_error, + (bt_errclass errclass, + AST * ast, + char * fmt, + ...), + errclass, ast->filename, ast->line, NULL, -1, fmt) + +GEN_ERRFUNC (notify, + (const char * fmt, ...), + BTERR_NOTIFY, NULL, -1, NULL, -1, fmt) + +GEN_ERRFUNC (usage_warning, + (const char * fmt, ...), + BTERR_USAGEWARN, NULL, -1, NULL, -1, fmt) + +GEN_ERRFUNC (usage_error, + (const char * fmt, ...), + BTERR_USAGEERR, NULL, -1, NULL, -1, fmt) + +GEN_ERRFUNC (internal_error, + (const char * fmt, ...), + BTERR_INTERNAL, NULL, -1, NULL, -1, fmt) + + +/* ====================================================================== + * Functions to be used outside of the library + */ + +/* ------------------------------------------------------------------------ +@NAME : bt_reset_error_counts() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Resets all the error counters to zero. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/08, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void bt_reset_error_counts (void) +{ + int i; + + for (i = 0; i < NUM_ERRCLASSES; i++) + errclass_counts[i] = 0; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_get_error_count() +@INPUT : errclass +@OUTPUT : +@RETURNS : +@DESCRIPTION: Returns number of errors seen in the specified class. +@GLOBALS : errclass_counts +@CALLS : +@CREATED : +@MODIFIED : +-------------------------------------------------------------------------- */ +int bt_get_error_count (bt_errclass errclass) +{ + return errclass_counts[errclass]; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_get_error_counts() +@INPUT : counts - pointer to an array big enough to hold all the counts + if NULL, the array will be allocated for you (and you + must free() it when done with it) +@OUTPUT : +@RETURNS : counts - either the passed-in pointer, or the newly- + allocated array if you pass in NULL +@DESCRIPTION: Returns a newly-allocated array with the number of errors + in each error class, indexed by the members of the + eclass_t enum. +@GLOBALS : errclass_counts +@CALLS : +@CREATED : 1997/01/06, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +int *bt_get_error_counts (int *counts) +{ + int i; + + if (counts == NULL) + counts = (int *) malloc (sizeof (int) * NUM_ERRCLASSES); + for (i = 0; i < NUM_ERRCLASSES; i++) + counts[i] = errclass_counts[i]; + + return counts; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_error_status +@INPUT : saved_counts - an array of error counts as returned by + bt_get_error_counts, or NULL not to compare + to a previous checkpoint +@OUTPUT : +@RETURNS : +@DESCRIPTION: Computes a bitmap where a bit is set for each error class + that has more errors now than it used to have (or, if + saved_counts is NULL, the bit is set of there are have been + any errors in the corresponding error class). + + Eg. "x & (1<<E_SYNTAX)" (where x is returned by bt_error_status) + is true if there have been any syntax errors. +@GLOBALS : +@CALLS : +@CREATED : +@MODIFIED : +-------------------------------------------------------------------------- */ +ushort bt_error_status (int *saved_counts) +{ + int i; + ushort status; + + status = 0; + + if (saved_counts) + { + for (i = 0; i < NUM_ERRCLASSES; i++) + status |= ( (errclass_counts[i] > saved_counts[i]) << i); + } + else + { + for (i = 0; i < NUM_ERRCLASSES; i++) + status |= ( (errclass_counts[i] > 0) << i); + } + + return status; +} /* bt_error_status () */ diff --git a/src/translators/btparse/error.h b/src/translators/btparse/error.h new file mode 100644 index 0000000..aede151 --- /dev/null +++ b/src/translators/btparse/error.h @@ -0,0 +1,65 @@ +/* ------------------------------------------------------------------------ +@NAME : error.c +@DESCRIPTION: Prototypes for the error-generating functions (i.e. functions + defined in error.c, and meant only for use elswhere in the + library). +@CREATED : Summer 1996, Greg Ward +@MODIFIED : +@VERSION : $Id: error.h,v 1.11 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef ERROR_H +#define ERROR_H + +#include <stdarg.h> +#include "btparse.h" /* for AST typedef */ + +#define MAX_ERROR 1024 + +#define ERRFUNC_BODY(class,filename,line,item_desc,item,format) \ +{ \ + va_list arglist; \ + \ + va_start (arglist, format); \ + report_error (class, filename, line, item_desc, item, format, arglist); \ + va_end (arglist); \ +} + +#define GEN_ERRFUNC(name,params,class,filename,line,item_desc,item,format) \ +void name params \ +ERRFUNC_BODY (class, filename, line, item_desc, item, format) + +#define GEN_PRIVATE_ERRFUNC(name,params, \ + class,filename,line,item_desc,item,format) \ +static GEN_ERRFUNC(name,params,class,filename,line,item_desc,item,format) + +/* + * Prototypes for functions exported by error.c but only used within + * the library -- functions that can be called by outsiders are declared + * in btparse.h. + */ + +void print_error (bt_error *err); +void report_error (bt_errclass class, + char * filename, int line, const char * item_desc, int item, + const char * format, va_list arglist); + +void general_error (bt_errclass class, + char * filename, int line, const char * item_desc, int item, + char * format, ...); +void error (bt_errclass class, char * filename, int line, char * format, ...); +void ast_error (bt_errclass class, AST * ast, char * format, ...); + +void notify (const char *format,...); +void usage_warning (const char * format, ...); +void usage_error (const char * format, ...); +void internal_error (const char * format, ...); + +#endif diff --git a/src/translators/btparse/format_name.c b/src/translators/btparse/format_name.c new file mode 100644 index 0000000..d6c99ae --- /dev/null +++ b/src/translators/btparse/format_name.c @@ -0,0 +1,841 @@ +/* ------------------------------------------------------------------------ +@NAME : format_name.c +@DESCRIPTION: bt_format_name() and support functions: everything needed + to turn a bt_name structure (as returned by bt_split_name()) + back into a string according to a highly customizable format. +@GLOBALS : +@CREATED : +@MODIFIED : +@VERSION : $Id: format_name.c,v 1.12 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include "btparse.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ +#include "bt_debug.h" + + +static char EmptyString[] = ""; + + +#if DEBUG +/* prototypes to shut "gcc -Wmissing-prototypes" up */ +void print_tokens (char *partname, char **tokens, int num_tokens); +void dump_name (bt_name * name); +void dump_format (bt_name_format * format); +#endif + + +/* ---------------------------------------------------------------------- + * Interface to create/customize bt_name_format structures + */ + +/* ------------------------------------------------------------------------ +@NAME : bt_create_name_format +@INPUT : parts - a string of letters (maximum four, from the set + f, v, l, j, with no repetition) denoting the order + and presence of name parts. Also used to determine + certain pre-part text strings. + abbrev_first - flag: should first names be abbreviated? +@OUTPUT : +@RETURNS : +@DESCRIPTION: Creates a bt_name_format structure, slightly customized + according to the caller's choice of token order and + whether to abbreviate the first name. Use + bt_free_name_format() to free the structure (and any sub- + structures that may be allocated here). Use + bt_set_format_text() and bt_set_format_options() for + further customization of the format structure; do not + fiddle its fields directly. + + Fills in the structures `parts' field according to `parts' + string: 'f' -> BTN_FIRST, and so on. + + Sets token join methods: inter-token join (within each part) + is set to BTJ_MAYTIE (a "discretionary tie") for all parts; + inter-part join is set to BTJ_SPACE, except for a 'von' + token immediately preceding a 'last' token; there, we have + a discretionary tie. + + Sets abbreviation flags: FALSE for everything except `first', + which follows `abbrev_first' argument. + + Sets surrounding text (pre- and post-part, pre- and post- + token): empty string for everything, except: + - post-token for 'first' is "." if abbrev_first true + - if 'jr' immediately preceded by 'last': + pre-part for 'jr' is ", ", join for 'last' is nothing + - if 'first' immediately preceded by 'last' + pre-part for 'first' is ", " , join for 'last' is nothing + - if 'first' immediately preceded by 'jr' and 'jr' immediately + preceded by 'last': + pre-part for 'first' and 'jr' is ", " , + join for 'last' and 'jr' is nothing +@CREATED : 1997/11/02, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +bt_name_format * +bt_create_name_format (char * parts, boolean abbrev_first) +{ + int num_parts; + int num_valid_parts; + bt_name_format * + format; + int part_pos[BT_MAX_NAMEPARTS]; + int i; + + /* + * Check that the part list (a string with one letter -- f, v, l, or j + * -- for each part is valid: no longer than four characters, and no + * invalid characters. + */ + + num_parts = strlen (parts); + num_valid_parts = strspn (parts, BT_VALID_NAMEPARTS); + if (num_parts > BT_MAX_NAMEPARTS) + { + usage_error ("bt_create_name_format: part list must have no more than " + "%d letters", BT_MAX_NAMEPARTS); + } + if (num_valid_parts != num_parts) + { + usage_error ("bt_create_name_format: bad part abbreviation \"%c\" " + "(must be one of \"%s\")", + parts[num_valid_parts], BT_VALID_NAMEPARTS); + } + + + /* User input is OK -- let's create the structure */ + + format = (bt_name_format *) malloc (sizeof (bt_name_format)); + format->num_parts = num_parts; + for (i = 0; i < num_parts; i++) + { + switch (parts[i]) + { + case 'f': format->parts[i] = BTN_FIRST; break; + case 'v': format->parts[i] = BTN_VON; break; + case 'l': format->parts[i] = BTN_LAST; break; + case 'j': format->parts[i] = BTN_JR; break; + default: internal_error ("bad part abbreviation \"%c\"", parts[i]); + } + part_pos[format->parts[i]] = i; + } + for (; i < BT_MAX_NAMEPARTS; i++) + { + format->parts[i] = BTN_NONE; + } + + + /* + * Set the token join methods: between tokens for all parts is a + * discretionary tie, and the join between parts is a space (except for + * 'von': if followed by 'last', we will have a discretionary tie). + */ + for (i = 0; i < num_parts; i++) + { + format->join_tokens[i] = BTJ_MAYTIE; + format->join_part[i] = BTJ_SPACE; + } + if (part_pos[BTN_VON] + 1 == part_pos[BTN_LAST]) + format->join_part[BTN_VON] = BTJ_MAYTIE; + + + /* + * Now the abbreviation flags: follow 'abbrev_first' flag for 'first', + * and FALSE for everything else. + */ + format->abbrev[BTN_FIRST] = abbrev_first; + format->abbrev[BTN_VON] = FALSE; + format->abbrev[BTN_LAST] = FALSE; + format->abbrev[BTN_JR] = FALSE; + + + + /* + * Now fill in the "surrounding text" fields (pre- and post-part, pre- + * and post-token) -- start out with everything NULL (empty string), + * and then tweak it to handle abbreviated first names, 'jr' following + * 'last', and 'first' following 'last' or 'last' and 'jr'. In the + * last three cases, we put in some pre-part text (", "), and also + * set the join method for the *previous* part (jr or last) to + * BTJ_NOTHING, so we don't get extraneous space before the ", ". + */ + for (i = 0; i < BT_MAX_NAMEPARTS; i++) + { + format->pre_part[i] = EmptyString; + format->post_part[i] = EmptyString; + format->pre_token[i] = EmptyString; + format->post_token[i] = EmptyString; + } + + /* abbreviated first name: + * "Blow J" -> "Blow J.", or "J Blow" -> "J. Blow" + */ + if (abbrev_first) + { + format->post_token[BTN_FIRST] = "."; + } + /* 'jr' after 'last': "Joe Blow Jr." -> "Joe Blow, Jr." */ + if (part_pos[BTN_JR] == part_pos[BTN_LAST]+1) + { + format->pre_part[BTN_JR] = ", "; + format->join_part[BTN_LAST] = BTJ_NOTHING; + /* 'first' after 'last' and 'jr': "Blow, Jr. Joe"->"Blow, Jr., Joe" */ + if (part_pos[BTN_FIRST] == part_pos[BTN_JR]+1) + { + format->pre_part[BTN_FIRST] = ", "; + format->join_part[BTN_JR] = BTJ_NOTHING; + } + } + /* first after last: "Blow Joe" -> "Blow, Joe" */ + if (part_pos[BTN_FIRST] == part_pos[BTN_LAST]+1) + { + format->pre_part[BTN_FIRST] = ", "; + format->join_part[BTN_LAST] = BTJ_NOTHING; + } + + DBG_ACTION + (1, printf ("bt_create_name_format(): returning structure %p\n", format)) + + return format; + +} /* bt_create_name_format() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_free_name_format() +@INPUT : format - free()'d, so this is an invalid pointer after the call +@OUTPUT : +@RETURNS : +@DESCRIPTION: Frees a bt_name_format structure created by + bt_create_name_format(). +@CREATED : 1997/11/02, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_free_name_format (bt_name_format * format) +{ + free (format); +} + + + +/* ------------------------------------------------------------------------ +@NAME : bt_set_format_text +@INPUT : format - the format structure to update + part - which name-part to change the surrounding text for + pre_part - "pre-part" text, or NULL to leave alone + post_part - "post-part" text, or NULL to leave alone + pre_token - "pre-token" text, or NULL to leave alone + post_token - "post-token" text, or NULL to leave alone +@OUTPUT : format - pre_part, post_part, pre_token, post_token + arrays updated (only those with corresponding + non-NULL parameters are touched) +@RETURNS : +@DESCRIPTION: Sets the "surrounding text" for a particular name part in + a name format structure. +@CREATED : 1997/11/02, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_set_format_text (bt_name_format * format, + bt_namepart part, + char * pre_part, + char * post_part, + char * pre_token, + char * post_token) +{ + if (pre_part) format->pre_part[part] = pre_part; + if (post_part) format->post_part[part] = post_part; + if (pre_token) format->pre_token[part] = pre_token; + if (post_token) format->post_token[part] = post_token; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_set_format_options() +@INPUT : format + part + abbrev + join_tokens + join_part +@OUTPUT : format - abbrev, join_tokens, join_part arrays all updated +@RETURNS : +@DESCRIPTION: Sets various formatting options for a particular name part in + a name format structure. +@CREATED : 1997/11/02, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_set_format_options (bt_name_format * format, + bt_namepart part, + boolean abbrev, + bt_joinmethod join_tokens, + bt_joinmethod join_part) +{ + format->abbrev[part] = abbrev; + format->join_tokens[part] = join_tokens; + format->join_part[part] = join_part; +} + + + +/* ---------------------------------------------------------------------- + * Functions for actually formatting a name (given a name and a name + * format structure). + */ + +/* ------------------------------------------------------------------------ +@NAME : count_virtual_char() +@INPUT : string + offset +@OUTPUT : vchar_count +@INOUT : depth + in_special +@RETURNS : +@DESCRIPTION: Munches a single physical character from a string, updating + the virtual character count, the depth, and an "in special + character" flag. + + The virtual character count is incremented by any character + not part of a special character, and also by the right-brace + that closes a special character. The depth is incremented by + a left brace, and decremented by a right brace. in_special + is set to TRUE when we encounter a left brace at depth zero + that is immediately followed by a backslash; it is set to + false when we encounter the end of the special character, + i.e. when in_special is TRUE and we hit a right brace that + brings us back to depth zero. + + *vchar_count and *depth should both be set to zero the first + time you call count_virtual_char() on a particular string, + and in_special should be set to FALSE. +@CALLS : +@CALLERS : string_length() + string_prefix() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +count_virtual_char (char * string, + int offset, + int * vchar_count, + int * depth, + boolean * in_special) +{ + switch (string[offset]) + { + case '{': + { + /* start of a special char? */ + if (*depth == 0 && string[offset+1] == '\\') + *in_special = TRUE; + (*depth)++; + break; + } + case '}': + { + /* end of a special char? */ + if (*depth == 1 && *in_special) + { + *in_special = FALSE; + (*vchar_count)++; + } + (*depth)--; + break; + } + default: + { + /* anything else? (possibly inside a special char) */ + if (! *in_special) (*vchar_count)++; + } + } +} /* count_virtual_char () */ + + +/* this should probably be publicly available, documented, etc. */ +/* ------------------------------------------------------------------------ +@NAME : string_length() +@INPUT : string +@OUTPUT : +@RETURNS : "virtual length" of `string' +@DESCRIPTION: Counts the number of "virtual characters" in a string. A + virtual character is either an entire BibTeX special character, + or any character outside of a special character. + + Thus, "Hello" has virtual length 5, and so does + "H{\\'e}ll{\\\"o}". "{\\noop Hello there how are you?}" has + virtual length one. +@CALLS : count_virtual_char() +@CALLERS : format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static int +string_length (char * string) +{ + int length; + int depth; + boolean in_special; + int i; + + length = 0; + depth = 0; + in_special = FALSE; + + for (i = 0; string[i] != 0; i++) + { + count_virtual_char (string, i, &length, &depth, &in_special); + } + + return length; +} /* string_length() */ + + +/* ------------------------------------------------------------------------ +@NAME : string_prefix() +@INPUT : string + prefix_len +@OUTPUT : +@RETURNS : physical length of the prefix of `string' with a virtual length + of `prefix_len' +@DESCRIPTION: Counts the number of physical characters from the beginning + of `string' needed to extract a sub-string with virtual + length `prefix_len'. +@CALLS : count_virtual_char() +@CALLERS : format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static int +string_prefix (char * string, int prefix_len) +{ + int i; + int vchars_seen; + int depth; + boolean in_special; + + vchars_seen = 0; + depth = 0; + in_special = FALSE; + + for (i = 0; string[i] != 0; i++) + { + count_virtual_char (string, i, &vchars_seen, &depth, &in_special); + if (vchars_seen == prefix_len) + return i+1; + } + + return i; + +} /* string_prefix() */ + + +/* ------------------------------------------------------------------------ +@NAME : append_text() +@INOUT : string +@INPUT : offset + text + start + len +@OUTPUT : +@RETURNS : number of characters copied from text+start to string+offset +@DESCRIPTION: Copies at most `len' characters from text+start to + string+offset. (I don't use strcpy() or strncpy() for this + because I need to get the number of characters actually + copied.) +@CALLS : +@CALLERS : format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static int +append_text (char * string, + int offset, + const char * text, + int start, + int len) +{ + int i; + + if (text == NULL) return 0; /* no text -- none appended! */ + + for (i = 0; text[start+i] != 0; i++) + { + if (len > 0 && i == len) + break; /* exit loop without i++, right?!? */ + string[offset+i] = text[start+i]; + } /* for i */ + + return i; /* number of characters copied */ + +} /* append_text () */ + + +/* ------------------------------------------------------------------------ +@NAME : append_join +@INOUT : string +@INPUT : offset + method + should_tie +@OUTPUT : +@RETURNS : number of charactersa appended to string+offset (either 0 or 1) +@DESCRIPTION: Copies a "join character" ('~' or ' ') or nothing to + string+offset, according to the join method specified by + `method' and the `should_tie' flag. + + Specifically: if `method' is BTJ_SPACE, a space is appended + and 1 is returned; if `method' is BTJ_FORCETIE, a TeX "tie" + character ('~') is appended and 1 is returned. If `method' + is BTJ_NOTHING, `string' is unchanged and 0 is returned. If + `method' is BTJ_MAYTIE then either a tie (if should_tie is + true) or a space (otherwise) is appended, and 1 is returned. +@CALLS : +@CALLERS : format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +@COMMENTS : This should allow "tie" strings other than TeX's '~' -- I + think this could be done by putting a "tie string" field in + the name format structure, and using it here. +-------------------------------------------------------------------------- */ +static int +append_join (char * string, + int offset, + bt_joinmethod method, + boolean should_tie) +{ + switch (method) + { + case BTJ_MAYTIE: /* a "discretionary tie" -- pay */ + { /* attention to should_tie */ + if (should_tie) + string[offset] = '~'; + else + string[offset] = ' '; + return 1; + } + case BTJ_SPACE: + { + string[offset] = ' '; + return 1; + } + case BTJ_FORCETIE: + { + string[offset] = '~'; + return 1; + } + case BTJ_NOTHING: + { + return 0; + } + default: + internal_error ("bad token join method %d", (int) method); + } + + return 0; /* can't happen -- just here to */ + /* keep gcc -Wall happy */ +} /* append_join () */ + + +#define STRLEN(s) (s == NULL) ? 0 : strlen (s) + +/* ------------------------------------------------------------------------ +@NAME : format_firstpass() +@INPUT : name + format +@OUTPUT : +@RETURNS : +@DESCRIPTION: Makes the first pass over a name for formatting, in order to + establish an upper bound on the length of the formatted name. +@CALLS : +@CALLERS : bt_format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static unsigned +format_firstpass (bt_name * name, + bt_name_format * format) +{ + int i; /* loop over parts */ + int j; /* loop over tokens */ + unsigned max_length; + bt_namepart part; + char ** tok; + int num_tok; + + max_length = 0; + + for (i = 0; i < format->num_parts; i++) + { + part = format->parts[i]; /* 'cause I'm a lazy typist */ + tok = name->parts[part]; + num_tok = name->part_len[part]; + + assert ((tok != NULL) == (num_tok > 0)); + if (tok) + { + max_length += STRLEN (format->pre_part[part]); + max_length += STRLEN (format->post_part[part]); + max_length += STRLEN (format->pre_token[part]) * num_tok; + max_length += STRLEN (format->post_token[part]) * num_tok; + max_length += num_tok + 1; /* one join char per token, plus */ + /* join char to next part */ + + /* + * We ignore abbreviation here -- just overestimates the maximum + * length, so no big deal. Also saves us the bother of computing + * the physical length of the prefix of virtual length 1. + */ + for (j = 0; j < num_tok; j++) + max_length += strlen (tok[j]); + } + + } /* for i (loop over parts) */ + + return max_length; + +} /* format_firstpass() */ + + +/* ------------------------------------------------------------------------ +@NAME : format_name() +@INPUT : format + tokens - token list (eg. from format_firstpass()) + num_tokens - token count list (eg. from format_firstpass()) +@OUTPUT : fname - filled in, must be preallocated by caller +@RETURNS : +@DESCRIPTION: Performs the second pass over a name and format, to actually + put the name into a single string according to `format'. +@CALLS : +@CALLERS : bt_format_name() +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +format_name (bt_name_format * format, + char *** tokens, + int * num_tokens, + char * fname) +{ + bt_namepart parts[BT_MAX_NAMEPARTS]; /* culled list from format */ + int num_parts; + + int offset; /* into fname */ + int i; /* loop over parts */ + int j; /* loop over tokens */ + bt_namepart part; + int prefix_len; + int token_len; /* "physical" length (characters) */ + int token_vlen; /* "virtual" length (special char */ + /* counts as one character) */ + boolean should_tie; + + /* + * Cull format->parts down by keeping only those parts that are actually + * present in the current name (keeps the main loop simpler: makes it + * easy to know if the "next part" is present or not, so we know whether + * to append a join character. + */ + num_parts = 0; + for (i = 0; i < format->num_parts; i++) + { + part = format->parts[i]; + if (tokens[part]) /* name actually has this part */ + parts[num_parts++] = part; + } + + offset = 0; + token_vlen = -1; /* sanity check, and keeps */ + /* "gcc -O -Wall" happy */ + + for (i = 0; i < num_parts; i++) + { + part = parts[i]; + + offset += append_text (fname, offset, + format->pre_part[part], 0, -1); + + for (j = 0; j < num_tokens[part]; j++) + { + offset += append_text (fname, offset, + format->pre_token[part], 0, -1); + if (format->abbrev[part]) + { + prefix_len = string_prefix (tokens[part][j], 1); + token_len = append_text (fname, offset, + tokens[part][j], 0, prefix_len); + token_vlen = 1; + } + else + { + token_len = append_text (fname, offset, + tokens[part][j], 0, -1); + token_vlen = string_length (tokens[part][j]); + } + offset += token_len; + offset += append_text (fname, offset, + format->post_token[part], 0, -1); + + /* join to next token, but only if there is a next token! */ + if (j < num_tokens[part]-1) + { + should_tie = (num_tokens[part] > 1) + && (((j == 0) && (token_vlen < 3)) + || (j == num_tokens[part]-2)); + offset += append_join (fname, offset, + format->join_tokens[part], should_tie); + } + + } /* for j */ + + offset += append_text (fname, offset, + format->post_part[part], 0, -1); + /* join to the next part, but again only if there is a next part */ + if (i < num_parts-1) + { + if (token_vlen == -1) + { + internal_error ("token_vlen uninitialized -- no tokens in a part " + "that I checked existed"); + } + should_tie = (num_tokens[part] == 1 && token_vlen < 3); + offset += append_join (fname, offset, + format->join_part[part], should_tie); + } + + } /* for i (loop over parts) */ + + fname[offset] = 0; + +} /* format_name () */ + + +#if DEBUG + +#define STATIC /* so BibTeX.xs can call 'em too */ + +/* borrowed print_tokens() and dump_name() from t/name_test.c */ +STATIC void +print_tokens (char *partname, char **tokens, int num_tokens) +{ + int i; + + if (tokens) + { + printf ("%s = (", partname); + for (i = 0; i < num_tokens; i++) + { + printf ("%s%c", tokens[i], i == num_tokens-1 ? ')' : '|'); + } + putchar ('\n'); + } +} + + +STATIC void +dump_name (bt_name * name) +{ + if (name == NULL) + { + printf (" name: null\n"); + return; + } + + if (name->tokens == NULL) + { + printf (" name: null token list\n"); + return; + } + + printf (" name (%p):\n", name); + printf (" total number of tokens = %d\n", name->tokens->num_items); + print_tokens (" first", name->parts[BTN_FIRST], name->part_len[BTN_FIRST]); + print_tokens (" von", name->parts[BTN_VON], name->part_len[BTN_VON]); + print_tokens (" last", name->parts[BTN_LAST], name->part_len[BTN_LAST]); + print_tokens (" jr", name->parts[BTN_JR], name->part_len[BTN_JR]); +} + + +STATIC void +dump_format (bt_name_format * format) +{ + int i; + static char * nameparts[] = { "first", "von", "last", "jr" }; + static char * joinmethods[] = {"may tie", "space", "force tie", "nothing"}; + + printf (" name format (%p):\n", format); + printf (" order:"); + for (i = 0; i < format->num_parts; i++) + printf (" %s", nameparts[format->parts[i]]); + printf ("\n"); + + for (i = 0; i < BT_MAX_NAMEPARTS; i++) + { + printf (" %-5s: pre-part=%p (%s), post-part=%p (%s)\n", + nameparts[i], + format->pre_part[i], format->pre_part[i], + format->post_part[i], format->post_part[i]); + printf (" %-5s pre-token=%p (%s), post-token=%p (%s)\n", + "", + format->pre_token[i], format->pre_token[i], + format->post_token[i],format->post_token[i]); + printf (" %-5s abbrev=%s, join_tokens=%s, join_parts=%s\n", + "", + format->abbrev[i] ? "yes" : "no", + joinmethods[format->join_tokens[i]], + joinmethods[format->join_part[i]]); + } +} +#endif + + +/* ------------------------------------------------------------------------ +@NAME : bt_format_name() +@INPUT : name + format +@OUTPUT : +@RETURNS : formatted name (allocated with malloc(); caller must free() it) +@DESCRIPTION: Formats an already-split name according to a pre-constructed + format structure. +@GLOBALS : +@CALLS : format_firstpass(), format_name() +@CALLERS : +@CREATED : 1997/11/03, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +char * +bt_format_name (bt_name * name, + bt_name_format * format) +{ + unsigned max_length; + char * fname; + +#if DEBUG >= 2 + printf ("bt_format_name():\n"); + dump_name (name); + dump_format (format); +#endif + + max_length = format_firstpass (name, format); + fname = (char *) malloc ((max_length+1) * sizeof (char)); +#if 0 + memset (fname, '_', max_length); + fname[max_length] = 0; +#endif + format_name (format, name->parts, name->part_len, fname); + assert (strlen (fname) <= max_length); + return fname; + +} /* bt_format_name() */ diff --git a/src/translators/btparse/init.c b/src/translators/btparse/init.c new file mode 100644 index 0000000..4a1ec06 --- /dev/null +++ b/src/translators/btparse/init.c @@ -0,0 +1,42 @@ +/* ------------------------------------------------------------------------ +@NAME : init.c +@DESCRIPTION: Initialization and cleanup functions for the btparse library. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/19, Greg Ward +@MODIFIED : +@VERSION : $Id: init.c,v 1.8 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include "stdpccts.h" /* for zzfree_ast() prototype */ +#include "parse_auxiliary.h" /* for fix_token_names() proto */ +#include "prototypes.h" /* for other prototypes */ +/*#include "my_dmalloc.h"*/ + +void bt_initialize (void) +{ + /* Initialize data structures */ + + fix_token_names (); + init_macros (); +} + + +void bt_free_ast (AST *ast) +{ + zzfree_ast (ast); +} + + +void bt_cleanup (void) +{ + done_macros (); +} diff --git a/src/translators/btparse/input.c b/src/translators/btparse/input.c new file mode 100644 index 0000000..dbb7b44 --- /dev/null +++ b/src/translators/btparse/input.c @@ -0,0 +1,499 @@ +/* ------------------------------------------------------------------------ +@NAME : input.c +@DESCRIPTION: Routines for input of BibTeX data. +@GLOBALS : InputFilename + StringOptions +@CALLS : +@CREATED : 1997/10/14, Greg Ward (from code in bibparse.c) +@MODIFIED : +@VERSION : $Id: input.c,v 1.18 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <stdio.h> +#include <limits.h> +#include <assert.h> +#include "stdpccts.h" +#include "lex_auxiliary.h" +#include "prototypes.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + + +char * InputFilename; +ushort StringOptions[NUM_METATYPES] = +{ + 0, /* BTE_UNKNOWN */ + BTO_FULL, /* BTE_REGULAR */ + BTO_MINIMAL, /* BTE_COMMENT */ + BTO_MINIMAL, /* BTE_PREAMBLE */ + BTO_MACRO /* BTE_MACRODEF */ +}; + + +/* ------------------------------------------------------------------------ +@NAME : bt_set_filename +@INPUT : filename +@OUTPUT : +@RETURNS : +@DESCRIPTION: Sets the current input filename -- used for generating + error and warning messages. +@GLOBALS : InputFilename +@CALLS : +@CREATED : Feb 1997, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +#if 0 +void bt_set_filename (char *filename) +{ + InputFilename = filename; +} +#endif + +/* ------------------------------------------------------------------------ +@NAME : bt_set_stringopts +@INPUT : metatype + options +@OUTPUT : +@RETURNS : +@DESCRIPTION: Sets the string-processing options for a particular + entry metatype. Used later on by bt_parse_* to determine + just how to post-process each particular entry. +@GLOBALS : StringOptions +@CREATED : 1997/08/24, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void bt_set_stringopts (bt_metatype metatype, ushort options) +{ + if (metatype < BTE_REGULAR || metatype > BTE_MACRODEF) + usage_error ("bt_set_stringopts: illegal metatype"); + if (options & ~BTO_STRINGMASK) + usage_error ("bt_set_stringopts: illegal options " + "(must only set string option bits"); + + StringOptions[metatype] = options; +} + + +/* ------------------------------------------------------------------------ +@NAME : start_parse +@INPUT : infile input stream we'll read from (or NULL if reading + from string) + instring input string we'll read from (or NULL if reading + from stream) + line line number of the start of the string (just + use 1 if the string is standalone and independent; + if it comes from a file, you should supply the + line number where it starts for better error + messages) (ignored if infile != NULL) +@OUTPUT : +@RETURNS : +@DESCRIPTION: Prepares things for parsing, in particular initializes the + lexical state and lexical buffer, prepares DLG for + reading (either from a stream or a string), and reads + the first token. +@GLOBALS : +@CALLS : initialize_lexer_state() + alloc_lex_buffer() + zzrdstream() or zzrdstr() + zzgettok() +@CALLERS : +@CREATED : 1997/06/21, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +start_parse (FILE *infile, char *instring, int line) +{ + if ( (infile == NULL) == (instring == NULL) ) + { + internal_error ("start_parse(): exactly one of infile and " + "instring may be non-NULL"); + } + initialize_lexer_state (); + alloc_lex_buffer (ZZLEXBUFSIZE); + if (infile) + { + zzrdstream (infile); + } + else + { + zzrdstr (instring); + zzline = line; + } + + zzendcol = zzbegcol = 0; + zzgettok (); +} + + + +/* ------------------------------------------------------------------------ +@NAME : finish_parse() +@INPUT : err_counts - pointer to error count list (which is local to + the parsing functions, hence has to be passed in) +@OUTPUT : +@RETURNS : +@DESCRIPTION: Frees up what was needed to parse a whole file or a sequence + of strings: the lexical buffer and the error count list. +@GLOBALS : +@CALLS : free_lex_buffer() +@CALLERS : +@CREATED : 1997/06/21, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +finish_parse (int **err_counts) +{ + free_lex_buffer (); + free (*err_counts); + *err_counts = NULL; +} + + +/* ------------------------------------------------------------------------ +@NAME : parse_status() +@INPUT : saved_counts +@OUTPUT : +@RETURNS : false if there were serious errors in the recently-parsed input + true otherwise (no errors or just warnings) +@DESCRIPTION: Gets the "error status" bitmap relative to a saved set of + error counts and masks of non-serious errors. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/06/21, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static boolean +parse_status (int *saved_counts) +{ + ushort ignore_emask; + + /* + * This bit-twiddling fetches the error status (which has a bit + * for each error class), masks off the bits for trivial errors + * to get "true" if there were any serious errors, and then + * returns the opposite of that. + */ + ignore_emask = + (1<<BTERR_NOTIFY) | (1<<BTERR_CONTENT) | (1<<BTERR_LEXWARN); + return !(bt_error_status (saved_counts) & ~ignore_emask); +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_parse_entry_s() +@INPUT : entry_text - string containing the entire entry to parse, + or NULL meaning we're done, please cleanup + options - standard btparse options bitmap + line - current line number (if that makes any sense) + -- passed to the parser to set zzline, so that + lexical and syntax errors are properly localized +@OUTPUT : *top - newly-allocated AST for the entry + (or NULL if entry_text was NULL, ie. at EOF) +@RETURNS : 1 with *top set to AST for entry on successful read/parse + 1 with *top==NULL if entry_text was NULL, ie. at EOF + 0 if any serious errors seen in input (*top is still + set to the AST, but only for as much of the input as we + were able to parse) + (A "serious" error is a lexical or syntax error; "trivial" + errors such as warnings and notifications count as "success" + for the purposes of this function's return value.) +@DESCRIPTION: Parses a BibTeX entry contained in a string. +@GLOBALS : +@CALLS : ANTLR +@CREATED : 1997/01/18, GPW (from code in bt_parse_entry()) +@MODIFIED : +-------------------------------------------------------------------------- */ +AST * bt_parse_entry_s (char * entry_text, + char * filename, + int line, + ushort options, + boolean * status) +{ + AST * entry_ast = NULL; + static int * err_counts = NULL; + + if (options & BTO_STRINGMASK) /* any string options set? */ + { + usage_error ("bt_parse_entry_s: illegal options " + "(string options not allowed"); + } + + InputFilename = filename; + err_counts = bt_get_error_counts (err_counts); + + if (entry_text == NULL) /* signal to clean up */ + { + finish_parse (&err_counts); + if (status) *status = TRUE; + return NULL; + } + + zzast_sp = ZZAST_STACKSIZE; /* workaround apparent pccts bug */ + start_parse (NULL, entry_text, line); + + entry (&entry_ast); /* enter the parser */ + ++zzasp; /* why is this done? */ + + if (entry_ast == NULL) /* can happen with very bad input */ + { + if (status) *status = FALSE; + return entry_ast; + } + +#if DEBUG + dump_ast ("bt_parse_entry_s: single entry, after parsing:\n", + entry_ast); +#endif + bt_postprocess_entry (entry_ast, + StringOptions[entry_ast->metatype] | options); +#if DEBUG + dump_ast ("bt_parse_entry_s: single entry, after post-processing:\n", + entry_ast); +#endif + + if (status) *status = parse_status (err_counts); + return entry_ast; + +} /* bt_parse_entry_s () */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_parse_entry() +@INPUT : infile - file to read next entry from + options - standard btparse options bitmap +@OUTPUT : *top - AST for the entry, or NULL if no entries left in file +@RETURNS : same as bt_parse_entry_s() +@DESCRIPTION: Starts (or continues) parsing from a file. +@GLOBALS : +@CALLS : +@CREATED : Jan 1997, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +AST * bt_parse_entry (FILE * infile, + char * filename, + ushort options, + boolean * status) +{ + AST * entry_ast = NULL; + static int * err_counts = NULL; + static FILE * prev_file = NULL; + + if (prev_file != NULL && infile != prev_file) + { + usage_error ("bt_parse_entry: you can't interleave calls " + "across different files"); + } + + if (options & BTO_STRINGMASK) /* any string options set? */ + { + usage_error ("bt_parse_entry: illegal options " + "(string options not allowed)"); + } + + InputFilename = filename; + err_counts = bt_get_error_counts (err_counts); + + if (feof (infile)) + { + if (prev_file != NULL) /* haven't already done the cleanup */ + { + prev_file = NULL; + finish_parse (&err_counts); + } + else + { + usage_warning ("bt_parse_entry: second attempt to read past eof"); + } + + if (status) *status = TRUE; + return NULL; + } + + /* + * Here we do some nasty poking about the innards of PCCTS in order to + * enter the parser multiple times on the same input stream. This code + * comes from expanding the macro invokation: + * + * ANTLR (entry (top), infile); + * + * When LL_K, ZZINF_LOOK, and DEMAND_LOOK are all undefined, this + * ultimately expands to + * + * zzbufsize = ZZLEXBUFSIZE; + * { + * static char zztoktext[ZZLEXBUFSIZE]; + * zzlextext = zztoktext; + * zzrdstream (f); + * zzgettok(); + * } + * entry (top); + * ++zzasp; + * + * (I'm expanding hte zzenterANTLR, zzleaveANTLR, and zzPrimateLookAhead + * macros, but leaving ZZLEXBUFSIZE -- a simple constant -- alone.) + * + * There are two problems with this: 1) zztoktext is a statically + * allocated buffer, and when it overflows we just ignore further + * characters that should belong to that lexeme; and 2) zzrdstream() and + * zzgettok() are called every time we enter the parser, which means the + * token left over from the previous entry will be discarded when we + * parse entries 2 .. N. + * + * I handle the static buffer problem with alloc_lex_buffer() and + * realloc_lex_buffer() (in lex_auxiliary.c), and by rewriting the ZZCOPY + * macro to call realloc_lex_buffer() when overflow is detected. + * + * I handle the extra token-read by hanging on to a static file + * pointer, prev_file, between calls to bt_parse_entry() -- when + * the program starts it is NULL, and we reset it to NULL on + * finishing a file. Thus, any call that is the first on a given + * file will allocate the lexical buffer and read the first token; + * thereafter, we skip those steps, and free the buffer on reaching + * end-of-file. Currently, this method precludes interleaving + * calls to bt_parse_entry() on different files -- perhaps I could + * fix this with the zz{save,restore}_{antlr,dlg}_state() + * functions? + */ + + zzast_sp = ZZAST_STACKSIZE; /* workaround apparent pccts bug */ + +#if defined(LL_K) || defined(ZZINF_LOOK) || defined(DEMAND_LOOK) +# error One of LL_K, ZZINF_LOOK, or DEMAND_LOOK was defined +#endif + if (prev_file == NULL) /* only read from input stream if */ + { /* starting afresh with a file */ + start_parse (infile, NULL, 0); + prev_file = infile; + } + assert (prev_file == infile); + + entry (&entry_ast); /* enter the parser */ + ++zzasp; /* why is this done? */ + + if (entry_ast == NULL) /* can happen with very bad input */ + { + if (status) *status = FALSE; + return entry_ast; + } + +#if DEBUG + dump_ast ("bt_parse_entry(): single entry, after parsing:\n", + entry_ast); +#endif + bt_postprocess_entry (entry_ast, + StringOptions[entry_ast->metatype] | options); +#if DEBUG + dump_ast ("bt_parse_entry(): single entry, after post-processing:\n", + entry_ast); +#endif + + if (status) *status = parse_status (err_counts); + return entry_ast; + +} /* bt_parse_entry() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_parse_file () +@INPUT : filename - name of file to open. If NULL or "-", we read + from stdin rather than opening a new file. + options +@OUTPUT : top +@RETURNS : 0 if any entries in the file had serious errors + 1 if all entries were OK +@DESCRIPTION: Parses an entire BibTeX file, and returns a linked list + of ASTs (or, if you like, a forest) for the entries in it. + (Any entries with serious errors are omitted from the list.) +@GLOBALS : +@CALLS : bt_parse_entry() +@CREATED : 1997/01/18, from process_file() in bibparse.c +@MODIFIED : +@COMMENTS : This function bears a *striking* resemblance to bibparse.c's + process_file(). Eventually, I plan to replace this with + a generalized process_file() that takes a function pointer + to call for each entry. Until I decide on the right interface + for that, though, I'm sticking with this simpler (but possibly + memory-intensive) approach. +-------------------------------------------------------------------------- */ +AST * bt_parse_file (char * filename, + ushort options, + boolean * status) +{ + FILE * infile; + AST * entries, + * cur_entry, + * last; + boolean entry_status, + overall_status; + + if (options & BTO_STRINGMASK) /* any string options set? */ + { + usage_error ("bt_parse_file: illegal options " + "(string options not allowed"); + } + + /* + * If a string was given, and it's *not* "-", then open that filename. + * Otherwise just use stdin. + */ + + if (filename != NULL && strcmp (filename, "-") != 0) + { + InputFilename = filename; + infile = fopen (filename, "r"); + if (infile == NULL) + { + perror (filename); + return 0; + } + } + else + { + InputFilename = "(stdin)"; + infile = stdin; + } + + entries = NULL; + last = NULL; + +#if 1 + /* explicit loop over entries, with junk cleaned out by read_entry () */ + + overall_status = TRUE; /* assume success */ + while ((cur_entry = bt_parse_entry + (infile, InputFilename, options, &entry_status))) + { + overall_status &= entry_status; + if (!entry_status) continue; /* bad entry -- try next one */ + if (!cur_entry) break; /* at eof -- we're done */ + if (last == NULL) /* this is the first entry */ + entries = cur_entry; + else /* have already seen one */ + last->right = cur_entry; + + last = cur_entry; + } + +#else + /* let the PCCTS lexer/parser handle everything */ + + initialize_lexer_state (); + ANTLR (bibfile (top), infile); + +#endif + + fclose (infile); + InputFilename = NULL; + if (status) *status = overall_status; + return entries; + +} /* bt_parse_file() */ diff --git a/src/translators/btparse/lex_auxiliary.c b/src/translators/btparse/lex_auxiliary.c new file mode 100644 index 0000000..8fac463 --- /dev/null +++ b/src/translators/btparse/lex_auxiliary.c @@ -0,0 +1,939 @@ +/* ------------------------------------------------------------------------ +@NAME : lex_auxiliary.c +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: The code and global variables here have three main purposes: + - maintain the lexical buffer (zztoktext, which + traditionally with PCCTS is a static array; I have + changed things so that it's dynamically allocated and + resized on overflow) + - keep track of lexical state that's not handled by PCCTS + code (like "where are we in terms of BibTeX entries?" or + "what are the delimiters for the current entry/string?") + - everything called from lexical actions is here, to keep + the grammar file itself neat and clean +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : Greg Ward, 1996/07/25-28 +@MODIFIED : Jan 1997 + Jun 1997 +@VERSION : $Id: lex_auxiliary.c,v 1.31 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <stdarg.h> +#include <assert.h> +#include "lex_auxiliary.h" +#include "stdpccts.h" +#include "error.h" +#include "prototypes.h" +/*#include "my_dmalloc.h"*/ + +#define DUPE_TEXT 0 + +extern char * InputFilename; /* from input.c */ + +GEN_PRIVATE_ERRFUNC (lexical_warning, (const char * fmt, ...), + BTERR_LEXWARN, InputFilename, zzline, NULL, -1, fmt) +GEN_PRIVATE_ERRFUNC (lexical_error, (const char * fmt, ...), + BTERR_LEXERR, InputFilename, zzline, NULL, -1, fmt) + + + +/* ---------------------------------------------------------------------- + * Global variables + */ + +/* First, the lexical buffer. This is used elsewhere, so can't be static */ +char * zztoktext = NULL; + +/* + * Now, the lexical state -- first, stuff that arises from scanning + * at top-level and the beginnings of entries; + * EntryState: + * toplevel when we start scanning a file, or when we are in in_entry + * mode and see '}' or ')' + * after_at when we are in toplevel mode and see an '@' + * after_type when we are in after_at mode and see a name (!= 'comment') + * in_comment when we are in after_at mode and see a name (== 'comment') + * in_entry when we are in after_type mode and see '{' or '(' + * EntryOpener: + * the character ('(' or '{') which opened the entry currently being + * scanned (we use this to make sure that the entry opener and closer + * match; if not, we issue a warning) + * EntryMetatype: (NB. typedef for bt_metatype is in btparse.h) + * classifies entries according to the syntax we will use to parse them; + * also winds up (after being changed to a bt_nodetype value) in the + * node that roots the entry AST: + * comment - anything between () or {} + * preamble - a single compound value + * string - a list of "name = compound_value" assignments; no key + * alias - a single "name = compound_value" assignment (where + * the compound value in this case is presumably a + * name, rather than a string -- this is not syntactically + * checked though) + * modify, + * entry - a key followed by a list of "name = compound_value" + * assignments + * JunkCount: + * the number of non-whitespace, non-'@' characters seen at toplevel + * between two entries (used to print out a warning when we hit + * the beginning of entry, to help people catch "old style" implicit + * comments + */ +static enum { toplevel, after_at, after_type, in_comment, in_entry } + EntryState; +static char EntryOpener; /* '(' or '{' */ +static bt_metatype + EntryMetatype; +static int JunkCount; /* non-whitespace chars at toplevel */ + +/* + * String state -- these are maintained and used by the functions called + * from actions in the string lexer. + * BraceDepth: + * brace depth within a string; we can only end the current string + * when this is zero + * ParenDepth: + * parenthesis depth within a string; needed for @comment entries + * that are paren-delimited (because the comment in that case is + * a paren-delimited string) + * StringOpener: + * similar to EntryOpener, but stronger than merely warning of token + * mismatch -- this determines which character ('"' or '}') can + * actually end the string + * StringStart: + * line on which current string started; if we detect an apparent + * runaway, this is used to report where the runaway started + * ApparentRunaway: + * flags if we have already detected (and warned) that the current + * string appears to be a runaway, so that we don't warn again + * (and again and again and again) + * QuoteWarned: + * flags if we have already warned about seeing a '"' in a string, + * because they tend to come in pairs and one warning per string + * is enough + * + * (See bibtex.g for an explanation of my runaway string detection heuristic.) + */ +static char StringOpener = '\0'; /* '{' or '"' */ +static int BraceDepth; /* depth of brace-nesting */ +static int ParenDepth; /* depth of parenthesis-nesting */ +static int StringStart = -1; /* start line of current string */ +static int ApparentRunaway; /* current string looks like runaway */ +static int QuoteWarned; /* already warned about " in string? */ + + + +/* ---------------------------------------------------------------------- + * Miscellaneous functions: + * lex_info() (handy for debugging) + * zzcr_attr() (called from PCCTS-generated code) + */ + +void lex_info (void) +{ + printf ("LA(1) = \"%s\" token %d, %s\n", LATEXT(1), LA(1), zztokens[LA(1)]); +#ifdef LL_K + printf ("LA(2) = \"%s\" token %d, %s\n", LATEXT(2), LA(2), zztokens[LA(2)]); +#endif +} + + +void zzcr_attr (Attrib *a, int tok, char *txt) +{ + if (tok == STRING) + { + int len = strlen (txt); + + assert ((txt[0] == '{' && txt[len-1] == '}') + || (txt[0] == '"' && txt[len-1] == '"')); + txt[len-1] = (char) 0; /* remove closing quote from string */ + txt++; /* so we'll skip the opening quote */ + } + +#if DUPE_TEXT + a->text = strdup (txt); +#else + a->text = txt; +#endif + a->token = tok; + a->line = zzline; + a->offset = zzbegcol; +#if DEBUG > 1 + dprintf ("zzcr_attr: input txt = %p (%s)\n", txt, txt); + dprintf (" dupe txt = %p (%s)\n", a->text, a->text); +#endif +} + + +#if DUPE_TEXT +void zzd_attr (Attrib *attr) +{ + free (attr->text); +} +#endif + + +/* ---------------------------------------------------------------------- + * Lexical buffer functions: + * alloc_lex_buffer() + * realloc_lex_buffer() + * free_lex_buffer() + * lexer_overflow() + * zzcopy() (only if ZZCOPY_FUNCTION is defined and true) + */ + + +/* + * alloc_lex_buffer() + * + * allocates the lexical buffer with `size' characters. Clears the buffer, + * points zzlextext at it, and sets zzbufsize to `size'. + * + * Does nothing if the buffer is already allocated. + * + * globals: zztoktext, zzlextext, zzbufsize + * callers: bt_parse_entry() (in input.c) + */ +void alloc_lex_buffer (int size) +{ + if (zztoktext == NULL) + { + zztoktext = (char *) malloc (size * sizeof (char)); + memset (zztoktext, 0, size); + zzlextext = zztoktext; + zzbufsize = size; + } +} /* alloc_lex_buffer() */ + + +/* + * realloc_lex_buffer() + * + * Reallocates the lexical buffer -- size is increased by `size_increment' + * characters (which could be negative). Updates all globals that point + * to or into the buffer (zzlextext, zzbegexpr, zzendexpr), as well as + * zztoktext (the buffer itself) zzbufsize (the buffer size). + * + * This is only meant to be called (ultimately) from zzgettok(), part of + * the DLG code. (In fact, zzgettok() invokes the ZZCOPY() macro, which + * calls lexer_overflow() on buffer overflow, which calls + * realloc_lex_buffer(). Whatever.) The `lastpos' and `nextpos' arguments + * correspond, respectively, to a local variable in zzgettok() and a static + * global in dlgauto.h (hence really in scan.c). They both point into + * the lexical buffer, so have to be passed by reference here so that + * we can update them to point into the newly-reallocated buffer. + * + * globals: zztottext, zzbufsize, zzlextext, zzbegexpr, zzendexpr + * callers: lexer_overflow() + */ +static void +realloc_lex_buffer (int size_increment, + unsigned char ** lastpos, + unsigned char ** nextpos) +{ + int beg, end, next; + + if (zztoktext == NULL) + internal_error ("attempt to reallocate unallocated lexical buffer"); + + zztoktext = (char *) realloc (zztoktext, zzbufsize+size_increment); + memset (zztoktext+zzbufsize, 0, size_increment); + zzbufsize += size_increment; + + beg = zzbegexpr - zzlextext; + end = zzendexpr - zzlextext; + next = *nextpos - zzlextext; + zzlextext = zztoktext; + + if (lastpos != NULL) + *lastpos = zzlextext+zzbufsize-1; + zzbegexpr = zzlextext + beg; + zzendexpr = zzlextext + end; + *nextpos = zzlextext + next; + +} /* realloc_lex_buffer() */ + + +/* + * free_lex_buffer() + * + * Frees the lexical buffer allocated by alloc_lex_buffer(). + */ +void free_lex_buffer (void) +{ + if (zztoktext == NULL) + internal_error ("attempt to free unallocated (or already freed) " + "lexical buffer"); + + free (zztoktext); + zztoktext = NULL; +} /* free_lex_buffer() */ + + +/* + * lexer_overflow() + * + * Prints a warning and calls realloc_lex_buffer() to increase the size + * of the lexical buffer by ZZLEXBUFSIZE (a constant -- hence the buffer + * size increases linearly, not exponentially). + * + * Also prints a couple of lines of useful debugging stuff if DEBUG is true. + */ +void lexer_overflow (unsigned char **lastpos, unsigned char **nextpos) +{ +#if DEBUG + char head[16], tail[16]; + + printf ("zzcopy: overflow detected\n"); + printf (" zzbegcol=%d, zzendcol=%d, zzline=%d\n", + zzbegcol, zzendcol, zzline); + strncpy (head, zzlextext, 15); head[15] = 0; + strncpy (tail, zzlextext+ZZLEXBUFSIZE-15, 15); tail[15] = 0; + printf (" zzlextext=>%s...%s< (last char=%d (%c))\n", + head, tail, + zzlextext[ZZLEXBUFSIZE-1], zzlextext[ZZLEXBUFSIZE-1]); + printf (" zzchar = %d (%c), zzbegexpr=zzlextext+%d\n", + zzchar, zzchar, zzbegexpr-zzlextext); +#endif + + notify ("lexical buffer overflowed (reallocating to %d bytes)", + zzbufsize+ZZLEXBUFSIZE); + realloc_lex_buffer (ZZLEXBUFSIZE, lastpos, nextpos); + +} /* lexer_overflow () */ + + +#if ZZCOPY_FUNCTION +/* + * zzcopy() + * + * Does the same as the ZZCOPY macro (in lex_auxiliary.h), but as a + * function for easier debugging. + */ +void zzcopy (char **nextpos, char **lastpos, int *ovf_flag) +{ + if (*nextpos >= *lastpos) + { + lexer_overflow (lastpos, nextpos); + } + + **nextpos = zzchar; + (*nextpos)++; +} +#endif + + + +/* ---------------------------------------------------------------------- + * Report/maintain lexical state + * report_state() (only meaningful if DEBUG) + * initialize_lexer_state() + * + * Note that the lexical action functions, below, also fiddle with + * the lexical state variables an awful lot. + */ + +#if DEBUG +char *state_names[] = + { "toplevel", "after_at", "after_type", "in_comment", "in_entry" }; +char *metatype_names[] = + { "unknown", "comment", "preamble", "string", "alias", "modify", "entry" }; + +static void +report_state (char *where) +{ + printf ("%s: lextext=%s (line %d, offset %d), token=%d, " + "EntryState=%s\n", + where, zzlextext, zzline, zzbegcol, NLA, + state_names[EntryState]); +} +#else +# define report_state(where) +/* +static void +report_state (char *where) { } +*/ +#endif + +void initialize_lexer_state (void) +{ + zzmode (START); + EntryState = toplevel; + EntryOpener = (char) 0; + EntryMetatype = BTE_UNKNOWN; + JunkCount = 0; +} + + +bt_metatype entry_metatype (void) +{ + return EntryMetatype; +} + + + +/* ---------------------------------------------------------------------- + * Lexical actions (START and LEX_ENTRY modes) + */ + +/* + * newline () + * + * Does everything needed to handle newline outside of a quoted string: + * increments line counter and skips the newline. + */ +void newline (void) +{ + zzline++; + zzskip(); +} + + +void comment (void) +{ + zzline++; + zzskip(); +} + + +void at_sign (void) +{ + if (EntryState == toplevel) + { + EntryState = after_at; + zzmode (LEX_ENTRY); + if (JunkCount > 0) + { + lexical_warning ("%d characters of junk seen at toplevel", JunkCount); + JunkCount = 0; + } + } + else + { + /* internal_error ("lexer recognized \"@\" at other than top-level"); */ + lexical_warning ("\"@\" in strange place -- should get syntax error"); + } + report_state ("at_sign"); +} + + +void toplevel_junk (void) +{ + JunkCount += strlen (zzlextext); + zzskip (); +} + + +void name (void) +{ + report_state ("name (pre)"); + + switch (EntryState) + { + case toplevel: + { + internal_error ("junk at toplevel (\"%s\")", zzlextext); + break; + } + case after_at: + { + char * etype = zzlextext; + EntryState = after_type; + + if (strcasecmp (etype, "comment") == 0) + { + EntryMetatype = BTE_COMMENT; + EntryState = in_comment; + } + + else if (strcasecmp (etype, "preamble") == 0) + EntryMetatype = BTE_PREAMBLE; + + else if (strcasecmp (etype, "string") == 0) + EntryMetatype = BTE_MACRODEF; +/* + else if (strcasecmp (etype, "alias") == 0) + EntryMetatype = BTE_ALIAS; + + else if (strcasecmp (etype, "modify") == 0) + EntryMetatype = BTE_MODIFY; +*/ + else + EntryMetatype = BTE_REGULAR; + + break; + } + case after_type: + case in_comment: + case in_entry: + break; /* do nothing */ + } + + report_state ("name (post)"); + +} + + +void lbrace (void) +{ + /* + * Currently takes a restrictive view of "when an lbrace is an entry + * opener" -- ie. *only* after '@name' (as determined by EntryState), + * where name is not 'comment'. This means that lbrace usually + * determines a string (in particular, when it's seen at toplevel -- + * which will happen under certain error situations), which in turn + * means that some unexpected things can become strings (like whole + * entries). + */ + + if (EntryState == in_entry || EntryState == in_comment) + { + start_string ('{'); + } + else if (EntryState == after_type) + { + EntryState = in_entry; + EntryOpener = '{'; + NLA = ENTRY_OPEN; + } + else + { + lexical_warning ("\"{\" in strange place -- should get a syntax error"); + } + + report_state ("lbrace"); +} + + +void rbrace (void) +{ + if (EntryState == in_entry) + { + if (EntryOpener == '(') + lexical_warning ("entry started with \"(\", but ends with \"}\""); + NLA = ENTRY_CLOSE; + initialize_lexer_state (); + } + else + { + lexical_warning ("\"}\" in strange place -- should get a syntax error"); + } + report_state ("rbrace"); +} + + +void lparen (void) +{ + if (EntryState == in_comment) + { + start_string ('('); + } + else if (EntryState == after_type) + { + EntryState = in_entry; + EntryOpener = '('; + } + else + { + lexical_warning ("\"(\" in strange place -- should get a syntax error"); + } + report_state ("lparen"); +} + + +void rparen (void) +{ + if (EntryState == in_entry) + { + if (EntryOpener == '{') + lexical_warning ("entry started with \"{\", but ends with \")\""); + initialize_lexer_state (); + } + else + { + lexical_warning ("\")\" in strange place -- should get a syntax error"); + } + report_state ("rparen"); +} + + +/* ---------------------------------------------------------------------- + * Stuff for processing strings. + */ + + +/* + * start_string () + * + * Called when we see a '{' or '"' in the field data. Records which quote + * character was used, and calls open_brace() to increment the depth + * counter if it was a '{'. Switches to LEX_STRING mode, and tells the + * lexer to continue slurping characters into the same buffer. + */ +void start_string (char start_char) +{ + StringOpener = start_char; + BraceDepth = 0; + ParenDepth = 0; + StringStart = zzline; + ApparentRunaway = 0; + QuoteWarned = 0; + if (start_char == '{') + open_brace (); + if (start_char == '(') + ParenDepth++; + if (start_char == '"' && EntryState == in_comment) + { + lexical_error ("comment entries must be delimited by either braces or parentheses"); + EntryState = toplevel; + zzmode (START); + return; + } + +#ifdef USER_ZZMODE_STACK + if (zzauto != LEX_ENTRY || EntryState != in_entry) +#else + if (EntryState != in_entry && EntryState != in_comment) +#endif + { + lexical_warning ("start of string seen at weird place"); + } + + zzmore (); + zzmode (LEX_STRING); +} + + +/* + * end_string () + * + * Called when we see either a '"' (at depth 0) or '}' (if it brings us + * down to depth 0) in a quoted string. Just makes sure that braces are + * balanced, and then goes back to the LEX_FIELD mode. + */ +void end_string (char end_char) +{ + char match; + +#ifndef ALLOW_WARNINGS + match = (char) 0; /* silence "might be used" */ + /* uninitialized" warning */ +#endif + + switch (end_char) + { + case '}': match = '{'; break; + case ')': match = '('; break; + case '"': match = '"'; break; + default: + internal_error ("end_string(): invalid end_char \"%c\"", end_char); + } + + assert (StringOpener == match); + + /* + * If we're at non-zero BraceDepth, that probably means mismatched braces + * somewhere -- complain about it and reset BraceDepth to minimize future + * confusion. + */ + + if (BraceDepth > 0) + { + lexical_error ("unbalanced braces: too many {'s"); + BraceDepth = 0; + } + + StringOpener = (char) 0; + StringStart = -1; + NLA = STRING; + + if (EntryState == in_comment) + { + int len = strlen (zzlextext); + + /* + * ARG! no, this is wrong -- what if unbalanced braces in the string + * and we try to output put it later? + * + * ARG! again, this is no more wrong than when we strip quotes in + * post_parse.c, and blithely assume that we can put them back on + * later for output in BibTeX syntax. Hmmm. + * + * Actually, it looks like this isn't a problem after all: you + * can't have unbalanced braces in a BibTeX string (at least + * not as parsed by btparse). + */ + + if (zzlextext[0] == '(') /* convert to standard quote delims */ + { + zzlextext[ 0] = '{'; + zzlextext[len-1] = '}'; + } + + EntryState = toplevel; + zzmode (START); + } + else + { + zzmode (LEX_ENTRY); + } + + report_state ("string"); +} + + +/* + * open_brace () + * + * Called when we see a '{', either to start a string (in which case + * it's called from start_string()) or inside a string (called directly + * from the lexer). + */ +void open_brace (void) +{ + BraceDepth++; + zzmore (); + report_state ("open_brace"); +} + + +/* + * close_brace () + * + * Called when we see a '}' inside a string. Decrements the depth counter + * and checks to see if we are down to depth 0, in which case the string is + * ended and the current lookahead token is set to STRING. Otherwise, + * just tells the lexer to keep slurping characters into the buffer. + */ +void close_brace (void) +{ + BraceDepth--; + if (StringOpener == '{' && BraceDepth == 0) + { + end_string ('}'); + } + + /* + * This could happen if some bonehead puts an unmatched right-brace + * in a quote-delimited string (eg. "Hello}"). To attempt to recover, + * we reset the depth to zero and continue slurping into the string. + */ + else if (BraceDepth < 0) + { + lexical_error ("unbalanced braces: too many }'s"); + BraceDepth = 0; + zzmore (); + } + + /* Otherwise, it's just any old right brace in a string -- keep eating */ + else + { + zzmore (); + } + report_state ("close_brace"); +} + + +void lparen_in_string (void) +{ + ParenDepth++; + zzmore (); +} + + +void rparen_in_string (void) +{ + ParenDepth--; + if (StringOpener == '(' && ParenDepth == 0) + { + end_string (')'); + } + else + { + zzmore (); + } +} + + +/* + * quote_in_string () + * + * Called when we see '"' in a string. Ends the string if the quote is at + * depth 0 and the string was started with a quote, otherwise instructs the + * lexer to continue munching happily along. (Also prints a warning, + * assuming that input is destined for processing by TeX and you really + * want either `` or '' rather than ".) + */ +void quote_in_string (void) +{ + if (StringOpener == '"' && BraceDepth == 0) + { + end_string ('"'); + } + else + { + boolean at_top = FALSE;; + + /* + * Note -- this warning assumes that strings are destined + * to be processed by TeX, so it should be optional. Hmmm. + */ + + if (StringOpener == '"' || StringOpener == '(') + at_top = (BraceDepth == 0); + else if (StringOpener == '{') + at_top = (BraceDepth == 1); + else + internal_error ("Illegal string opener \"%c\"", StringOpener); + + if (!QuoteWarned && at_top) + { + lexical_warning ("found \" at brace-depth zero in string " + "(TeX accents in BibTeX should be inside braces)"); + QuoteWarned = 1; + } + zzmore (); + } +} + + +/* + * check_runaway_string () + * + * Called from the lexer whenever we see a newline in a string. See + * bibtex.g for a detailed explanation; basically, this function + * looks for an entry start ("@name{") or new field ("name=") immediately + * after a newline (with possible whitespace). This is a heuristic + * check for runaway strings, under the assumption that text that looks + * like a new entry or new field won't actually occur inside a string + * very often. + */ +void check_runaway_string (void) +{ + int len; + int i; + + /* + * could these be made significantly more efficient by a 256-element + * lookup table instead of calling strchr()? + */ + static const char *alpha_chars = "abcdefghijklmnopqrstuvwxyz"; + static const char *name_chars = "abcdefghijklmnopqrstuvwxyz0123456789:+/'.-"; + + /* + * on entry: zzlextext contains the whole string, starting with { + * and with newlines/tabs converted to space; zzbegexpr points to + * a chunk of the string starting with newline (newlines and + * tabs have not yet been converted) + */ + +#if DEBUG > 1 + printf ("check_runaway_string(): zzline=%d\n", zzline); + printf ("zzlextext=>%s<\nzzbegexpr=>%s<\n", + zzlextext, zzbegexpr); +#endif + + + /* + * increment zzline to take the leading newline into account -- but + * first a sanity check to be sure that newline is there! + */ + + if (zzbegexpr[0] != '\n') + { + lexical_warning ("huh? something's wrong (buffer overflow?) near " + "offset %d (line %d)", zzendcol, zzline); + /* internal_error ("zzbegexpr (line %d, offset %d-%d, " + "text >%s<, expr >%s<)" + "should start with a newline", + zzline, zzbegcol, zzendcol, zzlextext, zzbegexpr); + */ + } + else + { + zzline++; + } + + /* standardize whitespace (convert all to space) */ + + len = strlen (zzbegexpr); + for (i = 0; i < len; i++) + { + if (isspace (zzbegexpr[i])) + zzbegexpr[i] = ' '; + } + + + if (!ApparentRunaway) /* haven't already warned about it */ + { + enum { none, entry, field, giveup } guess; + + i = 1; + guess = none; + while (i < len && zzbegexpr[i] == ' ') i++; + + if (zzbegexpr[i] == '@') + { + i++; + while (i < len && zzbegexpr[i] == ' ') i++; + guess = entry; + } + + if (strchr (alpha_chars, tolower (zzbegexpr[i])) != NULL) + { + while (i < len && strchr (name_chars, tolower (zzbegexpr[i])) != NULL) + i++; + while (i < len && zzbegexpr[i] == ' ') i++; + if (i == len) + { + guess = giveup; + } + else + { + if (guess == entry) + { + if (zzbegexpr[i] != '{' && zzbegexpr[i] != '(') + guess = giveup; + } + else /* assume it's a field */ + { + if (zzbegexpr[i] == '=') + guess = field; + else + guess = giveup; + } + } + } + else /* no name seen after WS or @ */ + { + guess = giveup; + } + + if (guess == none) + internal_error ("gee, I should have made a guess by now"); + + if (guess != giveup) + { + lexical_warning ("possible runaway string started at line %d", + StringStart); + ApparentRunaway = 1; + } + } + + zzmore(); +} + diff --git a/src/translators/btparse/lex_auxiliary.h b/src/translators/btparse/lex_auxiliary.h new file mode 100644 index 0000000..ebbf053 --- /dev/null +++ b/src/translators/btparse/lex_auxiliary.h @@ -0,0 +1,71 @@ +/* ------------------------------------------------------------------------ +@NAME : lex_auxiliary.h +@DESCRIPTION: Macros and function prototypes needed by the lexical scanner. + Some of these are called from internal PCCTS code, and some + are explicitly called from the lexer actions in bibtex.g. +@CREATED : Summer 1996, Greg Ward +@MODIFIED : +@VERSION : $Id: lex_auxiliary.h,v 1.15 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +#ifndef LEX_AUXILIARY_H +#define LEX_AUXILIARY_H + +#include "btparse.h" +#include "attrib.h" + +#define ZZCOPY_FUNCTION 0 + +#if ZZCOPY_FUNCTION +#define ZZCOPY zzcopy (&zznextpos, &lastpos, &zzbufovf) +#else +#define ZZCOPY \ + if (zznextpos >= lastpos) \ + { \ + lexer_overflow (&lastpos, &zznextpos); \ + } \ + *(zznextpos++) = zzchar; +#endif + + +/* Function prototypes: */ + +void lex_info (void); +void zzcr_attr (Attrib *a, int tok, char *txt); + +void alloc_lex_buffer (int size); +void free_lex_buffer (void); +void lexer_overflow (unsigned char **lastpos, unsigned char **nextpos); +#if ZZCOPY_FUNCTION +void zzcopy (char **nextpos, char **lastpos, int *ovf_flag); +#endif + +void initialize_lexer_state (void); +bt_metatype entry_metatype (void); + +void newline (void); +void comment (void); +void at_sign (void); +void toplevel_junk (void); +void name (void); +void lbrace (void); +void rbrace (void); +void lparen (void); +void rparen (void); + +void start_string (char start_char); +void end_string (char end_char); +void open_brace (void); +void close_brace (void); +void lparen_in_string (void); +void rparen_in_string (void); +void quote_in_string (void); +void check_runaway_string (void); + +#endif /* ! defined LEX_AUXILIARY_H */ diff --git a/src/translators/btparse/macros.c b/src/translators/btparse/macros.c new file mode 100644 index 0000000..06db983 --- /dev/null +++ b/src/translators/btparse/macros.c @@ -0,0 +1,367 @@ +/* ------------------------------------------------------------------------ +@NAME : macros.c +@DESCRIPTION: Front-end to the standard PCCTS symbol table code (sym.c) + to abstract my "macro table". +@GLOBALS : +@CALLS : +@CREATED : 1997/01/12, Greg Ward +@MODIFIED : +@VERSION : $Id: macros.c,v 1.19 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <string.h> +#include "sym.h" +#include "prototypes.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ +#include "bt_debug.h" + + +/* + * NUM_MACROS and STRING_SIZE define the size of the static data + * structure that holds the macro table. The defaults are to allocate + * 4096 bytes of string space that will be divided up amongst 547 + * macros. This should be fine for most applications, but if you have a + * big macro table you might need to change these and recompile (don't + * forget to rebuild and reinstall Text::BibTeX if you're using it!). + * You can set these as high as you like; just remember that a block of + * STRING_SIZE bytes will be allocated and not freed as long as you're + * using btparse. Also, NUM_MACROS defines the size of a hashtable, so + * it should probably be a prime a bit greater than a power of 2 -- or + * something like that. I'm not sure of the exact Knuthian + * specification. + */ +#define NUM_MACROS 547 +#define STRING_SIZE 4096 + +Sym *AllMacros = NULL; /* `scope' so we can get back list */ + /* of all macros when done */ + + +GEN_PRIVATE_ERRFUNC (macro_warning, + (char * filename, int line, const char * fmt, ...), + BTERR_CONTENT, filename, line, NULL, -1, fmt) + + +/* ------------------------------------------------------------------------ +@NAME : init_macros() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Initializes the symbol table used to store macro values. +@GLOBALS : AllMacros +@CALLS : zzs_init(), zzs_scope() (sym.c) +@CALLERS : bt_initialize() (init.c) +@CREATED : Jan 1997, GPW +-------------------------------------------------------------------------- */ +void +init_macros (void) +{ + zzs_init (NUM_MACROS, STRING_SIZE); + zzs_scope (&AllMacros); +} + + +/* ------------------------------------------------------------------------ +@NAME : done_macros() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Frees up all the macro values in the symbol table, and + then frees up the symbol table itself. +@GLOBALS : AllMacros +@CALLS : zzs_rmscope(), zzs_done() +@CALLERS : bt_cleanup() (init.c) +@CREATED : Jan 1997, GPW +-------------------------------------------------------------------------- */ +void +done_macros (void) +{ + bt_delete_all_macros (); + zzs_done (); +} + + +static void +delete_macro_entry (Sym * sym) +{ + Sym * cur; + Sym * prev; + + /* + * Yechh! All this mucking about with the scope list really + * ought to be handled by the symbol table code. Must write + * my own someday. + */ + + /* Find this entry in the list of all macro table entries */ + cur = AllMacros; + prev = NULL; + while (cur != NULL && cur != sym) + { + prev = cur; + cur = cur->scope; + } + + if (cur == NULL) /* uh-oh -- wasn't found! */ + { + internal_error ("macro table entry for \"%s\" not found in scope list", + sym->symbol); + } + + /* Now unlink from the "scope" list */ + if (prev == NULL) /* it's the head of the list */ + AllMacros = cur->scope; + else + prev->scope = cur->scope; + + /* Remove it from the macro hash table */ + zzs_del (sym); + + /* And finally, free up the entry's text and the entry itself */ + if (sym->text) free (sym->text); + free (sym); +} /* delete_macro_entry() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_add_macro_value() +@INPUT : assignment - AST node representing "macro = value" + options - string-processing options that were used to + process this string after parsing +@OUTPUT : +@RETURNS : +@DESCRIPTION: Adds a value to the symbol table used for macros. + + If the value was not already post-processed as a macro value + (expand macros, paste substrings, but don't collapse + whitespace), then this post-processing is done before adding + the macro text to the table. + + If the macro is already defined, a warning is printed and + the old text is overridden. +@GLOBALS : +@CALLS : bt_add_macro_text() + bt_postprocess_field() +@CALLERS : bt_postprocess_entry() (post_parse.c) +@CREATED : Jan 1997, GPW +-------------------------------------------------------------------------- */ +void +bt_add_macro_value (AST *assignment, ushort options) +{ + AST * value; + char * macro; + char * text; + boolean free_text; + + if (assignment == NULL || assignment->down == NULL) return; + value = assignment->down; + + /* + * If the options that were used to process the macro's expansion text + * are anything other than BTO_MACRO, then we'll have to do it ourselves. + */ + + if ((options & BTO_STRINGMASK) != BTO_MACRO) + { + text = bt_postprocess_field (assignment, BTO_MACRO, FALSE); + free_text = TRUE; /* because it's alloc'd by */ + /* bt_postprocess_field() */ + } + else + { + /* + * First a sanity check to make sure that the presumed post-processing + * had the desired effect. + */ + + if (value->nodetype != BTAST_STRING || value->right != NULL) + { + internal_error ("add_macro: macro value was not " + "correctly preprocessed"); + } + + text = assignment->down->text; + free_text = FALSE; + } + + macro = assignment->text; + bt_add_macro_text (macro, text, assignment->filename, assignment->line); + if (free_text && text != NULL) + free (text); + +} /* bt_add_macro_value() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_add_macro_text() +@INPUT : macro - the name of the macro to define + text - the macro text + filename, line - where the macro is defined; pass NULL + for filename if no file, 0 for line if no line number + (just used to generate warning message) +@OUTPUT : +@RETURNS : +@DESCRIPTION: Sets the text value for a macro. If the macro is already + defined, a warning is printed and the old value is overridden. +@GLOBALS : +@CALLS : zzs_get(), zzs_newadd() +@CALLERS : bt_add_macro_value() + (exported from library) +@CREATED : 1997/11/13, GPW (from code in bt_add_macro_value()) +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_add_macro_text (char * macro, char * text, char * filename, int line) +{ + Sym * sym; + Sym * new_rec; + +#if DEBUG == 1 + printf ("adding macro \"%s\" = \"%s\"\n", macro, text); +#elif DEBUG >= 2 + printf ("add_macro: macro = %p (%s)\n" + " text = %p (%s)\n", + macro, macro, text, text); +#endif + + if ((sym = zzs_get (macro))) + { + macro_warning (filename, line, + "overriding existing definition of macro \"%s\"", + macro); + delete_macro_entry (sym); + } + + new_rec = zzs_newadd (macro); + new_rec->text = (text != NULL) ? strdup (text) : NULL; + DBG_ACTION + (2, printf (" saved = %p (%s)\n", + new_rec->text, new_rec->text);) + +} /* bt_add_macro_text() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_delete_macro() +@INPUT : macro - name of macro to delete +@DESCRIPTION: Deletes a macro from the macro table. +@CALLS : zzs_get() +@CALLERS : +@CREATED : 1998/03/01, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_delete_macro (char * macro) +{ + Sym * sym; + + sym = zzs_get (macro); + if (! sym) return; + delete_macro_entry (sym); +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_delete_all_macros() +@DESCRIPTION: Deletes all macros from the macro table. +@CALLS : zzs_rmscore() +@CALLERS : +@CREATED : 1998/03/01, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_delete_all_macros (void) +{ + Sym *cur, *next; + + DBG_ACTION (2, printf ("bt_delete_all_macros():\n");) + + /* + * Use the current `scope' (same one for all macros) to get access to + * a linked list of all macros. Then traverse the list, free()'ing + * both the text (which was strdup()'d in add_macro(), below) and + * the records themselves (which are calloc()'d by zzs_new()). + */ + + cur = zzs_rmscope (&AllMacros); + while (cur != NULL) + { + DBG_ACTION + (2, printf (" freeing macro \"%s\" (%p=\"%s\") at %p\n", + cur->symbol, cur->text, cur->text, cur);) + + next = cur->scope; + if (cur->text != NULL) free (cur->text); + free (cur); + cur = next; + } +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_macro_length() +@INPUT : macro - the macro name +@OUTPUT : +@RETURNS : length of the macro's text, or zero if the macro is undefined +@DESCRIPTION: Returns length of a macro's text. +@GLOBALS : +@CALLS : zzs_get() +@CALLERS : bt_postprocess_value() + (exported from library) +@CREATED : Jan 1997, GPW +-------------------------------------------------------------------------- */ +int +bt_macro_length (char *macro) +{ + Sym *sym; + + DBG_ACTION + (2, printf ("bt_macro_length: looking up \"%s\"\n", macro);) + + sym = zzs_get (macro); + if (sym) + return strlen (sym->text); + else + return 0; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_macro_text() +@INPUT : macro - the macro name + filename, line - where the macro was invoked; NULL for + `filename' and zero for `line' if not applicable +@OUTPUT : +@RETURNS : The text of the macro, or NULL if it's undefined. +@DESCRIPTION: Fetches a macros text; prints warning and returns NULL if + macro is undefined. +@CALLS : zzs_get() +@CALLERS : bt_postprocess_value() +@CREATED : Jan 1997, GPW +-------------------------------------------------------------------------- */ +char * +bt_macro_text (char * macro, char * filename, int line) +{ + Sym * sym; + + DBG_ACTION + (2, printf ("bt_macro_text: looking up \"%s\"\n", macro);) + + sym = zzs_get (macro); + if (!sym) + { + macro_warning (filename, line, "undefined macro \"%s\"", macro); + return NULL; + } + + return sym->text; +} diff --git a/src/translators/btparse/mode.h b/src/translators/btparse/mode.h new file mode 100644 index 0000000..25b36ce --- /dev/null +++ b/src/translators/btparse/mode.h @@ -0,0 +1,3 @@ +#define START 0 +#define LEX_ENTRY 1 +#define LEX_STRING 2 diff --git a/src/translators/btparse/modify.c b/src/translators/btparse/modify.c new file mode 100644 index 0000000..2d8d9c1 --- /dev/null +++ b/src/translators/btparse/modify.c @@ -0,0 +1,75 @@ +/* ------------------------------------------------------------------------ +@NAME : modify.c +@DESCRIPTION: Routines for modifying the AST for a single entry. +@GLOBALS : +@CALLS : +@CREATED : 1999/11/25, Greg Ward (based on code supplied by + Stphane Genaud <genaud@icps.u-strasbg.fr>) +@MODIFIED : +@VERSION : $Id: modify.c,v 1.2 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <string.h> +#include "btparse.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + + +/* ------------------------------------------------------------------------ +@NAME : bt_set_text () +@INPUT : node + new_text +@OUTPUT : node->text +@RETURNS : +@DESCRIPTION: Replace the text member of an AST node with a new string. + The passed in string, 'new_text', is duplicated, so the + caller may free it without worry. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1999/11/25, GPW (from Stphane Genaud) +@MODIFIED : +-------------------------------------------------------------------------- */ +void bt_set_text (AST * node, char * new_text) +{ + free(node->text); + node->text = strdup (new_text); +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_entry_set_key () +@INPUT : entry + new_key +@OUTPUT : entry->down->text +@RETURNS : +@DESCRIPTION: Changes the key of a regular entry to 'new_key'. If 'entry' + is not a regular entry, or if it doesn't already have a child + node holding an entry key, bombs via 'usage_error()'. + Otherwise a duplicate of 'new_key' is copied into the entry + AST (so the caller can free that string without worry). +@CALLS : bt_set_text () +@CREATED : 1999/11/25, GPW (from Stphane Genaud) +@MODIFIED : +-------------------------------------------------------------------------- */ +void bt_entry_set_key (AST * entry, char * new_key) +{ + if (entry->metatype == BTE_REGULAR && + entry->down && entry->down->nodetype == BTAST_KEY) + { + bt_set_text (entry->down, new_key); + } + else + { + usage_error ("can't set entry key -- not a regular entry, " + "or doesn't have a key already"); + } +} diff --git a/src/translators/btparse/my_alloca.h b/src/translators/btparse/my_alloca.h new file mode 100644 index 0000000..0466157 --- /dev/null +++ b/src/translators/btparse/my_alloca.h @@ -0,0 +1,35 @@ +/* ------------------------------------------------------------------------ +@NAME : my_alloca.h +@DESCRIPTION: All-out assault at making alloca() available on any Unix + platform. Stolen from the GNU Autoconf manual. +@CREATED : 1997/10/30, Greg Ward +@VERSION : $Id: my_alloca.h,v 1.1 1997/10/31 03:56:17 greg Rel $ +@COPYRIGHT : This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef MY_ALLOCA_H +#define MY_ALLOCA_H + +#ifdef __GNUC__ +# ifndef alloca +# define alloca __builtin_alloca +# endif +#else +# if HAVE_ALLOCA_H +# include <alloca.h> +# else +# ifdef _AIX +# pragma alloca +# else +# ifndef alloca /* predefined by HP cc +Olibcalls */ +char *alloca (); +# endif +# endif +# endif +#endif + +#endif /* MY_ALLOCA_H */ diff --git a/src/translators/btparse/names.c b/src/translators/btparse/names.c new file mode 100644 index 0000000..11c4bfd --- /dev/null +++ b/src/translators/btparse/names.c @@ -0,0 +1,915 @@ +/* ------------------------------------------------------------------------ +@NAME : names.c +@DESCRIPTION: Functions for dealing with BibTeX names and lists of names: + bt_split_list + bt_split_name +@GLOBALS : +@CALLS : +@CREATED : 1997/05/05, Greg Ward (as string_util.c) +@MODIFIED : 1997/05/14-05/16, GW: added all the code to split individual + names, renamed file to names.c +@VERSION : $Id: names.c,v 1.23 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <assert.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include "btparse.h" +#include "prototypes.h" +#include "error.h" +#include "my_alloca.h" +/*#include "my_dmalloc.h"*/ +#include "bt_debug.h" + + +#define MAX_COMMAS 2 + +#define update_depth(s,offs,depth) \ +switch (s[offs]) \ +{ \ + case '{': depth++; break; \ + case '}': depth--; break; \ +} + +/* + * `name_loc' specifies where a name is found -- used for generating + * useful warning messages. `line' and `name_num' are both 1-based. + */ +typedef struct +{ + char * filename; + int line; + int name_num; +} name_loc; + + +GEN_PRIVATE_ERRFUNC (name_warning, + (name_loc * loc, const char * fmt, ...), + BTERR_CONTENT, loc->filename, loc->line, + "name", loc->name_num, fmt) + + +/* ------------------------------------------------------------------------ +@NAME : bt_split_list() +@INPUT : string - string to split up; whitespace must be collapsed + eg. by bt_postprocess_string() + delim - delimiter to use; must be lowercase and should be + free of whitespace (code requires that delimiters + in string be surrounded by whitespace) + filename - source of string (for warning messages) + line - 1-based line number into file (for warning messages) + description - what substrings are (eg. "name") (for warning + messages); if NULL will use "substring" +@OUTPUT : substrings (*substrings is allocated by bt_split_list() for you) +@RETURNS : number of substrings found +@DESCRIPTION: Splits a string using a fixed delimiter, in the BibTeX way: + * delimiters at beginning or end of string are ignored + * delimiters in string must be surrounded by whitespace + * case insensitive + * delimiters at non-zero brace depth are ignored + + The list of substrings is returned as *substrings, which + is an array of pointers into a duplicate of string. This + duplicate copy has been scribbled on such that there is + a nul byte at the end of every substring. You should + call bt_free_list() to free both the duplicate copy + of string and *substrings itself. Do *not* walk over + the array free()'ing the substrings yourself, as this is + invalid -- they were not malloc()'d! +@GLOBALS : +@CALLS : +@CALLERS : anyone (exported by library) +@CREATED : 1997/05/05, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +bt_stringlist * +bt_split_list (char * string, + char * delim, + char * filename, + int line, + char * description) +{ + int depth; /* brace depth */ + int i, j; /* offset into string and delim */ + int inword; /* flag telling if prev. char == ws */ + int string_len; + int delim_len; + int maxdiv; /* upper limit on no. of divisions */ + int maxoffs; /* max offset of delim in string */ + int numdiv; /* number of divisions */ + int * start; /* start of each division */ + int * stop; /* stop of each division */ + bt_stringlist * + list; /* structure to return */ + + if (string == NULL) + return NULL; + if (description == NULL) + description = "substring"; + + string_len = strlen (string); + delim_len = strlen (delim); + maxdiv = (string_len / delim_len) + 1; + maxoffs = string_len - delim_len + 1; + + /* + * This is a bit of a band-aid solution to the "split empty string" + * bug (formerly hit the internal_error() at the end of hte function). + * Still need a general "detect and fix unpreprocessed string" -- + * admittedly a different bug/misfeature. + */ + if (string_len == 0) + return NULL; + + start = (int *) alloca (maxdiv * sizeof (int)); + stop = (int *) alloca (maxdiv * sizeof (int)); + + list = (bt_stringlist *) malloc (sizeof (bt_stringlist)); + + depth = 0; + i = j = 0; + inword = 1; /* so leading delim ignored */ + numdiv = 0; + start[0] = 0; /* first substring @ start of string */ + + while (i < maxoffs) + { + /* does current char. in string match current char. in delim? */ + if (depth == 0 && !inword && tolower (string[i]) == delim[j]) + { + j++; i++; + + /* have we found an entire delim, followed by a space? */ + if (j == delim_len && string[i] == ' ') + { + + stop[numdiv] = i - delim_len - 1; + start[++numdiv] = ++i; + j = 0; + +#if DEBUG + printf ("found complete delim; i == %d, numdiv == %d: " + "stop[%d] == %d, start[%d] == %d\n", + i, numdiv, + numdiv-1, stop[numdiv-1], + numdiv, start[numdiv]); +#endif + } + } + + /* no match between string and delim, at non-zero depth, or in a word */ + else + { + update_depth (string, i, depth); + inword = (i < string_len) && (string[i] != ' '); + i++; + j = 0; + } + } + + stop[numdiv] = string_len; /* last substring ends just past eos */ + list->num_items = numdiv+1; + + + /* + * OK, now we know how many divisions there are and where they are -- + * so let's split that string up for real! + * + * list->items will be an array of pointers into a duplicate of + * `string'; we duplicate `string' so we can safely scribble on it and + * free() it later (in bt_free_list()). + */ + + list->items = (char **) malloc (list->num_items * sizeof (char *)); + list->string = strdup (string); + + for (i = 0; i < list->num_items; i++) + { + /* + * Possible cases: + * - stop < start is for empty elements, e.g. "and and" seen in + * input. (`start' for empty element will be the 'a' of the + * second 'and', and its stop will be the ' ' *before* the + * second 'and'.) + * - stop > start is for anything else between two and's (the usual) + * - stop == start should never happen if the loop above is correct + */ + + if (stop[i] > start[i]) /* the usual case */ + { + list->string[stop[i]] = 0; + list->items[i] = list->string+start[i]; + } + else if (stop[i] < start[i]) /* empty element */ + { + list->items[i] = NULL; + general_error (BTERR_CONTENT, filename, line, + description, i+1, "empty %s", description); + } + else /* should not happen! */ + { + internal_error ("stop == start for substring %d", i); + } + } + + return list; +/* return num_substrings; */ + +} /* bt_split_list () */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_free_list() +@INPUT : list +@OUTPUT : +@RETURNS : +@DESCRIPTION: Frees the list of strings created by bt_split_list(). +@GLOBALS : +@CALLS : +@CALLERS : anyone (exported by library) +@CREATED : 1997/05/06, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void bt_free_list (bt_stringlist *list) +{ + if (list && list->string) free (list->string); + if (list && list->items) free (list->items); + if (list) free (list); +} + + + +/* ---------------------------------------------------------------------- + * Stuff for splitting up a single name + */ + + +/* ------------------------------------------------------------------------ +@NAME : find_commas +@INPUT : name - string to search for commas + max_commas - maximum number of commas to allow (if more than + this number are seen, a warning is printed and + the excess commas are removed) +@OUTPUT : +@RETURNS : number of commas found +@DESCRIPTION: Counts and records positions of commas at brace-depth 0. + Modifies string in-place to remove whitespace around commas, + excess commas, and any trailing commas; warns on excess or + trailing commas. Excess commas are removed by replacing them + with space and calling bt_postprocess_string() to collapse + whitespace a second time; trailing commas are simply replaced + with (char) 0 to truncate the string. + + Assumes whitespace has been collapsed (ie. no space at + beginning or end of string, and all internal strings of + whitespace reduced to exactly one space). +@GLOBALS : +@CALLS : name_warning() (if too many commas, or commas at end) +@CALLERS : bt_split_name() +@CREATED : 1997/05/14, Greg Ward +@MODIFIED : +-------------------------------------------------------------------------- */ +static int +find_commas (name_loc * loc, char *name, int max_commas) +{ + int i, j; + int depth; + int num_commas; + int len; + boolean at_comma; + boolean warned; + + i = j = 0; + depth = 0; + num_commas = 0; + len = strlen (name); + warned = 0; + + /* First pass to check for and blank out excess commas */ + + for (i = 0; i < len; i++) + { + if (depth == 0 && name[i] == ',') + { + num_commas++; + if (num_commas > max_commas) + { + if (! warned) + { + name_warning (loc, "too many commas in name (removing extras)"); + warned = TRUE; + } + name[i] = ' '; + } + } + } + + /* + * If we blanked out a comma, better re-collapse whitespace. (This is + * a bit of a cop-out -- I could probably adjust i and j appropriately + * in the above loop to do the collapsing for me, but my brain + * hurt when I tried to think it through. Some other time, perhaps. + */ + + if (warned) + bt_postprocess_string (name, BTO_COLLAPSE); + + /* Now the real comma-finding loop (only if necessary) */ + + if (num_commas == 0) + return 0; + + num_commas = 0; + i = 0; + while (i < len) + { + at_comma = (depth == 0 && name[i] == ','); + if (at_comma) + { + while (j > 0 && name[j-1] == ' ') j--; + num_commas++; + } + + update_depth (name, i, depth); + if (i != j) + name[j] = name[i]; + + i++; j++; + if (at_comma) + { + while (i < len && name[i] == ' ') i++; + } + } /* while i */ + + if (i != j) name[j] = (char) 0; + j--; + + if (name[j] == ',') + { + name_warning (loc, "comma(s) at end of name (removing)"); + while (name[j] == ',') + { + name[j--] = (char) 0; + num_commas--; + } + } + + return num_commas; + +} /* find_commas() */ + + +/* ------------------------------------------------------------------------ +@NAME : find_tokens +@INPUT : name - string to tokenize (should be a private copy + that we're free to clobber and mangle) +@OUTPUT : comma_token- number of token immediately preceding each comma + (caller must allocate with at least one element + per comma in `name') +@RETURNS : newly-allocated bt_stringlist structure +@DESCRIPTION: Finds tokens in a string; delimiter is space or comma at + brace-depth zero. Assumes whitespace has been collapsed + and find_commas has been run on the string to remove + whitespace around commas and any trailing commas. + + The bt_stringlist structure returned can (and should) be + freed with bt_free_list(). +@GLOBALS : +@CALLS : +@CALLERS : bt_split_name() +@CREATED : 1997/05/14, Greg Ward +@MODIFIED : +-------------------------------------------------------------------------- */ +static bt_stringlist * +find_tokens (char * name, + int * comma_token) +{ + int i; /* index into name */ + int num_tok; + int in_boundary; /* previous char was ' ' or ',' */ + int cur_comma; /* index into comma_token */ + int len; + int depth; + bt_stringlist * + tokens; + + i = 0; + in_boundary = 1; /* so first char will start a token */ + cur_comma = 0; + len = strlen (name); + depth = 0; + + tokens = (bt_stringlist *) malloc (sizeof (bt_stringlist)); + /* tokens->string = name ? strdup (name) : NULL; */ + tokens->string = name; + num_tok = 0; + tokens->items = NULL; + + if (len == 0) /* empty string? */ + return tokens; /* return empty token list */ + + tokens->items = (char **) malloc (sizeof (char *) * len); + + while (i < len) + { + if (depth == 0 && in_boundary) /* at start of a new token */ + { + tokens->items[num_tok++] = name+i; + } + + if (depth == 0 && (name[i] == ' ' || name[i] == ',')) + { + /* if we're at a comma, record the token preceding the comma */ + + if (name[i] == ',') + { + comma_token[cur_comma++] = num_tok-1; + } + + /* + * if already in a boundary zone, we have an empty token + * (caused by multiple consecutive commas) + */ + if (in_boundary) + { + tokens->items[num_tok-1] = NULL; + } + num_tok--; + + /* in any case, mark the end of one token and prepare for the + * start of the next + */ + name[i] = (char) 0; + in_boundary = 1; + } + else + { + in_boundary = 0; /* inside a token */ + } + + update_depth (name, i, depth); + i++; + + } /* while i */ + + tokens->num_items = num_tok; + return tokens; + +} /* find_tokens() */ + + +/* ------------------------------------------------------------------------ +@NAME : find_lc_tokens() +@INPUT : tokens +@OUTPUT : first_lc + last_lc +@RETURNS : +@DESCRIPTION: Finds the first contiguous string of lowercase tokens in + `name'. The string must already be tokenized by + find_tokens(), and the input args num_tok, tok_start, and + tok_stop are the return value and the two same-named output + arguments from find_tokens(). +@GLOBALS : +@CALLS : +@CALLERS : bt_split_name() +@CREATED : 1997/05/14, Greg Ward +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +find_lc_tokens (bt_stringlist * tokens, + int * first_lc, + int * last_lc) +{ + int i; /* iterate over token list this time */ + int in_lc_sequence; /* in contig. sequence of lc tokens? */ + + *first_lc = *last_lc = -1; /* haven't found either yet */ + in_lc_sequence = 0; + + i = 0; + while (i < tokens->num_items) + { + if (*first_lc == -1 && islower (tokens->items[i][0])) + { + *first_lc = i; + + i++; + while (i < tokens->num_items && islower (tokens->items[i][0])) + i++; + + *last_lc = i-1; + } + else + { + i++; + } + } +} /* find_lc_tokens() */ + + +/* ------------------------------------------------------------------------ +@NAME : resolve_token_range() +@INPUT : tokens - structure containing the token list + tok_range - two-element array with start and stop token number +@OUTPUT : *part - set to point to first token in range, or NULL + if empty range + *num_tok - number of tokens in the range +@RETURNS : +@DESCRIPTION: Given a list of tokens and a range of token numbers (as a + two-element array, tok_range), computes the number of tokens + in the range. If this is >= 0, sets *part to point + to the first token in the range; otherwise, sets *part + to NULL. +@CALLERS : +@CREATED : May 1997, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +resolve_token_range (bt_stringlist *tokens, + int * tok_range, + char *** part, + int * num_tok) +{ + *num_tok = (tok_range[1] - tok_range[0]) + 1; + if (*num_tok <= 0) + { + *num_tok = 0; + *part = NULL; + } + else + { + *part = tokens->items + tok_range[0]; + } +} /* resolve_token_range() */ + + +/* ------------------------------------------------------------------------ +@NAME : split_simple_name() +@INPUT : name + first_lc + last_lc +@OUTPUT : name +@RETURNS : +@DESCRIPTION: Splits up a name (represented as a string divided into + non-overlapping, whitespace-separated tokens) according + to the BibTeX rules for names without commas. Specifically: + * tokens up to (but not including) the first lowercase + token, or the last token of the string if there + are no lowercase tokens, become the `first' part + * the earliest contiguous sequence of lowercase tokens, + up to (but not including) the last token of the string, + becomes the `von' part + * the tokens following the `von' part, or the last + single token if there is no `von' part, become + the `last' part + * there is no `jr' part +@GLOBALS : +@CALLS : name_warning() (if last lc token taken as lastname) + resolve_token_range() +@CALLERS : bt_split_name() +@CREATED : 1997/05/15, Greg Ward +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +split_simple_name (name_loc * loc, + bt_name * name, + int first_lc, + int last_lc) +{ + int first_t[2], von_t[2], last_t[2]; + int end; + + end = name->tokens->num_items-1; /* token number of last token */ + + if (first_lc > -1) /* any lowercase tokens at all? */ + { + first_t[0] = 0; /* first name goes from beginning */ + first_t[1] = first_lc-1; /* to just before first lc token */ + + if (last_lc == end) /* sequence of lowercase tokens */ + { /* goes all the way to end of string */ + last_lc--; /* -- roll it back by one so we */ + /* still have a lastname */ +#ifdef WARN_LC_LASTNAME + /* + * disable this warning for now because "others" is used fairly + * often as a name in BibTeX databases -- oops! + */ + name_warning (loc, + "no capitalized token at end of name; " + "using \"%s\" as lastname", + name->tokens->items[end]); +#else +# ifndef ALLOW_WARNINGS + loc = NULL; /* avoid "unused parameter" warning */ +# endif +#endif + } + + von_t[0] = first_lc; /* `von' part covers sequence of */ + von_t[1] = last_lc; /* lowercase tokens */ + last_t[0] = last_lc+1; /* lastname from after `von' to end */ + last_t[1] = end; /* of string */ + } + else /* no lowercase tokens */ + { + von_t[0] = 0; /* empty `von' part */ + von_t[1] = -1; + first_t[0] = 0; /* `first' goes from first to second */ + first_t[1] = end-1; /* last token */ + last_t[0] = last_t[1] = end; /* and `last' is just the last token */ + } + + resolve_token_range (name->tokens, first_t, + name->parts+BTN_FIRST, name->part_len+BTN_FIRST); + resolve_token_range (name->tokens, von_t, + name->parts+BTN_VON, name->part_len+BTN_VON); + resolve_token_range (name->tokens, last_t, + name->parts+BTN_LAST, name->part_len+BTN_LAST); + name->parts[BTN_JR] = NULL; /* no jr part possible */ + name->part_len[BTN_JR] = 0; + +} /* split_simple_name() */ + + +/* ------------------------------------------------------------------------ +@NAME : split_general_name() +@INPUT : name + num_commas + comma_token + first_lc + last_lc +@OUTPUT : name +@RETURNS : +@DESCRIPTION: Splits a name according to the BibTeX rules for names + with 1 or 2 commas (> 2 commas is handled elsewhere, + namely by bt_split_name() calling find_commas() with + max_commas == 2). Specifically: + * an initial string of lowercase tokens, up to (but not + including) the token before the first comma, becomes + the `von' part + * tokens from immediately after the `von' part, + or from the beginning of the string if no `von', + up to the first comma become the `last' part + + if one comma: + * all tokens following the sole comma become the + `first' part + + if two commas: + * tokens between the two commas become the `jr' part + * all tokens following the second comma become the + `first' part +@GLOBALS : +@CALLS : name_warning() (if last lc token taken as lastname) + resolve_token_range() +@CALLERS : bt_split_name() +@CREATED : 1997/05/15, Greg Ward +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +split_general_name (name_loc * loc, + bt_name * name, + int num_commas, + int * comma_token, + int first_lc, + int last_lc) +{ + int first_t[2], von_t[2], last_t[2], jr_t[2]; + int end; + + end = name->tokens->num_items-1; /* last token number */ + + if (first_lc == 0) /* we have an initial string of */ + { /* lowercase tokens */ + if (last_lc == comma_token[0]) /* lc string ends at first comma */ + { + name_warning (loc, "no capitalized tokens before first comma"); + last_lc--; + } + + von_t[0] = first_lc; /* `von' covers the sequence of */ + von_t[1] = last_lc; /* lowercase tokens */ + } + else /* no lowercase tokens at start */ + { + von_t[0] = 0; /* empty `von' part */ + von_t[1] = -1; + } + + last_t[0] = von_t[1] + 1; /* start right after end of `von' */ + last_t[1] = comma_token[0]; /* and end at first comma */ + + if (num_commas == 1) + { + first_t[0] = comma_token[0]+1; /* start right after comma */ + first_t[1] = end; /* stop at end of string */ + jr_t[0] = 0; /* empty `jr' part */ + jr_t[1] = -1; + } + else /* more than 1 comma */ + { + jr_t[0] = comma_token[0]+1; /* start after first comma */ + jr_t[1] = comma_token[1]; /* stop at second comma */ + first_t[0] = comma_token[1]+1; /* start after second comma */ + first_t[1] = end; /* and go to end */ + } + + resolve_token_range (name->tokens, first_t, + name->parts+BTN_FIRST, name->part_len+BTN_FIRST); + resolve_token_range (name->tokens, von_t, + name->parts+BTN_VON, name->part_len+BTN_VON); + resolve_token_range (name->tokens, last_t, + name->parts+BTN_LAST, name->part_len+BTN_LAST); + resolve_token_range (name->tokens, jr_t, + name->parts+BTN_JR, name->part_len+BTN_JR); + +} /* split_general_name() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_split_name() +@INPUT : name + filename + line + name_num +@OUTPUT : +@RETURNS : newly-allocated bt_name structure containing the four + parts as token-lists +@DESCRIPTION: Splits a name according to the BibTeX rules. There are + actually two sets of rules: one for names with no commas, + and one for names with 1 or 2 commas. (If a name has + more than 2 commas, the extras are removed and it's treated + as though it had just the first 2.) + + See split_simple_name() for the no-comma rules, and + split_general_name() for the 1-or-2-commas rules. + + The bt_name structure returned can (and should) be freed + with bt_free_name() when you no longer need it. +@GLOBALS : +@CALLS : +@CALLERS : anyone (exported by library) +@CREATED : 1997/05/14, Greg Ward +@MODIFIED : +@COMMENTS : The name-splitting code all implicitly assumes that the + string being split has been post-processed to collapse + whitespace in the BibTeX way. This means that it tends to + dump core on such things as leading whitespace, or more than + one space in a row inside the string. This could probably be + alleviated with a call to bt_postprocess_string(), possibly + preceded by a check for any of those occurences. Before + doing that, though, I want to examine the code carefully to + determine just what assumptions it makes -- so I can + check/correct for all of them. +-------------------------------------------------------------------------- */ +bt_name * +bt_split_name (char * name, + char * filename, + int line, + int name_num) +{ + name_loc loc; + bt_stringlist * + tokens; + int comma_token[MAX_COMMAS]; + int len; + int num_commas; + int first_lc, last_lc; + bt_name * split_name; + int i; + + DBG_ACTION (1, printf ("bt_split_name(): name=%p (%s)\n", name, name)) + + split_name = (bt_name *) malloc (sizeof (bt_name)); + if (name == NULL) + { + len = 0; + } + else + { + name = strdup (name); /* private copy that we may clobber */ + len = strlen (name); + } + + DBG_ACTION (1, printf ("bt_split_name(): split_name=%p\n", split_name)) + + if (len == 0) /* non-existent or empty string? */ + { + split_name->tokens = NULL; + for (i = 0; i < BT_MAX_NAMEPARTS; i++) + { + split_name->parts[i] = NULL; + split_name->part_len[i] = 0; + } + return split_name; + } + + loc.filename = filename; /* so called functions can generate */ + loc.line = line; /* decent warning messages */ + loc.name_num = name_num; + + num_commas = find_commas (&loc, name, MAX_COMMAS); + assert (num_commas <= MAX_COMMAS); + + DBG_ACTION (1, printf ("found %d commas: ", num_commas)) + + tokens = find_tokens (name, comma_token); + +#if DEBUG + printf ("found %d tokens:\n", tokens->num_items); + for (i = 0; i < tokens->num_items; i++) + { + printf (" %d: ", i); + + if (tokens->items[i]) /* non-empty token? */ + { + printf (">%s<\n", tokens->items[i]); + } + else + { + printf ("(empty)\n"); + } + } +#endif + +#if DEBUG + printf ("comma tokens: "); + for (i = 0; i < num_commas; i++) + printf ("%d ", comma_token[i]); + printf ("\n"); +#endif + + find_lc_tokens (tokens, &first_lc, &last_lc); +#if DEBUG + printf ("(first,last) lc tokens = (%d,%d)\n", first_lc, last_lc); +#endif + + if (strlen (name) == 0) /* name now empty? */ + { + split_name->tokens = NULL; + for (i = 0; i < BT_MAX_NAMEPARTS; i++) + { + split_name->parts[i] = NULL; + split_name->part_len[i] = 0; + } + } + else + { + split_name->tokens = tokens; + if (num_commas == 0) /* no commas -- "simple" format */ + { + split_simple_name (&loc, split_name, + first_lc, last_lc); + } + else + { + split_general_name (&loc, split_name, + num_commas, comma_token, + first_lc, last_lc); + } + } + +#if DEBUG + printf ("bt_split_name(): returning structure %p\n", split_name); +#endif + return split_name; +} /* bt_split_name() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_free_name() +@INPUT : name +@OUTPUT : +@RETURNS : +@DESCRIPTION: Frees up any memory allocated for a bt_name structure + (namely, the `tokens' field [a bt_stringlist structure, + this freed with bt_free_list()] and the structure itself.) +@CALLS : bt_free_list() +@CALLERS : anyone (exported) +@CREATED : 1997/11/14, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_free_name (bt_name * name) +{ + DBG_ACTION (2, printf ("bt_free_name(): freeing name %p " + "(%d tokens, string=%p (%s), last[0]=%s)\n", + name, + name->tokens->num_items, + name->tokens->string, + name->tokens->string, + name->parts[BTN_LAST][0])); + bt_free_list (name->tokens); + free (name); + DBG_ACTION (2, printf ("bt_free_name(): done, everything freed\n")); +} diff --git a/src/translators/btparse/parse_auxiliary.c b/src/translators/btparse/parse_auxiliary.c new file mode 100644 index 0000000..f509741 --- /dev/null +++ b/src/translators/btparse/parse_auxiliary.c @@ -0,0 +1,336 @@ +/* ------------------------------------------------------------------------ +@NAME : parse_auxiliary.c +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Anything needed by the parser that's too hairy to go in the + grammar itself. Currently, just stuff needed for generating + syntax errors. (See error.c for how they're actually + printed.) +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1996/08/07, Greg Ward +@MODIFIED : +@VERSION : $Id: parse_auxiliary.c,v 1.20 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include "stdpccts.h" +#include "error.h" +#include "lex_auxiliary.h" +#include "parse_auxiliary.h" +/*#include "my_dmalloc.h"*/ + +extern char * InputFilename; /* from input.c */ + +GEN_PRIVATE_ERRFUNC (syntax_error, (char * fmt, ...), + BTERR_SYNTAX, InputFilename, zzline, NULL, -1, fmt) + + +/* this is stolen from PCCTS' err.h */ +static SetWordType bitmask[] = +{ + 0x00000001, 0x00000002, 0x00000004, 0x00000008, + 0x00000010, 0x00000020, 0x00000040, 0x00000080 +}; + +static struct +{ + int token; + const char *new_name; +} new_tokens[] = +{ + { AT, "\"@\"" }, + { NAME, "name (entry type, key, field, or macro name)" }, + { LBRACE, "left brace (\"{\")" }, + { RBRACE, "right brace (\"}\")" }, + { ENTRY_OPEN, "start of entry (\"{\" or \"(\")" }, + { ENTRY_CLOSE,"end of entry (\"}\" or \")\")" }, + { EQUALS, "\"=\"" }, + { HASH, "\"#\"" }, + { COMMA, "\",\"" }, + { NUMBER, "number" }, + { STRING, "quoted string ({...} or \"...\")" } +}; + + +#ifdef CLEVER_TOKEN_STUFF +char **token_names; +#endif + + +void +fix_token_names (void) +{ + int i; + int num_replace; + +#ifdef CLEVER_TOKEN_STUFF /* clever, but it doesn't work... */ + /* arg! this doesn't work because I don't know how to find out the + * number of tokens + */ + + int num_tok; + + num_tok = (sizeof(zztokens) / sizeof(*zztokens)); + sizeof (zztokens); + sizeof (*zztokens); + token_names = (char **) malloc (sizeof (char *) * num_tok); + + for (i = 0; i < num_tok; i++) + { + token_names[i] = zztokens[i]; + } +#endif + + num_replace = (sizeof(new_tokens) / sizeof(*new_tokens)); + for (i = 0; i < num_replace; i++) + { + const char *new = new_tokens[i].new_name; + const char **old = zztokens + new_tokens[i].token; + + *old = new; + } +} + + +#ifdef USER_ZZSYN + +static void +append_token_set (char *msg, SetWordType *a) +{ + SetWordType *p = a; + SetWordType *endp = &(p[zzSET_SIZE]); + unsigned e = 0; + int tokens_printed = 0; + + do + { + SetWordType t = *p; + SetWordType *b = &(bitmask[0]); + do + { + if (t & *b) + { + strcat (msg, zztokens[e]); + tokens_printed++; + if (tokens_printed < zzset_deg (a) - 1) + strcat (msg, ", "); + else if (tokens_printed == zzset_deg (a) - 1) + strcat (msg, " or "); + } + e++; + } while (++b < &(bitmask[sizeof(SetWordType)*8])); + } while (++p < endp); +} + + +void +zzsyn(const char * text, + int tok, + char * egroup, + SetWordType * eset, + int etok, + int k, + const char * bad_text) +{ + static char msg [MAX_ERROR]; + int len; + +#ifndef ALLOW_WARNINGS + text = NULL; /* avoid "unused parameter" warning */ +#endif + + /* Initial message: give location of error */ + + msg[0] = (char) 0; /* make sure string is empty to start! */ + if (tok == zzEOF_TOKEN) + strcat (msg, "at end of input"); + else + sprintf (msg, "found \"%s\"", bad_text); + + len = strlen (msg); + + + /* Caller supplied neither a single token nor set of tokens expected... */ + + if (!etok && !eset) + { + syntax_error (msg); + return; + } + else + { + strcat (msg, ", "); + len += 2; + } + + + /* I'm not quite sure what this is all about, or where k would be != 1... */ + + if (k != 1) + { + sprintf (msg+len, "; \"%s\" not", bad_text); + if (zzset_deg (eset) > 1) strcat (msg, " in"); + len = strlen (msg); + } + + + /* This is the code that usually gets run */ + + if (zzset_deg (eset) > 0) + { + if (zzset_deg (eset) == 1) + strcat (msg, "expected "); + else + strcat (msg, "expected one of: "); + + append_token_set (msg, eset); + } + else + { + sprintf (msg+len, "expected %s", zztokens[etok]); + if (etok == ENTRY_CLOSE) + { + strcat (msg, " (skipping to next \"@\")"); + initialize_lexer_state (); + } + } + + len = strlen (msg); + if (egroup && strlen (egroup) > 0) + sprintf (msg+len, " in %s", egroup); + + syntax_error (msg); + +} +#endif /* USER_ZZSYN */ + + +void +check_field_name (AST * field) +{ + char * name; + + if (! field || field->nodetype != BTAST_FIELD) + return; + + name = field->text; + if (strchr ("0123456789", name[0])) + syntax_error ("invalid field name \"%s\": cannot start with digit", + name); +} + + +#ifdef STACK_DUMP_CODE + +static void +show_ast_stack_elem (int num) +{ + extern const char *nodetype_names[]; /* nicked from bibtex_ast.c */ + /* bt_nodetype nodetype; + bt_metatype metatype; */ + AST *elem; + + elem = zzastStack[num]; + printf ("zzastStack[%3d] = ", num); + if (elem) + { + /* get_node_type (elem, &nodetype, &metatype); */ + if (elem->nodetype <= BTAST_MACRO) + { + printf ("{ %s: \"%s\" (line %d, char %d) }\n", + nodetype_names[elem->nodetype], + elem->text, elem->line, elem->offset); + } + else + { + printf ("bogus node (uninitialized?)\n"); + } + } + else + { + printf ("NULL\n"); + } +} + + +static void +show_ast_stack_top (char *label) +{ + if (label) + printf ("%s: ast stack top: ", label); + else + printf ("ast stack top: "); + show_ast_stack_elem (zzast_sp); +} + + +static void +dump_ast_stack (char *label) +{ + int i; + + if (label) + printf ("%s: complete ast stack:\n", label); + else + printf ("complete ast stack:\n"); + + for (i = zzast_sp; i < ZZAST_STACKSIZE; i++) + { + printf (" "); + show_ast_stack_elem (i); + } +} + + +static void +show_attrib_stack_elem (int num) +{ + Attrib elem; + + elem = zzaStack[num]; + printf ("zzaStack[%3d] = ", num); + printf ("{ \"%s\" (token %d (%s), line %d, char %d) }\n", + elem.text, elem.token, zztokens[elem.token], + elem.line, elem.offset); +} + + +static void +show_attrib_stack_top (char *label) +{ + if (label) + printf ("%s: attrib stack top: ", label); + else + printf ("attrib stack top: "); + show_attrib_stack_elem (zzasp); +} + + +static void +dump_attrib_stack (char *label) +{ + int i; + + if (label) + printf ("%s: complete attrib stack:\n", label); + else + printf ("complete attrib stack:\n"); + + for (i = zzasp; i < ZZA_STACKSIZE; i++) + { + printf (" "); + show_attrib_stack_elem (i); + } +} + +#endif /* STACK_DUMP_CODE */ diff --git a/src/translators/btparse/parse_auxiliary.h b/src/translators/btparse/parse_auxiliary.h new file mode 100644 index 0000000..5500513 --- /dev/null +++ b/src/translators/btparse/parse_auxiliary.h @@ -0,0 +1,32 @@ +/* ------------------------------------------------------------------------ +@NAME : parse_auxiliary.h +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Prototype declarations for functions in parse_auxiliary.c +@GLOBALS : +@CALLS : +@CREATED : 1997/01/08, Greg Ward +@MODIFIED : +@VERSION : $Id: parse_auxiliary.h,v 1.5 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef PARSE_AUXILIARY_H +#define PARSE_AUXILIARY_H + +#include "stdpccts.h" /* for SetWordType typedef */ + +void fix_token_names (void); +void zzsyn (const char *text, int tok, + char *egroup, SetWordType *eset, int etok, + int k, const char *bad_text); +void check_field_name (AST * field); + +#endif /* PARSE_AUXILIARY_H */ diff --git a/src/translators/btparse/postprocess.c b/src/translators/btparse/postprocess.c new file mode 100644 index 0000000..7f7bfd4 --- /dev/null +++ b/src/translators/btparse/postprocess.c @@ -0,0 +1,498 @@ +/* ------------------------------------------------------------------------ +@NAME : postprocess.c +@DESCRIPTION: Operations applied to the AST (or strings in it) after + parsing is complete. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/12, Greg Ward (from code in bibparse.c, lex_auxiliary.c) +@MODIFIED : +@VERSION : $Id: postprocess.c,v 1.25 2000/05/02 23:06:31 greg Exp $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include "btparse.h" +#include "error.h" +#include "parse_auxiliary.h" +#include "prototypes.h" +/*#include "my_dmalloc.h"*/ + +#define DEBUG 1 + + +/* ------------------------------------------------------------------------ +@NAME : bt_postprocess_string () +@INPUT : s + options +@OUTPUT : s (modified in place according to the flags) +@RETURNS : (void) +@DESCRIPTION: Make a pass over string s (which is modified in-place) to + optionally collapse whitespace according to BibTeX rules + (if the BTO_COLLAPSE bit in options is true). + + Rules for collapsing whitespace are: + * whitespace at beginning/end of string is deleted + * within the string, each whitespace sequence is replaced by + a single space + + Note that part of the work is done by the lexer proper, + namely conversion of tabs and newlines to spaces. +@GLOBALS : +@CALLS : +@CREATED : originally in lex_auxiliary.c; moved here 1997/01/12 +@MODIFIED : +@COMMENTS : this only collapses whitespace now -- rename it??? +-------------------------------------------------------------------------- */ +void +bt_postprocess_string (char * s, ushort options) +{ + boolean collapse_whitespace; + char *i, *j; + int len; + + if (s == NULL) return; /* quit if no string supplied */ + +#if DEBUG > 1 + printf ("bt_postprocess_string: looking at >%s<\n", s); +#endif + + /* Extract any relevant options (just one currently) to local flags. */ + collapse_whitespace = options & BTO_COLLAPSE; + + /* + * N.B. i and j will both point into s; j is always >= i, and + * we copy characters from j to i. Whitespace is collapsed/deleted + * by advancing j without advancing i. + */ + i = j = s; /* start both at beginning of string */ + + /* + * If we're supposed to collapse whitespace, then advance j to the + * first non-space character. + */ + if (collapse_whitespace) + { + while (*j == ' ' && *j != (char) 0) + j++; + } + + while (*j != (char) 0) + { + /* + * If we're in a string of spaces (ie. current and previous char. + * are both space), and we're supposed to be collapsing whitespace, + * then skip until we hit a non-space character (or end of string). + */ + if (collapse_whitespace && *j == ' ' && *(j-1) == ' ') + { + while (*j == ' ') j++; /* skip spaces */ + if (*j == (char) 0) /* reached end of string? */ + break; + } + + /* Copy the current character from j down to i */ + *(i++) = *(j++); + } + *i = (char) 0; /* ensure string is terminated */ + + + /* + * And mop up whitespace (if any) at end of string -- note that if there + * was any whitespace there, it has already been collapsed to exactly + * one space. + */ + len = strlen (s); + if (len > 0 && collapse_whitespace && s[len-1] == ' ') + { + s[--len] = (char) 0; + } + +#if DEBUG > 1 + printf (" transformed to >%s<\n", s); +#endif + +} /* bt_postprocess_string */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_postprocess_value() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Post-processes a series of strings (compound value), + frequently found as the value of a "field = value" or "macro + = value" assignment. The actions taken here are governed by + the bits in 'options', but there are two distinct modes of + operation: pasting or not. + + We paste strings if and only if the BTO_PASTE bit in options + is set and there are two or more simple values in the + compound value. In this case, the BTO_EXPAND bit must be set + (it would be very silly to paste together strings with + unexpanded macro names!), and we make two passes over the + data: one to postprocess individual strings and accumulate + the one big string, and a second to postprocess the big + string. In the first pass, the caller-supplied 'options' + variable is largely ignored; we will never collapse + whitespace in the individual strings. The caller's wishes + are fully respected when we make the final post-processing + pass over the concatenation of the individual strings, + though. + + If we're not pasting strings, then the character of the + individual simple values will be preserved; macros might not + be expanded (depending on the BTO_EXPAND bit), numbers will + stay numbers, and strings will be post-processed + independently according to the 'options' variable. (Beware + -- this means you might collapse whitespace in individual + sub-strings, which would be bad if you intend to concatenate + them later in the BibTeX sense.) + + The 'replace' parameter is used to govern whether the + existing strings in the AST should be replaced with their + post-processed versions. This can extend as far as + collapsing a series of simple values into a single BTAST_STRING + node, if we paste sub-strings together. If replace is FALSE, + the returned string is allocated here, and you must free() it + later. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/10, GPW +@MODIFIED : 1997/08/25, GPW: renamed from bt_postprocess_field(), and changed + to take the head of a list of simple values, + rather than the parent of that list +-------------------------------------------------------------------------- */ +char * +bt_postprocess_value (AST * value, ushort options, boolean replace) +{ + AST * simple_value; /* current simple value */ + boolean pasting; + ushort string_opts; /* what to do to individual strings */ + int tot_len; /* total length of pasted string */ + char * new_string; /* in case of string pasting */ + char * tmp_string; + boolean free_tmp; /* should we free() tmp_string? */ + + if (value == NULL) return NULL; + if (value->nodetype != BTAST_STRING && + value->nodetype != BTAST_NUMBER && + value->nodetype != BTAST_MACRO) + { + usage_error ("bt_postprocess_value: invalid AST node (not a value)"); + } + + + /* + * We will paste strings iff the user wants us to, and there are at least + * two simple values in the list headed by 'value'. + */ + + pasting = (options & BTO_PASTE) && (value->right); + + /* + * If we're to concatenate (paste) sub-strings, we need to know the + * total length of them. So make a pass over all the sub-strings + * (simple values), adding up their lengths. + */ + + tot_len = 0; /* these are out here to keep */ + new_string = NULL; /* gcc -Wall happy */ + tmp_string = NULL; + + if (pasting) + { + simple_value = value; + while (simple_value) + { + switch (simple_value->nodetype) + { + case BTAST_MACRO: + tot_len += bt_macro_length (simple_value->text); + break; + case BTAST_STRING: + tot_len += (simple_value->text) + ? (strlen (simple_value->text)) : 0; + break; + case BTAST_NUMBER: + tot_len += (simple_value->text) + ? (strlen (simple_value->text)) : 0; + break; + default: + internal_error ("simple value has bad nodetype (%d)", + (int) simple_value->nodetype); + } + simple_value = simple_value->right; + } + + /* Now allocate the buffer in which we'll accumulate the whole string */ + + new_string = (char *) calloc (tot_len+1, sizeof (char)); + } + + + /* + * Before entering the main loop, figure out just what + * bt_postprocess_string() is supposed to do -- eg. if pasting strings, + * we should not (yet) collapse whitespace. (That'll be done on the + * final, concatenated string -- assuming the caller put BTO_COLLAPSE in + * the options bitmap.) + */ + + if (pasting) + { + string_opts = options & ~BTO_COLLAPSE; /* turn off collapsing */ + } + else + { + string_opts = options; /* leave it alone */ + } + + /* + * Sanity check: if we continue blindly on, we might stupidly + * concatenate a macro name and a literal string. So check for that. + * Converting numbers is superficial, but requiring that it be done + * keeps people honest. + */ + + if (pasting && ! (options & (BTO_CONVERT|BTO_EXPAND))) + { + usage_error ("bt_postprocess_value(): " + "must convert numbers and expand macros " + "when pasting substrings"); + } + + /* + * Now the main loop to process each string, and possibly tack it onto + * new_string. + */ + + simple_value = value; + while (simple_value) + { + tmp_string = NULL; + free_tmp = FALSE; + + /* + * If this simple value is a macro and we're supposed to expand + * macros, then do so. We also have to post-process the string + * returned from the macro table, because they're stored there + * without whitespace collapsed; if we're supposed to be doing that + * to the current value (and we're not pasting), this is where it + * will get done. + */ + if (simple_value->nodetype == BTAST_MACRO && (options & BTO_EXPAND)) + { + tmp_string = bt_macro_text (simple_value->text, + simple_value->filename, + simple_value->line); + if (tmp_string != NULL) + { + tmp_string = strdup (tmp_string); + free_tmp = TRUE; + bt_postprocess_string (tmp_string, string_opts); + } + + if (replace) + { + simple_value->nodetype = BTAST_STRING; + if (simple_value->text) + free (simple_value->text); + simple_value->text = tmp_string; + free_tmp = FALSE; /* mustn't free, it's now in the AST */ + } + } + + /* + * If the current simple value is a literal string, then just + * post-process it. This will be done in-place if 'replace' is + * true, otherwise a copy of the string will be post-processed. + */ + else if (simple_value->nodetype == BTAST_STRING && simple_value->text) + { + if (replace) + { + tmp_string = simple_value->text; + } + else + { + tmp_string = strdup (simple_value->text); + free_tmp = TRUE; + } + + bt_postprocess_string (tmp_string, string_opts); + } + + /* + * Finally, if the current simple value is a number, change it to a + * string (depending on options) and get its value. We generally + * treat strings as numbers as equivalent, except of course numbers + * aren't post-processed -- there can't be any whitespace in them! + * The BTO_CONVERT option is mainly a sop to my strong-typing + * tendencies. + */ + if (simple_value->nodetype == BTAST_NUMBER) + { + if (replace && (options & BTO_CONVERT)) + simple_value->nodetype = BTAST_STRING; + + if (simple_value->text) + { + if (replace) + tmp_string = simple_value->text; + else + { + tmp_string = strdup (simple_value->text); + free_tmp = TRUE; + } + } + } + + if (pasting) + { + if (tmp_string) + strcat (new_string, tmp_string); + if (free_tmp) + free (tmp_string); + } + else + { + /* + * N.B. if tmp_string is NULL (eg. from a single undefined macro) + * we make a strdup() of the empty string -- this is so we can + * safely free() the string returned from this function + * at some future point. + * + * This strdup() seems to cause a 1-byte memory leak in some + * circumstances. I s'pose I should look into that some rainy + * afternoon... + */ + + new_string = (tmp_string != NULL) ? tmp_string : strdup (""); + } + + simple_value = simple_value->right; + } + + if (pasting) + { + int len; + + len = strlen (new_string); + assert (len <= tot_len); /* hope we alloc'd enough! */ + + bt_postprocess_string (new_string, options); + + /* + * If replacing data in the AST, delete all but first child of + * `field', and replace text for first child with new_string. + */ + + if (replace) + { + assert (value->right != NULL); /* there has to be > 1 simple value! */ + zzfree_ast (value->right); /* free from second simple value on */ + value->right = NULL; /* remind ourselves they're gone */ + if (value->text) /* free text of first simple value */ + free (value->text); + value->text = new_string; /* and replace it with concatenation */ + } + } + + return new_string; + +} /* bt_postprocess_value() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_postprocess_field() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Postprocesses all the strings in a single "field = value" + assignment subtree. Just checks that 'field' does indeed + point to an BTAST_FIELD node (presumably the parent of a list + of simple values), downcases the field name, and calls + bt_postprocess_value() on the value. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/08/25, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +char * +bt_postprocess_field (AST * field, ushort options, boolean replace) +{ + if (field == NULL) return NULL; + if (field->nodetype != BTAST_FIELD) + usage_error ("bt_postprocess_field: invalid AST node (not a field)"); + + strlwr (field->text); /* downcase field name */ + return bt_postprocess_value (field->down, options, replace); + +} /* bt_postprocess_field() */ + + + +/* ------------------------------------------------------------------------ +@NAME : bt_postprocess_entry() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Postprocesses all the strings in an entry: collapse whitespace, + concatenate substrings, expands macros, and whatnot. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/10, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_postprocess_entry (AST * top, ushort options) +{ + AST *cur; + + if (top == NULL) return; /* not even an entry at all! */ + if (top->nodetype != BTAST_ENTRY) + usage_error ("bt_postprocess_entry: " + "invalid node type (not entry root)"); + strlwr (top->text); /* downcase entry type */ + + if (top->down == NULL) return; /* no children at all */ + + cur = top->down; + if (cur->nodetype == BTAST_KEY) + cur = cur->right; + + switch (top->metatype) + { + case BTE_REGULAR: + case BTE_MACRODEF: + { + while (cur) + { + bt_postprocess_field (cur, options, TRUE); + if (top->metatype == BTE_MACRODEF && ! (options & BTO_NOSTORE)) + bt_add_macro_value (cur, options); + + cur = cur->right; + } + break; + } + + case BTE_COMMENT: + case BTE_PREAMBLE: + bt_postprocess_value (cur, options, TRUE); + break; + default: + internal_error ("bt_postprocess_entry: unknown entry metatype (%d)", + (int) top->metatype); + } + +} /* bt_postprocess_entry() */ diff --git a/src/translators/btparse/prototypes.h b/src/translators/btparse/prototypes.h new file mode 100644 index 0000000..88beada --- /dev/null +++ b/src/translators/btparse/prototypes.h @@ -0,0 +1,47 @@ +/* ------------------------------------------------------------------------ +@NAME : prototypes.h +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Prototype declarations for functions from various places. + Only functions that are private to the library (but shared + between files within the library) are declared here. + Functions that are "exported from" the library (ie. usable + by and expected to be used by library user) are declared in + btparse.h. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/12, Greg Ward +@MODIFIED : +@VERSION : $Id: prototypes.h,v 1.14 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +#ifndef PROTOTYPES_H +#define PROTOTYPES_H + +#include <stdio.h> +#include "btparse.h" /* for types */ + +/* util.c */ +#if !HAVE_STRLWR +char *strlwr (char *s); +#endif +#if !HAVE_STRUPR +char *strupr (char *s); +#endif + +/* macros.c */ +void init_macros (void); +void done_macros (void); + +/* bibtex_ast.c */ +void dump_ast (char *msg, AST *root); + +#endif /* PROTOTYPES_H */ diff --git a/src/translators/btparse/scan.c b/src/translators/btparse/scan.c new file mode 100644 index 0000000..b9899e4 --- /dev/null +++ b/src/translators/btparse/scan.c @@ -0,0 +1,615 @@ + +/* parser.dlg -- DLG Description of scanner + * + * Generated from: bibtex.g + * + * Terence Parr, Will Cohen, and Hank Dietz: 1989-1994 + * Purdue University Electrical Engineering + * With AHPCRC, University of Minnesota + * ANTLR Version 1.33 + */ + +#include <stdio.h> +#define ANTLR_VERSION 133 + +#define ZZCOL +#define USER_ZZSYN + +#include "btconfig.h" +#include "btparse.h" +#include "attrib.h" +#include "lex_auxiliary.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + +extern char * InputFilename; /* for zzcr_ast call in pccts/ast.c */ +#include "antlr.h" +#include "ast.h" +#include "tokens.h" +#include "dlgdef.h" +LOOKAHEAD +void zzerraction() +{ + (*zzerr)("invalid token"); + zzadvance(); + zzskip(); +} +/* + * D L G tables + * + * Generated from: parser.dlg + * + * 1989-1994 by Will Cohen, Terence Parr, and Hank Dietz + * Purdue University Electrical Engineering + * DLG Version 1.33 + */ + +#include "mode.h" + + + +static void act1() +{ + NLA = 1; + } + + +static void act2() +{ + NLA = AT; + at_sign (); + } + + +static void act3() +{ + NLA = 3; + newline (); + } + + +static void act4() +{ + NLA = COMMENT; + comment (); + } + + +static void act5() +{ + NLA = 5; + zzskip (); + } + + +static void act6() +{ + NLA = 6; + toplevel_junk (); + } + +static unsigned char shift0[257] = { + 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 4, 2, 5, 5, 4, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 4, 5, 5, 5, 5, 3, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5 +}; + + +static void act7() +{ + NLA = 1; + } + + +static void act8() +{ + NLA = 7; + newline (); + } + + +static void act9() +{ + NLA = COMMENT; + comment (); + } + + +static void act10() +{ + NLA = 8; + zzskip (); + } + + +static void act11() +{ + NLA = NUMBER; + } + + +static void act12() +{ + NLA = NAME; + name (); + } + + +static void act13() +{ + NLA = LBRACE; + lbrace (); + } + + +static void act14() +{ + NLA = RBRACE; + rbrace (); + } + + +static void act15() +{ + NLA = ENTRY_OPEN; + lparen (); + } + + +static void act16() +{ + NLA = ENTRY_CLOSE; + rparen (); + } + + +static void act17() +{ + NLA = EQUALS; + } + + +static void act18() +{ + NLA = HASH; + } + + +static void act19() +{ + NLA = COMMA; + } + + +static void act20() +{ + NLA = 18; + start_string ('"'); + } + +static unsigned char shift1[257] = { + 0, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 3, 1, 14, 14, 3, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 3, 5, 13, 11, 5, 2, 5, + 14, 8, 9, 5, 5, 12, 5, 5, 5, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, + 5, 5, 10, 5, 5, 14, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 14, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 6, 5, 7, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14 +}; + + +static void act21() +{ + NLA = 1; + } + + +static void act22() +{ + NLA = 19; + check_runaway_string (); + } + + +static void act23() +{ + NLA = 20; + zzreplchar (' '); zzmore (); + } + + +static void act24() +{ + NLA = 21; + open_brace (); + } + + +static void act25() +{ + NLA = 22; + close_brace (); + } + + +static void act26() +{ + NLA = 23; + lparen_in_string (); + } + + +static void act27() +{ + NLA = 24; + rparen_in_string (); + } + + +static void act28() +{ + NLA = STRING; + quote_in_string (); + } + + +static void act29() +{ + NLA = 26; + zzmore (); + } + +static unsigned char shift2[257] = { + 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 1, 3, 3, 2, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 8, 3, 3, 3, 3, + 3, 6, 7, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 9, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 4, 3, 5, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3 +}; + +#define DfaStates 38 +typedef unsigned char DfaState; + +static DfaState st0[7] = { + 1, 2, 3, 4, 5, 6, 38 +}; + +static DfaState st1[7] = { + 38, 38, 38, 38, 38, 38, 38 +}; + +static DfaState st2[7] = { + 38, 38, 38, 38, 38, 38, 38 +}; + +static DfaState st3[7] = { + 38, 38, 38, 38, 38, 38, 38 +}; + +static DfaState st4[7] = { + 38, 7, 8, 9, 7, 9, 38 +}; + +static DfaState st5[7] = { + 38, 38, 38, 38, 5, 38, 38 +}; + +static DfaState st6[7] = { + 38, 38, 38, 6, 38, 6, 38 +}; + +static DfaState st7[7] = { + 38, 7, 8, 7, 7, 7, 38 +}; + +static DfaState st8[7] = { + 38, 38, 38, 38, 38, 38, 38 +}; + +static DfaState st9[7] = { + 38, 7, 8, 9, 7, 9, 38 +}; + +static DfaState st10[16] = { + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 38, 38 +}; + +static DfaState st11[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st12[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st13[16] = { + 38, 25, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 38 +}; + +static DfaState st14[16] = { + 38, 38, 38, 14, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st15[16] = { + 38, 38, 38, 38, 15, 16, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st16[16] = { + 38, 38, 38, 38, 16, 16, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st17[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st18[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st19[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st20[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st21[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st22[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st23[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st24[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st25[16] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38, 38, 38, 38, 38, 38 +}; + +static DfaState st26[16] = { + 38, 25, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 38 +}; + +static DfaState st27[11] = { + 28, 29, 30, 31, 32, 33, 34, 35, 36, 31, + 38 +}; + +static DfaState st28[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st29[11] = { + 38, 38, 37, 37, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st30[11] = { + 38, 38, 31, 31, 38, 38, 38, 38, 38, 31, + 38 +}; + +static DfaState st31[11] = { + 38, 38, 31, 31, 38, 38, 38, 38, 38, 31, + 38 +}; + +static DfaState st32[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st33[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st34[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st35[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st36[11] = { + 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, + 38 +}; + +static DfaState st37[11] = { + 38, 38, 37, 37, 38, 38, 38, 38, 38, 38, + 38 +}; + + +DfaState *dfa[38] = { + st0, + st1, + st2, + st3, + st4, + st5, + st6, + st7, + st8, + st9, + st10, + st11, + st12, + st13, + st14, + st15, + st16, + st17, + st18, + st19, + st20, + st21, + st22, + st23, + st24, + st25, + st26, + st27, + st28, + st29, + st30, + st31, + st32, + st33, + st34, + st35, + st36, + st37 +}; + + +DfaState accepts[39] = { + 0, 1, 2, 3, 6, 5, 6, 0, 4, 6, + 0, 7, 8, 0, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 9, 0, 0, 21, 22, + 23, 29, 24, 25, 26, 27, 28, 22, 0 +}; + +void (*actions[30])() = { + zzerraction, + act1, + act2, + act3, + act4, + act5, + act6, + act7, + act8, + act9, + act10, + act11, + act12, + act13, + act14, + act15, + act16, + act17, + act18, + act19, + act20, + act21, + act22, + act23, + act24, + act25, + act26, + act27, + act28, + act29 +}; + +static DfaState dfa_base[] = { + 0, + 10, + 27 +}; + +static unsigned char *b_class_no[] = { + shift0, + shift1, + shift2 +}; + + + +#define ZZSHIFT(c) (b_class_no[zzauto][1+c]) +#define MAX_MODE 3 +#include "dlgauto.h" diff --git a/src/translators/btparse/stdpccts.h b/src/translators/btparse/stdpccts.h new file mode 100644 index 0000000..e232634 --- /dev/null +++ b/src/translators/btparse/stdpccts.h @@ -0,0 +1,31 @@ +#ifndef STDPCCTS_H +#define STDPCCTS_H +/* + * stdpccts.h -- P C C T S I n c l u d e + * + * Terence Parr, Will Cohen, and Hank Dietz: 1989-1994 + * Purdue University Electrical Engineering + * With AHPCRC, University of Minnesota + * ANTLR Version 1.33 + */ +#include <stdio.h> +#define ANTLR_VERSION 133 + +#define ZZCOL +#define USER_ZZSYN + +#include "btparse.h" +#include "attrib.h" +#include "lex_auxiliary.h" +#include "error.h" +/*#include "my_dmalloc.h"*/ + +extern char * InputFilename; /* for zzcr_ast call in pccts/ast.c */ +#define GENAST +#define zzSET_SIZE 4 +#include "antlr.h" +#include "ast.h" +#include "tokens.h" +#include "dlgdef.h" +#include "mode.h" +#endif diff --git a/src/translators/btparse/string_util.c b/src/translators/btparse/string_util.c new file mode 100644 index 0000000..3713608 --- /dev/null +++ b/src/translators/btparse/string_util.c @@ -0,0 +1,695 @@ +/* ------------------------------------------------------------------------ +@NAME : string_util.c +@DESCRIPTION: Various string-processing utility functions: + bt_purify_string() + bt_change_case() + + and their helpers: + foreign_letter() + purify_special_char() +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/10/19, Greg Ward +@MODIFIED : 1997/11/25, GPW: renamed to from purify.c to string_util.c + added bt_change_case() and friends +@VERSION : $Id: string_util.c,v 1.10 1999/10/28 22:50:28 greg Rel $ +-------------------------------------------------------------------------- */ + +#include <stdlib.h> +#include <ctype.h> +#include <string.h> +#include <assert.h> +#include "error.h" +#include "btparse.h" +#include "bt_debug.h" + + +/* + * These definitions should be fixed to be consistent with HTML + * entities, just for fun. And perhaps I should add entries for + * accented letters (at least those supported by TeX and HTML). + */ +typedef enum +{ + L_OTHER, /* not a "foreign" letter */ + L_OSLASH_L, /* Eastern European {\o} */ + L_OSLASH_U, + L_LSLASH_L, /* {\l} */ + L_LSLASH_U, + L_OELIG_L, /* Latin {\oe} ligature */ + L_OELIG_U, + L_AELIG_L, /* {\ae} ligature */ + L_AELIG_U, + L_SSHARP_L, /* German "sharp s" {\ss} */ + L_SSHARP_U, + L_ACIRCLE_L, /* Nordic {\aa} */ + L_ACIRCLE_U, + L_INODOT_L, /* undotted i: {\i} */ + L_JNODOT_L /* {\j} */ +} bt_letter; + + +static const char * uc_version[] = +{ + NULL, /* L_OTHER */ + "\\O", /* L_OSLASH_L */ + "\\O", /* L_OSLASH_U */ + "\\L", /* L_LSLASH_L */ + "\\L", /* L_LSLASH_U */ + "\\OE", /* L_OELIG_L */ + "\\OE", /* L_OELIG_U */ + "\\AE", /* L_AELIG_L */ + "\\AE", /* L_AELIG_U */ + "SS", /* L_SSHARP_L -- for LaTeX 2.09 */ + "\\SS", /* L_SSHARP_U */ + "\\AA", /* L_ACIRCLE_L */ + "\\AA", /* L_ACIRCLE_U */ + "I", /* L_INODOT_L */ + "J" /* L_JNODOT_L */ +}; + +static const char * lc_version[] = +{ + NULL, /* L_OTHER */ + "\\o", /* L_OSLASH_L */ + "\\o", /* L_OSLASH_U */ + "\\l", /* L_LSLASH_L */ + "\\l", /* L_LSLASH_U */ + "\\oe", /* L_OELIG_L */ + "\\oe", /* L_OELIG_U */ + "\\ae", /* L_AELIG_L */ + "\\ae", /* L_AELIG_U */ + "\\ss", /* L_SSHARP_L */ + "\\ss", /* L_SSHARP_U */ + "\\aa", /* L_ACIRCLE_L */ + "\\aa", /* L_ACIRCLE_U */ + "\\i", /* L_INODOT_L */ + "\\j" /* L_JNODOT_L */ +}; + + + +/* ------------------------------------------------------------------------ +@NAME : foreign_letter() +@INPUT : str + start + stop +@OUTPUT : letter +@RETURNS : TRUE if the string delimited by start and stop is a foreign + letter control sequence +@DESCRIPTION: Determines if a character sequence is one of (La)TeX's + "foreign letter" control sequences (l, o, ae, oe, aa, ss, plus + uppercase versions). If `letter' is non-NULL, returns which + letter was found in it (as a bt_letter value). +@CALLS : +@CALLERS : purify_special_char() +@CREATED : 1997/10/19, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static boolean +foreign_letter (char *str, int start, int stop, bt_letter * letter) +{ + char c1, c2; + bt_letter dummy; + + + /* + * This is written for speed, not flexibility -- adding new foreign + * letters would be trying and vexatious. + * + * N.B. my gold standard list of foreign letters is Kopka and Daly's + * *A Guide to LaTeX 2e*, section 2.5.6. + */ + + if (letter == NULL) /* so we can assign to *letter */ + letter = &dummy; /* without compunctions */ + *letter = L_OTHER; /* assume not a "foreign" letter */ + + c1 = str[start+0]; /* only two characters that we're */ + c2 = str[start+1]; /* interested in */ + + switch (stop - start) + { + case 1: /* one-character control sequences */ + switch (c1) /* (\o and \l) */ + { + case 'o': + *letter = L_OSLASH_L; return TRUE; + case 'O': + *letter = L_OSLASH_U; return TRUE; + case 'l': + *letter = L_LSLASH_L; return TRUE; + case 'L': + *letter = L_LSLASH_L; return TRUE; + case 'i': + *letter = L_INODOT_L; return TRUE; + case 'j': + *letter = L_JNODOT_L; return TRUE; + default: + return FALSE; + } + break; + case 2: /* two character control sequences */ + switch (c1) /* (\oe, \ae, \aa, and \ss) */ + { + case 'o': + if (c2 == 'e') { *letter = L_OELIG_L; return TRUE; } + case 'O': + if (c2 == 'E') { *letter = L_OELIG_U; return TRUE; } + + /* BibTeX 0.99 does not handle \aa and \AA -- but I do!*/ + case 'a': + if (c2 == 'e') + { *letter = L_AELIG_L; return TRUE; } + else if (c2 == 'a') + { *letter = L_ACIRCLE_L; return TRUE; } + else + return FALSE; + case 'A': + if (c2 == 'E') + { *letter = L_AELIG_U; return TRUE; } + else if (c2 == 'A') + { *letter = L_ACIRCLE_U; return TRUE; } + else + return FALSE; + + /* uppercase sharp-s -- new with LaTeX 2e (so far all I do + * is recognize it as a "foreign" letter) + */ + case 's': + if (c2 == 's') + { *letter = L_SSHARP_L; return TRUE; } + else + return FALSE; + case 'S': + if (c2 == 'S') + { *letter = L_SSHARP_U; return TRUE; } + else + return FALSE; + } + break; + default: + return FALSE; + } /* switch on length of control sequence */ + + internal_error ("foreign_letter(): should never reach end of function"); + return FALSE; /* to keep gcc -Wall happy */ + +} /* foreign_letter */ + + +/* ------------------------------------------------------------------------ +@NAME : purify_special_char() +@INPUT : *src, *dst - pointers into the input and output strings +@OUTPUT : *src - updated to point to the closing brace of the + special char + *dst - updated to point to the next available spot + for copying text to +@RETURNS : +@DESCRIPTION: "Purifies" a BibTeX special character. On input, *src should + point to the opening brace of a special character (ie. the + brace must be at depth 0 of the whole string, and the + character immediately following it must be a backslash). + *dst should point to the next spot to copy into the output + (purified) string. purify_special_char() will skip over the + opening brace and backslash; if the control sequence is one + of LaTeX's foreign letter sequences (as determined by + foreign_letter()), then it is simply copied to *dst. + Otherwise the control sequence is skipped. In either case, + text after the control sequence is either copied (alphabetic + characters) or skipped (anything else, including hyphens, + ties, and digits). +@CALLS : foreign_letter() +@CALLERS : bt_purify_string() +@CREATED : 1997/10/19, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +purify_special_char (char *str, int * src, int * dst) +{ + int depth; + int peek; + + assert (str[*src] == '{' && str[*src + 1] == '\\'); + depth = 1; + + *src += 2; /* jump to start of control sequence */ + peek = *src; /* scan to end of control sequence */ + while (isalpha (str[peek])) + peek++; + if (peek == *src) /* in case of single-char, non-alpha */ + peek++; /* control sequence (eg. {\'e}) */ + + if (foreign_letter (str, *src, peek, NULL)) + { + assert (peek - *src == 1 || peek - *src == 2); + str[(*dst)++] = str[(*src)++]; /* copy first char */ + if (*src < peek) /* copy second char, downcasing */ + str[(*dst)++] = tolower (str[(*src)++]); + } + else /* not a foreign letter -- skip */ + { /* the control sequence entirely */ + *src = peek; + } + + while (str[*src]) + { + switch (str[*src]) + { + case '{': + depth++; + (*src)++; + break; + case '}': + depth--; + if (depth == 0) return; /* done with special char */ + (*src)++; + break; + default: + if (isalpha (str[*src])) /* copy alphabetic chars */ + str[(*dst)++] = str[(*src)++]; + else /* skip everything else */ + (*src)++; + } + } + + /* + * If we get here, we have unbalanced braces -- the '}' case should + * always hit a depth == 0 point if braces are balanced. No warning, + * though, because a) BibTeX doesn't warn about purifying unbalanced + * strings, and b) we (should have) already warned about it in the + * lexer. + */ + +} /* purify_special_char() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_purify_string() +@INOUT : instr +@INPUT : options +@OUTPUT : +@RETURNS : instr - same as input string, but modified in place +@DESCRIPTION: "Purifies" a BibTeX string. This consists of copying + alphanumeric characters, converting hyphens and ties to + space, copying spaces, and skipping everything else. (Well, + almost -- special characters are handled specially, of + course. Basically, accented letters have the control + sequence skipped, while foreign letters have the control + sequence preserved in a reasonable manner. See + purify_special_char() for details.) +@CALLS : purify_special_char() +@CALLERS : +@CREATED : 1997/10/19, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_purify_string (char * string, ushort options) +{ + int src, /* both indeces into string */ + dst; + int depth; /* brace depth in string */ + unsigned orig_len; + + /* + * Since purification always copies or deletes chars, outstr will + * be no longer than string -- so nothing fancy is required to put + * an upper bound on its eventual size. + */ + + depth = 0; + src = 0; + dst = 0; + orig_len = strlen (string); + + DBG_ACTION (1, printf ("bt_purify_string(): input = %p (%s)\n", + string, string)); + + while (string[src] != (char) 0) + { + DBG_ACTION (2, printf (" next: >%c<: ", string[src])); + switch (string[src]) + { + case '~': /* "separator" characters -- */ + case '-': /* replaced with space */ + case ' ': /* and copy an actual space */ + string[dst++] = ' '; + src++; + DBG_ACTION (2, printf ("replacing with space")); + break; + case '{': + if (depth == 0 && string[src+1] == '\\') + { + DBG_ACTION (2, printf ("special char found")); + purify_special_char (string, &src, &dst); + } + else + { + DBG_ACTION (2, printf ("ordinary open brace")); + src++; + } + depth++; + break; + case '}': + DBG_ACTION (2, printf ("close brace")); + depth--; + src++; + break; + default: + if (isalnum (string[src])) /* any alphanumeric char -- */ + { + DBG_ACTION (2, printf ("alphanumeric -- copying")); + string[dst++] = string[src++]; /* copy it */ + } + else /* anything else -- skip it */ + { + DBG_ACTION (2, printf ("non-separator, non-brace, non-alpha")); + src++; + } + } /* switch string[src] */ + + DBG_ACTION (2, printf ("\n")); + + } /* while string[src] */ + + DBG_ACTION (1, printf ("bt_purify_string(): depth on exit: %d\n", depth)); + + string[dst] = (char) 0; + assert (strlen (string) <= orig_len); +} /* bt_purify_string() */ + + +/* ====================================================================== + * Case-transformation stuff + */ + + +/* ------------------------------------------------------------------------ +@NAME : convert_special_char() +@INPUT : transform +@INOUT : string + src + dst + start_sentence + after_colon +@RETURNS : +@DESCRIPTION: Does case conversion on a special character. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/11/25, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +convert_special_char (char transform, + char * string, + int * src, + int * dst, + boolean * start_sentence, + boolean * after_colon) +{ + int depth; + boolean done_special; + int cs_end; + int cs_len; /* counting the backslash */ + bt_letter letter; + const char * repl; + int repl_len; + +#ifndef ALLOW_WARNINGS + repl = NULL; /* silence "might be used" */ + /* uninitialized" warning */ +#endif + + /* First, copy just the opening brace */ + string[(*dst)++] = string[(*src)++]; + + /* + * Now loop over characters inside the braces -- stop when we reach + * the matching close brace, or when the string ends. + */ + depth = 1; /* because we're in a special char */ + done_special = FALSE; + + while (string[*src] != 0 && !done_special) + { + switch (string[*src]) + { + case '\\': /* a control sequence */ + { + cs_end = *src+1; /* scan over chars of c.s. */ + while (isalpha (string[cs_end])) + cs_end++; + + /* + * OK, now *src points to the backslash (so src+*1 points to + * first char. of control sequence), and cs_end points to + * character immediately following end of control sequence. + * Thus we analyze [*src+1..cs_end] to determine if the control + * sequence is a foreign letter, and use (cs_end - (*src+1) + 1) + * = (cs_end - *src) as the length of the control sequence. + */ + + cs_len = cs_end - *src; /* length of cs, counting backslash */ + + if (foreign_letter (string, *src+1, cs_end, &letter)) + { + if (letter == L_OTHER) + internal_error ("impossible foreign letter"); + + switch (transform) + { + case 'u': + repl = uc_version[(int) letter]; + break; + case 'l': + repl = lc_version[(int) letter]; + break; + case 't': + if (*start_sentence || *after_colon) + { + repl = uc_version[(int) letter]; + *start_sentence = *after_colon = FALSE; + } + else + { + repl = lc_version[(int) letter]; + } + break; + default: + internal_error ("impossible case transform \"%c\"", + transform); + } + + repl_len = strlen (repl); + if (repl_len > cs_len) + internal_error + ("replacement text longer than original cs"); + + strncpy (string + *dst, repl, repl_len); + *src = cs_end; + *dst += repl_len; + } /* control sequence is a foreign letter */ + else + { + /* not a foreign letter -- just copy the control seq. as is */ + + + strncpy (string + *dst, string + *src, cs_end - *src); + *src += cs_len; + assert (*src == cs_end); + *dst += cs_len; + } /* control sequence not a foreign letter */ + + break; + } /* case: '\\' */ + + case '{': + { + string[(*dst)++] = string[(*src)++]; + depth++; + break; + } + + case '}': + { + string[(*dst)++] = string[(*src)++]; + depth--; + if (depth == 0) + done_special = TRUE; + break; + } + + default: /* any other character */ + { + switch (transform) + { + /* + * Inside special chars, lowercase and title caps are same. + * (At least, that's bibtex's convention. I might change this + * at some point to be a bit smarter.) + */ + case 'l': + case 't': + string[(*dst)++] = tolower (string[(*src)++]); + break; + case 'u': + string[(*dst)++] = toupper (string[(*src)++]); + break; + default: + internal_error ("impossible case transform \"%c\"", + transform); + } + } /* default char */ + + } /* switch: current char */ + + } /* while: string or special char not done */ + +} /* convert_special_char() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_change_case() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Converts a string (in-place) to either uppercase, lowercase, + or "title capitalization"> +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/11/25, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_change_case (char transform, + char * string, + ushort options) +{ + int len; + int depth; + int src, dst; /* indeces into string */ + boolean start_sentence; + boolean after_colon; + + src = dst = 0; + len = strlen (string); + depth = 0; + + start_sentence = TRUE; + after_colon = FALSE; + + while (string[src] != 0) + { + switch (string[src]) + { + case '{': + + /* + * At start of special character? The entire special char. + * will be handled here, as follows: + * - text at any brace-depth within the s.c. is case-mangled; + * punctuation (sentence endings, colons) are ignored + * - control sequences are left alone, unless they are + * one of the "foreign letter" control sequences, in + * which case they're converted to the appropriate string + * according to the uc_version or lc_version tables. + */ + if (depth == 0 && string[src+1] == '\\') + { + convert_special_char (transform, string, &src, &dst, + &start_sentence, &after_colon); + } + + /* + * Otherwise, it's just something in braces. This is probably + * a proper noun or something encased in braces to protect it + * from case-mangling, so we do not case-mangle it. However, + * we *do* switch out of start_sentence or after_colon mode if + * we happen to be there (otherwise we'll do the wrong thing + * once we're out of the braces). + */ + else + { + string[dst++] = string[src++]; + start_sentence = after_colon = FALSE; + depth++; + } + break; + + case '}': + string[dst++] = string[src++]; + depth--; + break; + + /* + * Sentence-ending punctuation and colons are handled separately + * to allow for exact mimicing of BibTeX's behaviour. I happen + * to think that this behaviour (capitalize first word of sentences + * in a title) is better than BibTeX's, but I want to keep my + * options open for a future goal of perfect compatability. + */ + case '.': + case '?': + case '!': + start_sentence = TRUE; + string[dst++] = string[src++]; + break; + + case ':': + after_colon = TRUE; + string[dst++] = string[src++]; + break; + + default: + if (isspace (string[src])) + { + string[dst++] = string[src++]; + } + else + { + if (depth == 0) + { + switch (transform) + { + case 'u': + string[dst++] = toupper (string[src++]); + break; + case 'l': + string[dst++] = tolower (string[src++]); + break; + case 't': + if (start_sentence || after_colon) + { + /* + * XXX BibTeX only preserves case of character + * immediately after a colon; I do two things + * differently: first, I pay attention to sentence + * punctuation, and second I force uppercase + * at start of sentence or after a colon. + */ + string[dst++] = toupper (string[src++]); + start_sentence = after_colon = FALSE; + } + else + { + string[dst++] = tolower (string[src++]); + } + break; + default: + internal_error ("impossible case transform \"%c\"", + transform); + } + } /* depth == 0 */ + else + { + string[dst++] = string[src++]; + } + } /* not blank */ + } /* switch on current character */ + + } /* while not at end of string */ + +} /* bt_change_case */ diff --git a/src/translators/btparse/sym.c b/src/translators/btparse/sym.c new file mode 100644 index 0000000..2426dea --- /dev/null +++ b/src/translators/btparse/sym.c @@ -0,0 +1,372 @@ +/* + * Simple symbol table manager using coalesced chaining to resolve collisions + * + * Doubly-linked lists are used for fast removal of entries. + * + * 'sym.h' must have a definition for typedef "Sym". Sym must include at + * minimum the following fields: + * + * ... + * char *symbol; + * struct ... *next, *prev, **head, *scope; + * unsigned int hash; + * ... + * + * 'template.h' can be used as a template to create a 'sym.h'. + * + * 'head' is &(table[hash(itself)]). + * The hash table is not resizable at run-time. + * The scope field is used to link all symbols of a current scope together. + * Scope() sets the current scope (linked list) to add symbols to. + * Any number of scopes can be handled. The user passes the address of + * a pointer to a symbol table + * entry (INITIALIZED TO NULL first time). + * + * Available Functions: + * + * zzs_init(s1,s2) -- Create hash table with size s1, string table size s2. + * zzs_done() -- Free hash and string table created with zzs_init(). + * zzs_add(key,rec)-- Add 'rec' with key 'key' to the symbol table. + * zzs_newadd(key) -- create entry; add using 'key' to the symbol table. + * zzs_get(key) -- Return pointer to last record entered under 'key' + * Else return NULL + * zzs_del(p) -- Unlink the entry associated with p. This does + * NOT free 'p' and DOES NOT remove it from a scope + * list. If it was a part of your intermediate code + * tree or another structure. It will still be there. + * It is only removed from further consideration + * by the symbol table. + * zzs_keydel(s) -- Unlink the entry associated with key s. + * Calls zzs_del(p) to unlink. + * zzs_scope(sc) -- Specifies that everything added to the symbol + * table with zzs_add() is added to the list (scope) + * 'sc'. 'sc' is of 'Sym **sc' type and must be + * initialized to NULL before trying to add anything + * to it (passing it to zzs_scope()). Scopes can be + * switched at any time and merely links a set of + * symbol table entries. If a NULL pointer is + * passed, the current scope is returned. + * zzs_rmscope(sc) -- Remove (zzs_del()) all elements of scope 'sc' + * from the symbol table. The entries are NOT + * free()'d. A pointer to the first + * element in the "scope" is returned. The user + * can then manipulate the list as he/she chooses + * (such as freeing them all). NOTE that this + * function sets your scope pointer to NULL, + * but returns a pointer to the list for you to use. + * zzs_stat() -- Print out the symbol table and some relevant stats. + * zzs_new(key) -- Create a new record with calloc() of type Sym. + * Add 'key' to the string table and make the new + * records 'symbol' pointer point to it. + * zzs_strdup(s) -- Add s to the string table and return a pointer + * to it. Very fast allocation routine + * and does not require strlen() nor calloc(). + * + * Example: + * + * #include <stdio.h> + * #include "sym.h" + * + * main() + * { + * Sym *scope1=NULL, *scope2=NULL, *a, *p; + * + * zzs_init(101, 100); + * + * a = zzs_new("Apple"); zzs_add(a->symbol, a); -- No scope + * zzs_scope( &scope1 ); -- enter scope 1 + * a = zzs_new("Plum"); zzs_add(a->symbol, a); + * zzs_scope( &scope2 ); -- enter scope 2 + * a = zzs_new("Truck"); zzs_add(a->symbol, a); + * + * p = zzs_get("Plum"); + * if ( p == NULL ) fprintf(stderr, "Hmmm...Can't find 'Plum'\n"); + * + * p = zzs_rmscope(&scope1) + * for (; p!=NULL; p=p->scope) {printf("Scope1: %s\n", p->symbol);} + * p = zzs_rmscope(&scope2) + * for (; p!=NULL; p=p->scope) {printf("Scope2: %s\n", p->symbol);} + * } + * + * Terence Parr + * Purdue University + * February 1990 + * + * CHANGES + * + * Terence Parr + * May 1991 + * Renamed functions to be consistent with ANTLR + * Made HASH macro + * Added zzs_keydel() + * Added zzs_newadd() + * Fixed up zzs_stat() + * + * July 1991 + * Made symbol table entry save its hash code for fast comparison + * during searching etc... + */ + +/*#include "bt_config.h"*/ +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#ifdef MEMCHK +#include "trax.h" +#endif +#include "sym.h" +/*#include "my_dmalloc.h"*/ + +#define StrSame 0 + +static Sym **CurScope = NULL; +static unsigned size = 0; +static Sym **table=NULL; +static char *strings; +static char *strp; +static int strsize = 0; + +void +zzs_init(int sz, int strs) +{ + if ( sz <= 0 || strs <= 0 ) return; + table = (Sym **) calloc(sz, sizeof(Sym *)); + if ( table == NULL ) + { + fprintf(stderr, "Cannot allocate table of size %d\n", sz); + exit(1); + } + strings = (char *) calloc(strs, sizeof(char)); + if ( strings == NULL ) + { + fprintf(stderr, "Cannot allocate string table of size %d\n", strs); + exit(1); + } + size = sz; + strsize = strs; + strp = strings; +} + + +void +zzs_free(void) +{ + unsigned i; + Sym *cur, *next; + + for (i = 0; i < size; i++) + { + cur = table[i]; + while (cur != NULL) + { + next = cur->next; + free (cur); + cur = next; + } + } +} + + +void +zzs_done(void) +{ + if ( table != NULL ) free( table ); + if ( strings != NULL ) free( strings ); +} + +void +zzs_add(char *key, register Sym *rec) +{ + register unsigned int h=0; + register char *p=key; + + HASH_FUN(p, h); + rec->hash = h; /* save hash code for fast comp later */ + h %= size; + + if ( CurScope != NULL ) {rec->scope = *CurScope; *CurScope = rec;} + rec->next = table[h]; /* Add to doubly-linked list */ + rec->prev = NULL; + if ( rec->next != NULL ) (rec->next)->prev = rec; + table[h] = rec; + rec->head = &(table[h]); +} + +Sym * +zzs_get(char *key) +{ + register unsigned int h=0; + register char *p=key; + register Sym *q; + + HASH_FUN(p, h); + + for (q = table[h%size]; q != NULL; q = q->next) + { + if ( q->hash == h ) /* do we even have a chance of matching? */ + if ( strcasecmp(key, q->symbol) == StrSame ) return( q ); + } + return( NULL ); +} + +/* + * Unlink p from the symbol table. Hopefully, it's actually in the + * symbol table. + * + * If p is not part of a bucket chain of the symbol table, bad things + * will happen. + * + * Will do nothing if all list pointers are NULL + */ +void +zzs_del(register Sym *p) +{ + if ( p == NULL ) {fprintf(stderr, "zzs_del(NULL)\n"); exit(1);} + if ( p->prev == NULL ) /* Head of list */ + { + register Sym **t = p->head; + + if ( t == NULL ) return; /* not part of symbol table */ + (*t) = p->next; + if ( (*t) != NULL ) (*t)->prev = NULL; + } + else + { + (p->prev)->next = p->next; + if ( p->next != NULL ) (p->next)->prev = p->prev; + } + p->next = p->prev = NULL; /* not part of symbol table anymore */ + p->head = NULL; +} + +void +zzs_keydel(char *key) +{ + Sym *p = zzs_get(key); + + if ( p != NULL ) zzs_del( p ); +} + +/* S c o p e S t u f f */ + +/* Set current scope to 'scope'; return current scope if 'scope' == NULL */ +Sym ** +zzs_scope(Sym **scope) +{ + if ( scope == NULL ) return( CurScope ); + CurScope = scope; + return( scope ); +} + +/* Remove a scope described by 'scope'. Return pointer to 1st element in scope */ +Sym * +zzs_rmscope(register Sym **scope) +{ + register Sym *p; + Sym *start; + + if ( scope == NULL ) return(NULL); + start = p = *scope; + for (; p != NULL; p=p->scope) { zzs_del( p ); } + *scope = NULL; + return( start ); +} + +void +zzs_stat(void) +{ + static unsigned short count[20]; + unsigned int i,n=0,low=0, hi=0; + register Sym **p; + float avg=0.0; + + for (i=0; i<20; i++) count[i] = 0; + for (p=table; p<&(table[size]); p++) + { + register Sym *q = *p; + unsigned int len; + + if ( q != NULL && low==0 ) low = p-table; + len = 0; + if ( q != NULL ) printf("[%d]", p-table); + while ( q != NULL ) + { + len++; + n++; + printf(" %s", q->symbol); + q = q->next; + if ( q == NULL ) printf("\n"); + } + if ( len>=20 ) printf("zzs_stat: count table too small\n"); + else count[len]++; + if ( *p != NULL ) hi = p-table; + } + + printf("Storing %d recs used %d hash positions out of %d\n", + n, size-count[0], size); + printf("%f %% utilization\n", + ((float)(size-count[0]))/((float)size)); + for (i=0; i<20; i++) + { + if ( count[i] != 0 ) + { + avg += (((float)(i*count[i]))/((float)n)) * i; + printf("Buckets of len %d == %d (%f %% of recs)\n", + i, count[i], 100.0*((float)(i*count[i]))/((float)n)); + } + } + printf("Avg bucket length %f\n", avg); + printf("Range of hash function: %d..%d\n", low, hi); +} + +/* + * Given a string, this function allocates and returns a pointer to a + * symbol table record whose "symbol" pointer is reset to a position + * in the string table. + */ +Sym * +zzs_new(char *text) +{ + Sym *p; + char *zzs_strdup(register char *s); + + if ( (p = (Sym *) calloc(1,sizeof(Sym))) == 0 ) + { + fprintf(stderr,"Out of memory\n"); + exit(1); + } + p->symbol = zzs_strdup(text); + + return p; +} + +/* create a new symbol table entry and add it to the symbol table */ +Sym * +zzs_newadd(char *text) +{ + Sym *p = zzs_new(text); + if ( p != NULL ) zzs_add(text, p); + return p; +} + +/* Add a string to the string table and return a pointer to it. + * Bump the pointer into the string table to next avail position. + */ +char * +zzs_strdup(register char *s) +{ + register char *start=strp; + + while ( *s != '\0' ) + { + if ( strp >= &(strings[strsize-2]) ) + { + fprintf(stderr, "sym: string table overflow (%d chars)\n", strsize); + exit(-1); + } + *strp++ = *s++; + } + *strp++ = '\0'; + + return( start ); +} diff --git a/src/translators/btparse/sym.h b/src/translators/btparse/sym.h new file mode 100644 index 0000000..78983d1 --- /dev/null +++ b/src/translators/btparse/sym.h @@ -0,0 +1,33 @@ +#include <ctype.h> + +/* + * Declarations for symbol table in sym.c + */ + +/* define some hash function */ +#ifndef HASH_FUN +#define HASH_FUN(p, h) while ( *p != '\0' ) h = (h<<1) + tolower (*p++); +#endif + +/* minimum symbol table record */ +typedef struct _sym +{ + char *symbol; /* the macro name */ + char *text; /* its expansion */ + struct _sym *next, *prev, **head, *scope; + unsigned int hash; +} Sym, *SymPtr; + +void zzs_init(int, int); +void zzs_free(void); +void zzs_done(void); +void zzs_add(char *, Sym *); +Sym *zzs_get(char *); +void zzs_del(Sym *); +void zzs_keydel(char *); +Sym **zzs_scope(Sym **); +Sym *zzs_rmscope(Sym **); +void zzs_stat(void); +Sym *zzs_new(char *); +Sym *zzs_newadd(char *); +char *zzs_strdup(char *); diff --git a/src/translators/btparse/tex_tree.c b/src/translators/btparse/tex_tree.c new file mode 100644 index 0000000..0d7d33d --- /dev/null +++ b/src/translators/btparse/tex_tree.c @@ -0,0 +1,414 @@ +/* ------------------------------------------------------------------------ +@NAME : tex_tree.c +@DESCRIPTION: Functions for dealing with strings of TeX code: converting + them to tree representation, traversing the trees to glean + useful information, and converting back to string form. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/05/29, Greg Ward +@MODIFIED : +@VERSION : $Id: tex_tree.c,v 1.4 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include "error.h" +#include "btparse.h" +/*#include "my_dmalloc.h"*/ + +/* blech! temp hack until I make error.c perfect and magical */ +#define string_warning(w) fprintf (stderr, w); + +typedef struct treestack_s +{ + bt_tex_tree * node; + struct treestack_s + * prev, + * next; +} treestack; + + +/* ---------------------------------------------------------------------- + * Stack manipulation functions + */ + +/* ------------------------------------------------------------------------ +@NAME : push_treestack() +@INPUT : *stack + node +@OUTPUT : *stack +@RETURNS : +@DESCRIPTION: Creates and initializes new node in a stack, and pushes it + onto the stack. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +push_treestack (treestack **stack, bt_tex_tree *node) +{ + treestack *newtop; + + newtop = (treestack *) malloc (sizeof (treestack)); + newtop->node = node; + newtop->next = NULL; + newtop->prev = *stack; + + if (*stack != NULL) /* stack already has some entries */ + { + (*stack)->next = newtop; + *stack = newtop; + } + + *stack = newtop; + +} /* push_treestack() */ + + +/* ------------------------------------------------------------------------ +@NAME : pop_treestack +@INPUT : *stack +@OUTPUT : *stack +@RETURNS : +@DESCRIPTION: Pops an entry off of a stack of tex_tree nodes, frees up + the wrapper treestack node, and returns the popped tree node. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static bt_tex_tree * +pop_treestack (treestack **stack) +{ + treestack * oldtop; + bt_tex_tree * node; + + if (*stack == NULL) + internal_error ("attempt to pop off empty stack"); + oldtop = (*stack)->prev; + node = (*stack)->node; + free (*stack); + if (oldtop != NULL) + oldtop->next = NULL; + *stack = oldtop; + return node; + +} /* pop_treestack() */ + + +/* ---------------------------------------------------------------------- + * Tree creation/destruction functions + */ + +/* ------------------------------------------------------------------------ +@NAME : new_tex_tree +@INPUT : start +@OUTPUT : +@RETURNS : pointer to newly-allocated node +@DESCRIPTION: Allocates and initializes a bt_tex_tree node. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static bt_tex_tree * +new_tex_tree (char *start) +{ + bt_tex_tree * node; + + node = (bt_tex_tree *) malloc (sizeof (bt_tex_tree)); + node->start = start; + node->len = 0; + node->child = node->next = NULL; + return node; +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_build_tex_tree +@INPUT : string +@OUTPUT : +@RETURNS : pointer to a complete tree; call bt_free_tex_tree() to free + the entire tree +@DESCRIPTION: Traverses a string looking for TeX groups ({...}), and builds + a tree containing pointers into the string and describing + its brace-structure. +@GLOBALS : +@CALLS : +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +bt_tex_tree * +bt_build_tex_tree (char * string) +{ + int i; + int depth; + int len; + bt_tex_tree + * top, + * cur, + * new; + treestack + * stack; + + i = 0; + depth = 0; + len = strlen (string); + top = new_tex_tree (string); + stack = NULL; + + cur = top; + + while (i < len) + { + switch (string[i]) + { + case '{': /* go one level deeper */ + { + if (i == len-1) /* open brace in last character? */ + { + string_warning ("unbalanced braces: { at end of string"); + goto error; + } + + new = new_tex_tree (string+i+1); + cur->child = new; + push_treestack (&stack, cur); + cur = new; + depth++; + break; + } + case '}': /* pop level(s) off */ + { + while (i < len && string[i] == '}') + { + if (stack == NULL) + { + string_warning ("unbalanced braces: extra }"); + goto error; + } + cur = pop_treestack (&stack); + depth--; + i++; + } + i--; + + if (i == len-1) /* reached end of string? */ + { + if (depth > 0) /* but not at depth 0 */ + { + string_warning ("unbalanced braces: not enough }'s"); + goto error; + } + + /* + * if we get here, do nothing -- we've reached the end of + * the string and are at depth 0, so will just fall out + * of the while loop at the end of this iteration + */ + } + else /* still have characters left */ + { /* to worry about */ + new = new_tex_tree (string+i+1); + cur->next = new; + cur = new; + } + + break; + } + default: + { + cur->len++; + } + + } /* switch */ + + i++; + + } /* while i */ + + if (depth > 0) + { + string_warning ("unbalanced braces (not enough }'s)"); + goto error; + } + + return top; + +error: + bt_free_tex_tree (&top); + return NULL; + +} /* bt_build_tex_tree() */ + + +/* ------------------------------------------------------------------------ +@NAME : bt_free_tex_tree +@INPUT : *top +@OUTPUT : *top (set to NULL after it's free()'d) +@RETURNS : +@DESCRIPTION: Frees up an entire tree created by bt_build_tex_tree(). +@GLOBALS : +@CALLS : itself, free() +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_free_tex_tree (bt_tex_tree **top) +{ + if ((*top)->child) bt_free_tex_tree (&(*top)->child); + if ((*top)->next) bt_free_tex_tree (&(*top)->next); + free (*top); + *top = NULL; +} + + + +/* ---------------------------------------------------------------------- + * Tree traversal functions + */ + +/* ------------------------------------------------------------------------ +@NAME : bt_dump_tex_tree +@INPUT : node + depth + stream +@OUTPUT : +@RETURNS : +@DESCRIPTION: Dumps a TeX tree: one node per line, depth indented according + to depth. +@GLOBALS : +@CALLS : itself +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +void +bt_dump_tex_tree (bt_tex_tree *node, int depth, FILE *stream) +{ + char buf[256]; + + if (node == NULL) + return; + + if (node->len > 255) + internal_error ("augughgh! buf too small"); + strncpy (buf, node->start, node->len); + buf[node->len] = (char) 0; + + fprintf (stream, "%*s[%s]\n", depth*2, "", buf); + + bt_dump_tex_tree (node->child, depth+1, stream); + bt_dump_tex_tree (node->next, depth, stream); + +} + + +/* ------------------------------------------------------------------------ +@NAME : count_length +@INPUT : node +@OUTPUT : +@RETURNS : +@DESCRIPTION: Counts the total number of characters that will be needed + to print a string reconstructed from a TeX tree. (Length + of string in each node, plus two [{ and }] for each down + edge.) +@GLOBALS : +@CALLS : itself +@CALLERS : bt_flatten_tex_tree +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static int +count_length (bt_tex_tree *node) +{ + if (node == NULL) return 0; + return + node->len + + (node->child ? 2 : 0) + + count_length (node->child) + + count_length (node->next); +} + + +/* ------------------------------------------------------------------------ +@NAME : flatten_tree +@INPUT : node + *offset +@OUTPUT : *buf + *offset +@RETURNS : +@DESCRIPTION: Dumps a reconstructed string ("flat" representation of the + tree) into a pre-allocated buffer, starting at a specified + offset. +@GLOBALS : +@CALLS : itself +@CALLERS : bt_flatten_tex_tree +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +static void +flatten_tree (bt_tex_tree *node, char *buf, int *offset) +{ + strncpy (buf + *offset, node->start, node->len); + *offset += node->len; + + if (node->child) + { + buf[(*offset)++] = '{'; + flatten_tree (node->child, buf, offset); + buf[(*offset)++] = '}'; + } + + if (node->next) + { + flatten_tree (node->next, buf, offset); + } +} + + +/* ------------------------------------------------------------------------ +@NAME : bt_flatten_tex_tree +@INPUT : top +@OUTPUT : +@RETURNS : flattened string representation of the tree (as a string + allocated with malloc(), so you should free() it when + you're done with it) +@DESCRIPTION: Counts the number of characters needed for a "flat" + string representation of a tree, allocates a string of + that size, and generates the string. +@GLOBALS : +@CALLS : count_length, flatten_tree +@CALLERS : +@CREATED : 1997/05/29, GPW +@MODIFIED : +-------------------------------------------------------------------------- */ +char * +bt_flatten_tex_tree (bt_tex_tree *top) +{ + int len; + int offset; + char * buf; + + len = count_length (top); + buf = (char *) malloc (sizeof (char) * (len+1)); + offset = 0; + flatten_tree (top, buf, &offset); + return buf; +} diff --git a/src/translators/btparse/tokens.h b/src/translators/btparse/tokens.h new file mode 100644 index 0000000..6f9405a --- /dev/null +++ b/src/translators/btparse/tokens.h @@ -0,0 +1,41 @@ +#ifndef tokens_h +#define tokens_h +/* tokens.h -- List of labelled tokens and stuff + * + * Generated from: bibtex.g + * + * Terence Parr, Will Cohen, and Hank Dietz: 1989-1994 + * Purdue University Electrical Engineering + * ANTLR Version 1.33 + */ +#define zzEOF_TOKEN 1 +#define AT 2 +#define COMMENT 4 +#define NUMBER 9 +#define NAME 10 +#define LBRACE 11 +#define RBRACE 12 +#define ENTRY_OPEN 13 +#define ENTRY_CLOSE 14 +#define EQUALS 15 +#define HASH 16 +#define COMMA 17 +#define STRING 25 + +void bibfile(AST**_root); +void entry(AST**_root); +void body(AST**_root, bt_metatype metatype ); +void contents(AST**_root, bt_metatype metatype ); +void fields(AST**_root); +void field(AST**_root); +void value(AST**_root); +void simple_value(AST**_root); + +#endif +extern SetWordType zzerr1[]; +extern SetWordType zzerr2[]; +extern SetWordType zzerr3[]; +extern SetWordType zzerr4[]; +extern SetWordType setwd1[]; +extern SetWordType zzerr5[]; +extern SetWordType setwd2[]; diff --git a/src/translators/btparse/traversal.c b/src/translators/btparse/traversal.c new file mode 100644 index 0000000..c7e10a2 --- /dev/null +++ b/src/translators/btparse/traversal.c @@ -0,0 +1,187 @@ +/* ------------------------------------------------------------------------ +@NAME : traversal.c +@DESCRIPTION: Routines for traversing the AST for a single entry. +@GLOBALS : +@CALLS : +@CREATED : 1997/01/21, Greg Ward +@MODIFIED : +@VERSION : $Id: traversal.c,v 1.17 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ +/*#include "bt_config.h"*/ +#include <stdlib.h> +#include "btparse.h" +#include "parse_auxiliary.h" +#include "prototypes.h" +/*#include "my_dmalloc.h"*/ + + +AST *bt_next_entry (AST *entry_list, AST *prev_entry) +{ + if (entry_list == NULL || entry_list->nodetype != BTAST_ENTRY) + return NULL; + + if (prev_entry) + { + if (prev_entry->nodetype != BTAST_ENTRY) + return NULL; + else + return prev_entry->right; + } + else + return entry_list; +} + + +bt_metatype bt_entry_metatype (AST *entry) +{ + if (!entry) return BTE_UNKNOWN; + if (entry->nodetype != BTAST_ENTRY) + return BTE_UNKNOWN; + else + return entry->metatype; +} + + +char *bt_entry_type (AST *entry) +{ + if (!entry) return NULL; + if (entry->nodetype != BTAST_ENTRY) + return NULL; + else + return entry->text; +} + + +char *bt_entry_key (AST *entry) +{ + if (entry->metatype == BTE_REGULAR && + entry->down && entry->down->nodetype == BTAST_KEY) + { + return entry->down->text; + } + else + { + return NULL; + } +} + + +AST *bt_next_field (AST *entry, AST *prev, char **name) +{ + AST *field; + bt_metatype metatype; + + *name = NULL; + if (!entry || !entry->down) return NULL; /* protect against empty entry */ + + metatype = entry->metatype; + if (metatype != BTE_MACRODEF && metatype != BTE_REGULAR) + return NULL; + + if (prev == NULL) /* no previous field -- they must */ + { /* want the first one */ + field = entry->down; + if (metatype == BTE_REGULAR && field->nodetype == BTAST_KEY) + field = field->right; /* skip over citation key if present */ + } + else /* they really do want the next one */ + { + field = prev->right; + } + + if (!field) return NULL; /* protect against field-less entry */ + if (name) *name = field->text; + return field; +} /* bt_next_field() */ + + +AST *bt_next_macro (AST *entry, AST *prev, char **name) +{ + return bt_next_field (entry, prev, name); +} + + +AST *bt_next_value (AST *top, AST *prev, bt_nodetype *nodetype, char **text) +{ + bt_nodetype nt; /* type of `top' node (to check) */ + bt_metatype mt; + AST * value; + + if (nodetype) *nodetype = BTAST_BOGUS; + if (text) *text = NULL; + + if (!top) return NULL; + /* get_node_type (top, &nt, &mt); */ + nt = top->nodetype; + mt = top->metatype; + + if ((nt == BTAST_FIELD) || + (nt == BTAST_ENTRY && (mt == BTE_COMMENT || mt == BTE_PREAMBLE))) + { + if (prev == NULL) /* no previous value -- give 'em */ + { /* the first one */ + value = top->down; + if (!value) return NULL; + if (nodetype) *nodetype = value->nodetype; + } + else + { + value = prev->right; + if (!value) return NULL; + if (nodetype) *nodetype = value->nodetype; + } + + if (nt == BTAST_ENTRY && value->nodetype != BTAST_STRING) + internal_error ("found comment or preamble with non-string value"); + } + else + { + value = NULL; + } + + if (text && value) *text = value->text; + + return value; +} /* bt_next_value() */ + + +char *bt_get_text (AST *node) +{ + ushort pp_options = BTO_FULL; /* options for full processing: */ + /* expand macros, paste strings, */ + /* collapse whitespace */ + bt_nodetype nt; + bt_metatype mt; + + nt = node->nodetype; + mt = node->metatype; + + if (nt == BTAST_FIELD) + { +#if DEBUG + char *value; + + dump_ast ("bt_get_text (pre): node =\n", node); + value = bt_postprocess_field (node, pp_options, FALSE); + dump_ast ("bt_get_text (post): node =\n", node); + return value; +#else + return bt_postprocess_field (node, pp_options, FALSE); +#endif + } + else if (nt == BTAST_ENTRY && (mt == BTE_COMMENT || mt == BTE_PREAMBLE)) + { + return bt_postprocess_value (node->down, pp_options, FALSE); + } + else + { + return NULL; + } +} diff --git a/src/translators/btparse/util.c b/src/translators/btparse/util.c new file mode 100644 index 0000000..1330176 --- /dev/null +++ b/src/translators/btparse/util.c @@ -0,0 +1,79 @@ +/* ------------------------------------------------------------------------ +@NAME : util.c +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Miscellaneous utility functions. So far, just: + strlwr + strupr +@CREATED : Summer 1996, Greg Ward +@MODIFIED : +@VERSION : $Id: util.c,v 1.6 1999/11/29 01:13:10 greg Rel $ +@COPYRIGHT : Copyright (c) 1996-99 by Gregory P. Ward. All rights reserved. + + This file is part of the btparse library. This library 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 2 + of the License, or (at your option) any later version. +-------------------------------------------------------------------------- */ + +/*#include "bt_config.h"*/ +#include <string.h> +#include <ctype.h> +#include "prototypes.h" +/*#include "my_dmalloc.h"*/ + +/* ------------------------------------------------------------------------ +@NAME : strlwr() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Converts a string to lowercase in place. +@GLOBALS : +@CALLS : +@CREATED : 1996/01/06, GPW +@MODIFIED : +@COMMENTS : This should work the same as strlwr() in DOS compilers -- + why this isn't mandated by ANSI is a mystery to me... +-------------------------------------------------------------------------- */ +#if !HAVE_STRLWR +char *strlwr (char *s) +{ + int len, i; + + len = strlen (s); + for (i = 0; i < len; i++) + s[i] = tolower (s[i]); + + return s; +} +#endif + + + +/* ------------------------------------------------------------------------ +@NAME : strupr() +@INPUT : +@OUTPUT : +@RETURNS : +@DESCRIPTION: Converts a string to uppercase in place. +@GLOBALS : +@CALLS : +@CREATED : 1996/01/06, GPW +@MODIFIED : +@COMMENTS : This should work the same as strupr() in DOS compilers -- + why this isn't mandated by ANSI is a mystery to me... +-------------------------------------------------------------------------- */ +#if !HAVE_STRUPR +char *strupr (char *s) +{ + int len, i; + + len = strlen (s); + for (i = 0; i < len; i++) + s[i] = toupper (s[i]); + + return s; +} +#endif diff --git a/src/translators/csvexporter.cpp b/src/translators/csvexporter.cpp new file mode 100644 index 0000000..bb206e1 --- /dev/null +++ b/src/translators/csvexporter.cpp @@ -0,0 +1,190 @@ +/*************************************************************************** + copyright : (C) 2003-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 "csvexporter.h" +#include "../document.h" +#include "../collection.h" +#include "../filehandler.h" + +#include <klocale.h> +#include <kdebug.h> +#include <klineedit.h> +#include <kconfig.h> + +#include <qgroupbox.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qwhatsthis.h> + +using Tellico::Export::CSVExporter; + +CSVExporter::CSVExporter() : Tellico::Export::Exporter(), + m_includeTitles(true), + m_delimiter(QChar(',')), + m_widget(0) { +} + +QString CSVExporter::formatString() const { + return i18n("CSV"); +} + +QString CSVExporter::fileFilter() const { + return i18n("*.csv|CSV Files (*.csv)") + QChar('\n') + i18n("*|All Files"); +} + +QString& CSVExporter::escapeText(QString& text_) { + bool quotes = false; + if(text_.find('"') != -1) { + quotes = true; + // quotation marks will be escaped by using a double pair + text_.replace('"', QString::fromLatin1("\"\"")); + } + // if the text contains quotes or the delimiter, it needs to be surrounded by quotes + if(quotes || text_.find(m_delimiter) != -1) { + text_.prepend('"'); + text_.append('"'); + } + return text_; +} + +bool CSVExporter::exec() { + if(!collection()) { + return false; + } + + QString text; + + Data::FieldVec fields = collection()->fields(); + Data::FieldVec::Iterator fIt; + + if(m_includeTitles) { + for(fIt = fields.begin(); fIt != fields.end(); ++fIt) { + QString title = fIt->title(); + text += escapeText(title); + if(!fIt.nextEnd()) { + text += m_delimiter; + } + } + text += '\n'; + } + + bool format = options() & Export::ExportFormatted; + + QString tmp; + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + for(fIt = fields.begin(); fIt != fields.end(); ++fIt) { + tmp = entryIt->field(fIt->name(), format); + text += escapeText(tmp); + if(!fIt.nextEnd()) { + text += m_delimiter; + } + } + fIt = fields.begin(); + text += '\n'; + } + + return FileHandler::writeTextURL(url(), text, options() & ExportUTF8, options() & Export::ExportForce); +} + +QWidget* CSVExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("CSV Options"), m_widget); + l->addWidget(box); + + m_checkIncludeTitles = new QCheckBox(i18n("Include field titles as column headers"), box); + m_checkIncludeTitles->setChecked(m_includeTitles); + QWhatsThis::add(m_checkIncludeTitles, i18n("If checked, a header row will be added with the " + "field titles.")); + + QButtonGroup* delimiterGroup = new QButtonGroup(0, Qt::Vertical, i18n("Delimiter"), box); + QGridLayout* m_delimiterGroupLayout = new QGridLayout(delimiterGroup->layout()); + m_delimiterGroupLayout->setAlignment(Qt::AlignTop); + QWhatsThis::add(delimiterGroup, i18n("In addition to a comma, other characters may be used as " + "a delimiter, separating each value in the file.")); + + m_radioComma = new QRadioButton(delimiterGroup); + m_radioComma->setText(i18n("Comma")); + m_radioComma->setChecked(true); + QWhatsThis::add(m_radioComma, i18n("Use a comma as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioComma, 0, 0); + + m_radioSemicolon = new QRadioButton( delimiterGroup); + m_radioSemicolon->setText(i18n("Semicolon")); + QWhatsThis::add(m_radioSemicolon, i18n("Use a semi-colon as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioSemicolon, 0, 1); + + m_radioTab = new QRadioButton(delimiterGroup); + m_radioTab->setText(i18n("Tab")); + QWhatsThis::add(m_radioTab, i18n("Use a tab as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioTab, 1, 0); + + m_radioOther = new QRadioButton(delimiterGroup); + m_radioOther->setText(i18n("Other")); + QWhatsThis::add(m_radioOther, i18n("Use a custom string as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioOther, 1, 1); + + m_editOther = new KLineEdit(delimiterGroup); + m_editOther->setEnabled(m_radioOther->isChecked()); + QWhatsThis::add(m_editOther, i18n("A custom string, such as a colon, may be used as a delimiter.")); + m_delimiterGroupLayout->addWidget(m_editOther, 1, 2); + QObject::connect(m_radioOther, SIGNAL(toggled(bool)), + m_editOther, SLOT(setEnabled(bool))); + + if(m_delimiter == QChar(',')) { + m_radioComma->setChecked(true); + } else if(m_delimiter == QChar(';')) { + m_radioSemicolon->setChecked(true); + } else if(m_delimiter == QChar('\t')) { + m_radioTab->setChecked(true); + } else if(!m_delimiter.isEmpty()) { + m_radioOther->setChecked(true); + m_editOther->setEnabled(true); + m_editOther->setText(m_delimiter); + } + + l->addStretch(1); + return m_widget; +} + +void CSVExporter::readOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_includeTitles = group.readBoolEntry("Include Titles", m_includeTitles); + m_delimiter = group.readEntry("Delimiter", m_delimiter); +} + +void CSVExporter::saveOptions(KConfig* config_) { + m_includeTitles = m_checkIncludeTitles->isChecked(); + if(m_radioComma->isChecked()) { + m_delimiter = QChar(','); + } else if(m_radioSemicolon->isChecked()) { + m_delimiter = QChar(';'); + } else if(m_radioTab->isChecked()) { + m_delimiter = QChar('\t'); + } else { + m_delimiter = m_editOther->text(); + } + + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + group.writeEntry("Include Titles", m_includeTitles); + group.writeEntry("Delimiter", m_delimiter); +} + +#include "csvexporter.moc" diff --git a/src/translators/csvexporter.h b/src/translators/csvexporter.h new file mode 100644 index 0000000..23624e3 --- /dev/null +++ b/src/translators/csvexporter.h @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef CSVEXPORTER_H +#define CSVEXPORTER_H + +class KLineEdit; +class KConfig; + +class QWidget; +class QCheckBox; +class QRadioButton; + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class CSVExporter : public Exporter { +Q_OBJECT + +public: + CSVExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual void readOptions(KConfig* config); + virtual void saveOptions(KConfig* config); + +private: + QString& escapeText(QString& text); + + bool m_includeTitles; + QString m_delimiter; + + QWidget* m_widget; + QCheckBox* m_checkIncludeTitles; + QRadioButton* m_radioComma; + QRadioButton* m_radioSemicolon; + QRadioButton* m_radioTab; + QRadioButton* m_radioOther; + KLineEdit* m_editOther; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/csvimporter.cpp b/src/translators/csvimporter.cpp new file mode 100644 index 0000000..f0c0900 --- /dev/null +++ b/src/translators/csvimporter.cpp @@ -0,0 +1,552 @@ +/*************************************************************************** + copyright : (C) 2003-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 "csvimporter.h" +#include "translators.h" // needed for ImportAction +#include "../collectionfieldsdialog.h" +#include "../document.h" +#include "../collection.h" +#include "../progressmanager.h" +#include "../tellico_debug.h" +#include "../collectionfactory.h" +#include "../gui/collectiontypecombo.h" +#include "../latin1literal.h" +#include "../stringset.h" + +extern "C" { +#include "libcsv.h" +} + +#include <klineedit.h> +#include <kcombobox.h> +#include <knuminput.h> +#include <kpushbutton.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <kconfig.h> +#include <kmessagebox.h> + +#include <qgroupbox.h> +#include <qlayout.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qwhatsthis.h> +#include <qtable.h> +#include <qvaluevector.h> +#include <qregexp.h> + +using Tellico::Import::CSVImporter; + +static void writeToken(char* buffer, size_t len, void* data); +static void writeRow(char buffer, void* data); + +class CSVImporter::Parser { +public: + Parser(const QString& str) : stream(new QTextIStream(&str)) { csv_init(&parser, 0); } + ~Parser() { csv_free(parser); delete stream; stream = 0; } + + void setDelimiter(const QString& s) { Q_ASSERT(s.length() == 1); csv_set_delim(parser, s[0].latin1()); } + void reset(const QString& str) { delete stream; stream = new QTextIStream(&str); }; + bool hasNext() { return !stream->atEnd(); } + void skipLine() { stream->readLine(); } + + void addToken(const QString& t) { tokens += t; } + void setRowDone(bool b) { done = b; } + + QStringList nextTokens() { + tokens.clear(); + done = false; + while(hasNext() && !done) { + QCString line = stream->readLine().utf8() + '\n'; // need the eol char + csv_parse(parser, line, line.length(), &writeToken, &writeRow, this); + } + csv_fini(parser, &writeToken, &writeRow, this); + return tokens; + } + +private: + struct csv_parser* parser; + QTextIStream* stream; + QStringList tokens; + bool done; +}; + +static void writeToken(char* buffer, size_t len, void* data) { + CSVImporter::Parser* p = static_cast<CSVImporter::Parser*>(data); + p->addToken(QString::fromUtf8(buffer, len)); +} + +static void writeRow(char c, void* data) { + Q_UNUSED(c); + CSVImporter::Parser* p = static_cast<CSVImporter::Parser*>(data); + p->setRowDone(true); +} + +CSVImporter::CSVImporter(const KURL& url_) : Tellico::Import::TextImporter(url_), + m_coll(0), + m_existingCollection(0), + m_firstRowHeader(false), + m_delimiter(QString::fromLatin1(",")), + m_cancelled(false), + m_widget(0), + m_table(0), + m_hasAssignedFields(false), + m_parser(new Parser(text())) { + m_parser->setDelimiter(m_delimiter); +} + +CSVImporter::~CSVImporter() { + delete m_parser; + m_parser = 0; +} + +Tellico::Data::CollPtr CSVImporter::collection() { + // don't just check if m_coll is non-null since the collection can be created elsewhere + if(m_coll && m_coll->entryCount() > 0) { + return m_coll; + } + + if(!m_coll) { + m_coll = CollectionFactory::collection(m_comboColl->currentType(), true); + } + + const QStringList existingNames = m_coll->fieldNames(); + + QValueVector<int> cols; + QStringList names; + for(int col = 0; col < m_table->numCols(); ++col) { + QString t = m_table->horizontalHeader()->label(col); + if(m_existingCollection && m_existingCollection->fieldByTitle(t)) { + // the collection might have the right field, but a different title, say for translations + Data::FieldPtr f = m_existingCollection->fieldByTitle(t); + if(m_coll->hasField(f->name())) { + // might have different values settings + m_coll->removeField(f->name(), true /* force */); + } + m_coll->addField(new Data::Field(*f)); + cols.push_back(col); + names << f->name(); + } else if(m_coll->fieldByTitle(t)) { + cols.push_back(col); + names << m_coll->fieldNameByTitle(t); + } + } + + if(names.isEmpty()) { + myDebug() << "CSVImporter::collection() - no fields assigned" << endl; + return 0; + } + + m_parser->reset(text()); + + // if the first row are headers, skip it + if(m_firstRowHeader) { + m_parser->skipLine(); + } + + const uint numLines = text().contains('\n'); + const uint stepSize = QMAX(s_stepSize, numLines/100); + const bool showProgress = options() & ImportProgress; + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(numLines); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + uint j = 0; + while(!m_cancelled && m_parser->hasNext()) { + bool empty = true; + Data::EntryPtr entry = new Data::Entry(m_coll); + QStringList values = m_parser->nextTokens(); + for(uint i = 0; i < names.size(); ++i) { +// QString value = values[cols[i]].simplifyWhiteSpace(); + QString value = values[cols[i]].stripWhiteSpace(); + bool success = entry->setField(names[i], value); + // we might need to add a new allowed value + // assume that if the user is importing the value, it should be allowed + if(!success && m_coll->fieldByName(names[i])->type() == Data::Field::Choice) { + Data::FieldPtr f = m_coll->fieldByName(names[i]); + StringSet allow; + allow.add(f->allowed()); + allow.add(value); + f->setAllowed(allow.toList()); + m_coll->modifyField(f); + success = entry->setField(names[i], value); + } + if(empty && success) { + empty = false; + } + } + if(!empty) { + m_coll->addEntries(entry); + } + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + ++j; + } + + { + KConfigGroup config(KGlobal::config(), QString::fromLatin1("ImportOptions - CSV")); + config.writeEntry("Delimiter", m_delimiter); + config.writeEntry("First Row Titles", m_firstRowHeader); + } + + return m_coll; +} + +QWidget* CSVImporter::widget(QWidget* parent_, const char* name_) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* group = new QGroupBox(1, Qt::Horizontal, i18n("CSV Options"), m_widget); + l->addWidget(group); + + QHBox* box = new QHBox(group); + box->setSpacing(5); + QLabel* lab = new QLabel(i18n("Collection &type:"), box); + m_comboColl = new GUI::CollectionTypeCombo(box); + lab->setBuddy(m_comboColl); + QWhatsThis::add(m_comboColl, i18n("Select the type of collection being imported.")); + connect(m_comboColl, SIGNAL(activated(int)), SLOT(slotTypeChanged())); + // need a spacer + QWidget* w = new QWidget(box); + box->setStretchFactor(w, 1); + + m_checkFirstRowHeader = new QCheckBox(i18n("&First row contains field titles"), group); + QWhatsThis::add(m_checkFirstRowHeader, i18n("If checked, the first row is used as field titles.")); + connect(m_checkFirstRowHeader, SIGNAL(toggled(bool)), SLOT(slotFirstRowHeader(bool))); + + QHBox* hbox2 = new QHBox(group); + m_delimiterGroup = new QButtonGroup(0, Qt::Vertical, i18n("Delimiter"), hbox2); + QGridLayout* m_delimiterGroupLayout = new QGridLayout(m_delimiterGroup->layout(), 3, 3); + m_delimiterGroupLayout->setAlignment(Qt::AlignTop); + QWhatsThis::add(m_delimiterGroup, i18n("In addition to a comma, other characters may be used as " + "a delimiter, separating each value in the file.")); + connect(m_delimiterGroup, SIGNAL(clicked(int)), SLOT(slotDelimiter())); + + m_radioComma = new QRadioButton(m_delimiterGroup); + m_radioComma->setText(i18n("&Comma")); + m_radioComma->setChecked(true); + QWhatsThis::add(m_radioComma, i18n("Use a comma as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioComma, 1, 0); + + m_radioSemicolon = new QRadioButton( m_delimiterGroup); + m_radioSemicolon->setText(i18n("&Semicolon")); + QWhatsThis::add(m_radioSemicolon, i18n("Use a semi-colon as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioSemicolon, 1, 1); + + m_radioTab = new QRadioButton(m_delimiterGroup); + m_radioTab->setText(i18n("Ta&b")); + QWhatsThis::add(m_radioTab, i18n("Use a tab as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioTab, 2, 0); + + m_radioOther = new QRadioButton(m_delimiterGroup); + m_radioOther->setText(i18n("Ot&her:")); + QWhatsThis::add(m_radioOther, i18n("Use a custom string as the delimiter.")); + m_delimiterGroupLayout->addWidget(m_radioOther, 2, 1); + + m_editOther = new KLineEdit(m_delimiterGroup); + m_editOther->setEnabled(false); + m_editOther->setFixedWidth(m_widget->fontMetrics().width('X') * 4); + m_editOther->setMaxLength(1); + QWhatsThis::add(m_editOther, i18n("A custom string, such as a colon, may be used as a delimiter.")); + m_delimiterGroupLayout->addWidget(m_editOther, 2, 2); + connect(m_radioOther, SIGNAL(toggled(bool)), + m_editOther, SLOT(setEnabled(bool))); + connect(m_editOther, SIGNAL(textChanged(const QString&)), SLOT(slotDelimiter())); + + w = new QWidget(hbox2); + hbox2->setStretchFactor(w, 1); + + m_table = new QTable(5, 0, group); + m_table->setSelectionMode(QTable::Single); + m_table->setFocusStyle(QTable::FollowStyle); + m_table->setLeftMargin(0); + m_table->verticalHeader()->hide(); + m_table->horizontalHeader()->setClickEnabled(true); + m_table->setReadOnly(true); + m_table->setMinimumHeight(m_widget->fontMetrics().lineSpacing() * 8); + QWhatsThis::add(m_table, i18n("The table shows up to the first five lines of the CSV file.")); + connect(m_table, SIGNAL(currentChanged(int, int)), SLOT(slotCurrentChanged(int, int))); + connect(m_table->horizontalHeader(), SIGNAL(clicked(int)), SLOT(slotHeaderClicked(int))); + + QWidget* hbox = new QWidget(group); + QHBoxLayout* hlay = new QHBoxLayout(hbox, 5); + hlay->addStretch(10); + QWhatsThis::add(hbox, i18n("<qt>Set each column to correspond to a field in the collection by choosing " + "a column, selecting the field, then clicking the <i>Assign Field</i> button.</qt>")); + lab = new QLabel(i18n("Co&lumn:"), hbox); + hlay->addWidget(lab); + m_colSpinBox = new KIntSpinBox(hbox); + hlay->addWidget(m_colSpinBox); + m_colSpinBox->setMinValue(1); + connect(m_colSpinBox, SIGNAL(valueChanged(int)), SLOT(slotSelectColumn(int))); + lab->setBuddy(m_colSpinBox); + hlay->addSpacing(10); + + lab = new QLabel(i18n("&Data field in this column:"), hbox); + hlay->addWidget(lab); + m_comboField = new KComboBox(hbox); + hlay->addWidget(m_comboField); + connect(m_comboField, SIGNAL(activated(int)), SLOT(slotFieldChanged(int))); + lab->setBuddy(m_comboField); + hlay->addSpacing(10); + + m_setColumnBtn = new KPushButton(i18n("&Assign Field"), hbox); + hlay->addWidget(m_setColumnBtn); + m_setColumnBtn->setIconSet(SmallIconSet(QString::fromLatin1("apply"))); + connect(m_setColumnBtn, SIGNAL(clicked()), SLOT(slotSetColumnTitle())); + hlay->addStretch(10); + + l->addStretch(1); + + KConfigGroup config(KGlobal::config(), QString::fromLatin1("ImportOptions - CSV")); + m_delimiter = config.readEntry("Delimiter", m_delimiter); + m_firstRowHeader = config.readBoolEntry("First Row Titles", m_firstRowHeader); + + m_checkFirstRowHeader->setChecked(m_firstRowHeader); + if(m_delimiter == Latin1Literal(",")) { + m_radioComma->setChecked(true); + slotDelimiter(); // since the comma box was already checked, the slot won't fire + } else if(m_delimiter == Latin1Literal(";")) { + m_radioSemicolon->setChecked(true); + } else if(m_delimiter == Latin1Literal("\t")) { + m_radioTab->setChecked(true); + } else if(!m_delimiter.isEmpty()) { + m_radioOther->setChecked(true); + m_editOther->setEnabled(true); + m_editOther->setText(m_delimiter); + } + + return m_widget; +} + +bool CSVImporter::validImport() const { + // at least one column has to be defined + if(!m_hasAssignedFields) { + KMessageBox::sorry(m_widget, i18n("At least one column must be assigned to a field. " + "Only assigned columns will be imported.")); + } + return m_hasAssignedFields; +} + +void CSVImporter::fillTable() { + if(!m_table) { + return; + } + + m_parser->reset(text()); + // not skipping first row since the updateHeader() call depends on it + + int maxCols = 0; + int row = 0; + for( ; m_parser->hasNext() && row < m_table->numRows(); ++row) { + QStringList values = m_parser->nextTokens(); + if(static_cast<int>(values.count()) > m_table->numCols()) { + m_table->setNumCols(values.count()); + m_colSpinBox->setMaxValue(values.count()); + } + int col = 0; + for(QStringList::ConstIterator it = values.begin(); it != values.end(); ++it) { + m_table->setText(row, col, *it); + m_table->adjustColumn(col); + ++col; + } + if(col > maxCols) { + maxCols = col; + } + } + for( ; row < m_table->numRows(); ++row) { + for(int col = 0; col < m_table->numCols(); ++col) { + m_table->clearCell(row, col); + } + } + + m_table->setNumCols(maxCols); +} + +void CSVImporter::slotTypeChanged() { + // iterate over the collection names until it matches the text of the combo box + Data::Collection::Type type = static_cast<Data::Collection::Type>(m_comboColl->currentType()); + m_coll = CollectionFactory::collection(type, true); + + updateHeader(true); + m_comboField->clear(); + m_comboField->insertStringList(m_existingCollection ? m_existingCollection->fieldTitles() : m_coll->fieldTitles()); + m_comboField->insertItem('<' + i18n("New Field") + '>'); + + // hack to force a resize + m_comboField->setFont(m_comboField->font()); + m_comboField->updateGeometry(); +} + +void CSVImporter::slotFirstRowHeader(bool b_) { + m_firstRowHeader = b_; + updateHeader(false); + fillTable(); +} + +void CSVImporter::slotDelimiter() { + if(m_radioComma->isChecked()) { + m_delimiter = ','; + } else if(m_radioSemicolon->isChecked()) { + m_delimiter = ';'; + } else if(m_radioTab->isChecked()) { + m_delimiter = '\t'; + } else { + m_editOther->setFocus(); + m_delimiter = m_editOther->text(); + } + if(!m_delimiter.isEmpty()) { + m_parser->setDelimiter(m_delimiter); + fillTable(); + updateHeader(false); + } +} + +void CSVImporter::slotCurrentChanged(int, int col_) { + int pos = col_+1; + m_colSpinBox->setValue(pos); //slotSelectColumn() gets called because of the signal +} + +void CSVImporter::slotHeaderClicked(int col_) { + int pos = col_+1; + m_colSpinBox->setValue(pos); //slotSelectColumn() gets called because of the signal +} + +void CSVImporter::slotSelectColumn(int pos_) { + // pos is really the number of the position of the column + int col = pos_ - 1; + m_table->ensureCellVisible(0, col); + m_comboField->setCurrentItem(m_table->horizontalHeader()->label(col)); +} + +void CSVImporter::slotSetColumnTitle() { + int col = m_colSpinBox->value()-1; + const QString title = m_comboField->currentText(); + m_table->horizontalHeader()->setLabel(col, title); + m_hasAssignedFields = true; + // make sure none of the other columns have this title + bool found = false; + for(int i = 0; i < col; ++i) { + if(m_table->horizontalHeader()->label(i) == title) { + m_table->horizontalHeader()->setLabel(i, QString::number(i+1)); + found = true; + break; + } + } + // if found, then we're done + if(found) { + return; + } + for(int i = col+1; i < m_table->numCols(); ++i) { + if(m_table->horizontalHeader()->label(i) == title) { + m_table->horizontalHeader()->setLabel(i, QString::number(i+1)); + break; + } + } +} + +void CSVImporter::updateHeader(bool force_) { + if(!m_table) { + return; + } + if(!m_firstRowHeader && !force_) { + return; + } + + Data::CollPtr c = m_existingCollection ? m_existingCollection : m_coll; + for(int col = 0; col < m_table->numCols(); ++col) { + QString s = m_table->text(0, col); + Data::FieldPtr f; + if(c) { + c->fieldByTitle(s); + if(!f) { + f = c->fieldByName(s); + } + } + if(m_firstRowHeader && !s.isEmpty() && c && f) { + m_table->horizontalHeader()->setLabel(col, f->title()); + m_hasAssignedFields = true; + } else { + m_table->horizontalHeader()->setLabel(col, QString::number(col+1)); + } + } +} + +void CSVImporter::slotFieldChanged(int idx_) { + // only care if it's the last item -> add new field + if(idx_ < m_comboField->count()-1) { + return; + } + + Data::CollPtr c = m_existingCollection ? m_existingCollection : m_coll; + uint count = c->fieldTitles().count(); + CollectionFieldsDialog dlg(c, m_widget); +// dlg.setModal(true); + if(dlg.exec() == QDialog::Accepted) { + m_comboField->clear(); + m_comboField->insertStringList(c->fieldTitles()); + m_comboField->insertItem('<' + i18n("New Field") + '>'); + if(count != c->fieldTitles().count()) { + fillTable(); + } + m_comboField->setCurrentItem(0); + } +} + +void CSVImporter::slotActionChanged(int action_) { + Data::CollPtr currColl = Data::Document::self()->collection(); + if(!currColl) { + m_existingCollection = 0; + return; + } + + switch(action_) { + case Import::Replace: + { + int currType = m_comboColl->currentType(); + m_comboColl->reset(); + m_comboColl->setCurrentType(currType); + m_existingCollection = 0; + } + break; + + case Import::Append: + case Import::Merge: + { + m_comboColl->clear(); + QString name = CollectionFactory::nameMap()[currColl->type()]; + m_comboColl->insertItem(name, currColl->type()); + m_existingCollection = currColl; + } + break; + } + slotTypeChanged(); +} + +void CSVImporter::slotCancel() { + m_cancelled = true; +} + +#include "csvimporter.moc" diff --git a/src/translators/csvimporter.h b/src/translators/csvimporter.h new file mode 100644 index 0000000..6561584 --- /dev/null +++ b/src/translators/csvimporter.h @@ -0,0 +1,107 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef CSVIMPORTER_H +#define CSVIMPORTER_H + +class CSVImporterWidget; + +class KLineEdit; +class KComboBox; +class KIntSpinBox; +class KPushButton; + +class QButtonGroup; +class QCheckBox; +class QRadioButton; +class QTable; + +#include "textimporter.h" +#include "../datavectors.h" + +namespace Tellico { + namespace GUI { + class CollectionTypeCombo; + } + namespace Import { + +/** + * @author Robby Stephenson + */ +class CSVImporter : public TextImporter { +Q_OBJECT + +public: + class Parser; + + /** + */ + CSVImporter(const KURL& url); + ~CSVImporter(); + + /** + * @return A pointer to a @ref Data::Collection, or 0 if none can be created. + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget* parent, const char* name=0); + + virtual bool validImport() const; + +public slots: + void slotActionChanged(int action); + void slotCancel(); + +private slots: + void slotTypeChanged(); + void slotFieldChanged(int idx); + void slotFirstRowHeader(bool b); + void slotDelimiter(); + void slotCurrentChanged(int row, int col); + void slotHeaderClicked(int col); + void slotSelectColumn(int col); + void slotSetColumnTitle(); + +private: + void fillTable(); + void updateHeader(bool force); + + Data::CollPtr m_coll; + Data::CollPtr m_existingCollection; // used to grab fields from current collection in window + bool m_firstRowHeader; + QString m_delimiter; + bool m_cancelled; + + QWidget* m_widget; + GUI::CollectionTypeCombo* m_comboColl; + QCheckBox* m_checkFirstRowHeader; + QButtonGroup* m_delimiterGroup; + QRadioButton* m_radioComma; + QRadioButton* m_radioSemicolon; + QRadioButton* m_radioTab; + QRadioButton* m_radioOther; + KLineEdit* m_editOther; + QTable* m_table; + KIntSpinBox* m_colSpinBox; + KComboBox* m_comboField; + KPushButton* m_setColumnBtn; + bool m_hasAssignedFields; + + friend class Parser; + Parser* m_parser; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/dataimporter.h b/src/translators/dataimporter.h new file mode 100644 index 0000000..4d21a53 --- /dev/null +++ b/src/translators/dataimporter.h @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef DATAIMPORTER_H +#define DATAIMPORTER_H + +#include "importer.h" +#include "../filehandler.h" + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson + */ +class DataImporter : public Importer { +Q_OBJECT + +public: + enum Source { URL, Text }; + + /** + * @param url The URL of the file to import + */ +// DataImporter(const KURL& url) : Importer(url), m_data(FileHandler::readDataFile(url)), m_source(URL) {} + DataImporter(const KURL& url) : Importer(url), m_source(URL) { m_fileRef = FileHandler::fileRef(url); } + /** + * Since the conversion to a QCString appends a \0 character at the end, remove it. + * + * @param text The text. It MUST be in UTF-8. + */ + DataImporter(const QString& text) : Importer(text), m_data(text.utf8()), m_source(Text), m_fileRef(0) + { m_data.truncate(m_data.size()-1); } + /** + */ + virtual ~DataImporter() { delete m_fileRef; m_fileRef = 0; } + + Source source() const { return m_source; } + + virtual void setText(const QString& text) { + Importer::setText(text); m_data = text.utf8(); m_data.truncate(m_data.size()-1); m_source = Text; + } + +protected: + /** + * Return the data in the imported file + * + * @return the file data + */ + const QByteArray& data() const { return m_data; } + FileHandler::FileRef& fileRef() const { return *m_fileRef; } + +private: + QByteArray m_data; + Source m_source; + FileHandler::FileRef* m_fileRef; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/dcimporter.cpp b/src/translators/dcimporter.cpp new file mode 100644 index 0000000..c8bb59f --- /dev/null +++ b/src/translators/dcimporter.cpp @@ -0,0 +1,128 @@ +/*************************************************************************** + copyright : (C) 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 "dcimporter.h" +#include "../collections/bookcollection.h" +#include "tellico_xml.h" +#include "../tellico_debug.h" + +using Tellico::Import::DCImporter; + +DCImporter::DCImporter(const KURL& url_) : XMLImporter(url_) { +} + +DCImporter::DCImporter(const QString& text_) : XMLImporter(text_) { +} + +DCImporter::DCImporter(const QDomDocument& dom_) : XMLImporter(dom_) { +} + +Tellico::Data::CollPtr DCImporter::collection() { + const QString& dc = XML::nsDublinCore; + const QString& zing = XML::nsZing; + + Data::CollPtr c = new Data::BookCollection(true); + + QDomDocument doc = domDocument(); + + QRegExp authorDateRX(QString::fromLatin1(",?(\\s+\\d{4}-?(?:\\d{4})?\\.?)(.*)$")); + QRegExp dateRX(QString::fromLatin1("\\d{4}")); + + QDomNodeList recordList = doc.elementsByTagNameNS(zing, QString::fromLatin1("recordData")); + myDebug() << "DCImporter::collection() - number of records: " << recordList.count() << endl; + + enum { UnknownNS, UseNS, NoNS } useNS = UnknownNS; + +#define GETELEMENTS(s) (useNS == NoNS) \ + ? elem.elementsByTagName(QString::fromLatin1(s)) \ + : elem.elementsByTagNameNS(dc, QString::fromLatin1(s)) + + for(uint i = 0; i < recordList.count(); ++i) { + Data::EntryPtr e = new Data::Entry(c); + + QDomElement elem = recordList.item(i).toElement(); + + QDomNodeList nodeList = GETELEMENTS("title"); + if(nodeList.count() == 0) { // no title, skip + if(useNS == UnknownNS) { + nodeList = elem.elementsByTagName(QString::fromLatin1("title")); + if(nodeList.count() > 0) { + useNS = NoNS; + } else { + myDebug() << "DCImporter::collection() - no title, skipping" << endl; + continue; + } + } else { + myDebug() << "DCImporter::collection() - no title, skipping" << endl; + continue; + } + } else if(useNS == UnknownNS) { + useNS = UseNS; + } + QString s = nodeList.item(0).toElement().text(); + s.replace('\n', ' '); + s = s.simplifyWhiteSpace(); + e->setField(QString::fromLatin1("title"), s); + + nodeList = GETELEMENTS("creator"); + QStringList creators; + for(uint j = 0; j < nodeList.count(); ++j) { + QString s = nodeList.item(j).toElement().text(); + if(authorDateRX.search(s) > -1) { + // check if anything after date like [publisher] + if(authorDateRX.cap(2).stripWhiteSpace().isEmpty()) { + s.remove(authorDateRX); + s = s.simplifyWhiteSpace(); + creators << s; + } else { + myDebug() << "DCImporter::collection() - weird creator, skipping: " << s << endl; + } + } else { + creators << s; + } + } + e->setField(QString::fromLatin1("author"), creators.join(QString::fromLatin1("; "))); + + nodeList = GETELEMENTS("publisher"); + QStringList publishers; + for(uint j = 0; j < nodeList.count(); ++j) { + publishers << nodeList.item(j).toElement().text(); + } + e->setField(QString::fromLatin1("publisher"), publishers.join(QString::fromLatin1("; "))); + + nodeList = GETELEMENTS("subject"); + QStringList keywords; + for(uint j = 0; j < nodeList.count(); ++j) { + keywords << nodeList.item(j).toElement().text(); + } + e->setField(QString::fromLatin1("keyword"), keywords.join(QString::fromLatin1("; "))); + + nodeList = GETELEMENTS("date"); + if(nodeList.count() > 0) { + QString s = nodeList.item(0).toElement().text(); + if(dateRX.search(s) > -1) { + e->setField(QString::fromLatin1("pub_year"), dateRX.cap()); + } + } + + nodeList = GETELEMENTS("description"); + if(nodeList.count() > 0) { // no title, skip + e->setField(QString::fromLatin1("comments"), nodeList.item(0).toElement().text()); + } + + c->addEntries(e); + } +#undef GETELEMENTS + + return c; +} diff --git a/src/translators/dcimporter.h b/src/translators/dcimporter.h new file mode 100644 index 0000000..03eaedf --- /dev/null +++ b/src/translators/dcimporter.h @@ -0,0 +1,34 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_DCIMPORTER_H +#define TELLICO_IMPORT_DCIMPORTER_H + +#include "xmlimporter.h" + +namespace Tellico { + namespace Import { + +class DCImporter : public XMLImporter { +public: + DCImporter(const KURL& url); + DCImporter(const QString& text); + DCImporter(const QDomDocument& dom); + ~DCImporter() {} + + virtual Data::CollPtr collection(); +}; + + } +} +#endif diff --git a/src/translators/deliciousimporter.cpp b/src/translators/deliciousimporter.cpp new file mode 100644 index 0000000..5c434cd --- /dev/null +++ b/src/translators/deliciousimporter.cpp @@ -0,0 +1,87 @@ +/*************************************************************************** + copyright : (C) 2007 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 "deliciousimporter.h" +#include "../collection.h" +#include "../rtf2html/rtf2html.h" +#include "../imagefactory.h" +#include "../tellico_debug.h" + +#include <kstandarddirs.h> + +#include <qfile.h> + +using Tellico::Import::DeliciousImporter; + +DeliciousImporter::DeliciousImporter(const KURL& url_) : XSLTImporter(url_) { + QString xsltFile = locate("appdata", QString::fromLatin1("delicious2tellico.xsl")); + if(!xsltFile.isEmpty()) { + KURL u; + u.setPath(xsltFile); + XSLTImporter::setXSLTURL(u); + } else { + kdWarning() << "DeliciousImporter() - unable to find delicious2tellico.xml!" << endl; + } +} + +bool DeliciousImporter::canImport(int type) const { + return type == Data::Collection::Book; +} + +Tellico::Data::CollPtr DeliciousImporter::collection() { + Data::CollPtr coll = XSLTImporter::collection(); + if(!coll) { + return 0; + } + + KURL libraryDir = url(); + libraryDir.setPath(url().directory() + "Images/"); + const QStringList imageDirs = QStringList() + << QString::fromLatin1("Large Covers/") + << QString::fromLatin1("Medium Covers/") + << QString::fromLatin1("Small Covers/") + << QString::fromLatin1("Plain Covers/"); + const QString commField = QString::fromLatin1("comments"); + const QString uuidField = QString::fromLatin1("uuid"); + const QString coverField = QString::fromLatin1("cover"); + const bool isLocal = url().isLocalFile(); + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + QString comments = entry->field(commField); + if(!comments.isEmpty()) { + RTF2HTML rtf2html(comments); + entry->setField(commField, rtf2html.toHTML()); + } + + //try to add images + QString uuid = entry->field(uuidField); + if(!uuid.isEmpty() && isLocal) { + for(QStringList::ConstIterator it = imageDirs.begin(); it != imageDirs.end(); ++it) { + QString imgPath = libraryDir.path() + *it + uuid; + if(!QFile::exists(imgPath)) { + continue; + } + QString imgID = ImageFactory::addImage(imgPath, true); + if(!imgID.isEmpty()) { + entry->setField(coverField, imgID); + } + break; + } + } + } + coll->removeField(uuidField); + return coll; +} + +#include "deliciousimporter.moc" diff --git a/src/translators/deliciousimporter.h b/src/translators/deliciousimporter.h new file mode 100644 index 0000000..657160e --- /dev/null +++ b/src/translators/deliciousimporter.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_DELICIOUSIMPORTER_H +#define TELLICO_IMPORT_DELICIOUSIMPORTER_H + +#include "xsltimporter.h" +#include "../datavectors.h" + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson +*/ +class DeliciousImporter : public XSLTImporter { +Q_OBJECT + +public: + /** + */ + DeliciousImporter(const KURL& url); + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + virtual bool canImport(int type) const; + +private: + // private so it can't be changed accidently + void setXSLTURL(const KURL& url); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/exporter.cpp b/src/translators/exporter.cpp new file mode 100644 index 0000000..2fe78b7 --- /dev/null +++ b/src/translators/exporter.cpp @@ -0,0 +1,36 @@ +/*************************************************************************** + copyright : (C) 2005-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 "exporter.h" +#include "../document.h" +#include "../collection.h" + +using Tellico::Export::Exporter; + +Exporter::Exporter() : QObject(), m_options(Export::ExportUTF8 | Export::ExportComplete), m_coll(0) { +} + +Exporter::Exporter(Data::CollPtr coll) : QObject(), m_options(Export::ExportUTF8), m_coll(coll) { +} + +Exporter::~Exporter() { +} + +Tellico::Data::CollPtr Exporter::collection() const { + if(m_coll) { + return m_coll; + } + return Data::Document::self()->collection(); +} + +#include "exporter.moc" diff --git a/src/translators/exporter.h b/src/translators/exporter.h new file mode 100644 index 0000000..2ffc13b --- /dev/null +++ b/src/translators/exporter.h @@ -0,0 +1,89 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_EXPORTER_H +#define TELLICO_EXPORTER_H + +class KConfig; + +class QWidget; +class QString; + +#include "../entry.h" +#include "../datavectors.h" + +#include <kurl.h> + +#include <qobject.h> + +namespace Tellico { + namespace Export { + enum Options { + ExportFormatted = 1 << 0, // format entries when exported + ExportUTF8 = 1 << 1, // valid for some text files, export as utf-8 + ExportImages = 1 << 2, // should the images be included? + ExportForce = 1 << 3, // force the export, no confirmation of overwriting + ExportComplete = 1 << 4, // export complete document, including loans, etc. + ExportProgress = 1 << 5, // show progress bar + ExportClean = 1 << 6, // specifically for bibliographies, remove latex commands + ExportVerifyImages= 1 << 7, // don't put in an image link that's not in the cache + ExportImageSize = 1 << 8 // include image size in the generated XML + }; + +/** + * @author Robby Stephenson + */ +class Exporter : public QObject { +Q_OBJECT + +public: + Exporter(); + Exporter(Data::CollPtr coll); + virtual ~Exporter(); + + Data::CollPtr collection() const; + + void setURL(const KURL& url_) { m_url = url_; } + void setEntries(const Data::EntryVec& entries) { m_entries = entries; } + void setOptions(long options) { m_options = options; reset(); } + + virtual QString formatString() const = 0; + virtual QString fileFilter() const = 0; + const KURL& url() const { return m_url; } + const Data::EntryVec& entries() const { return m_entries; } + long options() const { return m_options; } + + /** + * Do the export + */ + virtual bool exec() = 0; + /** + * If changing options in the exporter should cause member variables to reset, implement + * that here + */ + virtual void reset() {} + + virtual QWidget* widget(QWidget* parent, const char* name=0) = 0; + virtual void readOptions(KConfig*) {} + virtual void saveOptions(KConfig*) {} + +private: + long m_options; + Data::CollPtr m_coll; + Data::EntryVec m_entries; + KURL m_url; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/filelistingimporter.cpp b/src/translators/filelistingimporter.cpp new file mode 100644 index 0000000..bef9288 --- /dev/null +++ b/src/translators/filelistingimporter.cpp @@ -0,0 +1,274 @@ +/*************************************************************************** + copyright : (C) 2005-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 "filelistingimporter.h" +#include "../collections/filecatalog.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../imagefactory.h" +#include "../tellico_utils.h" +#include "../tellico_kernel.h" +#include "../progressmanager.h" +#include "../core/netaccess.h" +#include "../tellico_debug.h" + +#include <kapplication.h> +#include <kmountpoint.h> +#include <kio/job.h> +#include <kio/netaccess.h> + +#include <qcheckbox.h> +#include <qvgroupbox.h> +#include <qlayout.h> +#include <qwhatsthis.h> +#include <qfile.h> +#include <qfileinfo.h> + +#include <stdio.h> + +namespace { + static const int FILE_PREVIEW_SIZE = 128; + // volume name starts at 16*2048+40 bytes into the header + static const int VOLUME_NAME_POS = 32808; + static const int VOLUME_NAME_SIZE = 32; +} + +using Tellico::Import::FileListingImporter; + +FileListingImporter::FileListingImporter(const KURL& url_) : Importer(url_), m_coll(0), m_widget(0), + m_job(0), m_cancelled(false) { + m_files.setAutoDelete(true); +} + +bool FileListingImporter::canImport(int type) const { + return type == Data::Collection::File; +} + +Tellico::Data::CollPtr FileListingImporter::collection() { + if(m_coll) { + return m_coll; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Scanning files..."), true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + // going to assume only one volume will ever be imported + QString volume = volumeName(); + + m_job = m_recursive->isChecked() + ? KIO::listRecursive(url(), true, false) + : KIO::listDir(url(), true, false); + connect(m_job, SIGNAL(entries(KIO::Job*, const KIO::UDSEntryList&)), + SLOT(slotEntries(KIO::Job*, const KIO::UDSEntryList&))); + + if(!KIO::NetAccess::synchronousRun(m_job, Kernel::self()->widget()) || m_cancelled) { + return 0; + } + + const bool usePreview = m_filePreview->isChecked(); + + const QString title = QString::fromLatin1("title"); + const QString url = QString::fromLatin1("url"); + const QString desc = QString::fromLatin1("description"); + const QString vol = QString::fromLatin1("volume"); + const QString folder = QString::fromLatin1("folder"); + const QString type = QString::fromLatin1("mimetype"); + const QString size = QString::fromLatin1("size"); + const QString perm = QString::fromLatin1("permissions"); + const QString owner = QString::fromLatin1("owner"); + const QString group = QString::fromLatin1("group"); + const QString created = QString::fromLatin1("created"); + const QString modified = QString::fromLatin1("modified"); + const QString metainfo = QString::fromLatin1("metainfo"); + const QString icon = QString::fromLatin1("icon"); + + m_coll = new Data::FileCatalog(true); + QString tmp; + const uint stepSize = QMAX(1, m_files.count()/100); + const bool showProgress = options() & ImportProgress; + + item.setTotalSteps(m_files.count()); + uint j = 0; + for(KFileItemListIterator it(m_files); !m_cancelled && it.current(); ++it, ++j) { + Data::EntryPtr entry = new Data::Entry(m_coll); + + const KURL u = it.current()->url(); + entry->setField(title, u.fileName()); + entry->setField(url, u.url()); + entry->setField(desc, it.current()->mimeComment()); + entry->setField(vol, volume); + tmp = KURL::relativePath(this->url().path(), u.directory()); + // remove "./" from the string + entry->setField(folder, tmp.right(tmp.length()-2)); + entry->setField(type, it.current()->mimetype()); + entry->setField(size, KIO::convertSize(it.current()->size())); + entry->setField(perm, it.current()->permissionsString()); + entry->setField(owner, it.current()->user()); + entry->setField(group, it.current()->group()); + + time_t t = it.current()->time(KIO::UDS_CREATION_TIME); + if(t > 0) { + QDateTime dt; + dt.setTime_t(t); + entry->setField(created, dt.toString(Qt::ISODate)); + } + t = it.current()->time(KIO::UDS_MODIFICATION_TIME); + if(t > 0) { + QDateTime dt; + dt.setTime_t(t); + entry->setField(modified, dt.toString(Qt::ISODate)); + } + const KFileMetaInfo& meta = it.current()->metaInfo(); + if(meta.isValid() && !meta.isEmpty()) { + const QStringList keys = meta.supportedKeys(); + QStringList strings; + for(QStringList::ConstIterator it2 = keys.begin(); it2 != keys.end(); ++it2) { + KFileMetaInfoItem item = meta.item(*it2); + if(item.isValid()) { + QString s = item.string(); + if(!s.isEmpty()) { + strings << item.key() + "::" + s; + } + } + } + entry->setField(metainfo, strings.join(QString::fromLatin1("; "))); + } + + if(!m_cancelled && usePreview) { + m_pixmap = NetAccess::filePreview(it.current(), FILE_PREVIEW_SIZE); + if(m_pixmap.isNull()) { + m_pixmap = it.current()->pixmap(0); + } + } else { + m_pixmap = it.current()->pixmap(0); + } + + if(!m_pixmap.isNull()) { + // is png best option? + QString id = ImageFactory::addImage(m_pixmap, QString::fromLatin1("PNG")); + if(!id.isEmpty()) { + entry->setField(icon, id); + } + } + + m_coll->addEntries(entry); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } + + if(m_cancelled) { + m_coll = 0; + return 0; + } + + return m_coll; +} + +QWidget* FileListingImporter::widget(QWidget* parent_, const char* name_) { + if(m_widget) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QVGroupBox* box = new QVGroupBox(i18n("File Listing Options"), m_widget); + + m_recursive = new QCheckBox(i18n("Recursive folder search"), box); + QWhatsThis::add(m_recursive, i18n("If checked, folders are recursively searched for all files.")); + // by default, make it checked + m_recursive->setChecked(true); + + m_filePreview = new QCheckBox(i18n("Generate file previews"), box); + QWhatsThis::add(m_filePreview, i18n("If checked, previews of the file contents are generated, which can slow down " + "the folder listing.")); + // by default, make it no previews + m_filePreview->setChecked(false); + + l->addWidget(box); + l->addStretch(1); + return m_widget; +} + +void FileListingImporter::slotEntries(KIO::Job* job_, const KIO::UDSEntryList& list_) { + if(m_cancelled) { + job_->kill(); + m_job = 0; + return; + } + + for(KIO::UDSEntryList::ConstIterator it = list_.begin(); it != list_.end(); ++it) { + KFileItem* item = new KFileItem(*it, url(), false, true); + if(item->isFile()) { + m_files.append(item); + } else { + delete item; + } + } +} + +QString FileListingImporter::volumeName() const { + // this functions turns /media/cdrom into /dev/hdc, then reads 32 bytes after the 16 x 2048 header + QString volume; + const KMountPoint::List mountPoints = KMountPoint::currentMountPoints(KMountPoint::NeedRealDeviceName); + for(KMountPoint::List::ConstIterator it = mountPoints.begin(), end = mountPoints.end(); it != end; ++it) { + // path() could be /media/cdrom + // which could be the mount point of the device + // I know it works for iso9660 (cdrom) and udf (dvd) + if(url().path() == (*it)->mountPoint() + && ((*it)->mountType() == Latin1Literal("iso9660") + || (*it)->mountType() == Latin1Literal("udf"))) { + volume = (*it)->mountPoint(); + if(!(*it)->realDeviceName().isEmpty()) { + QString devName = (*it)->realDeviceName(); + if(devName.endsWith(QChar('/'))) { + devName.truncate(devName.length()-1); + } + // QFile can't do a sequential seek, and I don't want to do a 32808x loop on getch() + FILE* dev = 0; + if((dev = fopen(devName.latin1(), "rb")) != 0) { + // returns 0 on success + if(fseek(dev, VOLUME_NAME_POS, SEEK_SET) == 0) { + char buf[VOLUME_NAME_SIZE]; + int ret = fread(buf, 1, VOLUME_NAME_SIZE, dev); + if(ret == VOLUME_NAME_SIZE) { + volume = QString::fromLatin1(buf, VOLUME_NAME_SIZE).stripWhiteSpace(); + } + } else { + myDebug() << "FileListingImporter::volumeName() - can't seek " << devName << endl; + } + fclose(dev); + } else { + myDebug() << "FileListingImporter::volumeName() - can't read " << devName << endl; + } + } + break; + } + } + return volume; +} + +void FileListingImporter::slotCancel() { + m_cancelled = true; + if(m_job) { + m_job->kill(); + } +} + +#include "filelistingimporter.moc" diff --git a/src/translators/filelistingimporter.h b/src/translators/filelistingimporter.h new file mode 100644 index 0000000..aca4602 --- /dev/null +++ b/src/translators/filelistingimporter.h @@ -0,0 +1,72 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_FILELISTINGIMPORTER_H +#define TELLICO_IMPORT_FILELISTINGIMPORTER_H + +#include "importer.h" +#include "../datavectors.h" + +#include <kio/global.h> +#include <kfileitem.h> + +#include <qguardedptr.h> + +class QCheckBox; +namespace KIO { + class Job; +} + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson + */ +class FileListingImporter : public Importer { +Q_OBJECT + +public: + FileListingImporter(const KURL& url); + + /** + * @return A pointer to a @ref Data::Collection, or 0 if none can be created. + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private slots: + void slotEntries(KIO::Job* job, const KIO::UDSEntryList& list); + +private: + QString volumeName() const; + + Data::CollPtr m_coll; + QWidget* m_widget; + QCheckBox* m_recursive; + QCheckBox* m_filePreview; + QGuardedPtr<KIO::Job> m_job; + KFileItemList m_files; + QPixmap m_pixmap; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/freedb_util.cpp b/src/translators/freedb_util.cpp new file mode 100644 index 0000000..6640ef6 --- /dev/null +++ b/src/translators/freedb_util.cpp @@ -0,0 +1,376 @@ +/*************************************************************************** + * * + * Modified from cd-discid.c, found at http://lly.org/~rcw/cd-discid/ * + * * + * Copyright (c) 1999-2003 Robert Woodcock <rcw@debian.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 "freedbimporter.h" +#include "../tellico_debug.h" + +using Tellico::Import::FreeDBImporter; + +extern "C" { +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <fcntl.h> +#include <sys/ioctl.h> + +/* Porting credits: + * Solaris: David Champion <dgc@uchicago.edu> + * FreeBSD: Niels Bakker <niels@bakker.net> + * OpenBSD: Marcus Daniel <danielm@uni-muenster.de> + * NetBSD: Chris Gilbert <chris@NetBSD.org> + * MacOSX: Evan Jones <ejones@uwaterloo.ca> http://www.eng.uwaterloo.ca/~ejones/ + */ + +#if defined(__linux__) + +// see http://bugs.kde.org/show_bug.cgi?id=86188 +#ifdef __STRICT_ANSI__ +#undef __STRICT_ANSI__ +#define _ANSI_WAS_HERE_ +#endif +#include <linux/types.h> +#include <linux/cdrom.h> +#ifdef _ANSI_WAS_HERE_ +#define __STRICT_ANSI__ +#undef _ANSI_WAS_HERE_ +#endif +#define cdte_track_address cdte_addr.lba + +#elif defined(sun) && defined(unix) && defined(__SVR4) + +#include <sys/cdio.h> +#define CD_MSF_OFFSET 150 +#define CD_FRAMES 75 +/* According to David Schweikert <dws@ee.ethz.ch>, cd-discid needs this + * to compile on Solaris */ +#define cdte_track_address cdte_addr.lba + +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) + +#include <netinet/in.h> +#include <sys/cdio.h> +#define CDROM_LBA CD_LBA_FORMAT /* first frame is 0 */ +#define CD_MSF_OFFSET 150 /* MSF offset of first frame */ +#define CD_FRAMES 75 /* per second */ +#define CDROM_LEADOUT 0xAA /* leadout track */ +#define CDROMREADTOCHDR CDIOREADTOCHEADER +#define CDROMREADTOCENTRY CDIOREADTOCENTRY +#define cdrom_tochdr ioc_toc_header +#define cdth_trk0 starting_track +#define cdth_trk1 ending_track +#define cdrom_tocentry ioc_read_toc_single_entry +#define cdte_track track +#define cdte_format address_format +#define cdte_track_address entry.addr.lba + +#elif defined(__OpenBSD__) || defined(__NetBSD__) + +#include <netinet/in.h> +#include <sys/cdio.h> +#define CDROM_LBA CD_LBA_FORMAT /* first frame is 0 */ +#define CD_MSF_OFFSET 150 /* MSF offset of first frame */ +#define CD_FRAMES 75 /* per second */ +#define CDROM_LEADOUT 0xAA /* leadout track */ +#define CDROMREADTOCHDR CDIOREADTOCHEADER +#define cdrom_tochdr ioc_toc_header +#define cdth_trk0 starting_track +#define cdth_trk1 ending_track +#define cdrom_tocentry cd_toc_entry +#define cdte_track track +#define cdte_track_address addr.lba + +#elif defined(__APPLE__) + +#include <sys/types.h> +#include <IOKit/storage/IOCDTypes.h> +#include <IOKit/storage/IOCDMediaBSDClient.h> +#define CD_FRAMES 75 /* per second */ +#define CD_MSF_OFFSET 150 /* MSF offset of first frame */ +#define cdrom_tochdr CDDiscInfo +#define cdth_trk0 numberOfFirstTrack +/* NOTE: Judging by the name here, we might have to do this: + * hdr.lastTrackNumberInLastSessionMSB << 8 * + * sizeof(hdr.lastTrackNumberInLastSessionLSB) + * | hdr.lastTrackNumberInLastSessionLSB; */ +#define cdth_trk1 lastTrackNumberInLastSessionLSB +#define cdrom_tocentry CDTrackInfo +#define cdte_track_address trackStartAddress + +#else +# warning "Your OS isn't supported yet for CDDB lookup." +#endif /* os selection */ + +} + +#include <config.h> + +namespace { + class CloseDrive { + public: + CloseDrive(int d) : drive(d) {} + ~CloseDrive() { ::close(drive); } + private: + int drive; + }; +} + +QValueList<uint> FreeDBImporter::offsetList(const QCString& drive_, QValueList<uint>& trackLengths_) { + QValueList<uint> list; + + int drive = ::open(drive_.data(), O_RDONLY | O_NONBLOCK); + CloseDrive closer(drive); + if(drive < 0) { + return list; + } + + cdrom_tochdr hdr; +#if defined(__APPLE__) + dk_cd_read_disc_info_t discInfoParams; + ::memset(&discInfoParams, 0, sizeof(discInfoParams)); + discInfoParams.buffer = &hdr; + discInfoParams.bufferLength = sizeof(hdr); + if(ioctl(drive, DKIOCCDREADDISCINFO, &discInfoParams) < 0 + || discInfoParams.bufferLength != sizeof(hdr)) { + return list; + } +#else + if(ioctl(drive, CDROMREADTOCHDR, &hdr) < 0) { + return list; + } +#endif + +// uchar first = hdr.cdth_trk0; + uchar last = hdr.cdth_trk1; + + cdrom_tocentry* TocEntry = new cdrom_tocentry[last+1]; +#if defined(__OpenBSD__) + ioc_read_toc_entry t; + t.starting_track = 0; +#elif defined(__NetBSD__) + ioc_read_toc_entry t; + t.starting_track = 1; +#endif +#if defined(__OpenBSD__) || defined(__NetBSD__) + t.address_format = CDROM_LBA; + t.data_len = (last + 1) * sizeof(cdrom_tocentry); + t.data = TocEntry; + + if (::ioctl(drive, CDIOREADTOCENTRYS, (char *) &t) < 0) + return list; + +#elif defined(__APPLE__) + dk_cd_read_track_info_t trackInfoParams; + ::memset(&trackInfoParams, 0, sizeof(trackInfoParams)); + trackInfoParams.addressType = kCDTrackInfoAddressTypeTrackNumber; + trackInfoParams.bufferLength = sizeof(*TocEntry); + + for(int i = 0; i < last; ++i) { + trackInfoParams.address = i + 1; + trackInfoParams.buffer = &TocEntry[i]; + ::ioctl(drive, DKIOCCDREADTRACKINFO, &trackInfoParams); + } + + /* MacOS X on G5-based systems does not report valid info for + * TocEntry[last-1].lastRecordedAddress + 1, so we compute the start + * of leadout from the start+length of the last track instead + */ + TocEntry[last].cdte_track_address = TocEntry[last-1].trackSize + TocEntry[last-1].trackStartAddress; +#else /* FreeBSD, Linux, Solaris */ + for(uint i = 0; i < last; ++i) { + /* tracks start with 1, but I must start with 0 on OpenBSD */ + TocEntry[i].cdte_track = i + 1; + TocEntry[i].cdte_format = CDROM_LBA; + ::ioctl(drive, CDROMREADTOCENTRY, &TocEntry[i]); + } + + TocEntry[last].cdte_track = CDROM_LEADOUT; + TocEntry[last].cdte_format = CDROM_LBA; + ::ioctl(drive, CDROMREADTOCENTRY, &TocEntry[last]); +#endif + +#if defined(__FreeBSD__) + TocEntry[last].cdte_track_address = ntohl(TocEntry[last].cdte_track_address); +#endif + + for(uint i = 0; i < last; ++i) { +#if defined(__FreeBSD__) + TocEntry[i].cdte_track_address = ntohl(TocEntry[i].cdte_track_address); +#endif + list.append(TocEntry[i].cdte_track_address + CD_MSF_OFFSET); + } + + list.append(TocEntry[0].cdte_track_address + CD_MSF_OFFSET); + list.append(TocEntry[last].cdte_track_address + CD_MSF_OFFSET); + + // hey, these are track lengths! :P + trackLengths_.clear(); + for(uint i = 0; i < last; ++i) { + trackLengths_.append((TocEntry[i+1].cdte_track_address - TocEntry[i].cdte_track_address) / CD_FRAMES); + } + + delete[] TocEntry; + return list; +} + +inline +ushort from2Byte(uchar* d) { + return (d[0] << 8 & 0xFF00) | (d[1] & 0xFF); +} + +#define SIZE 61 +// mostly taken from kover and k3b +// licensed under GPL +FreeDBImporter::CDText FreeDBImporter::getCDText(const QCString& drive_) { + CDText cdtext; +#ifdef USE_CDTEXT +// only works for linux ATM +#if defined(__linux__) + int drive = ::open(drive_.data(), O_RDONLY | O_NONBLOCK); + CloseDrive closer(drive); + if(drive < 0) { + return cdtext; + } + + cdrom_generic_command m_cmd; + ::memset(&m_cmd, 0, sizeof(cdrom_generic_command)); + + int dataLen; + + int format = 5; + uint track = 0; + uchar buffer[2048]; + + m_cmd.cmd[0] = 0x43; + m_cmd.cmd[1] = 0x0; + m_cmd.cmd[2] = format & 0x0F; + m_cmd.cmd[6] = track; + m_cmd.cmd[8] = 2; // we only read the length first + + m_cmd.buffer = buffer; + m_cmd.buflen = 2; + m_cmd.data_direction = CGC_DATA_READ; + + if(ioctl(drive, CDROM_SEND_PACKET, &m_cmd) != 0) { + myDebug() << "FreeDBImporter::getCDText() - access error" << endl; + return cdtext; + } + + dataLen = from2Byte(buffer) + 2; + m_cmd.cmd[7] = 2048 >> 8; + m_cmd.cmd[8] = 2048 & 0xFF; + m_cmd.buflen = 2048; + ::ioctl(drive, CDROM_SEND_PACKET, &m_cmd); + dataLen = from2Byte(buffer) + 2; + + ::memset(buffer, 0, dataLen); + + m_cmd.cmd[7] = dataLen >> 8; + m_cmd.cmd[8] = dataLen; + m_cmd.buffer = buffer; + m_cmd.buflen = dataLen; + ::ioctl(drive, CDROM_SEND_PACKET, &m_cmd); + + bool rc = false; + int buffer_size = (buffer[0] << 8) | buffer[1]; + buffer_size -= 2; + + char data[SIZE]; + short pos_data = 0; + char old_block_no = 0xff; + for(uchar* bufptr = buffer + 4; buffer_size >= 18; bufptr += 18, buffer_size -= 18) { + char code = *bufptr; + + if((code & 0x80) != 0x80) { + continue; + } + + char block_no = *(bufptr + 3); + if(block_no & 0x80) { + myDebug() << "FreeDBImporter::readCDText() - double byte code not supported" << endl; + continue; + } + block_no &= 0x70; + + if(block_no != old_block_no) { + if(rc) { + break; + } + pos_data = 0; + old_block_no = block_no; + } + + track = *(bufptr + 1); + if(track & 0x80) { + continue; + } + + uchar* txtstr = bufptr + 4; + + int length = 11; + while(length >= 0 && *(txtstr + length) == '\0') { + --length; + } + + ++length; + if(length < 12) { + ++length; + } + + for(int j = 0; j < length; ++j) { + char c = *(txtstr + j); + if(c == '\0') { + data[pos_data] = c; + if(track == 0) { + if(code == (char)0xFFFFFF80) { + cdtext.title = QString::fromUtf8(data); + } else if(code == (char)0xFFFFFF81) { + cdtext.artist = QString::fromUtf8(data); + } else if (code == (char)0xFFFFFF85) { + cdtext.message = QString::fromUtf8(data); + } + } else { + if(code == (char)0xFFFFFF80) { + if(cdtext.trackTitles.size() < track) { + cdtext.trackTitles.resize(track); + } + cdtext.trackTitles[track-1] = QString::fromUtf8(data); + } else if(code == (char)0xFFFFFF81) { + if(cdtext.trackArtists.size() < track) { + cdtext.trackArtists.resize(track); + } + cdtext.trackArtists[track-1] = QString::fromUtf8(data); + } + } + rc = true; + pos_data = 0; + ++track; + } else if(pos_data < (SIZE - 1)) { + data[pos_data++] = c; + } + } + } + if(cdtext.trackTitles.size() != cdtext.trackArtists.size()) { + int size = QMAX(cdtext.trackTitles.size(), cdtext.trackArtists.size()); + cdtext.trackTitles.resize(size); + cdtext.trackArtists.resize(size); + } +#endif +#endif + return cdtext; +} +#undef SIZE diff --git a/src/translators/freedbimporter.cpp b/src/translators/freedbimporter.cpp new file mode 100644 index 0000000..14d92d8 --- /dev/null +++ b/src/translators/freedbimporter.cpp @@ -0,0 +1,556 @@ +/*************************************************************************** + copyright : (C) 2004-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 "freedbimporter.h" +#include "../collections/musiccollection.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" +#include "../tellico_kernel.h" +#include "../progressmanager.h" + +#include <config.h> + +#ifdef HAVE_KCDDB +#ifdef QT_NO_CAST_ASCII +#define HAD_QT_NO_CAST_ASCII +#undef QT_NO_CAST_ASCII +#endif +#include <libkcddb/client.h> +#ifdef HAD_QT_NO_CAST_ASCII +#define QT_NO_CAST_ASCII +#undef HAD_QT_NO_CAST_ASCII +#endif +#endif + +#include <kcombobox.h> +#include <kconfig.h> +#include <kapplication.h> +#include <kinputdialog.h> + +#include <qfile.h> +#include <qdir.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qgroupbox.h> +#include <qwhatsthis.h> +#include <qradiobutton.h> +#include <qbuttongroup.h> +#include <qhbox.h> +#include <qcheckbox.h> + +using Tellico::Import::FreeDBImporter; + +FreeDBImporter::FreeDBImporter() : Tellico::Import::Importer(), m_coll(0), m_widget(0), m_cancelled(false) { +} + +bool FreeDBImporter::canImport(int type) const { + return type == Data::Collection::Album; +} + +Tellico::Data::CollPtr FreeDBImporter::collection() { + if(m_coll) { + return m_coll; + } + + m_cancelled = false; + if(m_radioCDROM->isChecked()) { + readCDROM(); + } else { + readCache(); + } + if(m_cancelled) { + m_coll = 0; + } + return m_coll; +} + +void FreeDBImporter::readCDROM() { +#ifdef HAVE_KCDDB + QString drivePath = m_driveCombo->currentText(); + if(drivePath.isEmpty()) { + setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>").arg(drivePath)); + myDebug() << "FreeDBImporter::readCDROM() - no drive!" << endl; + return; + } + + // now it's ok to add device to saved list + m_driveCombo->insertItem(drivePath); + QStringList drives; + for(int i = 0; i < m_driveCombo->count(); ++i) { + if(drives.findIndex(m_driveCombo->text(i)) == -1) { + drives += m_driveCombo->text(i); + } + } + + { + KConfigGroup config(KGlobal::config(), QString::fromLatin1("ImportOptions - FreeDB")); + config.writeEntry("CD-ROM Devices", drives); + config.writeEntry("Last Device", drivePath); + config.writeEntry("Cache Files Only", false); + } + + QCString drive = QFile::encodeName(drivePath); + QValueList<uint> lengths; + KCDDB::TrackOffsetList list; +#if 0 + // a1107d0a - Kruder & Dorfmeister - The K&D Sessions - Disc One. +/* list + << 150 // First track start. + << 29462 + << 66983 + << 96785 + << 135628 + << 168676 + << 194147 + << 222158 + << 247076 + << 278203 // Last track start. + << 10 // Disc start. + << 316732; // Disc end. +*/ + list + << 150 // First track start. + << 3296 + << 14437 + << 41279 + << 51362 + << 56253 + << 59755 + << 61324 + << 66059 + << 69073 + << 77790 + << 83214 + << 89726 + << 92078 + << 106325 + << 113117 + << 116040 + << 119877 + << 124377 + << 145466 + << 157583 + << 167208 + << 173486 + << 180120 + << 185279 + << 193270 + << 206451 + << 217303 // Last track start. + << 10 // Disc start. + << 224925; // Disc end. +/* + list + << 150 + << 106965 + << 127220 + << 151925 + << 176085 + << 5 + << 234500; +*/ +#else + list = offsetList(drive, lengths); +#endif + + if(list.isEmpty()) { + setStatusMessage(i18n("<qt>Tellico was unable to access the CD-ROM device - <i>%1</i>.</qt>").arg(drivePath)); + return; + } +// myDebug() << KCDDB::CDDB::trackOffsetListToId(list) << endl; +// for(KCDDB::TrackOffsetList::iterator it = list.begin(); it != list.end(); ++it) { +// myDebug() << *it << endl; +// } + + // the result info, could be multiple ones + KCDDB::CDInfo info; + KCDDB::Client client; + client.setBlockingMode(true); + KCDDB::CDDB::Result r = client.lookup(list); + // KCDDB doesn't return MultipleRecordFound properly, so check outselves + if(r == KCDDB::CDDB::MultipleRecordFound || client.lookupResponse().count() > 1) { + QStringList list; + KCDDB::CDInfoList infoList = client.lookupResponse(); + for(KCDDB::CDInfoList::iterator it = infoList.begin(); it != infoList.end(); ++it) { + list.append(QString::fromLatin1("%1, %2, %3").arg((*it).artist) + .arg((*it).title) + .arg((*it).genre)); + } + + // switch back to pointer cursor + GUI::CursorSaver cs(Qt::arrowCursor); + bool ok; + QString res = KInputDialog::getItem(i18n("Select CDDB Entry"), + i18n("Select a CDDB entry:"), + list, 0, false, &ok, + Kernel::self()->widget()); + if(ok) { + uint i = 0; + for(QStringList::ConstIterator it = list.begin(); it != list.end(); ++it, ++i) { + if(*it == res) { + break; + } + } + if(i < infoList.size()) { + info = infoList[i]; + } + } else { // cancelled dialog + m_cancelled = true; + } + } else if(r == KCDDB::CDDB::Success) { + info = client.bestLookupResponse(); + } else { +// myDebug() << "FreeDBImporter::readCDROM() - no success! Return value = " << r << endl; + QString s; + switch(r) { + case KCDDB::CDDB::NoRecordFound: + s = i18n("<qt>No records were found to match the CD.</qt>"); + break; + case KCDDB::CDDB::ServerError: + myDebug() << "Server Error" << endl; + break; + case KCDDB::CDDB::HostNotFound: + myDebug() << "Host Not Found" << endl; + break; + case KCDDB::CDDB::NoResponse: + myDebug() << "No Response" << endl; + break; + case KCDDB::CDDB::UnknownError: + myDebug() << "Unknown Error" << endl; + break; + default: + break; + } + if(s.isEmpty()) { + s = i18n("<qt>Tellico was unable to complete the CD lookup.</qt>"); + } + setStatusMessage(s); + return; + } + + if(!info.isValid()) { + // go ahead and try to read cd-text if we weren't cancelled + // could be the case we don't have net access + if(!m_cancelled) { + readCDText(drive); + } + return; + } + + m_coll = new Data::MusicCollection(true); + + Data::EntryPtr entry = new Data::Entry(m_coll); + // obviously a CD + entry->setField(QString::fromLatin1("medium"), i18n("Compact Disc")); + entry->setField(QString::fromLatin1("title"), info.title); + entry->setField(QString::fromLatin1("artist"), info.artist); + entry->setField(QString::fromLatin1("genre"), info.genre); + if(info.year > 0) { + entry->setField(QString::fromLatin1("year"), QString::number(info.year)); + } + entry->setField(QString::fromLatin1("keyword"), info.category); + QString extd = info.extd; + extd.replace('\n', QString::fromLatin1("<br/>")); + entry->setField(QString::fromLatin1("comments"), extd); + + QStringList trackList; + KCDDB::TrackInfoList t = info.trackInfoList; + for(uint i = 0; i < t.count(); ++i) { +#if KDE_IS_VERSION(3,4,90) + QString s = t[i].get(QString::fromLatin1("title")).toString() + "::" + info.artist; +#else + QString s = t[i].title + "::" + info.artist; +#endif + if(i < lengths.count()) { + s += "::" + Tellico::minutes(lengths[i]); + } + trackList << s; + // TODO: KDE4 will probably have track length too + } + entry->setField(QString::fromLatin1("track"), trackList.join(QString::fromLatin1("; "))); + + m_coll->addEntries(entry); + readCDText(drive); +#endif +} + +void FreeDBImporter::readCache() { +#ifdef HAVE_KCDDB + { + // remember the import options + KConfigGroup config(KGlobal::config(), QString::fromLatin1("ImportOptions - FreeDB")); + config.writeEntry("Cache Files Only", true); + } + + KCDDB::Config cfg; + cfg.readConfig(); + + QStringList dirs = cfg.cacheLocations(); + for(QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) { + dirs += Tellico::findAllSubDirs(*it); + } + + // using a QMap is a lazy man's way of getting unique keys + // the cddb info may be in multiple files, all with the same filename, the cddb id + QMap<QString, QString> files; + for(QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) { + if((*it).isEmpty()) { + continue; + } + + QDir dir(*it); + dir.setFilter(QDir::Files | QDir::Readable | QDir::Hidden); // hidden since I want directory files + const QStringList list = dir.entryList(); + for(QStringList::ConstIterator it2 = list.begin(); it2 != list.end(); ++it2) { + files.insert(*it2, dir.absFilePath(*it2), false); + } +// kapp->processEvents(); // really needed ? + } + + const QString title = QString::fromLatin1("title"); + const QString artist = QString::fromLatin1("artist"); + const QString year = QString::fromLatin1("year"); + const QString genre = QString::fromLatin1("genre"); + const QString track = QString::fromLatin1("track"); + const QString comments = QString::fromLatin1("comments"); + uint numFiles = files.count(); + + if(numFiles == 0) { + myDebug() << "FreeDBImporter::readCache() - no files found" << endl; + return; + } + + m_coll = new Data::MusicCollection(true); + + const uint stepSize = QMAX(1, numFiles / 100); + const bool showProgress = options() & ImportProgress; + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(numFiles); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + uint step = 1; + + KCDDB::CDInfo info; + for(QMap<QString, QString>::Iterator it = files.begin(); !m_cancelled && it != files.end(); ++it, ++step) { + // open file and read content + QFileInfo fileinfo(it.data()); // skip files larger than 10 kB + if(!fileinfo.exists() || !fileinfo.isReadable() || fileinfo.size() > 10*1024) { + myDebug() << "FreeDBImporter::readCache() - skipping " << it.data() << endl; + continue; + } + QFile file(it.data()); + if(!file.open(IO_ReadOnly)) { + continue; + } + QTextStream ts(&file); + // libkcddb always writes the cache files in utf-8 + ts.setEncoding(QTextStream::UnicodeUTF8); + QString cddbData = ts.read(); + file.close(); + + if(cddbData.isEmpty() || !info.load(cddbData) || !info.isValid()) { + myDebug() << "FreeDBImporter::readCache() - Error - CDDB record is not valid" << endl; + myDebug() << "FreeDBImporter::readCache() - File = " << it.data() << endl; + continue; + } + + // create a new entry and set fields + Data::EntryPtr entry = new Data::Entry(m_coll); + // obviously a CD + entry->setField(QString::fromLatin1("medium"), i18n("Compact Disc")); + entry->setField(title, info.title); + entry->setField(artist, info.artist); + entry->setField(genre, info.genre); + if(info.year > 0) { + entry->setField(QString::fromLatin1("year"), QString::number(info.year)); + } + entry->setField(QString::fromLatin1("keyword"), info.category); + QString extd = info.extd; + extd.replace('\n', QString::fromLatin1("<br/>")); + entry->setField(QString::fromLatin1("comments"), extd); + + // step through trackList + QStringList trackList; + KCDDB::TrackInfoList t = info.trackInfoList; + for(uint i = 0; i < t.count(); ++i) { +#if KDE_IS_VERSION(3,4,90) + trackList << t[i].get(QString::fromLatin1("title")).toString(); +#else + trackList << t[i].title; +#endif + } + entry->setField(track, trackList.join(QString::fromLatin1("; "))); + +#if 0 + // add CDDB info + const QString br = QString::fromLatin1("<br/>"); + QString comment; + if(!info.extd.isEmpty()) { + comment.append(info.extd + br); + } + if(!info.id.isEmpty()) { + comment.append(QString::fromLatin1("CDDB-ID: ") + info.id + br); + } + if(info.length > 0) { + comment.append("Length: " + QString::number(info.length) + br); + } + if(info.revision > 0) { + comment.append("Revision: " + QString::number(info.revision) + br); + } + entry->setField(comments, comment); +#endif + + // add this entry to the music collection + m_coll->addEntries(entry); + + if(showProgress && step%stepSize == 0) { + ProgressManager::self()->setProgress(this, step); + kapp->processEvents(); + } + } +#endif +} + +#define SETFIELD(name,value) \ + if(entry->field(QString::fromLatin1(name)).isEmpty()) { \ + entry->setField(QString::fromLatin1(name), value); \ + } + +void FreeDBImporter::readCDText(const QCString& drive_) { +#ifdef USE_CDTEXT + Data::EntryPtr entry; + if(m_coll) { + if(m_coll->entryCount() > 0) { + entry = m_coll->entries().front(); + } + } else { + m_coll = new Data::MusicCollection(true); + } + if(!entry) { + entry = new Data::Entry(m_coll); + entry->setField(QString::fromLatin1("medium"), i18n("Compact Disc")); + m_coll->addEntries(entry); + } + + CDText cdtext = getCDText(drive_); +/* + myDebug() << "CDText - title: " << cdtext.title << endl; + myDebug() << "CDText - title: " << cdtext.artist << endl; + for(int i = 0; i < cdtext.trackTitles.size(); ++i) { + myDebug() << i << "::" << cdtext.trackTitles[i] << " - " << cdtext.trackArtists[i] << endl; + } +*/ + + QString artist = cdtext.artist; + SETFIELD("title", cdtext.title); + SETFIELD("artist", artist); + SETFIELD("comments", cdtext.message); + QStringList tracks; + for(uint i = 0; i < cdtext.trackTitles.size(); ++i) { + tracks << cdtext.trackTitles[i] + "::" + cdtext.trackArtists[i]; + if(artist.isEmpty()) { + artist = cdtext.trackArtists[i]; + } + if(!artist.isEmpty() && artist.lower() != cdtext.trackArtists[i].lower()) { + artist = i18n("Various"); + } + } + SETFIELD("track", tracks.join(QString::fromLatin1("; "))); + + // something special for compilations and such + SETFIELD("title", i18n(Data::Collection::s_emptyGroupTitle)); + SETFIELD("artist", artist); +#endif +} +#undef SETFIELD + +QWidget* FreeDBImporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget) { + return m_widget; + } + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* bigbox = new QGroupBox(1, Qt::Horizontal, i18n("Audio CD Options"), m_widget); + + // cdrom stuff + QHBox* box = new QHBox(bigbox); + m_radioCDROM = new QRadioButton(i18n("Read data from CD-ROM device"), box); + m_driveCombo = new KComboBox(true, box); + m_driveCombo->setDuplicatesEnabled(false); + QString w = i18n("Select or input the CD-ROM device location."); + QWhatsThis::add(m_radioCDROM, w); + QWhatsThis::add(m_driveCombo, w); + + /********************************************************************************/ + + m_radioCache = new QRadioButton(i18n("Read all CDDB cache files only"), bigbox); + QWhatsThis::add(m_radioCache, i18n("Read data recursively from all the CDDB cache files " + "contained in the default cache folders.")); + + // cddb cache stuff + m_buttonGroup = new QButtonGroup(m_widget); + m_buttonGroup->hide(); // only use as button parent + m_buttonGroup->setExclusive(true); + m_buttonGroup->insert(m_radioCDROM); + m_buttonGroup->insert(m_radioCache); + connect(m_buttonGroup, SIGNAL(clicked(int)), SLOT(slotClicked(int))); + + l->addWidget(bigbox); + l->addStretch(1); + + // now read config options + KConfigGroup config(KGlobal::config(), QString::fromLatin1("ImportOptions - FreeDB")); + QStringList devices = config.readListEntry("CD-ROM Devices"); + if(devices.isEmpty()) { +#if defined(__OpenBSD__) + devices += QString::fromLatin1("/dev/rcd0c"); +#endif + devices += QString::fromLatin1("/dev/cdrom"); + devices += QString::fromLatin1("/dev/dvd"); + } + m_driveCombo->insertStringList(devices); + QString device = config.readEntry("Last Device"); + if(!device.isEmpty()) { + m_driveCombo->setCurrentText(device); + } + if(config.readBoolEntry("Cache Files Only", false)) { + m_radioCache->setChecked(true); + } else { + m_radioCDROM->setChecked(true); + } + // set enabled widgets + slotClicked(m_buttonGroup->selectedId()); + + return m_widget; +} + +void FreeDBImporter::slotClicked(int id_) { + QButton* button = m_buttonGroup->find(id_); + if(!button) { + return; + } + + m_driveCombo->setEnabled(button == m_radioCDROM); +} + +void FreeDBImporter::slotCancel() { + m_cancelled = true; +} + +#include "freedbimporter.moc" diff --git a/src/translators/freedbimporter.h b/src/translators/freedbimporter.h new file mode 100644 index 0000000..263f89d --- /dev/null +++ b/src/translators/freedbimporter.h @@ -0,0 +1,85 @@ +/*************************************************************************** + copyright : (C) 2004-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; * + * * + ***************************************************************************/ + +#ifndef FREEDBIMPORTER_H +#define FREEDBIMPORTER_H + +#include "importer.h" +#include "../datavectors.h" + +#include <qvaluevector.h> + +class QButtonGroup; +class QRadioButton; +class KComboBox; + +namespace Tellico { + namespace Import { + +/** + * The FreeDBImporter class takes care of importing audio files. + * + * @author Robby Stephenson + */ +class FreeDBImporter : public Importer { +Q_OBJECT + +public: + /** + */ + FreeDBImporter(); + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private slots: + void slotClicked(int id); + +private: + typedef QValueVector<QString> StringVector; + struct CDText { + friend class FreeDBImporter; + QString title; + QString artist; + QString message; + StringVector trackTitles; + StringVector trackArtists; + }; + + static QValueList<uint> offsetList(const QCString& drive, QValueList<uint>& trackLengths); + static CDText getCDText(const QCString& drive); + + void readCDROM(); + void readCache(); + void readCDText(const QCString& drive); + + Data::CollPtr m_coll; + QWidget* m_widget; + QButtonGroup* m_buttonGroup; + QRadioButton* m_radioCDROM; + QRadioButton* m_radioCache; + KComboBox* m_driveCombo; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/gcfilmsexporter.cpp b/src/translators/gcfilmsexporter.cpp new file mode 100644 index 0000000..b172996 --- /dev/null +++ b/src/translators/gcfilmsexporter.cpp @@ -0,0 +1,235 @@ +/*************************************************************************** + copyright : (C) 2005-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 "gcfilmsexporter.h" +#include "../collection.h" +#include "../document.h" +#include "../filehandler.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../stringset.h" +#include "../tellico_kernel.h" +#include "../imagefactory.h" + +#include <klocale.h> +#include <kio/netaccess.h> + +namespace { + char GCFILMS_DELIMITER = '|'; +} + +using Tellico::Export::GCfilmsExporter; + +GCfilmsExporter::GCfilmsExporter() : Tellico::Export::Exporter() { +} + +QString GCfilmsExporter::formatString() const { + return i18n("GCfilms"); +} + +QString GCfilmsExporter::fileFilter() const { + return i18n("*.gcf|GCfilms Data Files (*.gcf)") + QChar('\n') + i18n("*|All Files"); +#if 0 + i18n("*.gcs|GCstar Data Files (*.gcs)") +#endif +} + +bool GCfilmsExporter::exec() { + Data::CollPtr coll = collection(); + if(!coll) { + return false; + } + + QString text; + QTextOStream ts(&text); + + ts << "GCfilms|" << coll->entryCount() << "|"; + if(options() & Export::ExportUTF8) { + ts << "UTF8" << endl; + } + + char d = GCFILMS_DELIMITER; + bool format = options() & Export::ExportFormatted; + // when importing GCfilms, a url field is added + bool hasURL = coll->hasField(QString::fromLatin1("url")) + && coll->fieldByName(QString::fromLatin1("url"))->type() == Data::Field::URL; + + uint minRating = 1; + uint maxRating = 5; + Data::FieldPtr f = coll->fieldByName(QString::fromLatin1("rating")); + if(f) { + bool ok; + uint n = Tellico::toUInt(f->property(QString::fromLatin1("minimum")), &ok); + if(ok) { + minRating = n; + } + n = Tellico::toUInt(f->property(QString::fromLatin1("maximum")), &ok); + if(ok) { + maxRating = n; + } + } + + // only going to export images if it's a local path + KURL imageDir; + if(url().isLocalFile()) { + imageDir = url(); + imageDir.cd(QString::fromLatin1("..")); + imageDir.addPath(url().fileName().section('.', 0, 0) + QString::fromLatin1("_images/")); + if(!KIO::NetAccess::exists(imageDir, false, 0)) { + bool success = KIO::NetAccess::mkdir(imageDir, Kernel::self()->widget()); + if(!success) { + imageDir = KURL(); // means don't write images + } + } + } + + QStringList images; + for(Data::EntryVec::ConstIterator entry = entries().begin(); entry != entries().end(); ++entry) { + ts << entry->id() << d; + push(ts, "title", entry, format); + push(ts, "year", entry, format); + push(ts, "running-time", entry, format); + push(ts, "director", entry, format); + push(ts, "nationality", entry, format); + push(ts, "genre", entry, format); + // do image + QString tmp = entry->field(QString::fromLatin1("cover")); + if(!tmp.isEmpty() && !imageDir.isEmpty()) { + images << tmp; + ts << imageDir.path() << tmp; + } + ts << d; + + // do not format cast since the commas could get mixed up + const QStringList cast = entry->fields(QString::fromLatin1("cast"), false); + for(QStringList::ConstIterator it = cast.begin(); it != cast.end(); ++it) { + ts << (*it).section(QString::fromLatin1("::"), 0, 0); + if(it != cast.fromLast()) { + ts << ", "; + } + } + ts << d; + + // values[9] is the original title + ts << d; + + push(ts, "plot", entry, format); + + if(hasURL) { + push(ts, "url", entry, format); + } else { + ts << d; + } + + // values[12] is whether the film has been viewed or not + ts << d; + + push(ts, "medium", entry, format); + // values[14] is number of DVDS? + ts << d; + // values[15] is place? + ts << d; + + // gcfilms's ratings go 0-10, just multiply by two + bool ok; + int rat = Tellico::toUInt(entry->field(QString::fromLatin1("rating"), format), &ok); + if(ok) { + ts << rat * 10/(maxRating-minRating); + } + ts << d; + + push(ts, "comments", entry, format); + push(ts, "language", entry, format); // ignoring audio-tracks + + push(ts, "subtitle", entry, format); + + // values[20] is borrower name, values[21] is loan date + if(entry->field(QString::fromLatin1("loaned")).isEmpty()) { + ts << d << d; + } else { + // find loan + bool found = false; + const Data::BorrowerVec& borrowers = Data::Document::self()->collection()->borrowers(); + for(Data::BorrowerVec::ConstIterator b = borrowers.begin(); b != borrowers.end() && !found; ++b) { + const Data::LoanVec& loans = b->loans(); + for(Data::LoanVec::ConstIterator loan = loans.begin(); loan != loans.end(); ++loan) { + if(entry.data() == loan->entry()) { + ts << b->name() << d; + ts << loan->loanDate().day() << '/' + << loan->loanDate().month() << '/' + << loan->loanDate().year() << d; + found = true; + break; + } + } + } + } + + // values[22] is history ? + ts << d; + + // for certification, only thing we can do is assume default american ratings + tmp = entry->field(QString::fromLatin1("certification"), format); + int age = 0; + if(tmp == Latin1Literal("U (USA)")) { + age = 1; + } else if(tmp == Latin1Literal("G (USA)")) { + age = 2; + } else if(tmp == Latin1Literal("PG (USA)")) { + age = 5; + } else if(tmp == Latin1Literal("PG-13 (USA)")) { + age = 13; + } else if(tmp == Latin1Literal("R (USA)")) { + age = 17; + } + if(age > 0) { + ts << age << d; + } + ts << d; + + // all done + ts << endl; + } + + StringSet imageSet; + for(QStringList::ConstIterator it = images.begin(); it != images.end(); ++it) { + if(imageSet.has(*it)) { + continue; + } + if(ImageFactory::writeImage(*it, imageDir)) { + imageSet.add(*it); + } else { + kdWarning() << "GCfilmsExporter::exec() - unable to write image file: " + << imageDir << *it << endl; + } + } + + return FileHandler::writeTextURL(url(), text, options() & Export::ExportUTF8, options() & Export::ExportForce); +} + +void GCfilmsExporter::push(QTextOStream& ts_, QCString fieldName_, Data::EntryVec::ConstIterator entry_, bool format_) { + Data::FieldPtr f = collection()->fieldByName(QString::fromLatin1(fieldName_)); + // don't format multiple names cause commas will cause problems + if(f->formatFlag() == Data::Field::FormatName && (f->flags() & Data::Field::AllowMultiple)) { + format_ = false; + } + QString s = entry_->field(QString::fromLatin1(fieldName_), format_); + if(f->flags() & Data::Field::AllowMultiple) { + ts_ << s.replace(QString::fromLatin1("; "), QChar(',')); + } else { + ts_ << s; + } + ts_ << GCFILMS_DELIMITER; +} + +#include "gcfilmsexporter.moc" diff --git a/src/translators/gcfilmsexporter.h b/src/translators/gcfilmsexporter.h new file mode 100644 index 0000000..50ee31c --- /dev/null +++ b/src/translators/gcfilmsexporter.h @@ -0,0 +1,46 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_EXPORT_GCFILMSEXPORTER_H +#define TELLICO_EXPORT_GCFILMSEXPORTER_H + +class QTextOStream; + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class GCfilmsExporter : public Exporter { +Q_OBJECT + +public: + GCfilmsExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + // no options + virtual QWidget* widget(QWidget*, const char*) { return 0; } + +private: + void push(QTextOStream& ts, QCString fieldName, Data::EntryVec::ConstIterator entry, bool format); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/gcfilmsimporter.cpp b/src/translators/gcfilmsimporter.cpp new file mode 100644 index 0000000..e2ff9ca --- /dev/null +++ b/src/translators/gcfilmsimporter.cpp @@ -0,0 +1,273 @@ +/*************************************************************************** + copyright : (C) 2005-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 "gcfilmsimporter.h" +#include "../collections/videocollection.h" +#include "../latin1literal.h" +#include "../tellico_utils.h" +#include "../imagefactory.h" +#include "../borrower.h" +#include "../progressmanager.h" +#include "xslthandler.h" +#include "tellicoimporter.h" + +#include <kapplication.h> +#include <kstandarddirs.h> + +#include <qtextcodec.h> + +#define CHECKLIMITS(n) if(values.count() <= n) continue + +using Tellico::Import::GCfilmsImporter; + +GCfilmsImporter::GCfilmsImporter(const KURL& url_) : TextImporter(url_), m_coll(0), m_cancelled(false) { +} + +bool GCfilmsImporter::canImport(int type) const { + return type == Data::Collection::Video + || type == Data::Collection::Book + || type == Data::Collection::Album + || type == Data::Collection::Game + || type == Data::Collection::Wine + || type == Data::Collection::Coin; +} + +Tellico::Data::CollPtr GCfilmsImporter::collection() { + if(m_coll) { + return m_coll; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + QString str = text(); + QTextIStream t(&str); + QString line = t.readLine(); + if(line.startsWith(QString::fromLatin1("GCfilms"))) { + readGCfilms(str); + } else { + // need to reparse the string if it's in utf-8 + if(line.lower().find(QString::fromLatin1("utf-8")) > 0) { + str = QString::fromUtf8(str.local8Bit()); + } + readGCstar(str); + } + return m_coll; +} + +void GCfilmsImporter::readGCfilms(const QString& text_) { + m_coll = new Data::VideoCollection(true); + bool hasURL = false; + if(m_coll->hasField(QString::fromLatin1("url"))) { + hasURL = m_coll->fieldByName(QString::fromLatin1("url"))->type() == Data::Field::URL; + } else { + Data::FieldPtr field = new Data::Field(QString::fromLatin1("url"), i18n("URL"), Data::Field::URL); + field->setCategory(i18n("General")); + m_coll->addField(field); + hasURL = true; + } + + bool convertUTF8 = false; + QMap<QString, Data::BorrowerPtr> borrowers; + const QRegExp rx(QString::fromLatin1("\\s*,\\s*")); + QRegExp year(QString::fromLatin1("\\d{4}")); + QRegExp runTimeHr(QString::fromLatin1("(\\d+)\\s?hr?")); + QRegExp runTimeMin(QString::fromLatin1("(\\d+)\\s?mi?n?")); + + bool gotFirstLine = false; + uint total = 0; + + QTextIStream t(&text_); + + const uint length = text_.length(); + const uint stepSize = QMAX(s_stepSize, length/100); + const bool showProgress = options() & ImportProgress; + + ProgressManager::self()->setTotalSteps(this, length); + uint j = 0; + for(QString line = t.readLine(); !m_cancelled && !line.isNull(); line = t.readLine(), j += line.length()) { + // string was wrongly converted + QStringList values = QStringList::split('|', (convertUTF8 ? QString::fromUtf8(line.local8Bit()) : line), true); + if(values.empty()) { + continue; + } + + if(!gotFirstLine) { + if(values[0] != Latin1Literal("GCfilms")) { + setStatusMessage(i18n("<qt>The file is not a valid GCstar data file.</qt>")); + m_coll = 0; + return; + } + total = Tellico::toUInt(values[1], 0)+1; // number of lines really + if(values.size() > 2 && values[2] == Latin1Literal("UTF8")) { + // if locale encoding isn't utf8, need to do a reconversion + QTextCodec* codec = QTextCodec::codecForLocale(); + if(QCString(codec->name()).find("utf-8", 0, false) == -1) { + convertUTF8 = true; + } + } + gotFirstLine = true; + continue; + } + + bool ok; + + Data::EntryPtr entry = new Data::Entry(m_coll); + entry->setId(Tellico::toUInt(values[0], &ok)); + entry->setField(QString::fromLatin1("title"), values[1]); + if(year.search(values[2]) > -1) { + entry->setField(QString::fromLatin1("year"), year.cap()); + } + + uint time = 0; + if(runTimeHr.search(values[3]) > -1) { + time = Tellico::toUInt(runTimeHr.cap(1), &ok) * 60; + } + if(runTimeMin.search(values[3]) > -1) { + time += Tellico::toUInt(runTimeMin.cap(1), &ok); + } + if(time > 0) { + entry->setField(QString::fromLatin1("running-time"), QString::number(time)); + } + + entry->setField(QString::fromLatin1("director"), splitJoin(rx, values[4])); + entry->setField(QString::fromLatin1("nationality"), splitJoin(rx, values[5])); + entry->setField(QString::fromLatin1("genre"), splitJoin(rx, values[6])); + KURL u = KURL::fromPathOrURL(values[7]); + if(!u.isEmpty()) { + QString id = ImageFactory::addImage(u, true /* quiet */); + if(!id.isEmpty()) { + entry->setField(QString::fromLatin1("cover"), id); + } + } + entry->setField(QString::fromLatin1("cast"), splitJoin(rx, values[8])); + // values[9] is the original title + entry->setField(QString::fromLatin1("plot"), values[10]); + if(hasURL) { + entry->setField(QString::fromLatin1("url"), values[11]); + } + + CHECKLIMITS(12); + + // values[12] is whether the film has been viewed or not + entry->setField(QString::fromLatin1("medium"), values[13]); + // values[14] is number of DVDS? + // values[15] is place? + // gcfilms's ratings go 0-10, just divide by two + entry->setField(QString::fromLatin1("rating"), QString::number(int(Tellico::toUInt(values[16], &ok)/2))); + entry->setField(QString::fromLatin1("comments"), values[17]); + + CHECKLIMITS(18); + + QStringList s = QStringList::split(',', values[18]); + QStringList tracks, langs; + for(QStringList::ConstIterator it = s.begin(); it != s.end(); ++it) { + langs << (*it).section(';', 0, 0); + tracks << (*it).section(';', 1, 1); + } + entry->setField(QString::fromLatin1("language"), langs.join(QString::fromLatin1("; "))); + entry->setField(QString::fromLatin1("audio-track"), tracks.join(QString::fromLatin1("; "))); + + entry->setField(QString::fromLatin1("subtitle"), splitJoin(rx, values[19])); + + CHECKLIMITS(20); + + // values[20] is borrower name + if(!values[20].isEmpty()) { + QString tmp = values[20]; + Data::BorrowerPtr b = borrowers[tmp]; + if(!b) { + b = new Data::Borrower(tmp, QString()); + borrowers.insert(tmp, b); + } + // values[21] is loan date + if(!values[21].isEmpty()) { + tmp = values[21]; // assume date is dd/mm/yyyy + int d = Tellico::toUInt(tmp.section('/', 0, 0), &ok); + int m = Tellico::toUInt(tmp.section('/', 1, 1), &ok); + int y = Tellico::toUInt(tmp.section('/', 2, 2), &ok); + b->addLoan(new Data::Loan(entry, QDate(y, m, d), QDate(), QString())); + entry->setField(QString::fromLatin1("loaned"), QString::fromLatin1("true")); + } + } + // values[22] is history ? + // for certification, only thing we can do is assume default american ratings + // they're not translated one for one + CHECKLIMITS(23); + + int age = Tellico::toUInt(values[23], &ok); + if(age < 2) { + entry->setField(QString::fromLatin1("certification"), QString::fromLatin1("U (USA)")); + } else if(age < 3) { + entry->setField(QString::fromLatin1("certification"), QString::fromLatin1("G (USA)")); + } else if(age < 6) { + entry->setField(QString::fromLatin1("certification"), QString::fromLatin1("PG (USA)")); + } else if(age < 14) { + entry->setField(QString::fromLatin1("certification"), QString::fromLatin1("PG-13 (USA)")); + } else { + entry->setField(QString::fromLatin1("certification"), QString::fromLatin1("R (USA)")); + } + + m_coll->addEntries(entry); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } + + if(m_cancelled) { + m_coll = 0; + return; + } + + for(QMap<QString, Data::BorrowerPtr>::Iterator it = borrowers.begin(); it != borrowers.end(); ++it) { + if(!it.data()->isEmpty()) { + m_coll->addBorrower(it.data()); + } + } +} + +void GCfilmsImporter::readGCstar(const QString& text_) { + QString xsltFile = locate("appdata", QString::fromLatin1("gcstar2tellico.xsl")); + XSLTHandler handler(xsltFile); + if(!handler.isValid()) { + setStatusMessage(i18n("Tellico encountered an error in XSLT processing.")); + return; + } + + QString str = handler.applyStylesheet(text_); + + if(str.isEmpty()) { + setStatusMessage(i18n("<qt>The file is not a valid GCstar data file.</qt>")); + return; + } + + Import::TellicoImporter imp(str); + m_coll = imp.collection(); + setStatusMessage(imp.statusMessage()); +} + +inline +QString GCfilmsImporter::splitJoin(const QRegExp& rx, const QString& s) { + return QStringList::split(rx, s, false).join(QString::fromLatin1("; ")); +} + +void GCfilmsImporter::slotCancel() { + m_cancelled = true; +} + +#undef CHECKLIMITS +#include "gcfilmsimporter.moc" diff --git a/src/translators/gcfilmsimporter.h b/src/translators/gcfilmsimporter.h new file mode 100644 index 0000000..8fa9a0d --- /dev/null +++ b/src/translators/gcfilmsimporter.h @@ -0,0 +1,60 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_GCFILMSIMPORTER_H +#define TELLICO_IMPORT_GCFILMSIMPORTER_H + +#include "textimporter.h" +#include "../datavectors.h" + +class QRegExp; + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson +*/ +class GCfilmsImporter : public TextImporter { +Q_OBJECT + +public: + /** + */ + GCfilmsImporter(const KURL& url); + + /** + * + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + static QString splitJoin(const QRegExp& rx, const QString& s); + + void readGCfilms(const QString& text); + void readGCstar(const QString& text); + + Data::CollPtr m_coll; + bool m_cancelled; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/griffith2tellico.py b/src/translators/griffith2tellico.py new file mode 100755 index 0000000..24bfb41 --- /dev/null +++ b/src/translators/griffith2tellico.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# *************************************************************************** +# copyright : (C) 2007 by Robby Stephenson +# email : robby@periapsis.org +# based on : fr.allocine.py by Mathias Monnerville +# *************************************************************************** +# +# *************************************************************************** +# * * +# * 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; * +# * * +# *************************************************************************** + +import os, sys +import base64 +import xml.dom.minidom +try: + import sqlite3 +except: + print sys.stderr, "The Python sqlite3 module is required to import Griffith databases." + exit(1) + +DB_PATH = os.environ['HOME'] + '/.griffith/griffith.db' +POSTERS_PATH = os.environ['HOME'] + '/.griffith/posters/' + +XML_HEADER = """<?xml version="1.0" encoding="UTF-8"?>""" +DOCTYPE = """<!DOCTYPE tellico PUBLIC "-//Robby Stephenson/DTD Tellico V9.0//EN" "http://periapsis.org/tellico/dtd/v9/tellico.dtd">""" + +class BasicTellicoDOM: + def __init__(self): + self.__doc = xml.dom.minidom.Document() + self.__root = self.__doc.createElement('tellico') + self.__root.setAttribute('xmlns', 'http://periapsis.org/tellico/') + self.__root.setAttribute('syntaxVersion', '9') + + self.__collection = self.__doc.createElement('collection') + self.__collection.setAttribute('title', 'Griffith Import') + self.__collection.setAttribute('type', '3') + + self.__fields = self.__doc.createElement('fields') + # Add all default (standard) fields + self.__dfltField = self.__doc.createElement('field') + self.__dfltField.setAttribute('name', '_default') + + # change the rating to have a maximum of 10 + self.__ratingField = self.__doc.createElement('field') + self.__ratingField.setAttribute('name', 'rating') + self.__ratingField.setAttribute('title', 'Personal Rating') + self.__ratingField.setAttribute('flags', '2') + self.__ratingField.setAttribute('category', 'Personal') + self.__ratingField.setAttribute('format', '4') + self.__ratingField.setAttribute('type', '14') + self.__ratingField.setAttribute('i18n', 'yes') + propNode = self.__doc.createElement('prop') + propNode.setAttribute('name', 'maximum') + propNode.appendChild(self.__doc.createTextNode('10')) + self.__ratingField.appendChild(propNode); + propNode = self.__doc.createElement('prop') + propNode.setAttribute('name', 'minimum') + propNode.appendChild(self.__doc.createTextNode('1')) + self.__ratingField.appendChild(propNode); + + # Add a custom 'Original Title' field + self.__titleField = self.__doc.createElement('field') + self.__titleField.setAttribute('name', 'orig-title') + self.__titleField.setAttribute('title', 'Original Title') + self.__titleField.setAttribute('flags', '8') + self.__titleField.setAttribute('category', 'General') + self.__titleField.setAttribute('format', '1') + self.__titleField.setAttribute('type', '1') + self.__titleField.setAttribute('i18n', 'yes') + + self.__keywordField = self.__doc.createElement('field') + self.__keywordField.setAttribute('name', 'keyword') + self.__keywordField.setAttribute('title', 'Keywords') + self.__keywordField.setAttribute('flags', '7') + self.__keywordField.setAttribute('category', 'Personal') + self.__keywordField.setAttribute('format', '4') + self.__keywordField.setAttribute('type', '1') + self.__keywordField.setAttribute('i18n', 'yes') + + self.__urlField = self.__doc.createElement('field') + self.__urlField.setAttribute('name', 'url') + self.__urlField.setAttribute('title', 'URL') + self.__urlField.setAttribute('flags', '0') + self.__urlField.setAttribute('category', 'General') + self.__urlField.setAttribute('format', '4') + self.__urlField.setAttribute('type', '7') + self.__urlField.setAttribute('i18n', 'yes') + + self.__fields.appendChild(self.__dfltField) + self.__fields.appendChild(self.__ratingField) + self.__fields.appendChild(self.__titleField) + self.__fields.appendChild(self.__keywordField) + self.__fields.appendChild(self.__urlField) + self.__collection.appendChild(self.__fields) + + self.__images = self.__doc.createElement('images') + + self.__root.appendChild(self.__collection) + self.__doc.appendChild(self.__root) + self.__fieldsMap = dict(country='nationality', + classification='certification', + runtime='running-time', + o_title='orig-title', + notes='comments', + image='cover', + tag='keyword', + site='url') + + + def addMedia(self, media): + if len(media) == 0: return + # add default Tellico values + orig_media = 'DVD;VHS;VCD;DivX;Blu-ray;HD DVD'.split(';') + orig_media.extend(media) + # make sure unique + set = {} + media = [set.setdefault(e,e) for e in orig_media if e not in set] + + mediaField = self.__doc.createElement('field') + mediaField.setAttribute('name', 'medium') + mediaField.setAttribute('title', 'Medium') + mediaField.setAttribute('flags', '2') + mediaField.setAttribute('category', 'General') + mediaField.setAttribute('format', '4') + mediaField.setAttribute('type', '3') + mediaField.setAttribute('i18n', 'yes') + mediaField.setAttribute('allowed', ';'.join(media)) + self.__fields.appendChild(mediaField) + + def addEntry(self, movieData): + """ + Add a movie entry + """ + entryNode = self.__doc.createElement('entry') + entryNode.setAttribute('id', movieData['id']) + + for key, values in movieData.iteritems(): + if key == 'id': + continue + + if self.__fieldsMap.has_key(key): + field = self.__fieldsMap[key] + else: + field = key + + parentNode = self.__doc.createElement(field + 's') + + for value in values: + if len(value) == 0: continue + node = self.__doc.createElement(field) + if field == 'certification': value += " (USA)" + elif field == 'region': value = "Region " + value + elif field == 'cover': + imageNode = self.__doc.createElement('image') + imageNode.setAttribute('format', 'JPEG') + imageNode.setAttribute('id', value[0]) + imageNode.appendChild(self.__doc.createTextNode(value[1])) + self.__images.appendChild(imageNode) + value = value[0] # value was (id, md5) + + if field == 'cast': + for v in value: + columnNode = self.__doc.createElement('column') + columnNode.appendChild(self.__doc.createTextNode(v.strip())) + node.appendChild(columnNode) + + else: + node.appendChild(self.__doc.createTextNode(value.strip())) + + if node.hasChildNodes(): parentNode.appendChild(node) + + if parentNode.hasChildNodes(): entryNode.appendChild(parentNode) + + self.__collection.appendChild(entryNode) + + def printXML(self): + """ + Outputs XML content to stdout + """ + self.__collection.appendChild(self.__images) + print XML_HEADER; print DOCTYPE + print self.__root.toxml() + + +class GriffithParser: + def __init__(self): + self.__dbPath = DB_PATH + self.__domTree = BasicTellicoDOM() + + def run(self): + """ + Runs the parser: fetch movie ids, then fills and prints the DOM tree + to stdout (in tellico format) so that tellico can use it. + """ + self.__conn = sqlite3.connect(self.__dbPath) + self.__loadDatabase() + # Print results to stdout + self.__domTree.printXML() + + def __addMediaValues(self): + c = self.__conn.cursor() + c.execute("SELECT name FROM media") + + media = list([row[0].encode('utf-8') for row in c.fetchall()]) + self.__domTree.addMedia(media) + + + def __fetchMovieIds(self): + """ + Retrieve all movie ids + """ + c = self.__conn.cursor() + c.execute("SELECT movie_id FROM movies") + data = c.fetchall() + dataList = [row[0] for row in data] + return dataList + + def __fetchMovieInfo(self, id): + """ + Fetches movie information + """ + #cast is a reserved word + columns = ('title','director','rating','year','region', + 'country','genre','classification','plot', + 'runtime','o_title','studio','notes','image', + '[cast]','loaned','color','site') + + c = self.__conn.cursor() + c.execute("SELECT %s FROM movies WHERE movie_id=%s" % (','.join(columns),id)) + row = c.fetchone() + + data = {} + data['id'] = str(id) + + for i in range(len(columns)): + if row[i] == None : continue + + try: + value = row[i].encode('utf-8') + except: + value = str(row[i]) + + col = columns[i].replace('[','').replace(']','') + + if col == 'genre' or col == 'studio': + values = value.split('/') + elif col == 'plot' or col == 'notes': + value = value.replace('\n', '\n<br/>') + values = (value,) + elif col == 'cast': + values = [] + lines = value.split('\n') + for line in lines: + cast = line.split('as') + values.append(cast) + elif col == 'image': + imgfile = POSTERS_PATH + value + '.jpg' + img = file(imgfile,'rb').read() + values = ((value + '.jpg', base64.encodestring(img)),) + elif col == 'loaned': + if value == '0': value = '' + values = (value,) + elif col == 'color': + if value == '1': value = 'Color' + elif value == '2': value = 'Black & White' + values = (value,) + else: + values = (value,) + col = col.replace('"','') + data[col] = values + + # get medium + c.execute("SELECT name FROM media WHERE medium_id IN (SELECT medium_id FROM movies WHERE movie_id=%s)" % id) + + media = list([row[0].encode('utf-8') for row in c.fetchall()]) + if len(media) > 0: data['medium'] = media + + # get all tags + c.execute("SELECT name FROM tags WHERE tag_id IN (SELECT tag_id FROM movie_tag WHERE movie_id=%s)" % id) + + tags = list([row[0].encode('utf-8') for row in c.fetchall()]) + if len(tags) > 0: data['tag'] = tags + + # get all languages + c.execute("SELECT name FROM languages WHERE lang_id IN (SELECT lang_id FROM movie_lang WHERE movie_id=%s)" % id) + + langs = list([row[0].encode('utf-8') for row in c.fetchall()]) + if len(langs) > 0: data['language'] = langs + + return data + + + def __loadDatabase(self): + # Get all ids + self.__addMediaValues(); + ids = self.__fetchMovieIds() + + # Now retrieve data + if ids: + for entry in ids: + data = self.__fetchMovieInfo(entry) + self.__domTree.addEntry(data) + else: + return None + + + +def main(): + parser = GriffithParser() + parser.run() + +if __name__ == '__main__': + main() diff --git a/src/translators/griffithimporter.cpp b/src/translators/griffithimporter.cpp new file mode 100644 index 0000000..8b0394f --- /dev/null +++ b/src/translators/griffithimporter.cpp @@ -0,0 +1,107 @@ +/*************************************************************************** + copyright : (C) 2007 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 "griffithimporter.h" +#include "../collections/videocollection.h" +#include "tellicoimporter.h" +#include "../tellico_debug.h" + +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kprocess.h> + +#include <qdir.h> +#include <qfile.h> + +using Tellico::Import::GriffithImporter; + +GriffithImporter::~GriffithImporter() { + if(m_process) { + m_process->kill(); + delete m_process; + m_process = 0; + } +} + +Tellico::Data::CollPtr GriffithImporter::collection() { + QString filename = QDir::homeDirPath() + QString::fromLatin1("/.griffith/griffith.db"); + if(!QFile::exists(filename)) { + myWarning() << "GriffithImporter::collection() - database not found: " << filename << endl; + return 0; + } + + QString python = KStandardDirs::findExe(QString::fromLatin1("python")); + if(python.isEmpty()) { + myWarning() << "GriffithImporter::collection() - python not found!" << endl; + return 0; + } + + QString griffith = KGlobal::dirs()->findResource("appdata", QString::fromLatin1("griffith2tellico.py")); + if(griffith.isEmpty()) { + myWarning() << "GriffithImporter::collection() - griffith2tellico.py not found!" << endl; + return 0; + } + + m_process = new KProcess(); + connect(m_process, SIGNAL(receivedStdout(KProcess*, char*, int)), SLOT(slotData(KProcess*, char*, int))); + connect(m_process, SIGNAL(receivedStderr(KProcess*, char*, int)), SLOT(slotError(KProcess*, char*, int))); + connect(m_process, SIGNAL(processExited(KProcess*)), SLOT(slotProcessExited(KProcess*))); + *m_process << python << griffith; + if(!m_process->start(KProcess::Block, KProcess::AllOutput)) { + myDebug() << "ExecExternalFetcher::startSearch() - process failed to start" << endl; + return 0; + } + + return m_coll; +} + +void GriffithImporter::slotData(KProcess*, char* buffer_, int len_) { + QDataStream stream(m_data, IO_WriteOnly | IO_Append); + stream.writeRawBytes(buffer_, len_); +} + +void GriffithImporter::slotError(KProcess*, char* buffer_, int len_) { + QString msg = QString::fromLocal8Bit(buffer_, len_); + myDebug() << "GriffithImporter::slotError() - " << msg << endl; + setStatusMessage(msg); +} + + +void GriffithImporter::slotProcessExited(KProcess*) { +// myDebug() << "GriffithImporter::slotProcessExited()" << endl; + if(!m_process->normalExit() || m_process->exitStatus()) { + myDebug() << "GriffithImporter::slotProcessExited() - process did not exit successfully" << endl; + return; + } + + if(m_data.isEmpty()) { + myDebug() << "GriffithImporter::slotProcessExited() - no data" << endl; + return; + } + + QString text = QString::fromUtf8(m_data, m_data.size()); + TellicoImporter imp(text); + + m_coll = imp.collection(); + if(!m_coll) { + myDebug() << "GriffithImporter::slotProcessExited() - no collection pointer" << endl; + } else { + myLog() << "GriffithImporter::slotProcessExited() - results found: " << m_coll->entryCount() << endl; + } +} + +bool GriffithImporter::canImport(int type) const { + return type == Data::Collection::Video; +} + +#include "griffithimporter.moc" diff --git a/src/translators/griffithimporter.h b/src/translators/griffithimporter.h new file mode 100644 index 0000000..60bae07 --- /dev/null +++ b/src/translators/griffithimporter.h @@ -0,0 +1,63 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef GRIFFITHIMPORTER_H +#define GRIFFITHIMPORTER_H + +#include "importer.h" +#include "../datavectors.h" + +class KProcess; + +namespace Tellico { + namespace Import { + +/** + * An importer for importing collections used by Griffith, a movie colleciton manager. + * + * The database is assumed to be $HOME/.griffith/griffith.db. The file format is sqlite3, + * and a python script, depending on pysqlite, i sused to import the database + * + * @author Robby Stephenson + */ +class GriffithImporter : public Importer { +Q_OBJECT + +public: + /** + */ + GriffithImporter() : Importer(), m_coll(0), m_process(0) {} + /** + */ + virtual ~GriffithImporter(); + + /** + */ + virtual Data::CollPtr collection(); + virtual bool canImport(int type) const; + +private slots: + void slotData(KProcess* proc, char* buffer, int len); + void slotError(KProcess* proc, char* buffer, int len); + void slotProcessExited(KProcess* proc); + +private: + Data::CollPtr m_coll; + + KProcess* m_process; + QByteArray m_data; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/grs1importer.cpp b/src/translators/grs1importer.cpp new file mode 100644 index 0000000..7eca9e3 --- /dev/null +++ b/src/translators/grs1importer.cpp @@ -0,0 +1,130 @@ +/*************************************************************************** + copyright : (C) 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 "grs1importer.h" +#include "../collections/bibtexcollection.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../tellico_debug.h" + +using Tellico::Import::GRS1Importer; +GRS1Importer::TagMap* GRS1Importer::s_tagMap = 0; + +// static +void GRS1Importer::initTagMap() { + if(!s_tagMap) { + s_tagMap = new TagMap(); + // BT is special and is handled separately + s_tagMap->insert(TagPair(2, 1), QString::fromLatin1("title")); + s_tagMap->insert(TagPair(2, 2), QString::fromLatin1("author")); + s_tagMap->insert(TagPair(2, 4), QString::fromLatin1("year")); + s_tagMap->insert(TagPair(2, 7), QString::fromLatin1("publisher")); + s_tagMap->insert(TagPair(2, 31), QString::fromLatin1("publisher")); + s_tagMap->insert(TagPair(2, 20), QString::fromLatin1("language")); + s_tagMap->insert(TagPair(2, 21), QString::fromLatin1("keyword")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("isbn/issn")), QString::fromLatin1("isbn")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("isbn")), QString::fromLatin1("isbn")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("notes")), QString::fromLatin1("note")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("note")), QString::fromLatin1("note")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("series")), QString::fromLatin1("series")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("physical description")), QString::fromLatin1("note")); + s_tagMap->insert(TagPair(3, QString::fromLatin1("subtitle")), QString::fromLatin1("subtitle")); + } +} + +GRS1Importer::GRS1Importer(const QString& text_) : TextImporter(text_) { + initTagMap(); +} + +bool GRS1Importer::canImport(int type) const { + return type == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr GRS1Importer::collection() { + Data::CollPtr coll = new Data::BibtexCollection(true); + + Data::FieldPtr f = new Data::Field(QString::fromLatin1("isbn"), i18n("ISBN#")); + f->setCategory(i18n("Publishing")); + f->setDescription(i18n("International Standard Book Number")); + coll->addField(f); + + f = new Data::Field(QString::fromLatin1("language"), i18n("Language")); + f->setCategory(i18n("Publishing")); + f->setFlags(Data::Field::AllowCompletion | Data::Field::AllowGrouped | Data::Field::AllowMultiple); + coll->addField(f); + + Data::EntryPtr e = new Data::Entry(coll); + bool empty = true; + + // in format "(tag, tag) value" + QRegExp rx(QString::fromLatin1("\\s*\\((\\d+),\\s*(.+)\\s*\\)\\s*(.+)\\s*")); +// rx.setMinimal(true); + QRegExp dateRx(QString::fromLatin1(",[^,]*\\d{3,4}[^,]*")); // remove dates from authors + QRegExp pubRx(QString::fromLatin1("([^:]+):([^,]+),?")); // split location and publisher + + bool ok; + int n; + QVariant v; + QString tmp, field, val, str = text(); + if(str.isEmpty()) { + return 0; + } + QTextStream t(&str, IO_ReadOnly); + for(QString line = t.readLine(); !line.isNull(); line = t.readLine()) { +// myDebug() << line << endl; + if(!rx.exactMatch(line)) { + continue; + } + n = rx.cap(1).toInt(); + v = rx.cap(2).toInt(&ok); + if(!ok) { + v = rx.cap(2).lower(); + } + field = (*s_tagMap)[TagPair(n, v)]; + if(field.isEmpty()) { + continue; + } +// myDebug() << "field is " << field << endl; + // assume if multiple values, it's allowed + val = rx.cap(3).stripWhiteSpace(); + if(val.isEmpty()) { + continue; + } + empty = false; + if(field == Latin1Literal("title")) { + val = val.section('/', 0, 0).stripWhiteSpace(); // only take portion of title before slash + } else if(field == Latin1Literal("author")) { + val.replace(dateRx, QString::null); + } else if(field == Latin1Literal("publisher")) { + int pos = val.find(pubRx); + if(pos > -1) { + e->setField(QString::fromLatin1("address"), pubRx.cap(1)); + val = pubRx.cap(2); + } + } + + tmp = e->field(field); + if(!tmp.isEmpty()) { + tmp += QString::fromLatin1("; "); + } + e->setField(field, tmp + val); + } + + if(!empty) { + coll->addEntries(e); + } + return coll; +} + +#include "grs1importer.moc" diff --git a/src/translators/grs1importer.h b/src/translators/grs1importer.h new file mode 100644 index 0000000..a4929a4 --- /dev/null +++ b/src/translators/grs1importer.h @@ -0,0 +1,65 @@ +/*************************************************************************** + copyright : (C) 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_GRS1IMPORTER_H +#define TELLICO_IMPORT_GRS1IMPORTER_H + +#include "textimporter.h" +#include "../datavectors.h" + +#include <qvariant.h> +#include <qmap.h> +#include <qpair.h> + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson + */ +class GRS1Importer : public TextImporter { +Q_OBJECT + +public: + GRS1Importer(const QString& text); + virtual ~GRS1Importer() {} + + /** + * @return A pointer to a @ref Data::Collection, or 0 if none can be created. + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + virtual bool canImport(int type) const; + +private: + static void initTagMap(); + + class TagPair : public QPair<int, QVariant> { + public: + TagPair() : QPair<int, QVariant>(-1, QVariant()) {} + TagPair(int n, const QVariant& v) : QPair<int, QVariant>(n, v) {} + QString toString() const { return QString::number(first) + second.toString(); } + bool operator< (const TagPair& p) const { + return toString() < p.toString(); + } + }; + + typedef QMap<TagPair, QString> TagMap; + static TagMap* s_tagMap; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/htmlexporter.cpp b/src/translators/htmlexporter.cpp new file mode 100644 index 0000000..e947793 --- /dev/null +++ b/src/translators/htmlexporter.cpp @@ -0,0 +1,815 @@ +/*************************************************************************** + copyright : (C) 2003-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 "htmlexporter.h" +#include "xslthandler.h" +#include "tellicoxmlexporter.h" +#include "../document.h" +#include "../collection.h" +#include "../filehandler.h" +#include "../imagefactory.h" +#include "../latin1literal.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../progressmanager.h" +#include "../core/tellico_config.h" +#include "../tellico_debug.h" + +#include <kstandarddirs.h> +#include <kconfig.h> +#include <kglobal.h> +#include <kio/netaccess.h> +#include <kapplication.h> +#include <klocale.h> + +#include <qdom.h> +#include <qgroupbox.h> +#include <qlayout.h> +#include <qcheckbox.h> +#include <qwhatsthis.h> +#include <qfile.h> +#include <qhbox.h> +#include <qlabel.h> + +extern "C" { +#include <libxml/HTMLparser.h> +#include <libxml/HTMLtree.h> +} + +using Tellico::Export::HTMLExporter; + +HTMLExporter::HTMLExporter() : Tellico::Export::Exporter(), + m_handler(0), + m_printHeaders(true), + m_printGrouped(false), + m_exportEntryFiles(false), + m_cancelled(false), + m_parseDOM(true), + m_checkCreateDir(true), + m_imageWidth(0), + m_imageHeight(0), + m_widget(0), + m_xsltFile(QString::fromLatin1("tellico2html.xsl")) { +} + +HTMLExporter::HTMLExporter(Data::CollPtr coll_) : Tellico::Export::Exporter(coll_), + m_handler(0), + m_printHeaders(true), + m_printGrouped(false), + m_exportEntryFiles(false), + m_cancelled(false), + m_parseDOM(true), + m_checkCreateDir(true), + m_imageWidth(0), + m_imageHeight(0), + m_widget(0), + m_xsltFile(QString::fromLatin1("tellico2html.xsl")) { +} + +HTMLExporter::~HTMLExporter() { + delete m_handler; + m_handler = 0; +} + +QString HTMLExporter::formatString() const { + return i18n("HTML"); +} + +QString HTMLExporter::fileFilter() const { + return i18n("*.html|HTML Files (*.html)") + QChar('\n') + i18n("*|All Files"); +} + +void HTMLExporter::reset() { + // since the ExportUTF8 option may have changed, need to delete handler + delete m_handler; + m_handler = 0; + m_files.clear(); + m_links.clear(); + m_copiedFiles.clear(); +} + +bool HTMLExporter::exec() { + if(url().isEmpty() || !url().isValid()) { + kdWarning() << "HTMLExporter::exec() - trying to export to invalid URL" << endl; + return false; + } + + // check file exists first + // if we're not forcing, ask use + bool force = (options() & Export::ExportForce) || FileHandler::queryExists(url()); + if(!force) { + return false; + } + + if(!m_parseDOM) { + return FileHandler::writeTextURL(url(), text(), options() & Export::ExportUTF8, force); + } + + m_cancelled = false; + // TODO: maybe need label? + if(options() & ExportProgress) { + ProgressItem& item = ProgressManager::self()->newProgressItem(this, QString::null, true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + } + // ok if not ExportProgress, no worries + ProgressItem::Done done(this); + + htmlDocPtr htmlDoc = htmlParseDoc(reinterpret_cast<xmlChar*>(text().utf8().data()), NULL); + xmlNodePtr root = xmlDocGetRootElement(htmlDoc); + if(root == 0) { + myDebug() << "HTMLExporter::exec() - no root" << endl; + return false; + } + parseDOM(root); + + if(m_cancelled) { + return true; // intentionally cancelled + } + ProgressManager::self()->setProgress(this, 15); + + xmlChar* c; + int bytes; + htmlDocDumpMemory(htmlDoc, &c, &bytes); + QString allText; + if(bytes > 0) { + allText = QString::fromUtf8(reinterpret_cast<const char*>(c), bytes); + xmlFree(c); + } + + if(m_cancelled) { + return true; // intentionally cancelled + } + ProgressManager::self()->setProgress(this, 20); + + bool success = FileHandler::writeTextURL(url(), allText, options() & Export::ExportUTF8, force); + success &= copyFiles() && (!m_exportEntryFiles || writeEntryFiles()); + return success; +} + +bool HTMLExporter::loadXSLTFile() { + QString xsltfile = locate("appdata", m_xsltFile); + if(xsltfile.isNull()) { + myDebug() << "HTMLExporter::loadXSLTFile() - no xslt file for " << m_xsltFile << endl; + return false; + } + + KURL u; + u.setPath(xsltfile); + // do NOT do namespace processing, it messes up the XSL declaration since + // QDom thinks there are no elements in the Tellico namespace and as a result + // removes the namespace declaration + QDomDocument dom = FileHandler::readXMLFile(u, false); + if(dom.isNull()) { + myDebug() << "HTMLExporter::loadXSLTFile() - error loading xslt file: " << xsltfile << endl; + return false; + } + + // notes about utf-8 encoding: + // all params should be passed to XSLTHandler in utf8 + // input string to XSLTHandler should be in utf-8, EVEN IF DOM STRING SAYS OTHERWISE + + // the stylesheet prints utf-8 by default, if using locale encoding, need + // to change the encoding attribute on the xsl:output element + if(!(options() & Export::ExportUTF8)) { + XSLTHandler::setLocaleEncoding(dom); + } + + delete m_handler; + m_handler = new XSLTHandler(dom, QFile::encodeName(xsltfile), true /*translate*/); + if(!m_handler->isValid()) { + delete m_handler; + m_handler = 0; + return false; + } + + if(m_exportEntryFiles) { + // export entries to same place as all the other date files + m_handler->addStringParam("entrydir", QFile::encodeName(fileDir().fileName())+ '/'); + // be sure to link all the entries + m_handler->addParam("link-entries", "true()"); + } + + if(!m_collectionURL.isEmpty()) { + QString s = QString::fromLatin1("../") + m_collectionURL.fileName(); + m_handler->addStringParam("collection-file", s.utf8()); + } + + // look for a file that gets installed to know the installation directory + // if parseDOM, that means we want the locations to be the actual location + // otherwise, we assume it'll be relative + if(m_parseDOM && m_dataDir.isEmpty()) { + m_dataDir = KGlobal::dirs()->findResourceDir("appdata", QString::fromLatin1("pics/tellico.png")); + } else if(!m_parseDOM) { + m_dataDir.truncate(0); + } + if(!m_dataDir.isEmpty()) { + m_handler->addStringParam("datadir", QFile::encodeName(m_dataDir)); + } + + setFormattingOptions(collection()); + + return m_handler->isValid(); +} + +QString HTMLExporter::text() { + if((!m_handler || !m_handler->isValid()) && !loadXSLTFile()) { + kdWarning() << "HTMLExporter::text() - error loading xslt file: " << m_xsltFile << endl; + return QString::null; + } + + Data::CollPtr coll = collection(); + if(!coll) { + myDebug() << "HTMLExporter::text() - no collection pointer!" << endl; + return QString::null; + } + + if(m_groupBy.isEmpty()) { + m_printGrouped = false; // can't group if no groups exist + } + + GUI::CursorSaver cs; + writeImages(coll); + + // now grab the XML + TellicoXMLExporter exporter(coll); + exporter.setURL(url()); + exporter.setEntries(entries()); + exporter.setIncludeGroups(m_printGrouped); +// yes, this should be in utf8, always + exporter.setOptions(options() | Export::ExportUTF8 | Export::ExportImages); + QDomDocument output = exporter.exportXML(); +#if 0 + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t << output.toString(); + } + f.close(); +#endif + + QString text = m_handler->applyStylesheet(output.toString()); +#if 0 + QFile f2(QString::fromLatin1("/tmp/test.html")); + if(f2.open(IO_WriteOnly)) { + QTextStream t(&f2); + t << text; +// t << "\n\n-------------------------------------------------------\n\n"; +// t << Tellico::i18nReplace(text); + } + f2.close(); +#endif + // the XSLT file gets translated instead +// return Tellico::i18nReplace(text); + return text; +} + +void HTMLExporter::setFormattingOptions(Data::CollPtr coll) { + QString file = Kernel::self()->URL().fileName(); + if(file != i18n("Untitled")) { + m_handler->addStringParam("filename", QFile::encodeName(file)); + } + m_handler->addStringParam("cdate", KGlobal::locale()->formatDate(QDate::currentDate()).utf8()); + m_handler->addParam("show-headers", m_printHeaders ? "true()" : "false()"); + m_handler->addParam("group-entries", m_printGrouped ? "true()" : "false()"); + + QStringList sortTitles; + if(!m_sort1.isEmpty()) { + sortTitles << m_sort1; + } + if(!m_sort2.isEmpty()) { + sortTitles << m_sort2; + } + + // the third sort column may be same as first + if(!m_sort3.isEmpty() && sortTitles.findIndex(m_sort3) == -1) { + sortTitles << m_sort3; + } + + if(sortTitles.count() > 0) { + m_handler->addStringParam("sort-name1", coll->fieldNameByTitle(sortTitles[0]).utf8()); + if(sortTitles.count() > 1) { + m_handler->addStringParam("sort-name2", coll->fieldNameByTitle(sortTitles[1]).utf8()); + if(sortTitles.count() > 2) { + m_handler->addStringParam("sort-name3", coll->fieldNameByTitle(sortTitles[2]).utf8()); + } + } + } + + // no longer showing "sorted by..." since the column headers are clickable + // but still use "grouped by" + QString sortString; + if(m_printGrouped) { + QString s; + // if more than one, then it's the People pseudo-group + if(m_groupBy.count() > 1) { + s = i18n("People"); + } else { + s = coll->fieldTitleByName(m_groupBy[0]); + } + sortString = i18n("(grouped by %1)").arg(s); + + QString groupFields; + for(QStringList::ConstIterator it = m_groupBy.begin(); it != m_groupBy.end(); ++it) { + Data::FieldPtr f = coll->fieldByName(*it); + if(!f) { + continue; + } + if(f->flags() & Data::Field::AllowMultiple) { + groupFields += QString::fromLatin1("tc:") + *it + QString::fromLatin1("s/tc:") + *it; + } else { + groupFields += QString::fromLatin1("tc:") + *it; + } + int ncols = 0; + if(f->type() == Data::Field::Table) { + bool ok; + ncols = Tellico::toUInt(f->property(QString::fromLatin1("columns")), &ok); + if(!ok) { + ncols = 1; + } + } + if(ncols > 1) { + groupFields += QString::fromLatin1("/tc:column[1]"); + } + if(*it != m_groupBy.last()) { + groupFields += '|'; + } + } +// myDebug() << groupFields << endl; + m_handler->addStringParam("group-fields", groupFields.utf8()); + m_handler->addStringParam("sort-title", sortString.utf8()); + } + + QString pageTitle = coll->title(); + pageTitle += QChar(' ') + sortString; + m_handler->addStringParam("page-title", pageTitle.utf8()); + + QStringList showFields; + for(QStringList::ConstIterator it = m_columns.begin(); it != m_columns.end(); ++it) { + showFields << coll->fieldNameByTitle(*it); + } + m_handler->addStringParam("column-names", showFields.join(QChar(' ')).utf8()); + + if(m_imageWidth > 0 && m_imageHeight > 0) { + m_handler->addParam("image-width", QCString().setNum(m_imageWidth)); + m_handler->addParam("image-height", QCString().setNum(m_imageHeight)); + } + + // add system colors to stylesheet + const int type = coll->type(); + m_handler->addStringParam("font", Config::templateFont(type).family().latin1()); + m_handler->addStringParam("fontsize", QCString().setNum(Config::templateFont(type).pointSize())); + m_handler->addStringParam("bgcolor", Config::templateBaseColor(type).name().latin1()); + m_handler->addStringParam("fgcolor", Config::templateTextColor(type).name().latin1()); + m_handler->addStringParam("color1", Config::templateHighlightedTextColor(type).name().latin1()); + m_handler->addStringParam("color2", Config::templateHighlightedBaseColor(type).name().latin1()); + + // add locale code to stylesheet (for sorting) + m_handler->addStringParam("lang", KGlobal::locale()->languagesTwoAlpha().first().utf8()); +} + +void HTMLExporter::writeImages(Data::CollPtr coll_) { + // keep track of which image fields to write, this is for field names + StringSet imageFields; + for(QStringList::ConstIterator it = m_columns.begin(); it != m_columns.end(); ++it) { + if(coll_->fieldByTitle(*it)->type() == Data::Field::Image) { + imageFields.add(*it); + } + } + + // all the images potentially used in the HTML export need to be written to disk + // if we're exporting entry files, then we'll certainly want all the image fields written + // if we're not exporting to a file, then we might be exporting an entry template file + // and so we need to write all of them too. + if(m_exportEntryFiles || url().isEmpty()) { + // add all image fields to string list + Data::FieldVec fields = coll_->imageFields(); + for(Data::FieldVec::Iterator fieldIt = fields.begin(); fieldIt != fields.end(); ++fieldIt) { + imageFields.add(fieldIt->name()); + } + } + + // all of them are going to get written to tmp file + bool useTemp = url().isEmpty(); + KURL imgDir; + QString imgDirRelative; + // really some convoluted logic here + // basically, four cases. 1) we're writing to a tmp file, for printing probably + // so then write all the images to the tmp directory, 2) we're exporting to HTML, and + // this is the main collection file, in which case m_parseDOM is always true; + // 3) we're exporting HTML, and this is the first entry file, for which parseDOM is true + // and exportEntryFiles is false. Then the image file will get copied in copyFiles() and is + // probably an image in the entry template. 4) we're exporting HTML, and this is not the + // first entry file, in which case, we want to refer directly to the target dir + if(useTemp) { // everything goes in the tmp dir + imgDir.setPath(ImageFactory::tempDir()); + imgDirRelative = imgDir.path(); + } else if(m_parseDOM) { + imgDir = fileDir(); // copy to fileDir + imgDirRelative = Data::Document::self()->allImagesOnDisk() ? ImageFactory::dataDir() : ImageFactory::tempDir(); + createDir(); + } else { + imgDir = fileDir(); + imgDirRelative = KURL::relativeURL(url(), imgDir); + createDir(); + } + m_handler->addStringParam("imgdir", QFile::encodeName(imgDirRelative)); + + int count = 0; + const int processCount = 100; // process after every 100 events + + QStringList fieldsList = imageFields.toList(); + StringSet imageSet; // track which images are written + for(QStringList::ConstIterator fieldName = fieldsList.begin(); fieldName != fieldsList.end(); ++fieldName) { + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + QString id = entryIt->field(*fieldName); + // if no id or is already writen, continue + if(id.isEmpty() || imageSet.has(id)) { + continue; + } + imageSet.add(id); + // try writing + bool success = useTemp ? ImageFactory::writeCachedImage(id, ImageFactory::TempDir) + : ImageFactory::writeImage(id, imgDir, true); + if(!success) { + kdWarning() << "HTMLExporter::writeImages() - unable to write image file: " + << imgDir.path() << id << endl; + } + + if(++count == processCount) { + kapp->processEvents(); + count = 0; + } + } + } +} + +QWidget* HTMLExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("HTML Options"), m_widget); + l->addWidget(box); + + m_checkPrintHeaders = new QCheckBox(i18n("Print field headers"), box); + QWhatsThis::add(m_checkPrintHeaders, i18n("If checked, the field names will be " + "printed as table headers.")); + m_checkPrintHeaders->setChecked(m_printHeaders); + + m_checkPrintGrouped = new QCheckBox(i18n("Group the entries"), box); + QWhatsThis::add(m_checkPrintGrouped, i18n("If checked, the entries will be grouped by " + "the selected field.")); + m_checkPrintGrouped->setChecked(m_printGrouped); + + m_checkExportEntryFiles = new QCheckBox(i18n("Export individual entry files"), box); + QWhatsThis::add(m_checkExportEntryFiles, i18n("If checked, individual files will be created for each entry.")); + m_checkExportEntryFiles->setChecked(m_exportEntryFiles); + + l->addStretch(1); + return m_widget; +} + +void HTMLExporter::readOptions(KConfig* config_) { + KConfigGroup exportConfig(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_printHeaders = exportConfig.readBoolEntry("Print Field Headers", m_printHeaders); + m_printGrouped = exportConfig.readBoolEntry("Print Grouped", m_printGrouped); + m_exportEntryFiles = exportConfig.readBoolEntry("Export Entry Files", m_exportEntryFiles); + + // read current entry export template + m_entryXSLTFile = Config::templateName(collection()->type()); + m_entryXSLTFile = locate("appdata", QString::fromLatin1("entry-templates/") + + m_entryXSLTFile + QString::fromLatin1(".xsl")); +} + +void HTMLExporter::saveOptions(KConfig* config_) { + KConfigGroup cfg(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_printHeaders = m_checkPrintHeaders->isChecked(); + cfg.writeEntry("Print Field Headers", m_printHeaders); + m_printGrouped = m_checkPrintGrouped->isChecked(); + cfg.writeEntry("Print Grouped", m_printGrouped); + m_exportEntryFiles = m_checkExportEntryFiles->isChecked(); + cfg.writeEntry("Export Entry Files", m_exportEntryFiles); +} + +void HTMLExporter::setXSLTFile(const QString& filename_) { + if(m_xsltFile == filename_) { + return; + } + + m_xsltFile = filename_; + m_xsltFilePath = QString::null; + reset(); +} + +KURL HTMLExporter::fileDir() const { + if(url().isEmpty()) { + return KURL(); + } + KURL fileDir = url(); + // cd to directory of target URL + fileDir.cd(QString::fromLatin1("..")); + fileDir.addPath(fileDirName()); + return fileDir; +} + +QString HTMLExporter::fileDirName() const { + if(!m_collectionURL.isEmpty()) { + return QString::fromLatin1("/"); + } + return url().fileName().section('.', 0, 0) + QString::fromLatin1("_files/"); +} + +// how ugly is this? +const xmlChar* HTMLExporter::handleLink(const xmlChar* link_) { + return reinterpret_cast<xmlChar*>(qstrdup(handleLink(QString::fromUtf8(reinterpret_cast<const char*>(link_))).utf8())); +} + +QString HTMLExporter::handleLink(const QString& link_) { + if(m_links.contains(link_)) { + return m_links[link_]; + } + // assume that if the link_ is not relative, then we don't need to copy it + if(!KURL::isRelativeURL(link_)) { + return link_; + } + + if(m_xsltFilePath.isEmpty()) { + m_xsltFilePath = locate("appdata", m_xsltFile); + if(m_xsltFilePath.isNull()) { + kdWarning() << "HTMLExporter::handleLink() - no xslt file for " << m_xsltFile << endl; + } + } + + KURL u; + u.setPath(m_xsltFilePath); + u = KURL(u, link_); + + // one of the "quirks" of the html export is that img src urls are set to point to + // the tmpDir() when exporting entry files from a collection, but those images + // don't actually exist, and they get copied in writeImages() instead. + // so we only need to keep track of the url if it exists + const bool exists = KIO::NetAccess::exists(u, false, 0); + if(exists) { + m_files.append(u); + } + + // if we're exporting entry files, we want pics/ to + // go in pics/ + const bool isPic = link_.startsWith(m_dataDir + QString::fromLatin1("pics/")); + QString midDir; + if(m_exportEntryFiles && isPic) { + midDir = QString::fromLatin1("pics/"); + } + // pictures are special since they might not exist when the HTML is exported, since they might get copied later + // on the other hand, don't change the file location if it doesn't exist + if(isPic || exists) { + m_links.insert(link_, fileDirName() + midDir + u.fileName()); + } else { + m_links.insert(link_, link_); + } + return m_links[link_]; +} + +const xmlChar* HTMLExporter::analyzeInternalCSS(const xmlChar* str_) { + return reinterpret_cast<xmlChar*>(qstrdup(analyzeInternalCSS(QString::fromUtf8(reinterpret_cast<const char*>(str_))).utf8())); +} + +QString HTMLExporter::analyzeInternalCSS(const QString& str_) { + QString str = str_; + int start = 0; + int end = 0; + const QString url = QString::fromLatin1("url("); + for(int pos = str.find(url); pos >= 0; pos = str.find(url, pos+1)) { + pos += 4; // url( + if(str[pos] == '"' || str[pos] == '\'') { + ++pos; + } + + start = pos; + pos = str.find(')', start); + end = pos; + if(str[pos-1] == '"' || str[pos-1] == '\'') { + --end; + } + + str.replace(start, end-start, handleLink(str.mid(start, end-start))); + } + return str; +} + +void HTMLExporter::createDir() { + if(!m_checkCreateDir) { + return; + } + KURL dir = fileDir(); + if(dir.isEmpty()) { + myDebug() << "HTMLExporter::createDir() - called on empty URL!" << endl; + return; + } + if(KIO::NetAccess::exists(dir, false, 0)) { + m_checkCreateDir = false; + } else { + m_checkCreateDir = !KIO::NetAccess::mkdir(dir, m_widget); + } +} + +bool HTMLExporter::copyFiles() { + if(m_files.isEmpty()) { + return true; + } + const uint start = 20; + const uint maxProgress = m_exportEntryFiles ? 40 : 80; + const uint stepSize = QMAX(1, m_files.count()/maxProgress); + uint j = 0; + + createDir(); + KURL target; + for(KURL::List::ConstIterator it = m_files.begin(); it != m_files.end() && !m_cancelled; ++it, ++j) { + if(m_copiedFiles.has((*it).url())) { + continue; + } + + if(target.isEmpty()) { + target = fileDir(); + } + target.setFileName((*it).fileName()); + bool success = KIO::NetAccess::file_copy(*it, target, -1, true /* overwrite */, false /* resume */, m_widget); + if(success) { + m_copiedFiles.add((*it).url()); + } else { + kdWarning() << "HTMLExporter::copyFiles() - can't copy " << target << endl; + kdWarning() << KIO::NetAccess::lastErrorString() << endl; + } + if(j%stepSize == 0) { + if(options() & ExportProgress) { + ProgressManager::self()->setProgress(this, QMIN(start+j/stepSize, 99)); + } + kapp->processEvents(); + } + } + return true; +} + +bool HTMLExporter::writeEntryFiles() { + if(m_entryXSLTFile.isEmpty()) { + kdWarning() << "HTMLExporter::writeEntryFiles() - no entry XSLT file" << endl; + return false; + } + + const uint start = 60; + const uint stepSize = QMAX(1, entries().count()/40); + uint j = 0; + + // now worry about actually exporting entry files + // I can't reliable encode a string as a URI, so I'm punting, and I'll just replace everything but + // a-zA-Z0-9 with an underscore. This MUST match the filename template in tellico2html.xsl + // the id is used so uniqueness is guaranteed + const QRegExp badChars(QString::fromLatin1("[^-a-zA-Z0-9]")); + bool formatted = options() & Export::ExportFormatted; + + KURL outputFile = fileDir(); + + GUI::CursorSaver cs(Qt::waitCursor); + + HTMLExporter exporter(collection()); + long opt = options() | Export::ExportForce; + opt &= ~ExportProgress; + exporter.setOptions(opt); + exporter.setXSLTFile(m_entryXSLTFile); + exporter.setCollectionURL(url()); + bool parseDOM = true; + + const QString title = QString::fromLatin1("title"); + const QString html = QString::fromLatin1(".html"); + bool multipleTitles = collection()->fieldByName(title)->flags() & Data::Field::AllowMultiple; + Data::EntryVec entries = this->entries(); // not const since the pointer has to be copied + for(Data::EntryVecIt entryIt = entries.begin(); entryIt != entries.end() && !m_cancelled; ++entryIt, ++j) { + QString file = entryIt->field(title, formatted); + + // but only use the first title if it has multiple + if(multipleTitles) { + file = file.section(';', 0, 0); + } + file.replace(badChars, QChar('_')); + file += QChar('-') + QString::number(entryIt->id()) + html; + outputFile.setFileName(file); + + exporter.setEntries(Data::EntryVec(entryIt)); + exporter.setURL(outputFile); + exporter.exec(); + + // no longer need to parse DOM + if(parseDOM) { + parseDOM = false; + exporter.setParseDOM(false); + // this is rather stupid, but I'm too lazy to figure out the better way + // since we parsed the DOM for the first entry file to grab any + // images used in the template, need to resave it so the image links + // get written correctly + exporter.exec(); + } + + if(j%stepSize == 0) { + if(options() & ExportProgress) { + ProgressManager::self()->setProgress(this, QMIN(start+j/stepSize, 99)); + } + kapp->processEvents(); + } + } + // the images in "pics/" are special data images, copy them always + // since the entry files may refer to them, but we don't know that + QStringList dataImages; + dataImages << QString::fromLatin1("checkmark.png"); + for(uint i = 1; i <= 10; ++i) { + dataImages << QString::fromLatin1("stars%1.png").arg(i); + } + KURL dataDir; + dataDir.setPath(KGlobal::dirs()->findResourceDir("appdata", QString::fromLatin1("pics/tellico.png")) + "pics/"); + KURL target = fileDir(); + target.addPath(QString::fromLatin1("pics/")); + KIO::NetAccess::mkdir(target, m_widget); + for(QStringList::ConstIterator it = dataImages.begin(); it != dataImages.end(); ++it) { + dataDir.setFileName(*it); + target.setFileName(*it); + KIO::NetAccess::copy(dataDir, target, m_widget); + } + + return true; +} + +void HTMLExporter::slotCancel() { + m_cancelled = true; +} + +void HTMLExporter::parseDOM(xmlNode* node_) { + if(node_ == 0) { + myDebug() << "HTMLExporter::parseDOM() - no node" << endl; + return; + } + + bool parseChildren = true; + + if(node_->type == XML_ELEMENT_NODE) { + const QCString nodeName = QCString(reinterpret_cast<const char*>(node_->name)).upper(); + xmlElement* elem = reinterpret_cast<xmlElement*>(node_); + // to speed up things, check now for nodename + if(nodeName == "IMG" || nodeName == "SCRIPT" || nodeName == "LINK") { + for(xmlAttribute* attr = elem->attributes; attr; attr = reinterpret_cast<xmlAttribute*>(attr->next)) { + QCString attrName = QCString(reinterpret_cast<const char*>(attr->name)).upper(); + + if( (attrName == "SRC" && (nodeName == "IMG" || nodeName == "SCRIPT")) || + (attrName == "HREF" && nodeName == "LINK")) { +/* (attrName == "BACKGROUND" && (nodeName == "BODY" || + nodeName == "TABLE" || + nodeName == "TH" || + nodeName == "TD"))) */ + xmlChar* value = xmlGetProp(node_, attr->name); + if(value) { + xmlSetProp(node_, attr->name, handleLink(value)); + xmlFree(value); + } + // each node only has one significant attribute, so break now + break; + } + } + } else if(nodeName == "STYLE") { + // if the first child is a CDATA, use it, otherwise replace complete node + xmlNode* nodeToReplace = node_; + xmlNode* child = node_->children; + if(child && child->type == XML_CDATA_SECTION_NODE) { + nodeToReplace = child; + } + xmlChar* value = xmlNodeGetContent(nodeToReplace); + if(value) { + xmlNodeSetContent(nodeToReplace, analyzeInternalCSS(value)); + xmlFree(value); + } + // no longer need to parse child text nodes + parseChildren = false; + } + } + + if(parseChildren) { + xmlNode* child = node_->children; + while(child) { + parseDOM(child); + child = child->next; + } + } +} + +#include "htmlexporter.moc" diff --git a/src/translators/htmlexporter.h b/src/translators/htmlexporter.h new file mode 100644 index 0000000..be89bbf --- /dev/null +++ b/src/translators/htmlexporter.h @@ -0,0 +1,124 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef HTMLEXPORTER_H +#define HTMLEXPORTER_H + +class QCheckBox; + +#include "exporter.h" +#include "../stringset.h" + +#include <qstringlist.h> + +#include <libxml/xmlstring.h> + +extern "C" { + struct _xmlNode; +} + +namespace Tellico { + namespace Data { + class Collection; + } + class XSLTHandler; + + namespace Export { + +/** + * @author Robby Stephenson + */ +class HTMLExporter : public Exporter { +Q_OBJECT + +public: + HTMLExporter(); + HTMLExporter(Data::CollPtr coll); + ~HTMLExporter(); + + virtual bool exec(); + virtual void reset(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual void readOptions(KConfig*); + virtual void saveOptions(KConfig*); + + void setCollectionURL(const KURL& url) { m_collectionURL = url; m_links.clear(); } + void setXSLTFile(const QString& filename); + void setPrintHeaders(bool printHeaders) { m_printHeaders = printHeaders; } + void setPrintGrouped(bool printGrouped) { m_printGrouped = printGrouped; } + void setMaxImageSize(int w, int h) { m_imageWidth = w; m_imageHeight = h; } + void setGroupBy(const QStringList& groupBy) { m_groupBy = groupBy; } + void setSortTitles(const QStringList& l) + { m_sort1 = l[0]; m_sort2 = l[1]; m_sort3 = l[2]; } + void setColumns(const QStringList& columns) { m_columns = columns; } + void setParseDOM(bool parseDOM) { m_parseDOM = parseDOM; reset(); } + + QString text(); + +public slots: + void slotCancel(); + +private: + void setFormattingOptions(Data::CollPtr coll); + void writeImages(Data::CollPtr coll); + bool writeEntryFiles(); + KURL fileDir() const; + QString fileDirName() const; + + void parseDOM(_xmlNode* node); + QString handleLink(const QString& link); + const xmlChar* handleLink(const xmlChar* link); + QString analyzeInternalCSS(const QString& string); + const xmlChar* analyzeInternalCSS(const xmlChar* string); + bool copyFiles(); + bool loadXSLTFile(); + void createDir(); + + XSLTHandler* m_handler; + bool m_printHeaders : 1; + bool m_printGrouped : 1; + bool m_exportEntryFiles : 1; + bool m_cancelled : 1; + bool m_parseDOM : 1; + bool m_checkCreateDir : 1; + int m_imageWidth; + int m_imageHeight; + + QWidget* m_widget; + QCheckBox* m_checkPrintHeaders; + QCheckBox* m_checkPrintGrouped; + QCheckBox* m_checkExportEntryFiles; + QCheckBox* m_checkExportImages; + + KURL m_collectionURL; + QString m_xsltFile; + QString m_xsltFilePath; + QString m_dataDir; + QStringList m_groupBy; + QString m_sort1; + QString m_sort2; + QString m_sort3; + QStringList m_columns; + QString m_entryXSLTFile; + + KURL::List m_files; + QMap<QString, QString> m_links; + StringSet m_copiedFiles; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/importer.h b/src/translators/importer.h new file mode 100644 index 0000000..4df5ccb --- /dev/null +++ b/src/translators/importer.h @@ -0,0 +1,137 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef IMPORTER_H +#define IMPORTER_H + +class QWidget; + +#include "../datavectors.h" + +#include <klocale.h> +#include <kurl.h> + +#include <qobject.h> +#include <qstring.h> + +namespace Tellico { + namespace Import { + enum Options { + ImportProgress = 1 << 5 // show progress bar + }; + +/** + * The top-level abstract class for importing other document formats into Tellico. + * + * The Importer classes import a file, and return a pointer to a newly created + * @ref Data::Collection. Any errors or warnings are added to a status message queue. + * The calling function owns the collection pointer. + * + * @author Robby Stephenson + */ +class Importer : public QObject { +Q_OBJECT + +public: + Importer() : QObject(), m_options(ImportProgress) {} + /** + * The constructor should immediately load the contents of the file to be imported. + * Any warnings or errors should be added the the status message queue. + * + * @param url The URL of the file to import + */ + Importer(const KURL& url) : QObject(), m_options(ImportProgress), m_urls(url) {} + Importer(const KURL::List& urls) : QObject(), m_options(ImportProgress), m_urls(urls) {} + Importer(const QString& text) : QObject(), m_options(ImportProgress), m_text(text) {} + /** + */ + virtual ~Importer() {} + + /** + * Returns a pointer to a @ref Data::Collection containing the contents of the imported file. + * This function should probably only be called once, but the subclasses may cache the + * collection. The collection should not be created until this function is called. + * + * @return A pointer to a @ref Collection created on the stack, or 0 if none could be created. + */ + virtual Data::CollPtr collection() = 0; + /** + * Returns a string containing all the messages added to the queue in the course of loading + * and importing the file. + * + * @return The status message + */ + const QString& statusMessage() const { return m_statusMsg; } + /** + * Returns a widget with the setting specific to this importer, or 0 if no + * options are needed. + * + * @return A pointer to the setting widget + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + /** + * Checks to see if the importer can return a collection of this type + * + * @param type The collection type to check + * @return Whether the importer could return a collection of that type + */ + virtual bool canImport(int) const { return true; } + /** + * Validate the import settings + */ + virtual bool validImport() const { return true; } + virtual void setText(const QString& text) { m_text = text; } + long options() const { return m_options; } + void setOptions(long options) { m_options = options; } + /** + * Returns a string useful for the ProgressManager + */ + QString progressLabel() const { + if(url().isEmpty()) return i18n("Loading data..."); else return i18n("Loading %1...").arg(url().fileName()); + } + +public slots: + /** + * The import action was changed in the import dialog + */ + virtual void slotActionChanged(int) {} + +protected: + /** + * Return the URL of the imported file. + * + * @return the file URL + */ + KURL url() const { return m_urls.isEmpty() ? KURL() : m_urls[0]; } + KURL::List urls() const { return m_urls; } + QString text() const { return m_text; } + /** + * Adds a message to the status queue. + * + * @param msg A string containing a warning or error. + */ + void setStatusMessage(const QString& msg) { if(!msg.isEmpty()) m_statusMsg += msg + QChar(' '); } + + static const uint s_stepSize; + +private: + long m_options; + KURL::List m_urls; + QString m_text; + QString m_statusMsg; +}; + + } // end namespace +} // end namespace + +#endif diff --git a/src/translators/libcsv.c b/src/translators/libcsv.c new file mode 100644 index 0000000..4e53f63 --- /dev/null +++ b/src/translators/libcsv.c @@ -0,0 +1,490 @@ +/* +libcsv - parse and write csv data +Copyright (C) 2007 Robert Gamble + + available at http://libcsv.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + +#if ___STDC_VERSION__ >= 199901L +# include <stdint.h> +#else +# define SIZE_MAX ((size_t)-1) /* C89 doesn't have stdint.h or SIZE_MAX */ +#endif + +#include "libcsv.h" + +#define VERSION "2.0.0" + +#define ROW_NOT_BEGUN 0 +#define FIELD_NOT_BEGUN 1 +#define FIELD_BEGUN 2 +#define FIELD_MIGHT_HAVE_ENDED 3 + +/* + Explanation of states + ROW_NOT_BEGUN There have not been any fields encountered for this row + FIELD_NOT_BEGUN There have been fields but we are currently not in one + FIELD_BEGUN We are in a field + FIELD_MIGHT_HAVE_ENDED + We encountered a double quote inside a quoted field, the + field is either ended or the quote is literal +*/ + +#define MEM_BLK_SIZE 128 + +#define SUBMIT_FIELD(p) \ + do { \ + if (!quoted) \ + entry_pos -= spaces; \ + if (cb1) \ + cb1(p->entry_buf, entry_pos, data); \ + pstate = FIELD_NOT_BEGUN; \ + entry_pos = quoted = spaces = 0; \ + } while (0) + +#define SUBMIT_ROW(p, c) \ + do { \ + if (cb2) \ + cb2(c, data); \ + pstate = ROW_NOT_BEGUN; \ + entry_pos = quoted = spaces = 0; \ + } while (0) + +#define SUBMIT_CHAR(p, c) ((p)->entry_buf[entry_pos++] = (c)) + +static char *csv_errors[] = {"success", + "error parsing data while strict checking enabled", + "memory exhausted while increasing buffer size", + "data size too large", + "invalid status code"}; + +int +csv_error(struct csv_parser *p) +{ + return p->status; +} + +char * +csv_strerror(int status) +{ + if (status >= CSV_EINVALID || status < 0) + return csv_errors[CSV_EINVALID]; + else + return csv_errors[status]; +} + +int +csv_opts(struct csv_parser *p, unsigned char options) +{ + if (p == NULL) + return -1; + + p->options = options; + return 0; +} + +int +csv_init(struct csv_parser **p, unsigned char options) +{ + /* Initialize a csv_parser object returns 0 on success, -1 on error */ + if (p == NULL) + return -1; + + if ((*p = malloc(sizeof(struct csv_parser))) == NULL) + return -1; + + if ( ((*p)->entry_buf = malloc(MEM_BLK_SIZE)) == NULL ) { + free(*p); + return -1; + } + (*p)->pstate = ROW_NOT_BEGUN; + (*p)->quoted = 0; + (*p)->spaces = 0; + (*p)->entry_pos = 0; + (*p)->entry_size = MEM_BLK_SIZE; + (*p)->status = 0; + (*p)->options = options; + (*p)->quote_char = CSV_QUOTE; + (*p)->delim_char = CSV_COMMA; + (*p)->is_space = NULL; + (*p)->is_term = NULL; + + return 0; +} + +void +csv_free(struct csv_parser *p) +{ + /* Free the entry_buffer and the csv_parser object */ + if (p == NULL) + return; + + if (p->entry_buf) + free(p->entry_buf); + + free(p); + return; +} + +int +csv_fini(struct csv_parser *p, void (*cb1)(char *, size_t, void *), void (*cb2)(char c, void *), void *data) +{ + /* Finalize parsing. Needed, for example, when file does not end in a newline */ + int quoted = p->quoted; + int pstate = p->pstate; + size_t spaces = p->spaces; + size_t entry_pos = p->entry_pos; + + if (p == NULL) + return -1; + + + if (p->pstate == FIELD_BEGUN && p->quoted && p->options & CSV_STRICT && p->options & CSV_STRICT_FINI) { + p->status = CSV_EPARSE; + return -1; + } + + switch (p->pstate) { + case FIELD_MIGHT_HAVE_ENDED: + p->entry_pos -= p->spaces + 1; /* get rid of spaces and original quote */ + case FIELD_NOT_BEGUN: + case FIELD_BEGUN: + quoted = p->quoted, pstate = p->pstate; + spaces = p->spaces, entry_pos = p->entry_pos; + SUBMIT_FIELD(p); + SUBMIT_ROW(p, 0); + case ROW_NOT_BEGUN: /* Already ended properly */ + ; + } + + p->spaces = p->quoted = p->entry_pos = p->status = 0; + p->pstate = ROW_NOT_BEGUN; + + return 0; +} + +void +csv_set_delim(struct csv_parser *p, char c) +{ + if (p) p->delim_char = c; +} + +void +csv_set_quote(struct csv_parser *p, char c) +{ + if (p) p->quote_char = c; +} + +char +csv_get_delim(struct csv_parser *p) +{ + return p->delim_char; +} + +char +csv_get_quote(struct csv_parser *p) +{ + return p->quote_char; +} + +void +csv_set_space_func(struct csv_parser *p, int (*f)(char)) +{ + if (p) p->is_space = f; +} + +void +csv_set_term_func(struct csv_parser *p, int (*f)(char)) +{ + if (p) p->is_term = f; +} + +static int +csv_increase_buffer(struct csv_parser *p) +{ + size_t to_add = MEM_BLK_SIZE; + void *vp; + while ( p->entry_size >= SIZE_MAX - to_add ) + to_add /= 2; + if (!to_add) { + p->status = CSV_ETOOBIG; + return -1; + } + while ((vp = realloc(p->entry_buf, p->entry_size + to_add)) == NULL) { + to_add /= 2; + if (!to_add) { + p->status = CSV_ENOMEM; + return -1; + } + } + p->entry_buf = vp; + p->entry_size += to_add; + return 0; +} + +size_t +csv_parse(struct csv_parser *p, const char *s, size_t len, void (*cb1)(char *, size_t, void *), void (*cb2)(char c, void *), void *data) +{ + char c; /* The character we are currently processing */ + size_t pos = 0; /* The number of characters we have processed in this call */ + char delim = p->delim_char; + char quote = p->quote_char; + int (*is_space)(char) = p->is_space; + int (*is_term)(char) = p->is_term; + int quoted = p->quoted; + int pstate = p->pstate; + size_t spaces = p->spaces; + size_t entry_pos = p->entry_pos; + + while (pos < len) { + /* Check memory usage */ + if (entry_pos == p->entry_size) + if (csv_increase_buffer(p) != 0) { + p->quoted = quoted, p->pstate = pstate, p->spaces = spaces, p->entry_pos = entry_pos; + return pos; + } + + c = s[pos++]; + switch (pstate) { + case ROW_NOT_BEGUN: + case FIELD_NOT_BEGUN: + if (is_space ? is_space(c) : c == CSV_SPACE || c == CSV_TAB) { /* Space or Tab */ + continue; + } else if (is_term ? is_term(c) : c == CSV_CR || c == CSV_LF) { /* Carriage Return or Line Feed */ + if (pstate == FIELD_NOT_BEGUN) { + SUBMIT_FIELD(p); + SUBMIT_ROW(p, c); + } else { /* ROW_NOT_BEGUN */ + /* Don't submit empty rows by default */ + if (p->options & CSV_REPALL_NL) { + SUBMIT_ROW(p, c); + } + } + continue; + } else if (c == delim) { /* Comma */ + SUBMIT_FIELD(p); + break; + } else if (c == quote) { /* Quote */ + pstate = FIELD_BEGUN; + quoted = 1; + } else { /* Anything else */ + pstate = FIELD_BEGUN; + quoted = 0; + SUBMIT_CHAR(p, c); + } + break; + case FIELD_BEGUN: + if (c == quote) { /* Quote */ + if (quoted) { + SUBMIT_CHAR(p, c); + pstate = FIELD_MIGHT_HAVE_ENDED; + } else { + /* STRICT ERROR - double quote inside non-quoted field */ + if (p->options & CSV_STRICT) { + p->status = CSV_EPARSE; + p->quoted = quoted, p->pstate = pstate, p->spaces = spaces, p->entry_pos = entry_pos; + return pos-1; + } + SUBMIT_CHAR(p, c); + spaces = 0; + } + } else if (c == delim) { /* Comma */ + if (quoted) { + SUBMIT_CHAR(p, c); + } else { + SUBMIT_FIELD(p); + } + } else if (is_term ? is_term(c) : c == CSV_CR || c == CSV_LF) { /* Carriage Return or Line Feed */ + if (!quoted) { + SUBMIT_FIELD(p); + SUBMIT_ROW(p, c); + } else { + SUBMIT_CHAR(p, c); + } + } else if (!quoted && (is_space? is_space(c) : c == CSV_SPACE || c == CSV_TAB)) { /* Tab or space for non-quoted field */ + SUBMIT_CHAR(p, c); + spaces++; + } else { /* Anything else */ + SUBMIT_CHAR(p, c); + spaces = 0; + } + break; + case FIELD_MIGHT_HAVE_ENDED: + /* This only happens when a quote character is encountered in a quoted field */ + if (c == delim) { /* Comma */ + entry_pos -= spaces + 1; /* get rid of spaces and original quote */ + SUBMIT_FIELD(p); + } else if (is_term ? is_term(c) : c == CSV_CR || c == CSV_LF) { /* Carriage Return or Line Feed */ + entry_pos -= spaces + 1; /* get rid of spaces and original quote */ + SUBMIT_FIELD(p); + SUBMIT_ROW(p, c); + } else if (is_space ? is_space(c) : c == CSV_SPACE || c == CSV_TAB) { /* Space or Tab */ + SUBMIT_CHAR(p, c); + spaces++; + } else if (c == quote) { /* Quote */ + if (spaces) { + /* STRICT ERROR - unescaped double quote */ + if (p->options & CSV_STRICT) { + p->status = CSV_EPARSE; + p->quoted = quoted, p->pstate = pstate, p->spaces = spaces, p->entry_pos = entry_pos; + return pos-1; + } + spaces = 0; + SUBMIT_CHAR(p, c); + } else { + /* Two quotes in a row */ + pstate = FIELD_BEGUN; + } + } else { /* Anything else */ + /* STRICT ERROR - unescaped double quote */ + if (p->options & CSV_STRICT) { + p->status = CSV_EPARSE; + p->quoted = quoted, p->pstate = pstate, p->spaces = spaces, p->entry_pos = entry_pos; + return pos-1; + } + pstate = FIELD_BEGUN; + spaces = 0; + SUBMIT_CHAR(p, c); + } + break; + default: + break; + } + } + p->quoted = quoted, p->pstate = pstate, p->spaces = spaces, p->entry_pos = entry_pos; + return pos; +} + +size_t +csv_write (char *dest, size_t dest_size, const char *src, size_t src_size) +{ + size_t chars = 0; + + if (src == NULL) + return 0; + + if (dest == NULL) + dest_size = 0; + + if (dest_size > 0) + *dest++ = '"'; + chars++; + + while (src_size) { + if (*src == '"') { + if (dest_size > chars) + *dest++ = '"'; + if (chars < SIZE_MAX) chars++; + } + if (dest_size > chars) + *dest++ = *src; + if (chars < SIZE_MAX) chars++; + src_size--; + src++; + } + + if (dest_size > chars) + *dest = '"'; + if (chars < SIZE_MAX) chars++; + + return chars; +} + +int +csv_fwrite (FILE *fp, const char *src, size_t src_size) +{ + if (fp == NULL || src == NULL) + return 0; + + if (fputc('"', fp) == EOF) + return EOF; + + while (src_size) { + if (*src == '"') { + if (fputc('"', fp) == EOF) + return EOF; + } + if (fputc(*src, fp) == EOF) + return EOF; + src_size--; + src++; + } + + if (fputc('"', fp) == EOF) { + return EOF; + } + + return 0; +} + +size_t +csv_write2 (char *dest, size_t dest_size, const char *src, size_t src_size, char quote) +{ + size_t chars = 0; + + if (src == NULL) + return 0; + + if (dest == NULL) + dest_size = 0; + + if (dest_size > 0) + *dest++ = quote; + chars++; + + while (src_size) { + if (*src == quote) { + if (dest_size > chars) + *dest++ = quote; + if (chars < SIZE_MAX) chars++; + } + if (dest_size > chars) + *dest++ = *src; + if (chars < SIZE_MAX) chars++; + src_size--; + src++; + } + + if (dest_size > chars) + *dest = quote; + if (chars < SIZE_MAX) chars++; + + return chars; +} + +int +csv_fwrite2 (FILE *fp, const char *src, size_t src_size, char quote) +{ + if (fp == NULL || src == NULL) + return 0; + + if (fputc(quote, fp) == EOF) + return EOF; + + while (src_size) { + if (*src == quote) { + if (fputc(quote, fp) == EOF) + return EOF; + } + if (fputc(*src, fp) == EOF) + return EOF; + src_size--; + src++; + } + + if (fputc(quote, fp) == EOF) { + return EOF; + } + + return 0; +} diff --git a/src/translators/libcsv.h b/src/translators/libcsv.h new file mode 100644 index 0000000..9058192 --- /dev/null +++ b/src/translators/libcsv.h @@ -0,0 +1,84 @@ +/* +libcsv - parse and write csv data +Copyright (C) 2007 Robert Gamble + + available at http://libcsv.sf.net + + Original available under the terms of the GNU LGPL2, and according + to those terms, relicensed under the GNU GPL2 for inclusion in Tellico */ + +/*************************************************************************** + * * + * 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; * + * * + ***************************************************************************/ + + +#ifndef LIBCSV_H__ +#define LIBCSV_H__ +#include <stdlib.h> +#include <stdio.h> + +#define CSV_MAJOR 2 +#define CSV_MINOR 0 +#define CSV_RELEASE 1 + +/* Error Codes */ +#define CSV_SUCCESS 0 +#define CSV_EPARSE 1 /* Parse error in strict mode */ +#define CSV_ENOMEM 2 /* Out of memory while increasing buffer size */ +#define CSV_ETOOBIG 3 /* Buffer larger than SIZE_MAX needed */ +#define CSV_EINVALID 4 /* Invalid code,should never be received from csv_error*/ + + +/* parser options */ +#define CSV_STRICT 1 /* enable strict mode */ +#define CSV_REPALL_NL 2 /* report all unquoted carriage returns and linefeeds */ +#define CSV_STRICT_FINI 4 /* causes csv_fini to return CSV_EPARSE if last + field is quoted and doesn't containg ending + quote */ + +/* Character values */ +#define CSV_TAB 0x09 +#define CSV_SPACE 0x20 +#define CSV_CR 0x0d +#define CSV_LF 0x0a +#define CSV_COMMA 0x2c +#define CSV_QUOTE 0x22 + +struct csv_parser { + int pstate; /* Parser state */ + int quoted; /* Is the current field a quoted field? */ + size_t spaces; /* Number of continious spaces after quote or in a non-quoted field */ + char * entry_buf; /* Entry buffer */ + size_t entry_pos; /* Current position in entry_buf (and current size of entry) */ + size_t entry_size; /* Size of buffer */ + int status; /* Operation status */ + unsigned char options; + char quote_char; + char delim_char; + int (*is_space)(char); + int (*is_term)(char); +}; + +int csv_init(struct csv_parser **p, unsigned char options); +int csv_fini(struct csv_parser *p, void (*cb1)(char *, size_t, void *), void (*cb2)(char, void *), void *data); +void csv_free(struct csv_parser *p); +int csv_error(struct csv_parser *p); +char * csv_strerror(int error); +size_t csv_parse(struct csv_parser *p, const char *s, size_t len, void (*cb1)(char *, size_t, void *), void (*cb2)(char, void *), void *data); +size_t csv_write(char *dest, size_t dest_size, const char *src, size_t src_size); +int csv_fwrite(FILE *fp, const char *src, size_t src_size); +size_t csv_write2(char *dest, size_t dest_size, const char *src, size_t src_size, char quote); +int csv_fwrite2(FILE *fp, const char *src, size_t src_size, char quote); +int csv_opts(struct csv_parser *p, unsigned char options); +void csv_set_delim(struct csv_parser *p, char c); +void csv_set_quote(struct csv_parser *p, char c); +char csv_get_delim(struct csv_parser *p); +char csv_get_quote(struct csv_parser *p); +void csv_set_space_func(struct csv_parser *p, int (*f)(char)); +void csv_set_term_func(struct csv_parser *p, int (*f)(char)); + +#endif diff --git a/src/translators/onixexporter.cpp b/src/translators/onixexporter.cpp new file mode 100644 index 0000000..4479b2f --- /dev/null +++ b/src/translators/onixexporter.cpp @@ -0,0 +1,199 @@ +/*************************************************************************** + copyright : (C) 2005-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 "onixexporter.h" +#include "xslthandler.h" +#include "tellicoxmlexporter.h" +#include "../collection.h" +#include "../filehandler.h" +#include "../tellico_utils.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../tellico_debug.h" + +#include <config.h> + +#include <kstandarddirs.h> +#include <kapplication.h> +#include <kzip.h> +#include <kconfig.h> +#include <klocale.h> + +#include <qdom.h> +#include <qfile.h> +#include <qdatetime.h> +#include <qbuffer.h> +#include <qlayout.h> +#include <qwhatsthis.h> +#include <qcheckbox.h> +#include <qgroupbox.h> + +using Tellico::Export::ONIXExporter; + +ONIXExporter::ONIXExporter() : Tellico::Export::Exporter(), + m_handler(0), + m_xsltFile(QString::fromLatin1("tellico2onix.xsl")), + m_includeImages(true), + m_widget(0) { +} + +ONIXExporter::ONIXExporter(Data::CollPtr coll_) : Tellico::Export::Exporter(coll_), + m_handler(0), + m_xsltFile(QString::fromLatin1("tellico2onix.xsl")), + m_includeImages(true), + m_widget(0) { +} + +ONIXExporter::~ONIXExporter() { + delete m_handler; + m_handler = 0; +} + +QString ONIXExporter::formatString() const { + return i18n("ONIX Archive"); +} + +QString ONIXExporter::fileFilter() const { + return i18n("*.zip|Zip Files (*.zip)") + QChar('\n') + i18n("*|All Files"); +} + +bool ONIXExporter::exec() { + Data::CollPtr coll = collection(); + if(!coll) { + return false; + } + + QCString xml = text().utf8(); // encoded in utf-8 + + QByteArray data; + QBuffer buf(data); + + KZip zip(&buf); + zip.open(IO_WriteOnly); + zip.writeFile(QString::fromLatin1("onix.xml"), QString::null, QString::null, xml.length(), xml); + + // use a dict for fast random access to keep track of which images were written to the file + if(m_includeImages) { // for now, we're ignoring (options() & Export::ExportImages) + const QString cover = QString::fromLatin1("cover"); + StringSet imageSet; + for(Data::EntryVec::ConstIterator it = entries().begin(); it != entries().end(); ++it) { + const Data::Image& img = ImageFactory::imageById(it->field(cover)); + if(!img.isNull() && !imageSet.has(img.id()) + && (img.format() == "JPEG" || img.format() == "JPG" || img.format() == "GIF")) { /// onix only understands jpeg and gif + QByteArray ba = img.byteArray(); + zip.writeFile(QString::fromLatin1("images/") + it->field(cover), + QString::null, QString::null, ba.size(), ba); + imageSet.add(img.id()); + } + } + } + + zip.close(); + return FileHandler::writeDataURL(url(), data, options() & Export::ExportForce); +// return FileHandler::writeTextURL(url(), text(), options() & Export::ExportUTF8, options() & Export::ExportForce); +} + +QString ONIXExporter::text() { + QString xsltfile = locate("appdata", m_xsltFile); + if(xsltfile.isNull()) { + myDebug() << "ONIXExporter::text() - no xslt file for " << m_xsltFile << endl; + return QString::null; + } + + Data::CollPtr coll = collection(); + if(!coll) { + myDebug() << "ONIXExporter::text() - no collection pointer!" << endl; + return QString::null; + } + + // notes about utf-8 encoding: + // all params should be passed to XSLTHandler in utf8 + // input string to XSLTHandler should be in utf-8, EVEN IF DOM STRING SAYS OTHERWISE + + KURL u; + u.setPath(xsltfile); + // do NOT do namespace processing, it messes up the XSL declaration since + // QDom thinks there are no elements in the Tellico namespace and as a result + // removes the namespace declaration + QDomDocument dom = FileHandler::readXMLFile(u, false); + if(dom.isNull()) { + myDebug() << "ONIXExporter::text() - error loading xslt file: " << xsltfile << endl; + return QString::null; + } + + // the stylesheet prints utf-8 by default, if using locale encoding, need + // to change the encoding attribute on the xsl:output element + if(!(options() & Export::ExportUTF8)) { + XSLTHandler::setLocaleEncoding(dom); + } + + delete m_handler; + m_handler = new XSLTHandler(dom, QFile::encodeName(xsltfile)); + + QDateTime now = QDateTime::currentDateTime(); + m_handler->addStringParam("sentDate", now.toString(QString::fromLatin1("yyyyMMddhhmm")).utf8()); + + m_handler->addStringParam("version", VERSION); + + GUI::CursorSaver cs(Qt::waitCursor); + + // now grab the XML + TellicoXMLExporter exporter(coll); + exporter.setEntries(entries()); + exporter.setIncludeImages(false); // do not include images in XML +// yes, this should be in utf8, always + exporter.setOptions(options() | Export::ExportUTF8); + QDomDocument output = exporter.exportXML(); +#if 0 + QFile f(QString::fromLatin1("/tmp/test.xml")); + if(f.open(IO_WriteOnly)) { + QTextStream t(&f); + t << output.toString(); + } + f.close(); +#endif + return m_handler->applyStylesheet(output.toString()); +} + +QWidget* ONIXExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("ONIX Archive Options"), m_widget); + l->addWidget(box); + + m_checkIncludeImages = new QCheckBox(i18n("Include images in archive"), box); + m_checkIncludeImages->setChecked(m_includeImages); + QWhatsThis::add(m_checkIncludeImages, i18n("If checked, the images in the document will be included " + "in the zipped ONIX archive.")); + + return m_widget; +} + +void ONIXExporter::readOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_includeImages = group.readBoolEntry("Include Images", m_includeImages); +} + +void ONIXExporter::saveOptions(KConfig* config_) { + m_includeImages = m_checkIncludeImages->isChecked(); + + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + group.writeEntry("Include Images", m_includeImages); +} + +#include "onixexporter.moc" diff --git a/src/translators/onixexporter.h b/src/translators/onixexporter.h new file mode 100644 index 0000000..19d52dd --- /dev/null +++ b/src/translators/onixexporter.h @@ -0,0 +1,60 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef ONIXEXPORTER_H +#define ONIXEXPORTER_H + +class QCheckBox; + +#include "exporter.h" + +namespace Tellico { + namespace Data { + class Collection; + } + class XSLTHandler; + namespace Export { + +/** + * @author Robby Stephenson + */ +class ONIXExporter : public Exporter { +Q_OBJECT + +public: + ONIXExporter(); + ONIXExporter(Data::CollPtr coll); + ~ONIXExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget*, const char* name=0); + virtual void readOptions(KConfig*); + virtual void saveOptions(KConfig*); + + QString text(); + +private: + XSLTHandler* m_handler; + QString m_xsltFile; + bool m_includeImages; + + QWidget* m_widget; + QCheckBox* m_checkIncludeImages; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/pdfimporter.cpp b/src/translators/pdfimporter.cpp new file mode 100644 index 0000000..2d59b33 --- /dev/null +++ b/src/translators/pdfimporter.cpp @@ -0,0 +1,281 @@ +/*************************************************************************** + copyright : (C) 2007 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 "pdfimporter.h" +#include "tellicoimporter.h" +#include "xslthandler.h" +#include "../collections/bibtexcollection.h" +#include "../xmphandler.h" +#include "../filehandler.h" +#include "../imagefactory.h" +#include "../tellico_kernel.h" +#include "../fetch/fetchmanager.h" +#include "../fetch/crossreffetcher.h" +#include "../tellico_utils.h" +#include "../progressmanager.h" +#include "../core/netaccess.h" +#include "../tellico_debug.h" + +#include <kstandarddirs.h> +#include <kmessagebox.h> + +#include <config.h> +#ifdef HAVE_POPPLER +#include <poppler-qt.h> +#endif + +namespace { + static const int PDF_FILE_PREVIEW_SIZE = 196; +} + +using Tellico::Import::PDFImporter; + +PDFImporter::PDFImporter(const KURL::List& urls_) : Importer(urls_), m_cancelled(false) { +} + +bool PDFImporter::canImport(int type_) const { + return type_ == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr PDFImporter::collection() { + QString xsltfile = ::locate("appdata", QString::fromLatin1("xmp2tellico.xsl")); + if(xsltfile.isEmpty()) { + kdWarning() << "DropHandler::handleURL() - can not locate xmp2tellico.xsl" << endl; + return 0; + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(urls().count()); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + const bool showProgress = options() & ImportProgress; + + KURL u; + u.setPath(xsltfile); + + XSLTHandler xsltHandler(u); + if(!xsltHandler.isValid()) { + kdWarning() << "DropHandler::handleURL() - invalid xslt in xmp2tellico.xsl" << endl; + return 0; + } + + bool hasDOI = false; + bool hasArxiv = false; + + uint j = 0; + + Data::CollPtr coll; + XMPHandler xmpHandler; + KURL::List list = urls(); + for(KURL::List::Iterator it = list.begin(); it != list.end() && !m_cancelled; ++it, ++j) { + FileHandler::FileRef* ref = FileHandler::fileRef(*it); + if(!ref) { + continue; + } + + Data::CollPtr newColl; + Data::EntryPtr entry; + + QString xmp = xmpHandler.extractXMP(ref->fileName()); + // myDebug() << xmp << endl; + if(xmp.isEmpty()) { + setStatusMessage(i18n("Tellico was unable to read any metadata from the PDF file.")); + } else { + setStatusMessage(QString()); + + Import::TellicoImporter importer(xsltHandler.applyStylesheet(xmp)); + newColl = importer.collection(); + if(!newColl || newColl->entryCount() == 0) { + kdWarning() << "DropHandler::handleURL() - no collection found" << endl; + setStatusMessage(i18n("Tellico was unable to read any metadata from the PDF file.")); + } else { + entry = newColl->entries().front(); + hasDOI |= !entry->field(QString::fromLatin1("doi")).isEmpty(); + } + } + + if(!newColl) { + newColl = new Data::BibtexCollection(true); + } + if(!entry) { + entry = new Data::Entry(newColl); + newColl->addEntries(entry); + } + +#ifdef HAVE_POPPLER + + // now load from poppler + Poppler::Document* doc = Poppler::Document::load(ref->fileName()); + if(doc && !doc->isLocked()) { + // now the question is, do we overwrite XMP data with Poppler data? + // for now, let's say yes conditionally + QString s = doc->getInfo(QString::fromLatin1("Title")).simplifyWhiteSpace(); + if(!s.isEmpty()) { + entry->setField(QString::fromLatin1("title"), s); + } + // author could be separated by commas, "and" or whatever + // we're not going to overwrite it + if(entry->field(QString::fromLatin1("author")).isEmpty()) { + QRegExp rx(QString::fromLatin1("\\s*(and|,|;)\\s*")); + QStringList authors = QStringList::split(rx, doc->getInfo(QString::fromLatin1("Author")).simplifyWhiteSpace()); + entry->setField(QString::fromLatin1("author"), authors.join(QString::fromLatin1("; "))); + } + s = doc->getInfo(QString::fromLatin1("Keywords")).simplifyWhiteSpace(); + if(!s.isEmpty()) { + // keywords are also separated by semi-colons in poppler + entry->setField(QString::fromLatin1("keyword"), s); + } + + // now parse the first page text and try to guess + Poppler::Page* page = doc->getPage(0); + if(page) { + // a null rectangle means get all text on page + QString text = page->getText(Poppler::Rectangle()); + // borrowed from Referencer + QRegExp rx(QString::fromLatin1("(?:" + "(?:[Dd][Oo][Ii]:? *)" + "|" + "(?:[Dd]igital *[Oo]bject *[Ii]dentifier:? *)" + ")" + "(" + "[^\\.\\s]+" + "\\." + "[^\\/\\s]+" + "\\/" + "[^\\s]+" + ")")); + if(rx.search(text) > -1) { + QString doi = rx.cap(1); + myDebug() << "PDFImporter::collection() - in PDF file, found DOI: " << doi << endl; + entry->setField(QString::fromLatin1("doi"), doi); + hasDOI = true; + } + rx = QRegExp(QString::fromLatin1("arXiv:" + "(" + "[^\\/\\s]+" + "[\\/\\.]" + "[^\\s]+" + ")")); + if(rx.search(text) > -1) { + QString arxiv = rx.cap(1); + myDebug() << "PDFImporter::collection() - in PDF file, found arxiv: " << arxiv << endl; + if(entry->collection()->fieldByName(QString::fromLatin1("arxiv")) == 0) { + Data::FieldPtr field = new Data::Field(QString::fromLatin1("arxiv"), i18n("arXiv ID")); + field->setCategory(i18n("Publishing")); + entry->collection()->addField(field); + } + entry->setField(QString::fromLatin1("arxiv"), arxiv); + hasArxiv = true; + } + + delete page; + } + } else { + myDebug() << "PDFImporter::collection() - unable to read PDF info (poppler)" << endl; + } + delete doc; +#endif + + entry->setField(QString::fromLatin1("url"), (*it).url()); + // always an article? + entry->setField(QString::fromLatin1("entry-type"), QString::fromLatin1("article")); + + QPixmap pix = NetAccess::filePreview(ref->fileName(), PDF_FILE_PREVIEW_SIZE); + delete ref; // removes temp file + + if(!pix.isNull()) { + // is png best option? + QString id = ImageFactory::addImage(pix, QString::fromLatin1("PNG")); + if(!id.isEmpty()) { + Data::FieldPtr field = newColl->fieldByName(QString::fromLatin1("cover")); + if(!field && !newColl->imageFields().isEmpty()) { + field = newColl->imageFields().front(); + } else if(!field) { + field = new Data::Field(QString::fromLatin1("cover"), i18n("Front Cover"), Data::Field::Image); + newColl->addField(field); + } + entry->setField(field, id); + } + } + if(coll) { + coll->addEntries(newColl->entries()); + } else { + coll = newColl; + } + + if(showProgress) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } + + if(m_cancelled) { + return 0; + } + + if(hasDOI) { + myDebug() << "looking for DOI" << endl; + Fetch::FetcherVec vec = Fetch::Manager::self()->createUpdateFetchers(coll->type(), Fetch::DOI); + if(vec.isEmpty()) { + GUI::CursorSaver cs(Qt::arrowCursor); + KMessageBox::information(Kernel::self()->widget(), + i18n("Tellico is able to download information about entries with a DOI from " + "CrossRef.org. However, you must create an CrossRef account and add a new " + "data source with your account information."), + QString::null, + QString::fromLatin1("CrossRefSourceNeeded")); + } else { + Data::EntryVec entries = coll->entries(); + for(Fetch::FetcherVec::Iterator fetcher = vec.begin(); fetcher != vec.end(); ++fetcher) { + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + fetcher->updateEntrySynchronous(entry); + } + } + } + } + + if(m_cancelled) { + return 0; + } + + if(hasArxiv) { + Data::EntryVec entries = coll->entries(); + Fetch::FetcherVec vec = Fetch::Manager::self()->createUpdateFetchers(coll->type(), Fetch::ArxivID); + for(Fetch::FetcherVec::Iterator fetcher = vec.begin(); fetcher != vec.end(); ++fetcher) { + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + fetcher->updateEntrySynchronous(entry); + } + } + } + +// finally + Data::EntryVec entries = coll->entries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + if(entry->title().isEmpty()) { + // use file name + KURL u = entry->field(QString::fromLatin1("url")); + entry->setField(QString::fromLatin1("title"), u.fileName()); + } + } + + if(m_cancelled) { + return 0; + } + return coll; +} + +void PDFImporter::slotCancel() { + m_cancelled = true; +} + +#include "pdfimporter.moc" diff --git a/src/translators/pdfimporter.h b/src/translators/pdfimporter.h new file mode 100644 index 0000000..87da58e --- /dev/null +++ b/src/translators/pdfimporter.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_PDFIMPORTER_H +#define TELLICO_IMPORT_PDFIMPORTER_H + +#include "importer.h" + +namespace Tellico { + namespace Import { + +class PDFImporter : public Importer { +Q_OBJECT + +public: + PDFImporter(const KURL::List& urls); + + virtual bool canImport(int type) const; + + virtual Data::CollPtr collection(); + +public slots: + void slotCancel(); + +private: + bool m_cancelled; +}; + + } +} +#endif diff --git a/src/translators/pilotdb/Makefile.am b/src/translators/pilotdb/Makefile.am new file mode 100644 index 0000000..cf21d12 --- /dev/null +++ b/src/translators/pilotdb/Makefile.am @@ -0,0 +1,16 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = libpilotdb.a + +AM_CPPFLAGS = $(all_includes) + +libpilotdb_a_METASOURCES = AUTO + +libpilotdb_a_SOURCES = pilotdb.cpp strop.cpp + +SUBDIRS = libflatfile libpalm + +EXTRA_DIST = strop.cpp strop.h portability.h pilotdb.h pilotdb.cpp + +####### kdevelop will overwrite this part!!! (end)############ + +KDE_OPTIONS = noautodist diff --git a/src/translators/pilotdb/libflatfile/DB.cpp b/src/translators/pilotdb/libflatfile/DB.cpp new file mode 100644 index 0000000..40e639a --- /dev/null +++ b/src/translators/pilotdb/libflatfile/DB.cpp @@ -0,0 +1,1437 @@ +/* + * palm-db-tools: Read/write DB-format databases + * Copyright (C) 1999-2001 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * 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., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + */ + +#include <iostream> +#include <vector> +#include <string> +#include <stdexcept> +#include <sstream> +#include <time.h> + +#include <cstring> + +#include <kdebug.h> + +#include "../strop.h" + +#include "DB.h" + +#include <kdebug.h> + +#define charSeperator '/' +#define VIEWFLAG_USE_IN_EDITVIEW 0x01 + +#define INVALID_DEFAULT 0 +#define NOW_DEFAULT 1 +#define CONSTANT_DEFAULT 2 + +using namespace PalmLib::FlatFile; +using namespace PalmLib; + +namespace { + static const pi_uint16_t CHUNK_FIELD_NAMES = 0; + static const pi_uint16_t CHUNK_FIELD_TYPES = 1; + static const pi_uint16_t CHUNK_FIELD_DATA = 2; + static const pi_uint16_t CHUNK_LISTVIEW_DEFINITION = 64; + static const pi_uint16_t CHUNK_LISTVIEW_OPTIONS = 65; + static const pi_uint16_t CHUNK_LFIND_OPTIONS = 128; + static const pi_uint16_t CHUNK_ABOUT = 254; +} + +template <class Map, class Key> +static inline bool has_key(const Map& map, const Key& key) +{ + return map.find(key) != map.end(); +} + +bool PalmLib::FlatFile::DB::classify(PalmLib::Database& pdb) +{ + return (! pdb.isResourceDB()) + && (pdb.creator() == PalmLib::mktag('D','B','O','S')) + && (pdb.type() == PalmLib::mktag('D','B','0','0')); +} + +bool PalmLib::FlatFile::DB::match_name(const std::string& name) +{ + return (name == "DB") || (name == "db"); +} + +void PalmLib::FlatFile::DB::extract_chunks(const PalmLib::Block& appinfo) +{ + size_t i; + pi_uint16_t chunk_type; + pi_uint16_t chunk_size; + + if (appinfo.size() > 4) { + // Loop through each chunk in the block while data remains. + i = 4; + while (i < appinfo.size()) { + /* Stop the loop if there is not enough room for even one + * chunk header. + */ + if (i + 4 >= appinfo.size()) { +// throw PalmLib::error("header is corrupt"); + kdDebug() << "header is corrupt" << endl; + } + // Copy the chunk type and size into the local buffer. + chunk_type = get_short(appinfo.data() + i); + chunk_size = get_short(appinfo.data() + i + 2); + i += 4; + + // Copy the chunk into seperate storage. + Chunk chunk(appinfo.data() + i, chunk_size); + chunk.chunk_type = chunk_type; + m_chunks[chunk.chunk_type].push_back(chunk); + + /* Advance the index by the size of the chunk. */ + i += chunk.size(); + } + + // If everything was correct, then we should be exactly at the + // end of the block. + if (i != appinfo.size()) { +// throw PalmLib::error("header is corrupt"); + kdDebug() << "header is corrupt" << endl; + } + } else { +// throw PalmLib::error("header is corrupt"); + kdDebug() << "header is corrupt" << endl; + } +} + +void PalmLib::FlatFile::DB::extract_schema(unsigned numFields) +{ + unsigned i; + + if (!has_key(m_chunks, CHUNK_FIELD_NAMES) + || !has_key(m_chunks, CHUNK_FIELD_TYPES)) { +// throw PalmLib::error("database is missing its schema"); + kdDebug() << "database is missing its schema" << endl; + return; + } + + Chunk names_chunk = m_chunks[CHUNK_FIELD_NAMES][0]; + Chunk types_chunk = m_chunks[CHUNK_FIELD_TYPES][0]; + pi_char_t* p = names_chunk.data(); + pi_char_t* q = types_chunk.data(); + + // Ensure that the types chunk has the expected size. + if (types_chunk.size() != numFields * sizeof(pi_uint16_t)) { +// throw PalmLib::error("types chunk is corrupt"); + kdDebug() << "types chunk is corrupt" << endl; + } + // Loop for each field and extract the name and type. + for (i = 0; i < numFields; ++i) { + PalmLib::FlatFile::Field::FieldType type; + int len; + + // Determine the length of the name string. Ensure that the + // string does not go beyond the end of the chunk. + pi_char_t* null_p = reinterpret_cast<pi_char_t*> + (memchr(p, 0, names_chunk.size() - (p - names_chunk.data()))); + if (!null_p) { +// throw PalmLib::error("names chunk is corrupt"); + kdDebug() << "names chunk is corrupt" << endl; + } + len = null_p - p; + + switch (PalmLib::get_short(q)) { + case 0: + type = PalmLib::FlatFile::Field::STRING; + break; + + case 1: + type = PalmLib::FlatFile::Field::BOOLEAN; + break; + + case 2: + type = PalmLib::FlatFile::Field::INTEGER; + break; + + case 3: + type = PalmLib::FlatFile::Field::DATE; + break; + + case 4: + type = PalmLib::FlatFile::Field::TIME; + break; + + case 5: + type = PalmLib::FlatFile::Field::NOTE; + break; + + case 6: + type = PalmLib::FlatFile::Field::LIST; + break; + + case 7: + type = PalmLib::FlatFile::Field::LINK; + break; + + case 8: + type = PalmLib::FlatFile::Field::FLOAT; + break; + + case 9: + type = PalmLib::FlatFile::Field::CALCULATED; + break; + + case 10: + type = PalmLib::FlatFile::Field::LINKED; + break; + + default: +// throw PalmLib::error("unknown field type"); + kdDebug() << "PalmLib::FlatFile::DB::extract_schema() - unknown field type" << endl; + type = PalmLib::FlatFile::Field::STRING; + break; + } + + // Inform the superclass about this field. + appendField(std::string((char *) p, len), type, extract_fieldsdata(i, type)); + + // Advance to the information on the next field. + p += len + 1; + q += 2; + } +} + +void PalmLib::FlatFile::DB::extract_listviews() +{ + if (!has_key(m_chunks, CHUNK_LISTVIEW_DEFINITION)) + return; + +/* throw PalmLib::error("no list views in database");*/ + + const std::vector<Chunk>& chunks = m_chunks[CHUNK_LISTVIEW_DEFINITION]; + + for (std::vector<Chunk>::const_iterator iter = chunks.begin(); + iter != chunks.end(); ++iter) { + const Chunk& chunk = (*iter); + PalmLib::FlatFile::ListView lv; + + if (chunk.size() < (2 + 2 + 32)) { +// throw PalmLib::error("list view is corrupt"); + kdDebug() << "list view is corrupt" << endl; + } + pi_uint16_t flags = PalmLib::get_short(chunk.data()); + pi_uint16_t num_cols = PalmLib::get_short(chunk.data() + 2); + + lv.editoruse = false; + if (flags & VIEWFLAG_USE_IN_EDITVIEW) + lv.editoruse = true; + + if (chunk.size() != static_cast<unsigned> (2 + 2 + 32 + num_cols * 4)) { +// throw PalmLib::error("list view is corrupt"); + kdDebug() << "list view is corrupt" << endl; + } + // Determine the length of the name string. + pi_char_t* null_ptr = reinterpret_cast<pi_char_t*> + (memchr(chunk.data() + 4, 0, 32)); + if (null_ptr) + lv.name = std::string((char *) (chunk.data() + 4), + null_ptr - (chunk.data() + 4)); + else + lv.name = "Unknown"; + + const pi_char_t* p = chunk.data() + 2 + 2 + 32; + for (int i = 0; i < num_cols; ++i) { + pi_uint16_t field = PalmLib::get_short(p); + pi_uint16_t width = PalmLib::get_short(p + 2); + p += 2 * sizeof(pi_uint16_t); + + if (field >= getNumOfFields()) { +// throw PalmLib::error("list view is corrupt"); + kdDebug() << "list view is corrupt" << endl; + } + PalmLib::FlatFile::ListViewColumn col(field, width); + lv.push_back(col); + } + + appendListView(lv); + } +} + +std::string PalmLib::FlatFile::DB::extract_fieldsdata(pi_uint16_t field_search, PalmLib::FlatFile::Field::FieldType type) +{ + std::ostringstream theReturn; + + if (!has_key(m_chunks, CHUNK_FIELD_DATA)) + return std::string(theReturn.str()); + + std::vector<Chunk>& chunks = m_chunks[CHUNK_FIELD_DATA]; + + pi_uint16_t field_num = 0; + bool find = false; + std::vector<Chunk>::const_iterator iter = chunks.begin(); + for ( ; iter != chunks.end(); ++iter) { + const Chunk& chunk = (*iter); + + field_num = PalmLib::get_short(chunk.data()); + + if (field_num == field_search) { + find = true; + break; + } + } + + if (find) { + const Chunk& chunk = (*iter); + + switch (type) { + + case PalmLib::FlatFile::Field::STRING: + theReturn << std::string((const char *)chunk.data()+2, chunk.size() - 2); + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + break; + + case PalmLib::FlatFile::Field::INTEGER: + theReturn << PalmLib::get_long(chunk.data() + sizeof(pi_uint16_t)); + theReturn << charSeperator; + theReturn << PalmLib::get_short(chunk.data() + sizeof(pi_uint16_t) + sizeof(pi_uint32_t)); + break; + + case PalmLib::FlatFile::Field::FLOAT: { + pi_double_t value; + value.words.hi = PalmLib::get_long(chunk.data() + 2); + value.words.lo = PalmLib::get_long(chunk.data() + 6); + + theReturn << value.number; + } + break; + + case PalmLib::FlatFile::Field::DATE: + if (*(chunk.data() + sizeof(pi_uint16_t)) == NOW_DEFAULT) + theReturn << "now"; + else if (*(chunk.data() + sizeof(pi_uint16_t)) == CONSTANT_DEFAULT) { + const pi_char_t * ptr = chunk.data() + sizeof(pi_uint16_t) + 1; + struct tm date; + date.tm_year = PalmLib::get_short(ptr) - 1900; + date.tm_mon = (static_cast<int> (*(ptr + 2))) - 1; + date.tm_mday = static_cast<int> (*(ptr + 3)); + + (void) mktime(&date); + + char buf[1024]; + + // Clear out the output buffer. + memset(buf, 0, sizeof(buf)); + + // Convert and output the date using the format. + strftime(buf, sizeof(buf), "%Y/%m/%d", &date); + + theReturn << buf; + } + break; + + case PalmLib::FlatFile::Field::TIME: + if (*(chunk.data() + sizeof(pi_uint16_t)) == NOW_DEFAULT) + theReturn << "now"; + else if (*(chunk.data() + sizeof(pi_uint16_t)) == CONSTANT_DEFAULT) { + const pi_char_t * ptr = chunk.data() + sizeof(pi_uint16_t) + 1; + struct tm t; + const struct tm * tm_ptr; + time_t now; + + time(&now); + tm_ptr = localtime(&now); + memcpy(&t, tm_ptr, sizeof(tm)); + + t.tm_hour = static_cast<int> (*(ptr)); + t.tm_min = static_cast<int> (*(ptr + 1)); + t.tm_sec = 0; + + char buf[1024]; + + // Clear out the output buffer. + memset(buf, 0, sizeof(buf)); + + // Convert and output the date using the format. + strftime(buf, sizeof(buf), "%H:%M", &t); + + theReturn << buf; + } + break; + + case PalmLib::FlatFile::Field::NOTE: + break; + + case PalmLib::FlatFile::Field::LIST: { + unsigned short numItems = PalmLib::get_short(chunk.data() + sizeof(pi_uint16_t)); + int prevLength = 0; + std::string item; + + if (numItems > 0) { + for (unsigned short i = 0; i < numItems - 1; i++) { + item = std::string((const char *)chunk.data() + 3 * sizeof(pi_uint16_t) + prevLength); + theReturn << item << charSeperator; + prevLength += item.length() + 1; + } + item = std::string((const char *)chunk.data() + 3 * sizeof(pi_uint16_t) + prevLength); + theReturn << item; + } + } + break; + + case PalmLib::FlatFile::Field::LINK: + theReturn << std::string((const char *)chunk.data()+sizeof(pi_uint16_t)); +// theReturn << std::string((const char *)chunk.data()+sizeof(pi_uint16_t), chunk.size() - 2); + theReturn << charSeperator; + theReturn << PalmLib::get_short(chunk.data() + sizeof(pi_uint16_t) + 32 * sizeof(pi_char_t)); + break; + + case PalmLib::FlatFile::Field::LINKED: + theReturn << PalmLib::get_short(chunk.data() + sizeof(pi_uint16_t)); + theReturn << charSeperator; + theReturn << PalmLib::get_short(chunk.data() + 2 * sizeof(pi_uint16_t)); + break; + + case PalmLib::FlatFile::Field::CALCULATED: + break; + + default: + kdDebug() << "unknown field type" << endl; + break; + } + } + return std::string(theReturn.str()); +} + +void PalmLib::FlatFile::DB::extract_aboutinfo() +{ + if (!has_key(m_chunks, CHUNK_ABOUT)) + return; + + Chunk chunk = m_chunks[CHUNK_ABOUT][0]; + pi_char_t* header = chunk.data(); + pi_char_t* q = chunk.data() + PalmLib::get_short(header); + + setAboutInformation( (char*)q); +} + +void PalmLib::FlatFile::DB::parse_record(PalmLib::Record& record, + std::vector<pi_char_t *>& ptrs, + std::vector<size_t>& sizes) +{ + unsigned i; + + // Ensure that enough space for the offset table exists. + if (record.size() < getNumOfFields() * sizeof(pi_uint16_t)) { +// throw PalmLib::error("record is corrupt"); + kdDebug() << "record is corrupt" << endl; + } + // Extract the offsets from the record. Determine field pointers. + std::vector<pi_uint16_t> offsets(getNumOfFields()); + for (i = 0; i < getNumOfFields(); ++i) { + offsets[i] = get_short(record.data() + i * sizeof(pi_uint16_t)); + if (offsets[i] >= record.size()) { +// throw PalmLib::error("record is corrupt"); + kdDebug() << "record is corrupt" << endl; + } + ptrs.push_back(record.data() + offsets[i]); + } + + // Determine the field sizes. + for (i = 0; i < getNumOfFields() - 1; ++i) { + sizes.push_back(offsets[i + 1] - offsets[i]); + } + sizes.push_back(record.size() - offsets[getNumOfFields() - 1]); +} + +PalmLib::FlatFile::DB::DB(PalmLib::Database& pdb) + : Database("db", pdb), m_flags(0) +{ + // Split the application information block into its component chunks. + extract_chunks(pdb.getAppInfoBlock()); + + // Pull the header fields and schema out of the databasse. + m_flags = get_short(pdb.getAppInfoBlock().data()); + unsigned numFields = get_short(pdb.getAppInfoBlock().data() + 2); + extract_schema(numFields); + + // Extract all of the list views. + extract_listviews(); + + extract_aboutinfo(); + + for (unsigned i = 0; i < pdb.getNumRecords(); ++i) { + PalmLib::Record record = pdb.getRecord(i); + Record rec; + + std::vector<pi_char_t *> ptrs; + std::vector<size_t> sizes; + parse_record(record, ptrs, sizes); + for (unsigned j = 0; j < getNumOfFields(); ++j) { + PalmLib::FlatFile::Field f; + f.type = field_type(j); + + switch (field_type(j)) { + case PalmLib::FlatFile::Field::STRING: + f.type = PalmLib::FlatFile::Field::STRING; + f.v_string = std::string((char *) ptrs[j], sizes[j] - 1); + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + f.type = PalmLib::FlatFile::Field::BOOLEAN; + if (*(ptrs[j])) + f.v_boolean = true; + else + f.v_boolean = false; + break; + + case PalmLib::FlatFile::Field::INTEGER: + f.type = PalmLib::FlatFile::Field::INTEGER; + f.v_integer = PalmLib::get_long(ptrs[j]); + break; + + case PalmLib::FlatFile::Field::FLOAT: { + // Place data from database in a union for conversion. + pi_double_t value; + value.words.hi = PalmLib::get_long(ptrs[j]); + value.words.lo = PalmLib::get_long(ptrs[j] + 4); + + // Fill out the information for this field. + f.type = PalmLib::FlatFile::Field::FLOAT; + f.v_float = value.number; + } + break; + + case PalmLib::FlatFile::Field::DATE: + f.type = PalmLib::FlatFile::Field::DATE; + f.v_date.year = PalmLib::get_short(ptrs[j]); + f.v_date.month = static_cast<int> (*(ptrs[j] + 2)); + f.v_date.day = static_cast<int> (*(ptrs[j] + 3)); + break; + + case PalmLib::FlatFile::Field::TIME: + f.type = PalmLib::FlatFile::Field::TIME; + f.v_time.hour = static_cast<int> (*(ptrs[j])); + f.v_time.minute = static_cast<int> (*(ptrs[j] + 1)); + break; + + case PalmLib::FlatFile::Field::NOTE: + f.type = PalmLib::FlatFile::Field::NOTE; + f.v_string = std::string((char *) ptrs[j], sizes[j] - 3); + f.v_note = std::string((char *) (record.data() + get_short(ptrs[j] + strlen(f.v_string.c_str()) + 1))); + break; + + case PalmLib::FlatFile::Field::LIST: + f.type = PalmLib::FlatFile::Field::LIST; + if (!field(j).argument().empty()) { + std::string data = field(j).argument(); + unsigned int k; + std::string::size_type pos = 0; + pi_uint16_t itemID = *ptrs[j]; // TR: a list value is stored on 1 byte + + for (k = 0; k < itemID; k++) { + if ((pos = data.find(charSeperator, pos)) == std::string::npos) { + break; + } + pos++; + } + if (pos == std::string::npos) { + f.v_string = "N/A"; + } else { + if (data.find(charSeperator, pos) == std::string::npos) { + f.v_string = data.substr( pos, std::string::npos); + } else { + f.v_string = data.substr( pos, data.find(charSeperator, pos) - pos); + } + } + } + break; + + case PalmLib::FlatFile::Field::LINK: + f.type = PalmLib::FlatFile::Field::LINK; + f.v_integer = PalmLib::get_long(ptrs[j]); + f.v_string = std::string((char *) (ptrs[j] + 4), sizes[j] - 5); + break; + + case PalmLib::FlatFile::Field::LINKED: + f.type = PalmLib::FlatFile::Field::LINKED; + f.v_string = std::string((char *) ptrs[j], sizes[j] - 1); + break; + + case PalmLib::FlatFile::Field::CALCULATED: { + std::ostringstream value; + f.type = PalmLib::FlatFile::Field::CALCULATED; + switch (ptrs[j][0]) { + case 1: //string + value << std::string((char *) ptrs[j] + 1, sizes[j] - 2); + break; + case 2: //integer + value << PalmLib::get_long(ptrs[j] + 1); + break; + case 9: //float + { + pi_double_t fvalue; + fvalue.words.hi = PalmLib::get_long(ptrs[j] + 1); + fvalue.words.lo = PalmLib::get_long(ptrs[j] + 5); + + value << fvalue.number; + } + default: + value << "N/A"; + } + f.v_string = value.str(); + } break; + + default: + kdDebug() << "unknown field type" << endl; + break; + } + + // Append this field to the record. + rec.appendField(f); + } + rec.unique_id(record.unique_id()); + // Append this record to the database. + appendRecord(rec); + } +} + +void PalmLib::FlatFile::DB::make_record(PalmLib::Record& pdb_record, + const Record& record) const +{ + unsigned int i; + + // Determine the packed size of this record. + size_t size = getNumOfFields() * sizeof(pi_uint16_t); + for (i = 0; i < getNumOfFields(); i++) { +#ifdef HAVE_VECTOR_AT + const Field field = record.fields().at(i); +#else + const Field field = record.fields()[i]; +#endif + switch (field.type) { + case PalmLib::FlatFile::Field::STRING: + size += field.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::NOTE: + size += field.v_string.length() + 3; + size += field.v_note.length() + 1; + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + size += 1; + break; + + case PalmLib::FlatFile::Field::INTEGER: + size += 4; + break; + + case PalmLib::FlatFile::Field::FLOAT: + size += 8; + break; + + case PalmLib::FlatFile::Field::DATE: + size += sizeof(pi_uint16_t) + 2 * sizeof(pi_char_t); + break; + + case PalmLib::FlatFile::Field::TIME: + size += 2 * sizeof(pi_char_t); + break; + + case PalmLib::FlatFile::Field::LIST: + size += sizeof(pi_char_t); + break; + + case PalmLib::FlatFile::Field::LINK: + size += sizeof(pi_int32_t); + size += field.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::LINKED: + size += field.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::CALCULATED: + size += 1; + break; + + default: + kdDebug() << "unsupported field type" << endl; + break; + } + } + + // Allocate a block for the packed record and setup the pointers. + pi_char_t* buf = new pi_char_t[size]; + pi_char_t* p = buf + getNumOfFields() * sizeof(pi_uint16_t); + pi_char_t* offsets = buf; + + // Pack the fields into the buffer. + for (i = 0; i < getNumOfFields(); i++) { + pi_char_t* noteOffsetOffset = 0; + bool setNote = false; +#ifdef HAVE_VECTOR_AT + const Field fieldData = record.fields().at(i); +#else + const Field fieldData = record.fields()[i]; +#endif + + // Mark the offset to the start of this field in the offests table. + PalmLib::set_short(offsets, static_cast<pi_uint16_t> (p - buf)); + offsets += sizeof(pi_uint16_t); + + // Pack the field. + switch (fieldData.type) { + case PalmLib::FlatFile::Field::STRING: + memcpy(p, fieldData.v_string.c_str(), fieldData.v_string.length() + 1); + p += fieldData.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::NOTE: + if (setNote) + kdDebug() << "unsupported field type"; + memcpy(p, fieldData.v_string.c_str(), fieldData.v_string.length() + 1); + p += fieldData.v_string.length() + 1; + noteOffsetOffset = p; + p += 2; + setNote = true; + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + *p++ = ((fieldData.v_boolean) ? 1 : 0); + break; + + case PalmLib::FlatFile::Field::INTEGER: + PalmLib::set_long(p, fieldData.v_integer); + p += sizeof(pi_int32_t); + break; + + case PalmLib::FlatFile::Field::FLOAT: { + // Place data the data in a union for easy conversion. + pi_double_t value; + value.number = fieldData.v_float; + PalmLib::set_long(p, value.words.hi); + p += sizeof(pi_uint32_t); + PalmLib::set_long(p, value.words.lo); + p += sizeof(pi_uint32_t); + break; + } + + case PalmLib::FlatFile::Field::DATE: + PalmLib::set_short(p, fieldData.v_date.year); + p += sizeof(pi_uint16_t); + *p++ = static_cast<pi_char_t> (fieldData.v_date.month & 0xFF); + *p++ = static_cast<pi_char_t> (fieldData.v_date.day & 0xFF); + break; + + case PalmLib::FlatFile::Field::TIME: + *p++ = static_cast<pi_char_t> (fieldData.v_time.hour & 0xFF); + *p++ = static_cast<pi_char_t> (fieldData.v_time.minute & 0xFF); + break; + + case PalmLib::FlatFile::Field::LIST: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + std::string::size_type pos = 0, next; + unsigned int j = 0; + pi_int16_t itemID = -1; + + while ( (next = data.find(charSeperator, pos)) != std::string::npos) { + if (fieldData.v_string == data.substr( pos, next - pos)) { + itemID = j; + break; + } + j++; + pos = next + 1; + } + // TR: the following test handles the case where the field value + // equals the last item in list (bugfix) + if (itemID == -1 && fieldData.v_string == data.substr( pos, std::string::npos)) { + itemID = j; + } + p[0] = itemID; // TR: a list value is stored on 1 byte + p += sizeof(pi_char_t); + } + break; + + case PalmLib::FlatFile::Field::LINK: + PalmLib::set_long(p, fieldData.v_integer); + p += sizeof(pi_int32_t); + memcpy(p, fieldData.v_string.c_str(), fieldData.v_string.length() + 1); + p += fieldData.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::LINKED: + memcpy(p, fieldData.v_string.c_str(), fieldData.v_string.length() + 1); + p += fieldData.v_string.length() + 1; + break; + + case PalmLib::FlatFile::Field::CALCULATED: + *p = 13; + p++; + break; + + default: + kdDebug() << "unsupported field type"; + break; + } + if (setNote) { + if (fieldData.v_note.length()) { + memcpy(p, fieldData.v_note.c_str(), fieldData.v_note.length() + 1); + PalmLib::set_short(noteOffsetOffset, (pi_uint16_t)(p - buf)); + p += fieldData.v_note.length() + 1; + } else { + PalmLib::set_short(noteOffsetOffset, 0); + } + } + } + + // Place the packed data into the PalmOS record. + pdb_record.set_raw(buf, size); + delete [] buf; +} + +void PalmLib::FlatFile::DB::build_fieldsdata_chunks(std::vector<DB::Chunk>& chunks) const +{ + pi_char_t * buf = 0, * p; + unsigned int size, i; + + for (i = 0; i < getNumOfFields(); ++i) { + size = 0; + switch (field_type(i)) { + case PalmLib::FlatFile::Field::STRING: + if (!field(i).argument().empty()) { + size = (field(i).argument().length() + 1) + 2; + buf = new pi_char_t[size]; + PalmLib::set_short(buf, i); + strcpy((char *) (buf + 2), field(i).argument().c_str()); + } + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + break; + + case PalmLib::FlatFile::Field::INTEGER: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + std::pair< PalmLib::pi_int32_t, PalmLib::pi_int16_t> values(0, 0); + + if ( data.find(charSeperator) != std::string::npos) { + StrOps::convert_string(data.substr( 0, data.find(charSeperator)), values.first); + StrOps::convert_string(data.substr( data.find(charSeperator) + 1, std::string::npos), values.second); + } else + StrOps::convert_string(data, values.first); + + size = 2 + sizeof(pi_uint32_t) + sizeof(pi_uint16_t); + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + PalmLib::set_long(p, values.first); + p += sizeof(pi_uint32_t); + PalmLib::set_short(p, values.second); + p += sizeof(pi_uint16_t); + } + break; + + case PalmLib::FlatFile::Field::FLOAT: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + pi_double_t value; + + StrOps::convert_string(data, value.number); + + size = 2 + 2 * sizeof(pi_uint32_t); + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + PalmLib::set_long(p, value.words.hi); + p += sizeof(pi_uint32_t); + PalmLib::set_long(p, value.words.lo); + p += sizeof(pi_uint32_t); + } + break; + + case PalmLib::FlatFile::Field::DATE: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + struct tm date; + pi_char_t type; + + if (data.substr(0, 3) == "now") { + type = NOW_DEFAULT; + const struct tm * tm_ptr; + time_t now; + + time(&now); + tm_ptr = localtime(&now); + memcpy(&date, tm_ptr, sizeof(tm)); + } else +#ifdef strptime + if (strptime(data.c_str(), "%Y/%m/%d", &date)) +#else + if (StrOps::strptime(data.c_str(), "%Y/%m/%d", &date)) +#endif + type = CONSTANT_DEFAULT; + else + type = INVALID_DEFAULT; + + if (type != INVALID_DEFAULT) { + size = sizeof(pi_uint16_t) + 1 + sizeof(pi_uint16_t) + 2; + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + *p++ = static_cast<pi_char_t> (type & 0xFF); + PalmLib::set_short(p, date.tm_year + 1900); + p += sizeof(pi_uint16_t); + *p++ = static_cast<pi_char_t> ((date.tm_mon + 1) & 0xFF); + *p++ = static_cast<pi_char_t> (date.tm_mday & 0xFF); + } + + } + break; + + case PalmLib::FlatFile::Field::TIME: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + struct tm t; + pi_char_t type; + + if (data == "now") { + type = NOW_DEFAULT; + const struct tm * tm_ptr; + time_t now; + + time(&now); + tm_ptr = localtime(&now); + memcpy(&t, tm_ptr, sizeof(tm)); + } else +#ifdef strptime + if (!strptime(data.c_str(), "%H/%M", &t)) +#else + if (!StrOps::strptime(data.c_str(), "%H/%M", &t)) +#endif + type = CONSTANT_DEFAULT; + else + type = INVALID_DEFAULT; + + if (type != INVALID_DEFAULT) { + size = sizeof(pi_uint16_t) + 1 + sizeof(pi_uint16_t) + 2; + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + *p++ = static_cast<pi_char_t> (type & 0xFF); + *p++ = static_cast<pi_char_t> (t.tm_hour & 0xFF); + *p++ = static_cast<pi_char_t> (t.tm_min & 0xFF); + } + + } + break; + + case PalmLib::FlatFile::Field::NOTE: + break; + + case PalmLib::FlatFile::Field::LIST: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + std::vector<std::string> items; + std::string::size_type pos = 0, next; + std::vector<std::string>::iterator iter; + size = 2 + 2 * sizeof(pi_uint16_t); + while ( (next = data.find(charSeperator, pos)) != std::string::npos) { + std::string item = data.substr( pos, next - pos); + items.push_back(item); + size += item.length() + 1; + pos = next + 1; + } + if (pos != std::string::npos) { + std::string item = data.substr( pos, std::string::npos); + items.push_back(item); + size += item.length() + 1; + } + + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + PalmLib::set_short(p, items.size()); + p += sizeof(pi_uint16_t); + p += sizeof(pi_uint16_t); + for (iter = items.begin(); iter != items.end(); ++iter) { + std::string& item = (*iter); + strcpy((char *) p, item.c_str()); + p[item.length()] = 0; + p += item.length() + 1; + } + + } + break; + + case PalmLib::FlatFile::Field::LINK: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + std::string databasename; + pi_uint16_t fieldnum; + + if ( data.find(charSeperator) != std::string::npos) { + databasename = data.substr( 0, data.find(charSeperator)); + StrOps::convert_string(data.substr( data.find(charSeperator) + 1, std::string::npos), fieldnum); + } else { + databasename = data; + fieldnum = 0; + } + + size = 2 + 32 * sizeof(pi_char_t) + sizeof(pi_uint16_t); + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + strcpy((char *) p, databasename.c_str()); + p += 32 * sizeof(pi_char_t); + PalmLib::set_short(p, fieldnum); + p += sizeof(pi_uint16_t); + } + break; + + case PalmLib::FlatFile::Field::LINKED: + if (!field(i).argument().empty()) { + std::string data = field(i).argument(); + pi_uint16_t linknum; + pi_uint16_t fieldnum; + + if ( data.find(charSeperator) != std::string::npos) { + StrOps::convert_string(data.substr( 0, data.find(charSeperator)), linknum); + StrOps::convert_string(data.substr( data.find(charSeperator) + 1, std::string::npos), fieldnum); + if (field_type(linknum) != PalmLib::FlatFile::Field::LINK) { + unsigned int j = 0; + while (field_type(j) != PalmLib::FlatFile::Field::LINK && j < i) j++; + linknum = j; + } + } else { + unsigned int j = 0; + while (field_type(j) != PalmLib::FlatFile::Field::LINK && j < i) j++; + linknum = j; + fieldnum = 0; + } + + size = 2 + 2 * sizeof(pi_uint16_t); + buf = new pi_char_t[size]; + p = buf; + PalmLib::set_short(p, i); + p += sizeof(pi_uint16_t); + PalmLib::set_short(p, linknum); + p += sizeof(pi_uint16_t); + PalmLib::set_short(p, fieldnum); + p += sizeof(pi_uint16_t); + } + break; + + case PalmLib::FlatFile::Field::CALCULATED: + break; + + default: + kdDebug() << "unknown field type" << endl; + break; + } + + if (size) { + Chunk data_chunk(buf, size); + data_chunk.chunk_type = CHUNK_FIELD_DATA; + delete [] buf; + chunks.push_back(data_chunk); + } + } +} + +void PalmLib::FlatFile::DB::build_about_chunk(std::vector<DB::Chunk>& chunks) const +{ + pi_char_t* buf; + pi_char_t* p; + int headersize = 2*sizeof(pi_uint16_t); + std::string information = getAboutInformation(); + + if (!information.length()) + return; + // Build the names chunk. + buf = new pi_char_t[headersize + information.length() + 1]; + p = buf; + + PalmLib::set_short(p, headersize); + p += 2; + PalmLib::set_short(p, 1); //about type version + p += 2; + memcpy(p, information.c_str(), information.length() + 1); + p += information.length() + 1; + Chunk chunk(buf, headersize + information.length() + 1); + chunk.chunk_type = CHUNK_ABOUT; + delete [] buf; + chunks.push_back(chunk); + +} + +void PalmLib::FlatFile::DB::build_standard_chunks(std::vector<DB::Chunk>& chunks) const +{ + pi_char_t* buf; + pi_char_t* p; + unsigned i; + + // Determine the size needed for the names chunk. + size_t names_chunk_size = 0; + for (i = 0; i < getNumOfFields(); ++i) { + names_chunk_size += field_name(i).length() + 1; + } + + // Build the names chunk. + buf = new pi_char_t[names_chunk_size]; + p = buf; + for (i = 0; i < getNumOfFields(); ++i) { + const std::string name = field_name(i); + memcpy(p, name.c_str(), name.length() + 1); + p += name.length() + 1; + } + Chunk names_chunk(buf, names_chunk_size); + names_chunk.chunk_type = CHUNK_FIELD_NAMES; + delete [] buf; + + // Build the types chunk. + buf = new pi_char_t[getNumOfFields() * sizeof(pi_uint16_t)]; + p = buf; + for (i = 0; i < getNumOfFields(); ++i) { + // Pack the type of the current field. + switch (field_type(i)) { + case PalmLib::FlatFile::Field::STRING: + PalmLib::set_short(p, 0); + break; + + case PalmLib::FlatFile::Field::BOOLEAN: + PalmLib::set_short(p, 1); + break; + + case PalmLib::FlatFile::Field::INTEGER: + PalmLib::set_short(p, 2); + break; + + case PalmLib::FlatFile::Field::DATE: + PalmLib::set_short(p, 3); + break; + + case PalmLib::FlatFile::Field::TIME: + PalmLib::set_short(p, 4); + break; + + case PalmLib::FlatFile::Field::NOTE: + PalmLib::set_short(p, 5); + break; + + case PalmLib::FlatFile::Field::LIST: + PalmLib::set_short(p, 6); + break; + + case PalmLib::FlatFile::Field::LINK: + PalmLib::set_short(p, 7); + break; + + case PalmLib::FlatFile::Field::FLOAT: + PalmLib::set_short(p, 8); + break; + + case PalmLib::FlatFile::Field::CALCULATED: + PalmLib::set_short(p, 9); + break; + + case PalmLib::FlatFile::Field::LINKED: + PalmLib::set_short(p, 10); + break; + + default: + kdDebug() << "unsupported field type" << endl; + break; + } + + // Advance to the next position. + p += sizeof(pi_uint16_t); + } + Chunk types_chunk(buf, getNumOfFields() * sizeof(pi_uint16_t)); + types_chunk.chunk_type = CHUNK_FIELD_TYPES; + delete [] buf; + + // Build the list view options chunk. + buf = new pi_char_t[2 * sizeof(pi_uint16_t)]; + PalmLib::set_short(buf, 0); + PalmLib::set_short(buf + sizeof(pi_uint16_t), 0); + Chunk listview_options_chunk(buf, 2 * sizeof(pi_uint16_t)); + listview_options_chunk.chunk_type = CHUNK_LISTVIEW_OPTIONS; + delete [] buf; + + // Build the local find options chunk. + buf = new pi_char_t[sizeof(pi_uint16_t)]; + PalmLib::set_short(buf, 0); + Chunk lfind_options_chunk(buf, 1 * sizeof(pi_uint16_t)); + lfind_options_chunk.chunk_type = CHUNK_LFIND_OPTIONS; + delete [] buf; + + // Add all the chunks to the chunk list. + chunks.push_back(names_chunk); + chunks.push_back(types_chunk); + chunks.push_back(listview_options_chunk); + chunks.push_back(lfind_options_chunk); +} + +void PalmLib::FlatFile::DB::build_listview_chunk(std::vector<DB::Chunk>& chunks, + const ListView& lv) const +{ + // Calculate size and allocate space for the temporary buffer. + size_t size = 2 * sizeof(pi_uint16_t) + 32 + + lv.size() * (2 * sizeof(pi_uint16_t)); + pi_char_t* buf = new pi_char_t[size]; + + // Fill in the header details. + pi_uint16_t flags = 0; + if (lv.editoruse) { + std::cout << "editoruse\n"; + flags |= VIEWFLAG_USE_IN_EDITVIEW; + } + PalmLib::set_short(buf, flags); + PalmLib::set_short(buf + sizeof(pi_uint16_t), lv.size()); + memset((char *) (buf + 4), 0, 32); + strncpy((char *) (buf + 4), lv.name.c_str(), 32); + + // Fill in the column details. + pi_char_t* p = buf + 4 + 32; + for (ListView::const_iterator i = lv.begin(); i != lv.end(); ++i) { + const ListViewColumn& col = (*i); + PalmLib::set_short(p, col.field); + PalmLib::set_short(p + sizeof(pi_uint16_t), col.width); + p += 2 * sizeof(pi_uint16_t); + } + + // Create the chunk and place it in the chunks list. + Chunk chunk(buf, size); + chunk.chunk_type = CHUNK_LISTVIEW_DEFINITION; + delete [] buf; + chunks.push_back(chunk); +} + +void PalmLib::FlatFile::DB::build_appinfo_block(const std::vector<DB::Chunk>& chunks, PalmLib::Block& appinfo) const +{ + std::vector<Chunk>::const_iterator iter; + + // Determine the size of the final app info block. + size_t size = 2 * sizeof(pi_uint16_t); + for (iter = chunks.begin(); iter != chunks.end(); ++iter) { + const Chunk& chunk = (*iter); + size += 2 * sizeof(pi_uint16_t) + chunk.size(); + } + + // Allocate the temporary buffer. Fill in the header. + pi_char_t* buf = new pi_char_t[size]; + PalmLib::set_short(buf, m_flags); + PalmLib::set_short(buf + sizeof(pi_uint16_t), getNumOfFields()); + + // Pack the chunks into the buffer. + size_t i = 4; + for (iter = chunks.begin(); iter != chunks.end(); ++iter) { + const Chunk& chunk = (*iter); + // Set the chunk type and size. + PalmLib::set_short(buf + i, chunk.chunk_type); + PalmLib::set_short(buf + i + 2, chunk.size()); + i += 4; + + // Copy the chunk data into the buffer. + memcpy(buf + i, chunk.data(), chunk.size()); + i += chunk.size(); + } + + // Finally, move the buffer into the provided appinfo block. + appinfo.set_raw(buf, size); + delete [] buf; +} + +void PalmLib::FlatFile::DB::outputPDB(PalmLib::Database& pdb) const +{ + unsigned i; + + // Let the superclass have a chance. + SUPERCLASS(PalmLib::FlatFile, Database, outputPDB, (pdb)); + + // Set the database's type and creator. + pdb.type(PalmLib::mktag('D','B','0','0')); + pdb.creator(PalmLib::mktag('D','B','O','S')); + + // Create the app info block. + std::vector<Chunk> chunks; + build_standard_chunks(chunks); + for (i = 0; i < getNumOfListViews(); ++i) { + build_listview_chunk(chunks, getListView(i)); + } + build_fieldsdata_chunks(chunks); + build_about_chunk(chunks); + + PalmLib::Block appinfo; + build_appinfo_block(chunks, appinfo); + pdb.setAppInfoBlock(appinfo); + + // Output each record to the PalmOS database. + for (i = 0; i < getNumRecords(); ++i) { + Record record = getRecord(i); + PalmLib::Record pdb_record; + + make_record(pdb_record, record); + pdb.appendRecord(pdb_record); + } +} + +unsigned PalmLib::FlatFile::DB::getMaxNumOfFields() const +{ + return 0; +} + +bool +PalmLib::FlatFile::DB::supportsFieldType(const Field::FieldType& type) const +{ + switch (type) { + case PalmLib::FlatFile::Field::STRING: + case PalmLib::FlatFile::Field::BOOLEAN: + case PalmLib::FlatFile::Field::INTEGER: + case PalmLib::FlatFile::Field::FLOAT: + case PalmLib::FlatFile::Field::DATE: + case PalmLib::FlatFile::Field::TIME: + case PalmLib::FlatFile::Field::NOTE: + case PalmLib::FlatFile::Field::LIST: + case PalmLib::FlatFile::Field::LINK: + case PalmLib::FlatFile::Field::LINKED: + case PalmLib::FlatFile::Field::CALCULATED: + return true; + default: + return false; + } +} + +std::vector<std::string> +PalmLib::FlatFile::DB::field_argumentf(int i, std::string& format) +{ + std::vector<std::string> vtitles(0, std::string("")); + int j; + + switch (field_type(i)) { + case PalmLib::FlatFile::Field::STRING: + format = std::string("%s"); + vtitles.push_back(std::string("default value")); + break; + case PalmLib::FlatFile::Field::INTEGER: + format = std::string("%ld/%d"); + vtitles.push_back(std::string("default value")); + vtitles.push_back(std::string("increment")); + break; + case PalmLib::FlatFile::Field::FLOAT: + format = std::string("%f"); + vtitles.push_back(std::string("default value")); + break; + case PalmLib::FlatFile::Field::DATE: + format = std::string("%d/%d/%d"); + vtitles.push_back(std::string("Year (or now)")); + vtitles.push_back(std::string("Month")); + vtitles.push_back(std::string("Day in the month")); + break; + case PalmLib::FlatFile::Field::TIME: + format = std::string("%d/%d"); + vtitles.push_back(std::string("Hour (or now)")); + vtitles.push_back(std::string("Minute")); + break; + case PalmLib::FlatFile::Field::LIST: + format = std::string(""); + for (j = 0; j < 31; i++) { + format += std::string("%s/"); + std::ostringstream title; + title << "item " << j; + vtitles.push_back(title.str()); + } + format += std::string("%s"); + vtitles.push_back(std::string("item 32")); + break; + case PalmLib::FlatFile::Field::LINK: + format = std::string("%s/%d"); + vtitles.push_back(std::string("database")); + vtitles.push_back(std::string("field number")); + break; + case PalmLib::FlatFile::Field::LINKED: + format = std::string("%d/%d"); + vtitles.push_back(std::string("link field number")); + vtitles.push_back(std::string("field number")); + break; + case PalmLib::FlatFile::Field::CALCULATED: + case PalmLib::FlatFile::Field::BOOLEAN: + case PalmLib::FlatFile::Field::NOTE: + default: + format = std::string(""); + break; + } + return vtitles; +} + +unsigned PalmLib::FlatFile::DB::getMaxNumOfListViews() const +{ + return 0; +} + +void PalmLib::FlatFile::DB::doneWithSchema() +{ + // Let the superclass have a chance. + SUPERCLASS(PalmLib::FlatFile, Database, doneWithSchema, ()); +/* false from the 0.3.3 version + if (getNumOfListViews() < 1) + throw PalmLib::error("at least one list view must be specified"); +*/ +} + +void PalmLib::FlatFile::DB::setOption(const std::string& name, + const std::string& value) +{ + if (name == "find") { + if (!StrOps::string2boolean(value)) + m_flags &= ~(1); + else + m_flags |= 1; + } else if (name == "read-only" + || name == "readonly") { + if (!StrOps::string2boolean(value)) + m_flags &= ~(0x8000); + else + m_flags |= 0x8000; + } else { + SUPERCLASS(PalmLib::FlatFile, Database, setOption, (name, value)); + } +} + +PalmLib::FlatFile::Database::options_list_t +PalmLib::FlatFile::DB::getOptions(void) const +{ + typedef PalmLib::FlatFile::Database::options_list_t::value_type value; + PalmLib::FlatFile::Database::options_list_t result; + + result = SUPERCLASS(PalmLib::FlatFile, Database, getOptions, ()); + + if (m_flags & 1) + result.push_back(value("find", "true")); + + if (m_flags & 0x8000) + result.push_back(value("read-only", "true")); + + return result; +} diff --git a/src/translators/pilotdb/libflatfile/DB.h b/src/translators/pilotdb/libflatfile/DB.h new file mode 100644 index 0000000..dd09d36 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/DB.h @@ -0,0 +1,166 @@ +/* + * This class provides access to DB-format databases. + */ + +#ifndef __PALMLIB_FLATFILE_DB_H__ +#define __PALMLIB_FLATFILE_DB_H__ + +#include <map> +#include <string> + +#include "../libpalm/Block.h" +#include "../libpalm/Database.h" +#include "Database.h" + +namespace PalmLib { + namespace FlatFile { + + class DB : public Database { + public: + /** + * Return true if this class can handle the given PalmOS + * database. + * + * @param pdb PalmOS database to check for support. + */ + static bool classify(PalmLib::Database& pdb); + + /** + * Return true if this class is the database identified by + * name. + * + * @param name A database type name to check. + */ + static bool match_name(const std::string& name); + + /** + * Default constructor for an initially empty database. + */ + DB():Database("db"), m_flags(0) { } + + /** + * Constructor which fills the flat-file structure from a + * PalmOS database. + */ + DB(PalmLib::Database&); + + // destructor + virtual ~DB() { } + + /** + * After all processing to add fields and records is done, + * outputPDB is called to create the actual file format + * used by the flat-file database product. + * + * @param pdb An instance of PalmLib::Database. + */ + virtual void outputPDB(PalmLib::Database& pdb) const; + + /** + * Return the maximum number of fields allowed in the + * database. This class returns 0 since there is no limit. + */ + virtual unsigned getMaxNumOfFields() const; + + /** + * Return true for the field types that this class + * currently supports. Returns false otherwise. + * + * @param type The field type to check for support. + */ + virtual bool supportsFieldType(const Field::FieldType& type) const; + + /** + * write the format of the field's argument in format, + * and return a strings' vector with name of each argument part. + * the format use the same display as used by printf + */ + virtual std::vector<std::string> field_argumentf(int i, std::string& format); + + /** + * Return the maximum number of views supported by this + * type of flat-file database. + */ + virtual unsigned getMaxNumOfListViews() const; + + /** + * Hook the end of the schema processing. + */ + virtual void doneWithSchema(); + + /** + * Set a extra option. + * + * @param opt_name The name of the option to set. + * @param opt_value The value to assign to this option. + */ + virtual void setOption(const std::string& name, + const std::string& value); + + /** + * Get a list of extra options. + */ + virtual options_list_t getOptions(void) const; + + // Produce a PalmOS record from a flat-file record. + void make_record(PalmLib::Record& pdb_record, + const PalmLib::FlatFile::Record& record) const; + + private: + pi_uint16_t m_flags; + + class Chunk : public PalmLib::Block { + public: + Chunk() : PalmLib::Block(), chunk_type(0) { } + Chunk(const Chunk& rhs) + : PalmLib::Block(rhs), chunk_type(rhs.chunk_type) { } + Chunk(PalmLib::Block::const_pointer data, + const PalmLib::Block::size_type size) + : PalmLib::Block(data, size), chunk_type(0) { } + Chunk& operator = (const Chunk& rhs) { + Block::operator = (rhs); + chunk_type = rhs.chunk_type; + return *this; + } + + pi_uint16_t chunk_type; + }; + + typedef std::map<pi_uint16_t, std::vector<Chunk> > chunks_t; + chunks_t m_chunks; + + // Extract the chunks from an app info block to m_chunks. + void extract_chunks(const PalmLib::Block&); + + // Extract the schema. + void extract_schema(unsigned numFields); + + // Extract the list views from the app info block. + void extract_listviews(); + + //extract the field data + std::string extract_fieldsdata(pi_uint16_t field_search, + PalmLib::FlatFile::Field::FieldType type); + + void extract_aboutinfo(); + + // Determine location and size of each field. + void parse_record(PalmLib::Record& record, + std::vector<pi_char_t *>& ptrs, + std::vector<size_t>& sizes); + + // The following routines build various types of chunks + // for the app info block and assemble them all. + void build_fieldsdata_chunks(std::vector<Chunk>& chunks) const; + void build_standard_chunks(std::vector<Chunk>&) const; + void build_listview_chunk(std::vector<Chunk>&, + const ListView&) const; + void build_about_chunk(std::vector<Chunk>& chunks) const; + void build_appinfo_block(const std::vector<Chunk>&, + PalmLib::Block&) const; + }; + + } +} + +#endif diff --git a/src/translators/pilotdb/libflatfile/Database.cpp b/src/translators/pilotdb/libflatfile/Database.cpp new file mode 100644 index 0000000..578b82d --- /dev/null +++ b/src/translators/pilotdb/libflatfile/Database.cpp @@ -0,0 +1,331 @@ +/* + * palm-db-tools: Abstract adaptor for flat-file databases. + * Copyright (C) 1999-2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#include <iostream> +#include <sstream> +#include <stdexcept> +#include <sstream> +#include <utility> +#include <cctype> + +#include <kdebug.h> + +#include "Database.h" + +PalmLib::FlatFile::Database::Database(std::string p_Type, const PalmLib::Database& pdb) + : m_Type(p_Type) +{ + title(pdb.name()); + m_backup = pdb.backup(); + m_readonly = pdb.readonly(); + m_copy_prevention = pdb.copy_prevention(); +} + +void +PalmLib::FlatFile::Database::outputPDB(PalmLib::Database& pdb) const +{ + pdb.name(title()); + pdb.backup(m_backup); + pdb.readonly(m_readonly); + pdb.copy_prevention(m_copy_prevention); +} + +std::string +PalmLib::FlatFile::Database::title() const +{ + return m_title; +} + +void +PalmLib::FlatFile::Database::title(const std::string& title) +{ + m_title = title; +} + +unsigned +PalmLib::FlatFile::Database::getNumOfFields() const +{ + return m_fields.size(); +} + +std::string +PalmLib::FlatFile::Database::field_name(int i) const +{ + return m_fields[i].title(); +/* return m_fields[i].first;*/ +} + +PalmLib::FlatFile::Field::FieldType +PalmLib::FlatFile::Database::field_type(int i) const +{ + return m_fields[i].type(); +/* return m_fields[i].second;*/ +} + +PalmLib::FlatFile::FType +PalmLib::FlatFile::Database::field(int i) const +{ + return m_fields[i]; +} + +void +PalmLib::FlatFile::Database::appendField(PalmLib::FlatFile::FType field) +{ + if (! supportsFieldType(field.type())) { +// throw PalmLib::error("unsupported field type"); + kdDebug() << "unsupported field type" << endl; + return; + } + if (getMaxNumOfFields() != 0 && getNumOfFields() + 1 > getMaxNumOfFields()) { +// throw PalmLib::error("maximum number of fields reached"); + kdDebug() << "maximum number of fields reached" << endl; + return; + } + m_fields.push_back(field); +} + +void +PalmLib::FlatFile::Database::appendField(const std::string& name, + Field::FieldType type, std::string data) +{ + if (! supportsFieldType(type)) { + kdDebug() << "PalmLib::FlatFile::Database::appendField() - unsupported field type" << endl; + return; + } +// throw PalmLib::error("unsupported field type"); + if (getMaxNumOfFields() != 0 && getNumOfFields() + 1 > getMaxNumOfFields()) { + kdDebug() << "PalmLib::FlatFile::Database::appendField() - maximum number of fields reached" << endl; + return; + } +// throw PalmLib::error("maximum number of fields reached"); + +/* m_fields.push_back(std::make_pair(name, type));*/ +/* m_fields.push_back(PalmLib::FlatFile::make_ftype(name, type));*/ + m_fields.push_back(PalmLib::FlatFile::FType(name, type, data)); +} + +void +PalmLib::FlatFile::Database::insertField(int i, PalmLib::FlatFile::FType field) +{ + if (! supportsFieldType(field.type())) { +// throw PalmLib::error("unsupported field type"); + kdDebug() << "unsupported field type" << endl; + return; + } + if (getMaxNumOfFields() != 0 && getNumOfFields() + 1 > getMaxNumOfFields()) { +// throw PalmLib::error("maximum number of fields reached"); + kdDebug() << "maximum number of fields reached" << endl; + return; + } +/* m_fields.push_back(std::make_pair(name, type));*/ +/* m_fields.push_back(PalmLib::FlatFile::make_ftype(name, type));*/ + m_fields.insert(m_fields.begin() + i, field); +} + +void +PalmLib::FlatFile::Database::insertField(int i, const std::string& name, + Field::FieldType type, std::string data) +{ + if (! supportsFieldType(type)) { +// throw PalmLib::error("unsupported field type"); + kdDebug() << "unsupported field type" << endl; + return; + } + if (getMaxNumOfFields() != 0 && getNumOfFields() + 1 > getMaxNumOfFields()) { +// throw PalmLib::error("maximum number of fields reached"); + kdDebug() << "maximum number of fields reached" << endl; + return; + } +/* m_fields.push_back(std::make_pair(name, type));*/ +/* m_fields.push_back(PalmLib::FlatFile::make_ftype(name, type));*/ + m_fields.insert(m_fields.begin() + i, PalmLib::FlatFile::FType(name, type, data)); +} + +void +PalmLib::FlatFile::Database::removeField(int i) +{ + m_fields.erase(m_fields.begin() + i); +} + +unsigned +PalmLib::FlatFile::Database::getNumRecords() const +{ + return m_records.size(); +} + +PalmLib::FlatFile::Record +PalmLib::FlatFile::Database::getRecord(unsigned index) const +{ + if (index >= getNumRecords()) { + kdDebug() << "invalid index" << endl; + //throw std::out_of_range("invalid index"); + } + return m_records[index]; +} + +void +PalmLib::FlatFile::Database::appendRecord(PalmLib::FlatFile::Record rec) +{ + if (rec.fields().size() != getNumOfFields()) { +// throw PalmLib::error("the number of fields mismatch"); + kdDebug() << "the number of fields mismatch" << endl; + return; + } + for (unsigned int i = 0; i < getNumOfFields(); i++) { +#ifdef HAVE_VECTOR_AT + const Field field = rec.fields().at(i); +#else + const Field field = rec.fields()[i]; +#endif + if (field.type != field_type(i)) { + kdDebug() << "field " << i << " type " << field_type(i) << " mismatch: " << field.type << endl; + return; +// throw PalmLib::error(buffer.str()); + } + } + m_records.push_back(rec); +} + +void +PalmLib::FlatFile::Database::deleteRecord(unsigned index) +{ + m_records.erase(m_records.begin() + index); +} + +void +PalmLib::FlatFile::Database::clearRecords() +{ + m_records.clear(); +} + +unsigned +PalmLib::FlatFile::Database::getNumOfListViews() const +{ + return m_listviews.size(); +} + +PalmLib::FlatFile::ListView +PalmLib::FlatFile::Database::getListView(unsigned index) const +{ + return m_listviews[index]; +} + +void +PalmLib::FlatFile::Database::setListView(unsigned index, + const PalmLib::FlatFile::ListView& lv) +{ + // Ensure that the field numbers are within range. + for (PalmLib::FlatFile::ListView::const_iterator i = lv.begin(); + i != lv.end(); ++i) { + if ((*i).field >= getNumOfFields()) + return; + } + + m_listviews[index] = lv; +} + +void +PalmLib::FlatFile::Database::appendListView(const ListView& lv) +{ + // Enforce any limit of the maximum number of list views. + if (getMaxNumOfListViews() != 0 + && getNumOfListViews() + 1 > getMaxNumOfListViews()) + return; +// throw PalmLib::error("too many list views for this database type"); + + // Ensure that the field numbers are within range. + for (PalmLib::FlatFile::ListView::const_iterator i = lv.begin(); + i != lv.end(); ++i) { + if ((*i).field >= getNumOfFields()) + return; + } + m_listviews.push_back(lv); +} + +void +PalmLib::FlatFile::Database::removeListView(unsigned index) +{ + if (index < getNumOfListViews()) + m_listviews.erase( m_listviews.begin()+index); +} + +static void +strlower(std::string& str) +{ + for (std::string::iterator p = str.begin(); p != str.end(); ++p) { + if (isupper(*p)) + *p = tolower(*p); + } +} + +static bool +string2boolean(std::string str) +{ + strlower(str); + if (str == "on") + return true; + else if (str == "off") + return false; + else if (str == "true") + return true; + else if (str == "t") + return true; + else if (str == "false") + return false; + else if (str == "f") + return false; + else { + int num = 0; + + std::istringstream(str.c_str()) >> num; + return num != 0 ? true : false; + } +} + +void +PalmLib::FlatFile::Database::setOption(const std::string& name, + const std::string& value) +{ + if (name == "backup") + m_backup = string2boolean(value); + else if (name == "inROM") + m_readonly = string2boolean(value); + else if (name == "copy-prevention") + m_copy_prevention = string2boolean(value); +} + +PalmLib::FlatFile::Database::options_list_t +PalmLib::FlatFile::Database::getOptions() const +{ + PalmLib::FlatFile::Database::options_list_t set; + typedef PalmLib::FlatFile::Database::options_list_t::value_type value; + + if (m_backup) + set.push_back(value("backup", "true")); + else + set.push_back(value("backup", "false")); + + if (m_readonly) + set.push_back(value("inROM", "true")); + + if (m_copy_prevention) + set.push_back(value("copy-prevention", "true")); + + return set; +} + +void +PalmLib::FlatFile::Database::doneWithSchema() +{ + // Ensure that the database has at least one field. + if (getNumOfFields() == 0) + return; +// throw PalmLib::error("at least one field must be specified"); + + // Ensure that the database has a title. + if (title().empty()) + return; +// throw PalmLib::error("a title must be specified"); +} diff --git a/src/translators/pilotdb/libflatfile/Database.h b/src/translators/pilotdb/libflatfile/Database.h new file mode 100644 index 0000000..5bcba32 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/Database.h @@ -0,0 +1,320 @@ +/* + * palm-db-tools: Abstract adaptor for flat-file databases. + * Copyright (C) 1999-2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_FLATFILE_DATABASE_H__ +#define __PALMLIB_FLATFILE_DATABASE_H__ + +#include <vector> +#include <string> +#include <utility> + +#include "../libpalm/Database.h" +#include "Field.h" +#include "Record.h" +#include "ListView.h" +#include "FType.h" + +#define NOTETITLE_LENGTH 32 + +namespace PalmLib { + namespace FlatFile { + + // This class is an in-memory representation of a typical + // PalmOS flat-file database. The caller can request write the + // data to a real PalmLib::Database object at any time to + // actually obtain the data in a format usable on the Palm + // Pilot. + + class Database { + public: + // convenience type for the options list parsing + typedef std::vector< std::pair< std::string, std::string> > options_list_t; + + /** + * Default constructor which creates an empty + * database. Subclasses should provide a default + * constructor and an additional constructorwhich takes a + * PalmOS::Database as an argument. + */ + Database(std::string p_Type) + : m_backup(false), m_readonly(false), + m_copy_prevention(false), m_Type(p_Type) + { } + + /** + * Constructor which fills the flat-file structure from a + * PalmOS database. + * + * @param pdb PalmOS database to read from. + */ + Database(std::string p_Type, const PalmLib::Database& pdb); + + /** + * The destructor is empty since we have no other objects + * to dispose of. It is virtual since we have subclasses + * for specific flat-file database products. + */ + virtual ~Database() { } + + /** + * After all processing to add fields and records is done, + * outputPDB is called to create the actual file format + * used by the flat-file database product. This method is + * abstract since only subclasses know the specific file + * formats. + * + * @param pdb An instance of PalmLib::Database. + */ + virtual void outputPDB(PalmLib::Database& pdb) const; + + /** + * Return the title of this flat-file database. + */ + virtual std::string title() const; + + /** + * Set the title of this database. + * + * @param title New title of the database. + */ + virtual void title(const std::string& title); + + /** + * Return the maximum number of fields allowed in the + * database. The object will not allow the number of + * fields to exceed the returned value. This method is + * abstract since only the subclasses know the limit on + * fields. 0 is returned if there is no limit. + */ + virtual unsigned getMaxNumOfFields() const = 0; + + /** + * Return the number of fields in the database. + */ + virtual unsigned getNumOfFields() const; + + /** + * Accessor function for the name of a field. + */ + virtual std::string field_name(int i) const; + + /** + * Accessor function for type of a field. + */ + virtual Field::FieldType field_type(int i) const; + + /** + * Accessor function for the field informations + */ + virtual FType field(int i) const; + + /** + * write the format of the field's argument in format, + * and return a strings' vector with name of each argument part. + * the format use the same display as used by printf + */ + virtual std::vector<std::string> field_argumentf(int, std::string& format) + { format = std::string(""); return std::vector<std::string>(0, std::string(""));} + + /** + * Add a field to the flat-file database. An exception + * will be thrown if the maximum number of fields would be + * exceeded or the field type is unsupported. + * + * @param name Name of the new field. + * @param type The type of the new field. + */ + virtual void appendField(FType field); + virtual void appendField(const std::string& name, + Field::FieldType type, std::string data = std::string("")); + + /** + * Insert a field to the flat-file database. An exception + * will be thrown if the maximum number of fields would be + * exceeded or the field type is unsupported. + * + * @param name Name of the new field. + * @param type The type of the new field. + */ + virtual void insertField(int i, FType field); + virtual void insertField(int i, const std::string& name, + Field::FieldType type, std::string data = std::string("")); + + /** + * Remove a Field in the flat-file database. An Exception + * will thrown if the field doesn't exist. + */ + virtual void removeField(int i); + + /** + * Returns true if this database supports a specific field + * type. This method is abstract since only the subclasses + * know which field types are supported. + * + * @param type The field type that should be checked for support. + */ + virtual bool supportsFieldType(const Field::FieldType& type) const = 0; + + /** + * Return the number of records in the database. + */ + virtual unsigned getNumRecords() const; + + /** + * Return the record with the given index. The caller gets + * a private copy of the data and _not_ a reference to the + * data. + * + * @param index Index of the record to retrieve. + */ + virtual Record getRecord(unsigned index) const; + + /** + * Append a record to the database. An exception will be + * thrown if their are not enough fields or if field types + * mismatch. + * + * @param rec The record to append. + */ + virtual void appendRecord(Record rec); + + /** + * Remove all records from the database + */ + virtual void clearRecords(); + + /** + * Remove a record from the database + */ + virtual void deleteRecord(unsigned index); + + /** + * Return the maximum number of views supported by this + * type of flat-file database. This method is abstract + * since only the subclasses know the exact value. + */ + virtual unsigned getMaxNumOfListViews() const = 0; + + /** + * Return the actual number of views present in this + * database. + */ + virtual unsigned getNumOfListViews() const; + + /** + * Return a copy of the list view at the given index. + * + * @param index Index of the list view to return. + */ + virtual ListView getListView(unsigned index) const; + + /** + * Set the list view at the given index to the new list + * view. An exception may be thrown if field numbers are + * invalid or the list view doesn't pass muster with the + * subclass. + * + * @param index Index of the list view to set. + * @param listview The new list view. + */ + virtual void setListView(unsigned index, const ListView& listview); + + /** + * Append a new list view. This will fail if the maximum + * number of list views would be exceeded. + * + * @param listview The new list view to append. + */ + virtual void appendListView(const ListView& listview); + + /** + * Remove a list view. + * + * @param index Index of the list view to remove. + */ + virtual void removeListView(unsigned index); + + /** + * Process a special option. If the option is not + * supported, then it is silently ignored. Subclasses + * should call the base class first so that options common + * to all flat-file databases can be processed. + * + * @param name Name of the option. + * @param value String value assigned to the option. */ + virtual void setOption(const std::string& name, + const std::string& value); + + /** + * Return a list of of all extra options supported by this + * database. Subclasses should call the base class first + * and then merge any extra options. Get a list of extra + * options. + */ + virtual options_list_t getOptions(void) const; + + /** + * Hook function which should be invoked by a caller after + * all calls the meta-deta functions have completed. This + * allows the database type-specific code to do final + * checks on the meta-data. An exception will be throw if + * there is an error. Otherwise, nothing will happen. + */ + virtual void doneWithSchema(); + + /** + * Change and Return the about information + * of the database when it's supportted + */ + virtual void setAboutInformation(std::string _string) + { + about.information = _string; + } + + virtual std::string getAboutInformation() const + { + return about.information; + } + + std::string type() const + { + return m_Type; + } + + private: + // We provide a dummy copy constructor and assignment + // operator in order to prevent any copying of the object. + Database(const Database&) { } + Database& operator = (const Database&) { return *this; } + +/* typedef std::vector< std::pair< std::string, Field::FieldType > >*/ + typedef std::vector< FType> + field_list_t; + typedef std::vector<Record> record_list_t; + typedef std::vector<ListView> listview_list_t; + + typedef std::vector< std::pair< std::string, std::vector< std::string > > > + listitems_list_t; + + field_list_t m_fields; // database schema + record_list_t m_records; // the database itself + listitems_list_t m_list; // the items lists include in the database + listview_list_t m_listviews; // list views + bool m_backup; // backup flag for PDB + bool m_readonly; // readonly flag for PDB + bool m_copy_prevention; // copy prevention for PDB + std::string m_title; // name of database + class About + { + public: + std::string information; + } about; + std::string m_Type; + }; + + } // namespace FlatFile +} // namespace PalmLib + +#endif diff --git a/src/translators/pilotdb/libflatfile/FType.h b/src/translators/pilotdb/libflatfile/FType.h new file mode 100644 index 0000000..86396b3 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/FType.h @@ -0,0 +1,48 @@ +/* + * palm-db-tools: Field Type definitions for flat-file database objects. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_FLATFILE_FTYPE_H__ +#define __PALMLIB_FLATFILE_FTYPE_H__ + +#include <string> +#include <utility> + +#include "../libpalm/palmtypes.h" +#include "Field.h" + +namespace PalmLib { + namespace FlatFile { + + class FType { + public: + friend class PalmLib::FlatFile::Field; + FType(std::string title, PalmLib::FlatFile::Field::FieldType type) : + m_title(title), m_type(type), m_data("") { } + + FType(std::string title, PalmLib::FlatFile::Field::FieldType type, std::string data) : + m_title(title), m_type(type), m_data(data) { } + + virtual ~FType() { } + + std::string title() const {return m_title;} + virtual PalmLib::FlatFile::Field::FieldType type() const + { return m_type;} + + virtual std::string argument() const { return m_data;} + + void set_argument( const std::string data) { m_data = data;} + + void setTitle( const std::string value) { m_title = value;} + void setType( const PalmLib::FlatFile::Field::FieldType value) { m_type = value;} + private: + std::string m_title; + PalmLib::FlatFile::Field::FieldType m_type; + + std::string m_data; + }; + } +} + +#endif diff --git a/src/translators/pilotdb/libflatfile/Field.h b/src/translators/pilotdb/libflatfile/Field.h new file mode 100644 index 0000000..583bc21 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/Field.h @@ -0,0 +1,119 @@ +/* + * palm-db-tools: Field definitions for flat-file database objects. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_FLATFILE_FIELD_H__ +#define __PALMLIB_FLATFILE_FIELD_H__ + +#include <string> + +#include "../libpalm/palmtypes.h" + +namespace PalmLib { + namespace FlatFile { + + class Field { + public: + Field() : no_value(false), type(STRING), v_boolean(false), + v_integer(0), v_float(0) { } + ~Field() { } + + // True if this field has no value. (NULL in SQL terms) + bool no_value; + + enum FieldType { + STRING, + BOOLEAN, + INTEGER, + FLOAT, + DATE, + TIME, + DATETIME, + LIST, + LINK, + NOTE, + CALCULATED, + LINKED, + LAST + }; + + enum FieldType type; + + std::string v_string; + std::string v_note; + bool v_boolean; + PalmLib::pi_int32_t v_integer; + long double v_float; + struct { + int month; + int day; + int year; + } v_date; // valid for DATE and DATETIME + struct { + int hour; + int minute; + } v_time; // valid for TIME and DATETIME + + /* + friend Field operator = (const Field& y) + { + this.v_string = y.v_string; + this.v_boolean = y.v_boolean; + this.v_integer = y.v_integer; + this.v_float = y.v_float; + this.v_date.month = y.v_date.month; + this.v_date.day = y.v_date.day; + this.v_date.year = y.v_date.year; + this.v_time.hour = y.v_time.hour; + this.v_time.minute = y.v_time.minute; + this.v_note = y.v_note; + } +*/ + friend bool operator==(const Field& x, const Field& y) + { + if (x.type != y.type) + return false; + switch (x.type) { + case STRING: + return (x.v_string == y.v_string); + case BOOLEAN: + return (x.v_boolean == y.v_boolean); + case INTEGER: + return (x.v_integer == y.v_integer); + case FLOAT: + return (x.v_float == y.v_float); + case DATE: + return (x.v_date.month == y.v_date.month + && x.v_date.day == y.v_date.day + && x.v_date.year == y.v_date.year); + case TIME: + return (x.v_time.hour == y.v_time.hour + && x.v_time.minute == y.v_time.minute); + case DATETIME: + return (x.v_date.month == y.v_date.month + && x.v_date.day == y.v_date.day + && x.v_date.year == y.v_date.year + && x.v_time.hour == y.v_time.hour + && x.v_time.minute == y.v_time.minute); + case LIST: + return (x.v_string == y.v_string); + case LINK: + return (x.v_string == y.v_string); + case NOTE: + return (x.v_string == y.v_string + && x.v_note == y.v_note); + case CALCULATED: + return true; //a calculated field could be recalculate at each time + case LINKED: + return (x.v_string == y.v_string); + default: + return (x.v_string == y.v_string); + } + } + }; + + } +} + +#endif diff --git a/src/translators/pilotdb/libflatfile/ListView.h b/src/translators/pilotdb/libflatfile/ListView.h new file mode 100644 index 0000000..4229548 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/ListView.h @@ -0,0 +1,77 @@ +#ifndef __PALMOS__FLATFILE__VIEW_H__ +#define __PALMOS__FLATFILE__VIEW_H__ + +#include <string> +#include <vector> + +#include "ListViewColumn.h" + +namespace PalmLib { + namespace FlatFile { + + // The ListView class represents the a "list view" as + // implemented by the major PalmOS flat-file programs. The + // main idea is a series of columns that display a field of + // the database. + // + // For fun, this class exports the STL interface of the STL + // class it uses to store the ListViewColumn classes. + + class ListView { + private: + typedef std::vector<ListViewColumn> rep_type; + rep_type rep; + + public: + typedef rep_type::value_type value_type; + typedef rep_type::iterator iterator; + typedef rep_type::const_iterator const_iterator; + typedef rep_type::reference reference; + typedef rep_type::const_reference const_reference; + typedef rep_type::size_type size_type; + typedef rep_type::difference_type difference_type; + typedef rep_type::reverse_iterator reverse_iterator; + typedef rep_type::const_reverse_iterator const_reverse_iterator; + + // global fields + std::string name; + bool editoruse; + + // STL pull-up interface (probably not complete) + iterator begin() { return rep.begin(); } + const_iterator begin() const { return rep.begin(); } + iterator end() { return rep.end(); } + const_iterator end() const { return rep.end(); } + reverse_iterator rbegin() { return rep.rbegin(); } + const_reverse_iterator rbegin() const { return rep.rbegin(); } + reverse_iterator rend() { return rep.rend(); } + const_reverse_iterator rend() const { return rep.rend(); } + size_type size() const { return rep.size(); } + size_type max_size() const { return rep.max_size(); } + bool empty() const { return rep.empty(); } + reference front() { return rep.front(); } + const_reference front() const { return rep.front(); } + reference back() { return rep.back(); } + const_reference back() const { return rep.back(); } + void push_back(const ListViewColumn& x) { rep.push_back(x); } + void pop_back() { rep.pop_back(); } + void clear() { rep.clear(); } + void resize(size_type new_size, const ListViewColumn& x) + { rep.resize(new_size, x); } + void resize(size_type new_size) + { rep.resize(new_size, ListViewColumn()); } + + ListView() : rep(), name(""), editoruse(false) { } + ListView(const ListView& rhs) : rep(rhs.rep), name(rhs.name), editoruse(false) { } + ListView& operator = (const ListView& rhs) { + name = rhs.name; + rep = rhs.rep; + return *this; + } + + }; + + } +} + +#endif diff --git a/src/translators/pilotdb/libflatfile/ListViewColumn.h b/src/translators/pilotdb/libflatfile/ListViewColumn.h new file mode 100644 index 0000000..7b5330e --- /dev/null +++ b/src/translators/pilotdb/libflatfile/ListViewColumn.h @@ -0,0 +1,19 @@ +#ifndef __PALMLIB_FLATFILE_LISTVIEWCOLUMN_H__ +#define __PALMLIB_FLATFILE_LISTVIEWCOLUMN_H__ + +namespace PalmLib { + namespace FlatFile { + + // The ListViewColumn class stores the field number and column + // width for a column in a list view. + + struct ListViewColumn { + ListViewColumn() : field(0), width(80) { } + ListViewColumn(unsigned a1, unsigned a2) : field(a1), width(a2) { } + unsigned field; + unsigned width; + }; + } +} + +#endif diff --git a/src/translators/pilotdb/libflatfile/Makefile.am b/src/translators/pilotdb/libflatfile/Makefile.am new file mode 100644 index 0000000..d3ab012 --- /dev/null +++ b/src/translators/pilotdb/libflatfile/Makefile.am @@ -0,0 +1,20 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = liblibflatfile.a + +AM_CPPFLAGS = $(all_includes) + +liblibflatfile_a_METASOURCES = AUTO + +liblibflatfile_a_SOURCES = DB.cpp Database.cpp + + +EXTRA_DIST = Database.cpp Database.h DB.cpp DB.h Field.h FType.h ListView.h ListViewColumn.h Record.h + +####### kdevelop will overwrite this part!!! (end)############ + +# is this the right way to do this? I need to include the strop.o object file since its +# in the parent directory +liblibflatfile_a_LIBADD = ../strop.o +CLEANFILES = strop.Po + +KDE_OPTIONS = noautodist diff --git a/src/translators/pilotdb/libflatfile/Record.h b/src/translators/pilotdb/libflatfile/Record.h new file mode 100644 index 0000000..537953e --- /dev/null +++ b/src/translators/pilotdb/libflatfile/Record.h @@ -0,0 +1,45 @@ +/* + * palm-db-tools: Field definitions for flat-file database objects. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_FLATFILE_RECORD_H__ +#define __PALMLIB_FLATFILE_RECORD_H__ + +#include <vector> + +#include "Field.h" + +namespace PalmLib { + namespace FlatFile { +// typedef std::vector<Field> Record; + + class Record{ + public: + + const std::vector<Field> fields() const { return m_Fields; } + + void appendField(Field newfield) { m_Fields.push_back(newfield); } + bool created() const { return m_New;} + void created(bool on){ m_New = on;} + bool secret() const { return m_Secret;} + void secret(bool on) { m_Secret = on;} + + bool dirty() const { return m_Dirty; } + void dirty( bool on) { m_Dirty = on; } + + pi_uint32_t unique_id() const { return m_UID; } + void unique_id(pi_uint32_t id) { m_UID = id; } + private: + + std::vector<Field> m_Fields; + bool m_Secret; + bool m_New; + + bool m_Dirty; + pi_uint32_t m_UID; + }; + } +} + +#endif diff --git a/src/translators/pilotdb/libpalm/Block.cpp b/src/translators/pilotdb/libpalm/Block.cpp new file mode 100644 index 0000000..c58f6f1 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Block.cpp @@ -0,0 +1,85 @@ +/* + * palm-db-tools: Encapsulate "blocks" of data. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * The PalmLib::Block class represents a generic block of data. It is + * used to simplify passing arrays of pi_char_t around. + */ + +#include <cstring> + +#include "Block.h" + +void PalmLib::Block::reserve(PalmLib::Block::size_type new_size) +{ + if (new_size > capacity()) { + // Allocate a new buffer containing a copy of the old with the + // remainder zero'ed out. + pointer new_data = new pi_char_t[new_size]; + memcpy(new_data, m_data, m_size); + memset(new_data + m_size, 0, new_size - m_size); + + // Replace the existing buffer. + delete [] m_data; + m_data = new_data; + m_size = new_size; + } +} + +void PalmLib::Block::resize(size_type new_size) +{ + if (new_size < m_size) { + // Copy the data that will remain to a new buffer and switch to it. + pointer new_data = new pi_char_t[new_size]; + memcpy(new_data, m_data, new_size); + + // Replace the existing buffer. + delete [] m_data; + m_data = new_data; + m_size = new_size; + } else if (new_size > m_size) { + // Copy the data that will remain to a new buffer and switch to it. + pointer new_data = new pi_char_t[new_size]; + memcpy(new_data, m_data, m_size); + memset(new_data + m_size, 0, new_size - m_size); + + // Replace the existing buffer. + delete [] m_data; + m_data = new_data; + m_size = new_size; + } +} + +void PalmLib::Block::assign(PalmLib::Block::const_pointer data, + const PalmLib::Block::size_type size) +{ + clear(); + if (data && size > 0) { + m_size = size; + m_data = new pi_char_t[m_size]; + memcpy(m_data, data, m_size); + } +} + +void PalmLib::Block::assign(const PalmLib::Block::size_type size, + const PalmLib::Block::value_type value) +{ + clear(); + if (size > 0) { + m_size = size; + m_data = new pi_char_t[m_size]; + memset(m_data, value, m_size); + } +} + +bool operator == (const PalmLib::Block& lhs, const PalmLib::Block& rhs) +{ + if (lhs.size() == rhs.size()) { + if (lhs.data()) { + if (memcmp(lhs.data(), rhs.data(), lhs.size()) != 0) + return false; + } + return true; + } + return false; +} diff --git a/src/translators/pilotdb/libpalm/Block.h b/src/translators/pilotdb/libpalm/Block.h new file mode 100644 index 0000000..6ed6069 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Block.h @@ -0,0 +1,186 @@ +/* + * palm-db-tools: Encapsulate "blocks" of data. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * The PalmLib::Block class represents a generic block of data. It is + * used to make passing pi_char_t buffers around very easy. The Record + * and Resource classes both inherit from this class. A STL interface + * is also attempted though it is probably not complete. + */ + +#ifndef __PALMLIB_BLOCK_H__ +#define __PALMLIB_BLOCK_H__ + +#include <algorithm> +#include <iterator> + +#include "palmtypes.h" + +namespace PalmLib { + + class Block { + public: + // STL: container type definitions + typedef PalmLib::pi_char_t value_type; + typedef value_type* pointer; + typedef const value_type* const_pointer; + typedef value_type* iterator; + typedef const value_type* const_iterator; + typedef value_type& reference; + typedef const value_type& const_reference; + typedef size_t size_type; + typedef ptrdiff_t difference_type; + + // STL: reverisible container type definitions +#ifdef __GNUG__ + typedef std::reverse_iterator<const_iterator> const_reverse_iterator; + typedef std::reverse_iterator<iterator> reverse_iterator; +#endif + + /** + * Default constructor. + */ + Block() : m_data(0), m_size(0) { } + + /** + * Constructor which fills the block from buffer "raw" with + * length "len". + */ + Block(const_pointer raw, const size_type len) : m_data(0), m_size(0) { + assign(raw, len); + } + + /** + * Constructor which takes a size and allocates a zero'ed out + * buffer of that size. (STL: Sequence: default fill + * constructor) + */ + Block(const size_type size, const value_type value = 0) + : m_data(0), m_size(0) { + assign(size, value); + } + + /** + * Constructor which takes two iterators and builds the block + * from the region between the iterators. (STL: Sequence: + * range constructor) + */ + Block(const_iterator a, const_iterator b) : m_data(0), m_size(0) { + assign(a, b - a); + } + + /** + * Copy constructor. Just copies the data from the other block + * into this block. + */ + Block(const Block& rhs) : m_data(0), m_size(0) { + assign(rhs.data(), rhs.size()); + } + + /** + * Destructor. Just frees the buffer if it exists. + */ + virtual ~Block() { clear(); } + + /** + * Assignment operator. + * + * @param rhs The block whose contents should be copied. + */ + Block& operator = (const Block& rhs) { + assign(rhs.data(), rhs.size()); + return *this; + } + + // STL: Container + iterator begin() { return m_data; } + const_iterator begin() const { return m_data; } + iterator end() { return (m_data != 0) ? (m_data + m_size) : (0); } + const_iterator end() const + { return (m_data != 0) ? (m_data + m_size) : (0); } + size_type size() const { return m_size; } + size_type max_size() const { + return size_type(-1) / sizeof(value_type); + } + bool empty() const { return m_size == 0; } + + // STL: Reversible Container +#ifdef __GNUG__ + reverse_iterator rbegin() { return reverse_iterator(end()); } + const_reverse_iterator rbegin() const { + return const_reverse_iterator(end()); + } + reverse_iterator rend() { return reverse_iterator(begin()); } + const_reverse_iterator rend() const { + return const_reverse_iterator(begin()); + } +#endif + + // STL: Random Access Container + reference operator [] (size_type index) { return m_data[index]; } + const_reference operator [] (size_type index) const + { return m_data[index]; } + + // STL: Sequence (not complete) + void clear() { + if (m_data) { + delete [] m_data; + m_data = 0; + m_size = 0; + } + } + void resize(size_type n); + reference front() { return m_data[0]; } + const_reference front() const { return m_data[0]; } + + // STL: (present in vector but not part of a interface spec) + size_type capacity() const { return m_size; } + void reserve(size_type size); + + /** + * Return a pointer to the data area. If there are no + * contents, then the return value will be NULL. This is not + * an STL method but goes with this class as a singular data + * block and not a container (even though it is). + */ + iterator data() { return m_data; } + const_iterator data() const { return m_data; } + + /** + * Replace the existing contents of the Block with the buffer + * that starts at raw of size len. + * + * @param raw Pointer to the new contents. + * @param len Size of the new contents. + */ + void assign(const_pointer data, const size_type size); + + /** + * Replace the existing contents of the Block with a buffer + * consisting of size elements equal to fill. + * + * @param size The size of the new contents. + * @param value Value to fill the contents with. + */ + void assign(const size_type size, const value_type value = 0); + + // compatiblity functions (remove before final 0.3.0 release) + const_pointer raw_data() const { return data(); } + pointer raw_data() { return data(); } + size_type raw_size() const { return size(); } + void set_raw(const_pointer raw, const size_type len) + { assign(raw, len); } + + private: + pointer m_data; + size_type m_size; + }; + +} + +bool operator == (const PalmLib::Block& lhs, const PalmLib::Block& rhs); + +inline bool operator != (const PalmLib::Block& lhs, const PalmLib::Block& rhs) +{ return ! (lhs == rhs); } + +#endif diff --git a/src/translators/pilotdb/libpalm/Database.cpp b/src/translators/pilotdb/libpalm/Database.cpp new file mode 100644 index 0000000..38d896f --- /dev/null +++ b/src/translators/pilotdb/libpalm/Database.cpp @@ -0,0 +1,43 @@ +/* + * palm-db-tools: General interface to a PalmOS database. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This file implens an abstract interface to PalmOS + * databases. Subclasses would include the class that reads/writes PDB + * files and possibly databases that can be accessed over the HotSync + * protocols. + */ + +#include "palmtypes.h" +#include "Record.h" +#include "Database.h" + +#ifndef __GNUG__ + +// MSVC: Visual C++ doesn't like initializers in the header ... +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_RESOURCE = 0x0001; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_READ_ONLY = 0x0002; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_APPINFO_DIRTY = 0x0004; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_BACKUP = 0x0008; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_OK_TO_INSTALL_NEWER = 0x0010; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_RESET_AFTER_INSTALL = 0x0020; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_COPY_PREVENTION = 0x0040; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_STREAM = 0x0080; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_HIDDEN = 0x0100; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_LAUNCHABLE_DATA = 0x0200; +const PalmLib::pi_uint16_t PalmLib::Database::FLAG_HDR_OPEN = 0x8000; +const PalmLib::pi_char_t PalmLib::Record::FLAG_ATTR_DELETED = 0x80; +const PalmLib::pi_char_t PalmLib::Record::FLAG_ATTR_DIRTY = 0x40; +const PalmLib::pi_char_t PalmLib::Record::FLAG_ATTR_BUSY = 0x20; +const PalmLib::pi_char_t PalmLib::Record::FLAG_ATTR_SECRET = 0x10; + +#endif + +PalmLib::Database::Database(bool resourceDB) + : m_name(""), m_version(0), m_time_created(0), m_time_modified(0), + m_time_backup(0), m_modification(0), m_unique_id_seed(0) +{ + m_flags = resourceDB ? FLAG_HDR_RESOURCE : 0; + m_type = PalmLib::mktag(' ', ' ', ' ', ' '); + m_creator = PalmLib::mktag(' ', ' ', ' ', ' '); +} diff --git a/src/translators/pilotdb/libpalm/Database.h b/src/translators/pilotdb/libpalm/Database.h new file mode 100644 index 0000000..bcde8c0 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Database.h @@ -0,0 +1,181 @@ +/* + * palm-db-tools: General interface to a PalmOS database. + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This header defines an abstract interface to PalmOS + * databases. Subclasses would include the class that reads/writes PDB + * files and possibly databases that can be accessed over the HotSync + * protocols. + */ + +#ifndef __PALMLIB_DATABASE_H__ +#define __PALMLIB_DATABASE_H__ + +#include <string> + +#include "palmtypes.h" +#include "Block.h" +#include "Record.h" +#include "Resource.h" + +namespace PalmLib { + + class Database { + public: + // Constants for bits in the flags field of a PalmOS database. +#ifdef __GNUG__ + static const pi_uint16_t FLAG_HDR_RESOURCE = 0x0001; + static const pi_uint16_t FLAG_HDR_READ_ONLY = 0x0002; + static const pi_uint16_t FLAG_HDR_APPINFO_DIRTY = 0x0004; + static const pi_uint16_t FLAG_HDR_BACKUP = 0x0008; + static const pi_uint16_t FLAG_HDR_OK_TO_INSTALL_NEWER = 0x0010; + static const pi_uint16_t FLAG_HDR_RESET_AFTER_INSTALL = 0x0020; + static const pi_uint16_t FLAG_HDR_COPY_PREVENTION = 0x0040; + static const pi_uint16_t FLAG_HDR_STREAM = 0x0080; + static const pi_uint16_t FLAG_HDR_HIDDEN = 0x0100; + static const pi_uint16_t FLAG_HDR_LAUNCHABLE_DATA = 0x0200; + static const pi_uint16_t FLAG_HDR_OPEN = 0x8000; +#else + static const pi_uint16_t FLAG_HDR_RESOURCE; + static const pi_uint16_t FLAG_HDR_READ_ONLY; + static const pi_uint16_t FLAG_HDR_APPINFO_DIRTY; + static const pi_uint16_t FLAG_HDR_BACKUP; + static const pi_uint16_t FLAG_HDR_OK_TO_INSTALL_NEWER; + static const pi_uint16_t FLAG_HDR_RESET_AFTER_INSTALL; + static const pi_uint16_t FLAG_HDR_COPY_PREVENTION; + static const pi_uint16_t FLAG_HDR_STREAM; + static const pi_uint16_t FLAG_HDR_HIDDEN; + static const pi_uint16_t FLAG_HDR_LAUNCHABLE_DATA; + static const pi_uint16_t FLAG_HDR_OPEN; +#endif + + Database(bool resourceDB = false); + virtual ~Database() { } + + bool isResourceDB() const {return (m_flags & FLAG_HDR_RESOURCE) != 0;} + + virtual pi_uint32_t type() const { return m_type; } + virtual void type(pi_uint32_t new_type) { m_type = new_type; } + + virtual pi_uint32_t creator() const { return m_creator; } + virtual void creator(pi_uint32_t new_creator) + { m_creator = new_creator; } + + virtual pi_uint16_t version() const { return m_version; } + virtual void version(pi_uint16_t v) { m_version = v; } + + virtual pi_int32_t creation_time() const { return m_time_created; } + virtual void creation_time(pi_int32_t ct) { m_time_created = ct; } + + virtual pi_uint32_t modification_time() const + { return m_time_modified; } + virtual void modification_time(pi_uint32_t mt) + { m_time_modified = mt; } + + virtual pi_uint32_t backup_time() const { return m_time_backup; } + virtual void backup_time(pi_uint32_t bt) { m_time_backup = bt; } + + virtual pi_uint32_t modnum() const { return m_modification; } + virtual void modnum(pi_uint32_t new_modnum) + { m_modification = new_modnum; } + + virtual pi_uint32_t unique_id_seed() const + { return m_unique_id_seed; } + virtual void unique_id_seed(pi_uint32_t uid_seed) + { m_unique_id_seed = uid_seed; } + + virtual pi_uint16_t flags() const { return m_flags; } + virtual void flags(pi_uint16_t flags) + { m_flags = flags & ~(FLAG_HDR_RESOURCE | FLAG_HDR_OPEN); } + + virtual std::string name() const { return m_name; } + virtual void name(const std::string& new_name) { m_name = new_name; } + + virtual bool backup() const + { return (m_flags & FLAG_HDR_BACKUP) != 0; } + virtual void backup(bool state) { + if (state) + m_flags |= FLAG_HDR_BACKUP; + else + m_flags &= ~(FLAG_HDR_BACKUP); + } + + virtual bool readonly() const + { return (m_flags & FLAG_HDR_READ_ONLY) != 0; } + virtual void readonly(bool state) { + if (state) + m_flags |= FLAG_HDR_READ_ONLY; + else + m_flags &= ~(FLAG_HDR_READ_ONLY); + } + + virtual bool copy_prevention() const + { return (m_flags & FLAG_HDR_COPY_PREVENTION) != 0; } + virtual void copy_prevention(bool state) { + if (state) + m_flags |= FLAG_HDR_COPY_PREVENTION; + else + m_flags &= ~(FLAG_HDR_COPY_PREVENTION); + } + + // Return the total number of records/resources in this + // database. + virtual unsigned getNumRecords() const = 0; + + // Return the database's application info block as a Block + // object. + virtual Block getAppInfoBlock() const { return Block(); } + + // Set the database's app info block to the contents of the + // passed Block object. + virtual void setAppInfoBlock(const Block &) { } + + // Return the database's sort info block as a Block object. + virtual Block getSortInfoBlock() const { return Block(); } + + // Set the database's sort info block to the contents of the + // passed Block object. + virtual void setSortInfoBlock(const Block &) { } + + // Return the record identified by the given index. The caller + // owns the returned RawRecord object. + virtual Record getRecord(unsigned index) const = 0; + + // Set the record identified by the given index to the given + // record. + virtual void setRecord(unsigned index, const Record& rec) = 0; + + // Append the given record to the database. + virtual void appendRecord(const Record& rec) = 0; + + // returned if the specified (type, ID) combination is not + // present in the database. The caller owns the returned + // RawRecord object. + virtual Resource getResourceByType(pi_uint32_t type, + pi_uint32_t id) const = 0; + + // Return the resource present at the given index. NULL is + // returned if the index is invalid. The caller owns the + // returned RawRecord object. + virtual Resource getResourceByIndex(unsigned index) const = 0; + + // Set the resouce at given index to passed Resource object. + virtual void setResource(unsigned index, const Resource& rsrc) = 0; + + private: + std::string m_name; + pi_uint16_t m_flags; + pi_uint16_t m_version; + pi_uint32_t m_time_created; + pi_uint32_t m_time_modified; + pi_uint32_t m_time_backup; + pi_uint32_t m_modification; + pi_uint32_t m_type; + pi_uint32_t m_creator; + pi_uint32_t m_unique_id_seed; + + }; + +} // namespace PalmLib + +#endif diff --git a/src/translators/pilotdb/libpalm/Makefile.am b/src/translators/pilotdb/libpalm/Makefile.am new file mode 100644 index 0000000..ea92331 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Makefile.am @@ -0,0 +1,15 @@ +####### kdevelop will overwrite this part!!! (begin)########## +noinst_LIBRARIES = liblibpalm.a + +AM_CPPFLAGS = $(all_includes) + +liblibpalm_a_METASOURCES = AUTO + +liblibpalm_a_SOURCES = Database.cpp Block.cpp + + +EXTRA_DIST = Block.cpp Block.h palmtypes.h Record.h Resource.h Database.h Database.cpp + +####### kdevelop will overwrite this part!!! (end)############ + +KDE_OPTIONS = noautodist diff --git a/src/translators/pilotdb/libpalm/Record.h b/src/translators/pilotdb/libpalm/Record.h new file mode 100644 index 0000000..ecf19e3 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Record.h @@ -0,0 +1,168 @@ +/* + * palm-db-tools: Raw PalmOS Records + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_RECORD_H__ +#define __PALMLIB_RECORD_H__ + +#include "Block.h" + +namespace PalmLib { + + class Record : public Block { + public: +#ifdef __GNUG__ + static const pi_char_t FLAG_ATTR_DELETED = 0x80; + static const pi_char_t FLAG_ATTR_DIRTY = 0x40; + static const pi_char_t FLAG_ATTR_BUSY = 0x20; + static const pi_char_t FLAG_ATTR_SECRET = 0x10; +#else + static const pi_char_t FLAG_ATTR_DELETED; + static const pi_char_t FLAG_ATTR_DIRTY; + static const pi_char_t FLAG_ATTR_BUSY; + static const pi_char_t FLAG_ATTR_SECRET; +#endif + + /** + * Default constructor. + */ + Record() : Block(), m_attrs(0), m_unique_id(0) { } + + /** + * Copy constructor. + */ + Record(const Record& rhs) : Block(rhs.data(), rhs.size()) { + m_attrs = rhs.attrs(); + m_unique_id = rhs.unique_id(); + } + + /** + * Destructor. + */ + virtual ~Record() { } + + /** + * Constructor which lets the caller specify all the + * parameters. + * + * @param attrs Attribute byte (flags + category). + * @param unique_id Unique ID for this record. + * @param data Start of buffer to copy (or 0 for empty). + * @param size Size of the buffer to copy. + */ + Record(pi_char_t attrs, pi_uint32_t unique_id, + Block::const_pointer data, const Block::size_type size) + : Block(data, size), m_attrs(attrs), m_unique_id(unique_id) { } + + /** + * Constructor which lets the caller use the default fill + * constructor. + * @param attrs Attribute byte (flags + category). + * @param unique_id Unique ID for this record. + * @param size Size of buffer to generate. + * @param value Value to fill buffer with. + */ + Record(pi_char_t attrs, pi_uint32_t unique_id, + const size_type size, const value_type value = 0) + : Block(size, value), m_attrs(attrs), m_unique_id(unique_id) { } + + /** + * Assignment operator. + * + * @param rhs The PalmLib::Record we should become. */ + Record& operator = (const Record& rhs) { + Block::operator = (rhs); + m_attrs = rhs.attrs(); + m_unique_id = rhs.unique_id(); + return *this; + } + + /** + * Return the attributes byte which contains the category and + * flags. + */ + pi_char_t attrs() const { return m_attrs; } + + /** + * Return the state of the record's "deleted" flag. + */ + bool deleted() const { return (m_attrs & FLAG_ATTR_DELETED) != 0; } + + /** + * Set the state of the record's "deleted" flag. + * + * @param state New state of the "deleted" flag. + */ + void deleted(bool state) { + if (state) + m_attrs |= FLAG_ATTR_DELETED; + else + m_attrs &= ~(FLAG_ATTR_DELETED); + } + + /** + * Return the state of the record's "dirty" flag. + */ + bool dirty() const { return (m_attrs & FLAG_ATTR_DIRTY) != 0; } + + /** + * Set the state of the record's "dirty" flag. + * + * @param state New state of the "dirty" flag. + */ + void dirty(bool state) { + if (state) + m_attrs |= FLAG_ATTR_DIRTY; + else + m_attrs &= ~(FLAG_ATTR_DIRTY); + } + + /** + * Return the state of the record's "secret" flag. + */ + bool secret() const { return (m_attrs & FLAG_ATTR_SECRET) != 0; } + + /** + * Set the state of the record's "secret" flag. + * + * @param state New state of the "secret" flag. + */ + void secret(bool state) { + if (state) + m_attrs |= FLAG_ATTR_SECRET; + else + m_attrs &= ~(FLAG_ATTR_SECRET); + } + + /** + * Return the category of this record. + */ + pi_char_t category() const { return (m_attrs & 0x0F); } + + /** + * Set the category of this record. + */ + void category(pi_char_t cat) + { m_attrs &= ~(0x0F); m_attrs |= (cat & 0x0F); } + + /** + * Return the unique ID of this record. + */ + pi_uint32_t unique_id() const { return m_unique_id; } + + /** + * Set the unique ID of this record to uid. + * + * @param uid New unique ID for this record. + */ + void unique_id(pi_uint32_t uid) { m_unique_id = uid; } + + private: + pi_char_t m_attrs; + pi_uint32_t m_unique_id; + }; + +} + +#endif diff --git a/src/translators/pilotdb/libpalm/Resource.h b/src/translators/pilotdb/libpalm/Resource.h new file mode 100644 index 0000000..b98f718 --- /dev/null +++ b/src/translators/pilotdb/libpalm/Resource.h @@ -0,0 +1,85 @@ +/* + * palm-db-tools: PalmOS Resources + * Copyright (C) 2000 by Tom Dyas (tdyas@users.sourceforge.net) + */ + +#ifndef __PALMLIB_RESOURCE_H__ +#define __PALMLIB_RESOURCE_H__ + +#include "Block.h" +#include "palmtypes.h" + +namespace PalmLib { + + class Resource : public Block { + public: + /** + * Default constructor. + */ + Resource() : Block(), m_type(0), m_id(0) { } + + /** + * Copy constructor. + */ + Resource(const Resource& rhs) : Block(rhs.data(), rhs.size()) { + m_type = rhs.type(); + m_id = rhs.id(); + } + + /** + * Destructor. + */ + virtual ~Resource() { } + + /** + * Constructor which lets the caller specify all the + * parameters. + * + * @param type Resource type + * @param id Resource ID + * @param data Start of buffer to copy. + * @param size Size of the buffer to copy. + */ + Resource(pi_uint32_t type, pi_uint32_t id, + const_pointer data, const size_type size) + : Block(data, size), m_type(type), m_id(id) { } + + /** + * Constructor which lets the caller use the default fill + * constructor. + * + * @param type Resource type + * @param id Resource ID + * @param size Size of buffer to generate. + * @param value Value to fill buffer with. + */ + Resource(pi_uint32_t type, pi_uint32_t id, + const size_type size, const value_type value = 0) + : Block(size, value), m_type(type), m_id(id) { } + + /** + * Assignment operator. + */ + Resource& operator = (const Resource& rhs) { + Block::operator = (rhs); + m_type = rhs.type(); + m_id = rhs.id(); + return *this; + } + + // Accessor functions for the resource type. + pi_uint32_t type() const { return m_type; } + void type(const pi_uint32_t _type) { m_type = _type; } + + // Accessor functions for the resource ID. + pi_uint32_t id() const { return m_id; } + void id(const pi_uint32_t _id) { m_id = _id; } + + private: + pi_uint32_t m_type; + pi_uint32_t m_id; + }; + +} + +#endif diff --git a/src/translators/pilotdb/libpalm/palmtypes.h b/src/translators/pilotdb/libpalm/palmtypes.h new file mode 100644 index 0000000..5c12262 --- /dev/null +++ b/src/translators/pilotdb/libpalm/palmtypes.h @@ -0,0 +1,117 @@ +/* + * This file contains type definitions and helper functions to make + * access to data in Palm Pilot order easier. + */ + +#ifndef __LIBPALM_PALMTYPES_H__ +#define __LIBPALM_PALMTYPES_H__ + +#include <stdexcept> + +#include "../portability.h" + +namespace PalmLib { + +#if SIZEOF_UNSIGNED_CHAR == 1 + typedef unsigned char pi_char_t; +#else +#error Unable to determine the size of pi_char_t. +#endif + +#if SIZEOF_UNSIGNED_LONG == 2 + typedef unsigned long pi_uint16_t; +#elif SIZEOF_UNSIGNED_INT == 2 + typedef unsigned int pi_uint16_t; +#elif SIZEOF_UNSIGNED_SHORT == 2 + typedef unsigned short pi_uint16_t; +#else +#error Unable to determine the size of pi_uint16_t. +#endif + +#if SIZEOF_LONG == 2 + typedef long pi_int16_t; +#elif SIZEOF_INT == 2 + typedef int pi_int16_t; +#elif SIZEOF_SHORT == 2 + typedef short pi_int16_t; +#else +#error Unable to determine the size of pi_int16_t. +#endif + +#if SIZEOF_UNSIGNED_LONG == 4 + typedef unsigned long pi_uint32_t; +#elif SIZEOF_UNSIGNED_INT == 4 + typedef unsigned int pi_uint32_t; +#elif SIZEOF_UNSIGNED_SHORT == 4 + typedef unsigned short pi_uint32_t; +#else +#error Unable to determine the size of pi_uint32_t. +#endif + +#if SIZEOF_LONG == 4 + typedef long pi_int32_t; +#elif SIZEOF_INT == 4 + typedef int pi_int32_t; +#elif SIZEOF_SHORT == 4 + typedef short pi_int32_t; +#else +#error Unable to determine the size of pi_int32_t. +#endif + +typedef union { + double number; +#ifdef WORDS_BIGENDIAN + struct { + PalmLib::pi_uint32_t hi; + PalmLib::pi_uint32_t lo; + } words; +#else + struct { + PalmLib::pi_uint32_t lo; + PalmLib::pi_uint32_t hi; + } words; +#endif +} pi_double_t; + + inline pi_int32_t get_long(const pi_char_t* p) { + return ( (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3] ); + } + + inline pi_int32_t get_treble(const pi_char_t* p) { + return ( (p[0] << 16) || (p[1] << 8) || p[0]); + } + + inline pi_int16_t get_short(const pi_char_t* p) { + return ( (p[0] << 8) | p[1] ); + } + + inline void set_long(pi_char_t *p, pi_int32_t v) { + p[0] = (v >> 24) & 0xFF; + p[1] = (v >> 16) & 0xFF; + p[2] = (v >> 8 ) & 0xFF; + p[3] = (v ) & 0xFF; + } + + inline void set_treble(pi_char_t *p, pi_int32_t v) { + p[0] = (v >> 16) & 0xFF; + p[1] = (v >> 8 ) & 0xFF; + p[2] = (v ) & 0xFF; + } + + inline void set_short(pi_char_t *p, pi_int16_t v) { + p[0] = (v >> 8) & 0xFF; + p[1] = (v ) & 0xFF; + } + + inline pi_uint32_t mktag(pi_char_t c1, pi_char_t c2, + pi_char_t c3, pi_char_t c4) + { return (((c1)<<24)|((c2)<<16)|((c3)<<8)|(c4)); } + + class error : public std::runtime_error { + public: + error(const std::string & what_arg) : std::runtime_error(what_arg) { } + }; + +} // namespace PalmLib + +#endif diff --git a/src/translators/pilotdb/pilotdb.cpp b/src/translators/pilotdb/pilotdb.cpp new file mode 100644 index 0000000..d7779e4 --- /dev/null +++ b/src/translators/pilotdb/pilotdb.cpp @@ -0,0 +1,277 @@ +/*************************************************************************** + copyright : (C) 2003-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 "pilotdb.h" +#include "strop.h" +#include "libflatfile/Record.h" + +#include <kdebug.h> + +#include <qbuffer.h> + +using namespace PalmLib; +using Tellico::Export::PilotDB; + +namespace { + static const int PI_HDR_SIZE = 78; + static const int PI_RESOURCE_ENT_SIZE = 10; + static const int PI_RECORD_ENT_SIZE = 8; +} + +PilotDB::PilotDB() : Database(false), m_app_info(), m_sort_info(), + m_next_record_list_id(0) { + pi_int32_t now = StrOps::get_current_time(); + creation_time(now); + modification_time(now); + backup_time(now); +} + +PilotDB::~PilotDB() { + for(record_list_t::iterator i = m_records.begin(); i != m_records.end(); ++i) { + delete (*i); + } +} + +QByteArray PilotDB::data() { + QBuffer b; + b.open(IO_WriteOnly); + + pi_char_t buf[PI_HDR_SIZE]; + pi_int16_t ent_hdr_size = isResourceDB() ? PI_RESOURCE_ENT_SIZE : PI_RECORD_ENT_SIZE; + std::streampos offset = PI_HDR_SIZE + m_records.size() * ent_hdr_size + 2; + + for(int i=0; i<32; ++i) { + buf[i] = 0; + } + memcpy(buf, name().c_str(), QMIN(31, name().length())); + set_short(buf + 32, flags()); + set_short(buf + 34, version()); + set_long(buf + 36, creation_time()); + set_long(buf + 40, modification_time()); + set_long(buf + 44, backup_time()); + set_long(buf + 48, modnum()); + if(m_app_info.raw_size() > 0) { + set_long(buf + 52, offset); + offset += m_app_info.raw_size(); + } else { + set_long(buf + 52, 0); + } + if(m_sort_info.raw_size() > 0) { + set_long(buf + 56, offset); + offset += m_sort_info.raw_size(); + } else { + set_long(buf + 56, 0); + } + set_long(buf + 60, type()); + set_long(buf + 64, creator()); + set_long(buf + 68, unique_id_seed()); + set_long(buf + 72, m_next_record_list_id); + set_short(buf + 76, m_records.size()); + + // Write the PDB/PRC header to the string. + b.writeBlock(reinterpret_cast<char *>(buf), sizeof(buf)); + + for(record_list_t::iterator i = m_records.begin(); i != m_records.end(); ++i) { + Block* entry = *i; + + if(isResourceDB()) { + Resource * resource = reinterpret_cast<Resource *> (entry); + set_long(buf, resource->type()); + set_short(buf + 4, resource->id()); + set_long(buf + 6, offset); + } else { + Record * record = reinterpret_cast<Record *> (entry); + set_long(buf, offset); + buf[4] = record->attrs(); + set_treble(buf + 5, record->unique_id()); + } + b.writeBlock(reinterpret_cast<char *>(buf), ent_hdr_size); + offset += entry->raw_size(); + } + + b.writeBlock("\0", 1); + b.writeBlock("\0", 1); + + if(m_app_info.raw_size() > 0) { + b.writeBlock((char *) m_app_info.raw_data(), m_app_info.raw_size()); + } + + if(m_sort_info.raw_size() > 0) { + b.writeBlock((char *) m_sort_info.raw_data(), m_sort_info.raw_size()); + } + + for(record_list_t::iterator q = m_records.begin(); q != m_records.end(); ++q) { + Block* entry = *q; + b.writeBlock((char *) entry->raw_data(), entry->raw_size()); + } + + b.close(); + return b.buffer(); +} + +// Return the record identified by the given index. The caller owns +// the returned RawRecord object. +Record PilotDB::getRecord(unsigned index) const +{ + if (index >= m_records.size()) kdDebug() << "invalid index" << endl; + return *(reinterpret_cast<Record *> (m_records[index])); +} + +// Set the record identified by the given index to the given record. +void PilotDB::setRecord(unsigned index, const Record& rec) +{ +// if (index >= m_records.size()) kdDebug() << "invalid index"); + *(reinterpret_cast<Record *> (m_records[index])) = rec; +} + +// Append the given record to the database. +void PilotDB::appendRecord(const Record& rec) +{ + Record* record = new Record(rec); + + // If this new record has a unique ID that duplicates any other + // record, then reset the unique ID to an unused value. + if (m_uid_map.find(record->unique_id()) != m_uid_map.end()) { + uid_map_t::iterator iter = max_element(m_uid_map.begin(), + m_uid_map.end()); + pi_uint32_t maxuid = (*iter).first; + + // The new unique ID becomes the max plus one. + record->unique_id(maxuid + 1); + } + + m_uid_map[record->unique_id()] = record; + m_records.push_back(record); +} + + +void PilotDB::clearRecords() +{ + m_records.erase(m_records.begin(), m_records.end()); +} + +// Return the resource with the given type and ID. NULL is returned if +// the specified (type, ID) combination is not present in the +// database. The caller owns the returned RawRecord object. +Resource PilotDB::getResourceByType(pi_uint32_t type, pi_uint32_t id) const +{ + for (record_list_t::const_iterator i = m_records.begin(); + i != m_records.end(); ++i) { + Resource* resource = reinterpret_cast<Resource *> (*i); + if (resource->type() == type && resource->id() == id) + return *resource; + } + + kdWarning() << "PilotDB::getResourceByType() - not found!" << endl; + return Resource(); +} + +// Return the resource present at the given index. NULL is returned if +// the index is invalid. The caller owns the returned RawRecord +// object. +Resource PilotDB::getResourceByIndex(unsigned index) const +{ + if (index >= m_records.size()) kdDebug() << "invalid index" << endl; + return *(reinterpret_cast<Resource *> (m_records[index])); +} + +// Set the resouce at given index to passed RawResource object. +void PilotDB::setResource(unsigned index, const Resource& resource) +{ + if (index >= m_records.size()) kdDebug() << "invalid index" << endl; + *(reinterpret_cast<Resource *> (m_records[index])) = resource; +} + +FlatFile::Field PilotDB::string2field(FlatFile::Field::FieldType type, const std::string& fldstr) { + FlatFile::Field field; + + switch (type) { + case FlatFile::Field::STRING: + field.type = FlatFile::Field::STRING; + field.v_string = fldstr; + break; + + case FlatFile::Field::BOOLEAN: + field.type = FlatFile::Field::BOOLEAN; + field.v_boolean = StrOps::string2boolean(fldstr); + break; + + case FlatFile::Field::INTEGER: + field.type = FlatFile::Field::INTEGER; + StrOps::convert_string(fldstr, field.v_integer); + break; + + case FlatFile::Field::FLOAT: + field.type = FlatFile::Field::FLOAT; + StrOps::convert_string(fldstr, field.v_float); + break; + + case FlatFile::Field::NOTE: + field.type = FlatFile::Field::NOTE; + field.v_string = fldstr.substr(0,NOTETITLE_LENGTH - 1); + field.v_note = fldstr; + break; + + case FlatFile::Field::LIST: + field.type = FlatFile::Field::LIST; + field.v_string = fldstr; + break; + + case FlatFile::Field::LINK: + field.type = FlatFile::Field::LINK; + field.v_integer = 0; + field.v_string = fldstr; + break; + + case FlatFile::Field::LINKED: + field.type = FlatFile::Field::LINKED; + field.v_string = fldstr; + break; + + case FlatFile::Field::CALCULATED: + field.type = FlatFile::Field::CALCULATED; + field.v_string = fldstr; + break; + + case FlatFile::Field::DATE: + field.type = FlatFile::Field::DATE; + struct tm time; + if (!fldstr.empty()) { +#ifdef strptime + if(!strptime(fldstr.c_str(), "%Y/%m/%d", &time)) { +#else + if(!StrOps::strptime(fldstr.c_str(), "%Y/%m/%d", &time)) { +#endif + kdDebug() << "invalid date in field" << endl; + } + field.v_date.month = time.tm_mon + 1; + field.v_date.day = time.tm_mday; + field.v_date.year = time.tm_year + 1900; + field.v_time.hour = time.tm_hour; + field.v_time.minute = time.tm_min; + } else { + field.v_date.month = 0; + field.v_date.day = 0; + field.v_date.year = 0; + field.v_time.hour = 24; + field.v_time.minute = 0; + } + break; + + default: + kdWarning() << "PilotDB::string2field() - unsupported field type" << endl; + break; + } + + return field; +} diff --git a/src/translators/pilotdb/pilotdb.h b/src/translators/pilotdb/pilotdb.h new file mode 100644 index 0000000..dd21c7b --- /dev/null +++ b/src/translators/pilotdb/pilotdb.h @@ -0,0 +1,127 @@ +/*************************************************************************** + pilotdb.h + ------------------- + begin : Thu Nov 20 2003 + copyright : (C) 2003 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; * + * * + ***************************************************************************/ + +#ifndef PILOTDB_H +#define PILOTDB_H + +#include <map> +#include <vector> + +#include "libpalm/Database.h" +#include "libflatfile/Field.h" + +#include <qcstring.h> + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class PilotDB : public PalmLib::Database { +public: + PilotDB(); + ~PilotDB(); + + QByteArray data(); + + /** + * Return the total number of records/resources in this database. + */ + virtual unsigned getNumRecords() const { return m_records.size(); } + + /** + * Return the database's application info block as a Block + * object. The caller owns the returned object. + */ + virtual PalmLib::Block getAppInfoBlock() const { return m_app_info; } + + /** + * Set the database's app info block to the contents of the + * passed Block object. + */ + virtual void setAppInfoBlock(const PalmLib::Block& new_app_info) { m_app_info = new_app_info; } + + /** + * Return the database's sort info block as a Block + * object. The caller owns the returned object. + */ + virtual PalmLib::Block getSortInfoBlock() const { return m_sort_info; } + + /** + * Set the database's sort info block to the contents of the + * passed Block object. + */ + virtual void setSortInfoBlock(const PalmLib::Block& new_sort_info) { m_sort_info = new_sort_info; } + + /** + * Return the record identified by the given index. The caller + * owns the returned RawRecord object. + */ + virtual PalmLib::Record getRecord(unsigned index) const; + + /** + * Set the record identified by the given index to the given record. + */ + virtual void setRecord(unsigned index, const PalmLib::Record& rec); + + /** + * Append the given record to the database. + */ + virtual void appendRecord(const PalmLib::Record& rec); + + /** + * Delete all records + */ + virtual void clearRecords(); + + /** + * returned if the specified (type, ID) combination is not + * present in the database. The caller owns the returned RawRecord object. + */ + virtual PalmLib::Resource getResourceByType(PalmLib::pi_uint32_t type, PalmLib::pi_uint32_t id) const; + + /** + * Return the resource present at the given index. NULL is + * returned if the index is invalid. The caller owns the + * returned RawRecord object. + */ + virtual PalmLib::Resource getResourceByIndex(unsigned index) const; + + /** + * Set the resource at given index to passed Resource object. + */ + virtual void setResource(unsigned index, const PalmLib::Resource& rsrc); + + static PalmLib::FlatFile::Field string2field(PalmLib::FlatFile::Field::FieldType type, + const std::string& fldstr); + +protected: + typedef std::vector<PalmLib::Block *> record_list_t; + typedef std::map<PalmLib::pi_uint32_t, PalmLib::Record *> uid_map_t; + + record_list_t m_records; + uid_map_t m_uid_map; + +private: + PalmLib::Block m_app_info; + PalmLib::Block m_sort_info; + PalmLib::pi_int32_t m_next_record_list_id; +}; + + } //end namespace +} // end namespace +#endif diff --git a/src/translators/pilotdb/portability.h b/src/translators/pilotdb/portability.h new file mode 100644 index 0000000..cb41f79 --- /dev/null +++ b/src/translators/pilotdb/portability.h @@ -0,0 +1,72 @@ +/* + * palm-db-tools: Support Library: String Parsing Utility Functions + * Copyright (C) 1999-2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * 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., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __LIBSUPPORT_PORTABILITY_H__ +#define __LIBSUPPORT_PORTABILITY_H__ + +/* + * Pull in the correct configuration header. + */ + +#ifndef WIN32 +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#else +#include "win32/win32-config.h" +#endif + +#ifdef _MSC_VER +/* Borrowed from GLib: Make MSVC more pedantic, this is a recommended + * pragma list from _Win32_Programming_ by Rector and Newcomer. + */ +#pragma warning(error:4002) +#pragma warning(error:4003) +#pragma warning(1:4010) +#pragma warning(error:4013) +#pragma warning(1:4016) +#pragma warning(error:4020) +#pragma warning(error:4021) +#pragma warning(error:4027) +#pragma warning(error:4029) +#pragma warning(error:4033) +#pragma warning(error:4035) +#pragma warning(error:4045) +#pragma warning(error:4047) +#pragma warning(error:4049) +#pragma warning(error:4053) +#pragma warning(error:4071) +#pragma warning(disable:4101) +#pragma warning(error:4150) + +#pragma warning(disable:4244) /* No possible loss of data warnings */ +#pragma warning(disable:4305) /* No truncation from int to char warnings */ +#endif /* _MSC_VER */ + +/* MSVC is screwed up when it comes to calling base class virtual + * functions from a subclass. Thus, the following macro which makes + * calling the superclass nice and simple. + */ +#ifndef _MSC_VER +#define SUPERCLASS(namespace, class, function, args) namespace::class::function args +#else +#define SUPERCLASS(namespace, class, function, args) this-> class::function args +#endif + +#endif diff --git a/src/translators/pilotdb/strop.cpp b/src/translators/pilotdb/strop.cpp new file mode 100644 index 0000000..b8c7f55 --- /dev/null +++ b/src/translators/pilotdb/strop.cpp @@ -0,0 +1,589 @@ +/* + * palm-db-tools: Support Library: String Parsing Utility Functions + * Copyright (C) 1999-2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * 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., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + */ + +#include <string> +#include <vector> +#include <algorithm> +#include <cctype> +#include <sstream> + +#include "strop.h" +#include <kdebug.h> + +extern std::ostream* err; + +void StrOps::lower(std::string& str) +{ + for (std::string::iterator p = str.begin(); p != str.end(); ++p) { + if (isupper(*p)) + *p = tolower(*p); + } +} + +bool StrOps::string2boolean(const std::string& str) +{ + std::string value(str); + + StrOps::lower(value); + + if (value == "on") return true; + else if (str == "off") return false; + else if (str == "true") return true; + else if (str == "t") return true; + else if (str == "false") return false; + else if (str == "f") return false; + else { + int num = 0; + + std::istringstream(str.c_str()) >> num; + return num != 0 ? true : false; + } +} + +std::string StrOps::type2string(PalmLib::FlatFile::Field::FieldType t) +{ + switch (t) { + case PalmLib::FlatFile::Field::STRING: + return "string"; + + case PalmLib::FlatFile::Field::BOOLEAN: + return "boolean"; + + case PalmLib::FlatFile::Field::INTEGER: + return "integer"; + + case PalmLib::FlatFile::Field::FLOAT: + return "float"; + + case PalmLib::FlatFile::Field::DATE: + return "date"; + + case PalmLib::FlatFile::Field::TIME: + return "time"; + + case PalmLib::FlatFile::Field::DATETIME: + return "datetime"; + + case PalmLib::FlatFile::Field::NOTE: + return "note"; + + case PalmLib::FlatFile::Field::LIST: + return "list"; + + case PalmLib::FlatFile::Field::LINK: + return "link"; + + case PalmLib::FlatFile::Field::CALCULATED: + return "calculated"; + + case PalmLib::FlatFile::Field::LINKED: + return "linked"; + + default: + // If we don't support the field type, then fake it as a string. + return "string"; + } +} + +PalmLib::FlatFile::Field::FieldType StrOps::string2type(std::string typestr) +{ + StrOps::lower(typestr); + if (typestr == "string") + return PalmLib::FlatFile::Field::STRING; + else if (typestr == "str") + return PalmLib::FlatFile::Field::STRING; + else if (typestr == "note") + return PalmLib::FlatFile::Field::NOTE; + else if (typestr == "bool") + return PalmLib::FlatFile::Field::BOOLEAN; + else if (typestr == "boolean") + return PalmLib::FlatFile::Field::BOOLEAN; + else if (typestr == "integer") + return PalmLib::FlatFile::Field::INTEGER; + else if (typestr == "int") + return PalmLib::FlatFile::Field::INTEGER; + else if (typestr == "float") + return PalmLib::FlatFile::Field::FLOAT; + else if (typestr == "date") + return PalmLib::FlatFile::Field::DATE; + else if (typestr == "time") + return PalmLib::FlatFile::Field::TIME; + else if (typestr == "datetime") + return PalmLib::FlatFile::Field::DATETIME; + else if (typestr == "list") + return PalmLib::FlatFile::Field::LIST; + else if (typestr == "link") + return PalmLib::FlatFile::Field::LINK; + else if (typestr == "linked") + return PalmLib::FlatFile::Field::LINKED; + else if (typestr == "calculated") + return PalmLib::FlatFile::Field::CALCULATED; + else + kdDebug() << "unknown field type" << endl; + return PalmLib::FlatFile::Field::STRING; +} + +std::string StrOps::strip_back(const std::string& str, const std::string& what) +{ + std::string result(str); + std::string::reverse_iterator p = result.rbegin(); + + while (p != result.rend() + && (std::find(what.begin(), what.end(), *p) != what.end())) ++p; + + result.erase(p.base(), result.end()); + + return result; +} + +std::string StrOps::strip_front(const std::string& str,const std::string& what) +{ + std::string result(str); + std::string::iterator p = result.begin(); + + while (p != result.end() + && (std::find(what.begin(), what.end(), *p) != what.end())) ++p; + + result.erase(result.begin(), p); + + return result; +} + +StrOps::string_list_t StrOps::csv_to_array(const std::string& str, char delim, bool quoted_string) +{ + enum { STATE_NORMAL, STATE_QUOTES } state; + StrOps::string_list_t result; + std::string elem; + + state = STATE_NORMAL; + for (std::string::const_iterator p = str.begin(); p != str.end(); ++p) { + switch (state) { + case STATE_NORMAL: + if (quoted_string && *p == '"') { + state = STATE_QUOTES; + } else if (*p == delim) { + result.push_back(elem); + elem = ""; + } else { + elem += *p; + } + break; + + case STATE_QUOTES: + if (quoted_string && *p == '"') { + if ((p + 1) != str.end() && *(p+1) == '"') { + ++p; + elem += '"'; + } else { + state = STATE_NORMAL; + } + } else { + elem += *p; + } + break; + } + } + + switch (state) { + case STATE_NORMAL: + result.push_back(elem); + break; + case STATE_QUOTES: + kdDebug() << "unterminated quotes" << endl; + break; + } + + return result; +} + +StrOps::string_list_t +StrOps::str_to_array(const std::string& str, const std::string& delim, + bool multiple_delim, bool handle_comments) +{ + enum { STATE_NORMAL, STATE_COMMENT, STATE_QUOTE_DOUBLE, STATE_QUOTE_SINGLE, + STATE_BACKSLASH, STATE_BACKSLASH_DOUBLEQUOTE } state; + StrOps::string_list_t result; + std::string elem; + + state = STATE_NORMAL; + for (std::string::const_iterator p = str.begin(); p != str.end(); ++p) { + switch (state) { + case STATE_NORMAL: + if (*p == '"') { + state = STATE_QUOTE_DOUBLE; + } else if (*p == '\'') { + state = STATE_QUOTE_SINGLE; + } else if (std::find(delim.begin(), delim.end(), *p) != delim.end()) { + if (multiple_delim) { + ++p; + while (p != str.end() + && std::find(delim.begin(), delim.end(), *p) != delim.end()) { + ++p; + } + --p; + } + result.push_back(elem); + elem = ""; + } else if (*p == '\\') { + state = STATE_BACKSLASH; + } else if (handle_comments && *p == '#') { + state = STATE_COMMENT; + } else { + elem += *p; + } + break; + + case STATE_COMMENT: + break; + + case STATE_QUOTE_DOUBLE: + if (*p == '"') + state = STATE_NORMAL; + else if (*p == '\\') + state = STATE_BACKSLASH_DOUBLEQUOTE; + else + elem += *p; + break; + + case STATE_QUOTE_SINGLE: + if (*p == '\'') + state = STATE_NORMAL; + else + elem += *p; + break; + + case STATE_BACKSLASH: + elem += *p; + state = STATE_NORMAL; + break; + + case STATE_BACKSLASH_DOUBLEQUOTE: + switch (*p) { + case '\\': + elem += '\\'; + break; + + case 'n': + elem += '\n'; + break; + + case 'r': + elem += '\r'; + break; + + case 't': + elem += '\t'; + break; + + case 'v': + elem += '\v'; + break; + + case '"': + elem += '"'; + break; + + case 'x': + { + char buf[3]; + + // Extract and check the first hexadecimal character. + if ((p + 1) == str.end()) + kdDebug() << "truncated escape" << endl; + if (! isxdigit(*(p + 1))) + kdDebug() << "invalid hex character" << endl; + buf[0] = *++p; + + // Extract and check the second (if any) hex character. + if ((p + 1) != str.end() && isxdigit(*(p + 1))) { + buf[1] = *++p; + buf[2] = '\0'; + } else { + buf[1] = buf[2] = '\0'; + } + + std::istringstream stream(buf); + stream.setf(std::ios::hex, std::ios::basefield); + unsigned value; + stream >> value; + + elem += static_cast<char> (value & 0xFFu); + } + break; + } + + // Escape is done. Go back to the normal double quote state. + state = STATE_QUOTE_DOUBLE; + break; + } + } + + switch (state) { + case STATE_NORMAL: + result.push_back(elem); + break; + + case STATE_QUOTE_DOUBLE: + kdDebug() << "unterminated double quotes" << endl; + break; + + case STATE_QUOTE_SINGLE: + kdDebug() << "unterminated single quotes" << endl; + break; + + case STATE_BACKSLASH: + case STATE_BACKSLASH_DOUBLEQUOTE: + kdDebug() << "an escape character must follow a backslash" << endl; + break; + + default: + break; + } + + return result; +} + +PalmLib::pi_uint32_t +StrOps::get_current_time(void) +{ + time_t now; + + time(&now); + return static_cast<PalmLib::pi_uint32_t> (now) + PalmLib::pi_uint32_t(2082844800); +} + +char * +StrOps::strptime(const char *s, const char *format, struct tm *tm) +{ + char *data = (char *)s; + char option = false; + + while (*format != 0) { + if (*data == 0) + return NULL; + switch (*format) { + case '%': + option = true; + format++; + break; + case 'd': + if (option) { + tm->tm_mday = strtol(data, &data, 10); + if (tm->tm_mday < 1 || tm->tm_mday > 31) + return NULL; + } else if (*data != 'd') { + return data; + } + option = false; + format++; + break; + case 'm': + if (option) { + /* tm_mon between 0 and 11 */ + tm->tm_mon = strtol(data, &data, 10) - 1; + if (tm->tm_mon < 0 || tm->tm_mon > 11) + return NULL; + } else if (*data != 'm') { + return data; + } + option = false; + format++; + break; + case 'y': + if (option) { + tm->tm_year = strtol(data, &data, 10); + if (tm->tm_year < 60) tm->tm_year += 100; + } else if (*data != 'y') { + return data; + } + option = false; + format++; + break; + case 'Y': + if (option) { + tm->tm_year = strtol(data, &data, 10) - 1900; + } else if (*data != 'Y') { + return data; + } + option = false; + format++; + break; + case 'H': + if (option) { + tm->tm_hour = strtol(data, &data, 10); + if (tm->tm_hour < 0 || tm->tm_hour > 23) + return NULL; + } else if (*data != 'H') { + return data; + } + option = false; + format++; + break; + case 'M': + if (option) { + tm->tm_min = strtol(data, &data, 10); + if (tm->tm_min < 0 || tm->tm_min > 59) + return NULL; + } else if (*data != 'M') { + return data; + } + option = false; + format++; + break; + default: + if (option) + return data; + if (*data != *format) + return data; + format++; + data++; + } + } + return data; +} + +// Read a line from an istream w/o concern for buffer limitations. +std::string +StrOps::readline(std::istream& stream) +{ + std::string line; + char buf[1024]; + + while (1) { + // Read the next line (or part thereof) from the stream. + stream.getline(buf, sizeof(buf)); + // Bail out of the loop if the stream has reached end-of-file. + if ((stream.eof() && !buf[0]) || stream.bad()) + break; + + // Append the data read to the result string. + line.append(buf); + + // If the stream is good, then stop. Otherwise, clear the + // error indicator so that getline will work again. + if ((stream.eof() && buf[0]) || stream.good()) + break; + stream.clear(); + } + + return line; +} + +std::string +StrOps::quote_string(std::string str, bool extended_mode) +{ + std::string result; + std::ostringstream error; + + if (extended_mode) { + result += '"'; + for (std::string::iterator c = str.begin(); c != str.end(); ++c) { + switch (*c) { + case '\\': + result += '\\'; + result += '\\'; + break; + + case '\r': + result += '\\'; + result += 'r'; + break; + + case '\n': + result += '\\'; + result += 'n'; + break; + + case '\t': + result += '\\'; + result += 't'; + break; + + case '\v': + result += '\\'; + result += 'v'; + break; + + case '"': + result += '\\'; + result += '"'; + break; + + default: + if (isprint(*c)) { + result += *c; + } else { + std::ostringstream buf; + buf.width(2); + buf.setf(std::ios::hex, std::ios::basefield); + buf.setf(std::ios::left); + buf << ((static_cast<unsigned> (*c)) & 0xFF) << std::ends; + + result += "\\x"; + result += buf.str(); + } + break; + } + } + result += '"'; + } else { + result += '"'; + for (std::string::iterator c = str.begin(); c != str.end(); ++c) { + if (*c == '"') { + result += "\"\""; + } else if (*c == '\n' || *c == '\r') { + error << "use extended csv mode for newlines\n"; + *err << error.str(); + kdDebug() << error.str().c_str() << endl; + } else { + result += *c; + } + } + result += '"'; + } + + return result; +} + +std::string +StrOps::concatenatepath(std::string p_Path, + std::string p_FileName, std::string p_Ext) +{ + std::string l_FilePath; +#ifdef WIN32 + if (p_FileName[1] == ':' || p_FileName[0] == '\\') + return p_FileName; + else if (p_Path.empty()) + l_FilePath = p_FileName; + else + l_FilePath = p_Path + std::string("\\") + p_FileName; +#else + if (p_FileName[0] == '/') + return p_FileName; + else if (p_Path.empty()) + l_FilePath = p_FileName; + else + l_FilePath = p_Path + std::string("/") + p_FileName; + +#endif + if (!p_Ext.empty() && (p_FileName.rfind(p_Ext) == std::string::npos)) + l_FilePath += p_Ext; + + return l_FilePath; +} diff --git a/src/translators/pilotdb/strop.h b/src/translators/pilotdb/strop.h new file mode 100644 index 0000000..3d9dc8d --- /dev/null +++ b/src/translators/pilotdb/strop.h @@ -0,0 +1,153 @@ +/* + * palm-db-tools: Support Library: String Parsing Utility Functions + * Copyright (C) 1999-2000 by Tom Dyas (tdyas@users.sourceforge.net) + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * 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., 51 Franklin Street, Fifh Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __LIBSUPPORT_STROP_H__ +#define __LIBSUPPORT_STROP_H__ + +#include <stdexcept> +#include <vector> +#include <string> +#include <sstream> +#include <time.h> +#include "libflatfile/Database.h" + +namespace StrOps { + + // This exception is thrown whenever an error is encountered in + // csv_to_array and str_to_array. + class csv_parse_error : public std::runtime_error { + public: + csv_parse_error(const std::string& msg) : std::runtime_error(msg) { } + }; + + class csv_unterminated_quote : public std::runtime_error { + public: + csv_unterminated_quote(const std::string& msg) : std::runtime_error(msg) { } + }; + + // The results of any parse are returned as this type. + typedef std::vector<std::string> string_list_t; + + + /** + * Convert all uppercase characters in a string to lowercase. + */ + void lower(std::string& str); + + /** + * Convert a string to a boolean value. + * + * @param str The string containing a boolean value to convert. + */ + bool string2boolean(const std::string& str); + + /** + * Convert a string to a FieldType value. + * + * @param typestr The string containing a FieldType value to convert. + */ + PalmLib::FlatFile::Field::FieldType string2type(std::string typestr); + + /** + * Convert a FieldType value to a string. + * + * @param t The FieldType value containing a string to convert. + */ + std::string type2string(PalmLib::FlatFile::Field::FieldType t); + + /** + * Strip trailing characters from a string. + * + * @param str The string to strip characters from. + * @param what The string containing characters to strip. + */ + std::string strip_back(const std::string& str, const std::string& what); + + /** + * Strip leading characters from a string. + * + * @param str The string to strip characters from. + * @param what The string containing characters to strip. + */ + std::string strip_front(const std::string& str, const std::string& what); + + /** + * Convert a string to a target type using a istringstream. + */ + template<class T> + inline void convert_string(const std::string& str, T& result) { + std::istringstream(str.c_str()) >> result; + } + + /** + * Parse a string in CSV (comman-seperated values) format and + * return it as a list. + * + * @param str The string containing the CSV fields. + * @param delim The field delimiter. Defaults to a comma. + */ + string_list_t csv_to_array(const std::string& str, char delim = ',', bool quoted_string = true); + + + /** + * Parse an argv-style array and return it as a list. + * + * @param str The string to parse. + * @param delim String containing the delimiter characters. + * @param multiple_delim Should multiple delimiters count as one? + * @param handle_comments Handle # as a comment character. + */ + string_list_t str_to_array(const std::string& str, + const std::string& delim, + bool multiple_delim, + bool handle_comments); + + /** + * return the current date in the palm format. + */ + PalmLib::pi_uint32_t get_current_time(void); + + /** + * fill a char array with a tm structure in the format passed. + * @param s the char array filled. + * @param format the string of the format to use to print the date. + * @param tm a pointer to time structure. + */ + char *strptime(const char *s, const char *format, struct tm *tm); + + /** + * read one line from the input stream of a file + */ + std::string readline(std::istream& stream); + + /** + * add the quotes to a string + */ + std::string quote_string(std::string str, bool extended_mode); + + /** + * concatenate the path directory, the file name and the extension + * to give the file path to a file + */ + std::string concatenatepath(std::string p_Path, std::string p_FileName, + std::string p_Ext = std::string("")); + +} // namespace StrOps + +#endif diff --git a/src/translators/pilotdbexporter.cpp b/src/translators/pilotdbexporter.cpp new file mode 100644 index 0000000..b9e7367 --- /dev/null +++ b/src/translators/pilotdbexporter.cpp @@ -0,0 +1,232 @@ +/*************************************************************************** + copyright : (C) 2003-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 "pilotdbexporter.h" +#include "pilotdb/pilotdb.h" +#include "pilotdb/libflatfile/DB.h" + +#include "../collection.h" +#include "../filehandler.h" + +#include <klocale.h> +#include <kdebug.h> +#include <kconfig.h> +#include <kglobal.h> +#include <kcharsets.h> + +#include <qlayout.h> +#include <qgroupbox.h> +#include <qcheckbox.h> +#include <qwhatsthis.h> +#include <qtextcodec.h> +#include <qdatetime.h> + +using Tellico::Export::PilotDBExporter; + +PilotDBExporter::PilotDBExporter() : Tellico::Export::Exporter(), + m_backup(true), + m_widget(0), + m_checkBackup(0) { +} + +QString PilotDBExporter::formatString() const { + return i18n("PilotDB"); +} + +QString PilotDBExporter::fileFilter() const { + return i18n("*.pdb|Pilot Database Files (*.pdb)") + QChar('\n') + i18n("*|All Files"); +} + +bool PilotDBExporter::exec() { + Data::CollPtr coll = collection(); + if(!coll) { + return false; + } + + // This is something of a hidden preference cause I don't want to put it in the GUI right now + // Latin1 by default + QTextCodec* codec = 0; + { + // Latin1 is default + KConfigGroup group(KGlobal::config(), QString::fromLatin1("ExportOptions - %1").arg(formatString())); + codec = KGlobal::charsets()->codecForName(group.readEntry("Charset")); + } + if(!codec) { + kdWarning() << "PilotDBExporter::exec() - no QTextCodec!" << endl; + return false; +#ifndef NDEBUG + } else { + kdDebug() << "PilotDBExporter::exec() - encoding with " << codec->name() << endl; +#endif + } + + // DB 0.3.x format + PalmLib::FlatFile::DB db; + + // set database title + db.title(codec->fromUnicode(coll->title()).data()); + + // set backup option +// db.setOption("backup", (m_checkBackup && m_checkBackup->isChecked()) ? "true" : "false"); + + // all fields are added + // except that only one field of type NOTE + bool hasNote = false; + Data::FieldVec outputFields; // not all fields will be output + Data::FieldVec fields = coll->fields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + switch(fIt->type()) { + case Data::Field::Choice: + // the charSeparator is actually defined in DB.h + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::LIST, + codec->fromUnicode(fIt->allowed().join(QChar('/'))).data()); + outputFields.append(fIt); + break; + + case Data::Field::Number: + // the DB only supports single values of integers + if(fIt->flags() & Data::Field::AllowMultiple) { + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::STRING); + } else { + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::INTEGER); + } + outputFields.append(fIt); + break; + + case Data::Field::Bool: + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::BOOLEAN); + outputFields.append(fIt); + break; + + case Data::Field::Para: + if(hasNote) { // only one is allowed, according to palm-db-tools documentation + kdDebug() << "PilotDBExporter::data() - adding note as string" << endl; + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::STRING); + } else { + kdDebug() << "PilotDBExporter::data() - adding note" << endl; + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::NOTE); + hasNote = true; + } + outputFields.append(fIt); + break; + + case Data::Field::Date: + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::DATE); + outputFields.append(fIt); + break; + + case Data::Field::Image: + // don't include images + kdDebug() << "PilotDBExporter::data() - skipping " << fIt->title() << " image field" << endl; + break; + + default: + db.appendField(codec->fromUnicode(fIt->title()).data(), PalmLib::FlatFile::Field::STRING); + outputFields.append(fIt); + break; + } + } + + // add view with visible fields + if(m_columns.count() > 0) { + PalmLib::FlatFile::ListView lv; + lv.name = codec->fromUnicode(i18n("View Columns")).data(); + for(QStringList::ConstIterator it = m_columns.begin(); it != m_columns.end(); ++it) { + PalmLib::FlatFile::ListViewColumn col; + col.field = coll->fieldTitles().findIndex(*it); + lv.push_back(col); + } + db.appendListView(lv); + } + db.doneWithSchema(); + + Data::FieldVec::ConstIterator fIt, end = outputFields.constEnd(); + bool format = options() & Export::ExportFormatted; + + QRegExp br(QString::fromLatin1("<br/?>"), false /*case-sensitive*/); + QRegExp tags(QString::fromLatin1("<.*>")); + tags.setMinimal(true); + + QString value; + for(Data::EntryVec::ConstIterator entryIt = entries().begin(); entryIt != entries().end(); ++entryIt) { + PalmLib::FlatFile::Record record; + unsigned i = 0; + for(fIt = outputFields.constBegin(); fIt != end; ++fIt, ++i) { + value = entryIt->field(fIt->name(), format); + + if(fIt->type() == Data::Field::Date) { + QStringList s = QStringList::split('-', value, true); + bool ok = true; + int y = s.count() > 0 ? s[0].toInt(&ok) : QDate::currentDate().year(); + if(!ok) { + y = QDate::currentDate().year(); + } + int m = s.count() > 1 ? s[1].toInt(&ok) : 1; + if(!ok) { + m = 1; + } + int d = s.count() > 2 ? s[2].toInt(&ok) : 1; + if(!ok) { + d = 1; + } + QDate date(y, m, d); + value = date.toString(QString::fromLatin1("yyyy/MM/dd")); + } else if(fIt->type() == Data::Field::Para) { + value.replace(br, QChar('\n')); + value.replace(tags, QString::null); + } + // the number of fields in the record must match the number of fields in the database + record.appendField(PilotDB::string2field(db.field_type(i), + value.isEmpty() ? std::string() : codec->fromUnicode(value).data())); + } + // Add the record to the database. + db.appendRecord(record); + } + + PilotDB pdb; + db.outputPDB(pdb); + + return FileHandler::writeDataURL(url(), pdb.data(), options() & Export::ExportForce); +} + +QWidget* PilotDBExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("PilotDB Options"), m_widget); + l->addWidget(box); + + m_checkBackup = new QCheckBox(i18n("Set PDA backup flag for database"), box); + m_checkBackup->setChecked(m_backup); + QWhatsThis::add(m_checkBackup, i18n("Set PDA backup flag for database")); + + l->addStretch(1); + return m_widget; +} + +void PilotDBExporter::readOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_backup = group.readBoolEntry("Backup", m_backup); +} + +void PilotDBExporter::saveOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_backup = m_checkBackup->isChecked(); + group.writeEntry("Backup", m_backup); +} + +#include "pilotdbexporter.moc" diff --git a/src/translators/pilotdbexporter.h b/src/translators/pilotdbexporter.h new file mode 100644 index 0000000..13d603b --- /dev/null +++ b/src/translators/pilotdbexporter.h @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef PILOTDBEXPORTER_H +#define PILOTDBEXPORTER_H + +class QCheckBox; + +#include "exporter.h" + +#include <qstringlist.h> + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class PilotDBExporter : public Exporter { +Q_OBJECT + +public: + PilotDBExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget* parent, const char* name=0); + virtual void readOptions(KConfig* cfg); + virtual void saveOptions(KConfig* cfg); + + void setColumns(const QStringList& columns) { m_columns = columns; } + +private: + bool m_backup; + + QWidget* m_widget; + QCheckBox* m_checkBackup; + QStringList m_columns; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/referencerimporter.cpp b/src/translators/referencerimporter.cpp new file mode 100644 index 0000000..32ba251 --- /dev/null +++ b/src/translators/referencerimporter.cpp @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2007 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 "referencerimporter.h" +#include "../collection.h" +#include "../core/netaccess.h" +#include "../imagefactory.h" + +#include <kstandarddirs.h> + +using Tellico::Import::ReferencerImporter; + +ReferencerImporter::ReferencerImporter(const KURL& url_) : XSLTImporter(url_) { + QString xsltFile = locate("appdata", QString::fromLatin1("referencer2tellico.xsl")); + if(!xsltFile.isEmpty()) { + KURL u; + u.setPath(xsltFile); + XSLTImporter::setXSLTURL(u); + } else { + kdWarning() << "ReferencerImporter() - unable to find referencer2tellico.xml!" << endl; + } +} + +bool ReferencerImporter::canImport(int type) const { + return type == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr ReferencerImporter::collection() { + Data::CollPtr coll = XSLTImporter::collection(); + if(!coll) { + return 0; + } + + Data::FieldPtr field = coll->fieldByName(QString::fromLatin1("cover")); + if(!field && !coll->imageFields().isEmpty()) { + field = coll->imageFields().front(); + } else if(!field) { + field = new Data::Field(QString::fromLatin1("cover"), i18n("Front Cover"), Data::Field::Image); + coll->addField(field); + } + + Data::EntryVec entries = coll->entries(); + for(Data::EntryVecIt entry = entries.begin(); entry != entries.end(); ++entry) { + QString url = entry->field(QString::fromLatin1("url")); + if(url.isEmpty()) { + continue; + } + QPixmap pix = NetAccess::filePreview(url); + if(pix.isNull()) { + continue; + } + QString id = ImageFactory::addImage(pix, QString::fromLatin1("PNG")); + if(id.isEmpty()) { + continue; + } + entry->setField(field, id); + } + return coll; +} + +#include "referencerimporter.moc" diff --git a/src/translators/referencerimporter.h b/src/translators/referencerimporter.h new file mode 100644 index 0000000..65cc3a0 --- /dev/null +++ b/src/translators/referencerimporter.h @@ -0,0 +1,49 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORT_REFERENCERIMPORTER_H +#define TELLICO_IMPORT_REFERENCERIMPORTER_H + +#include "xsltimporter.h" +#include "../datavectors.h" + +namespace Tellico { + namespace Import { + +/** + * @author Robby Stephenson +*/ +class ReferencerImporter : public XSLTImporter { +Q_OBJECT + +public: + /** + */ + ReferencerImporter(const KURL& url); + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + virtual bool canImport(int type) const; + +private: + // private so it can't be changed accidently + void setXSLTURL(const KURL& url); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/risimporter.cpp b/src/translators/risimporter.cpp new file mode 100644 index 0000000..0420f66 --- /dev/null +++ b/src/translators/risimporter.cpp @@ -0,0 +1,315 @@ +/*************************************************************************** + copyright : (C) 2004-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 "risimporter.h" +#include "../collections/bibtexcollection.h" +#include "../document.h" +#include "../entry.h" +#include "../field.h" +#include "../latin1literal.h" +#include "../progressmanager.h" +#include "../filehandler.h" +#include "../isbnvalidator.h" +#include "../tellico_debug.h" + +#include <kapplication.h> + +#include <qdict.h> +#include <qregexp.h> +#include <qmap.h> + +using Tellico::Import::RISImporter; +QMap<QString, QString>* RISImporter::s_tagMap = 0; +QMap<QString, QString>* RISImporter::s_typeMap = 0; + +// static +void RISImporter::initTagMap() { + if(!s_tagMap) { + s_tagMap = new QMap<QString, QString>(); + // BT is special and is handled separately + s_tagMap->insert(QString::fromLatin1("TY"), QString::fromLatin1("entry-type")); + s_tagMap->insert(QString::fromLatin1("ID"), QString::fromLatin1("bibtex-key")); + s_tagMap->insert(QString::fromLatin1("T1"), QString::fromLatin1("title")); + s_tagMap->insert(QString::fromLatin1("TI"), QString::fromLatin1("title")); + s_tagMap->insert(QString::fromLatin1("T2"), QString::fromLatin1("booktitle")); + s_tagMap->insert(QString::fromLatin1("A1"), QString::fromLatin1("author")); + s_tagMap->insert(QString::fromLatin1("AU"), QString::fromLatin1("author")); + s_tagMap->insert(QString::fromLatin1("ED"), QString::fromLatin1("editor")); + s_tagMap->insert(QString::fromLatin1("YR"), QString::fromLatin1("year")); + s_tagMap->insert(QString::fromLatin1("PY"), QString::fromLatin1("year")); + s_tagMap->insert(QString::fromLatin1("N1"), QString::fromLatin1("note")); + s_tagMap->insert(QString::fromLatin1("AB"), QString::fromLatin1("abstract")); // should be note? + s_tagMap->insert(QString::fromLatin1("N2"), QString::fromLatin1("abstract")); + s_tagMap->insert(QString::fromLatin1("KW"), QString::fromLatin1("keyword")); + s_tagMap->insert(QString::fromLatin1("JF"), QString::fromLatin1("journal")); + s_tagMap->insert(QString::fromLatin1("JO"), QString::fromLatin1("journal")); + s_tagMap->insert(QString::fromLatin1("JA"), QString::fromLatin1("journal")); + s_tagMap->insert(QString::fromLatin1("VL"), QString::fromLatin1("volume")); + s_tagMap->insert(QString::fromLatin1("IS"), QString::fromLatin1("number")); + s_tagMap->insert(QString::fromLatin1("PB"), QString::fromLatin1("publisher")); + s_tagMap->insert(QString::fromLatin1("SN"), QString::fromLatin1("isbn")); + s_tagMap->insert(QString::fromLatin1("AD"), QString::fromLatin1("address")); + s_tagMap->insert(QString::fromLatin1("CY"), QString::fromLatin1("address")); + s_tagMap->insert(QString::fromLatin1("UR"), QString::fromLatin1("url")); + s_tagMap->insert(QString::fromLatin1("L1"), QString::fromLatin1("pdf")); + s_tagMap->insert(QString::fromLatin1("T3"), QString::fromLatin1("series")); + s_tagMap->insert(QString::fromLatin1("EP"), QString::fromLatin1("pages")); + } +} + +// static +void RISImporter::initTypeMap() { + if(!s_typeMap) { + s_typeMap = new QMap<QString, QString>(); + // leave capitalized, except for bibtex types + s_typeMap->insert(QString::fromLatin1("ABST"), QString::fromLatin1("Abstract")); + s_typeMap->insert(QString::fromLatin1("ADVS"), QString::fromLatin1("Audiovisual material")); + s_typeMap->insert(QString::fromLatin1("ART"), QString::fromLatin1("Art Work")); + s_typeMap->insert(QString::fromLatin1("BILL"), QString::fromLatin1("Bill/Resolution")); + s_typeMap->insert(QString::fromLatin1("BOOK"), QString::fromLatin1("book")); // bibtex + s_typeMap->insert(QString::fromLatin1("CASE"), QString::fromLatin1("Case")); + s_typeMap->insert(QString::fromLatin1("CHAP"), QString::fromLatin1("inbook")); // == "inbook" ? + s_typeMap->insert(QString::fromLatin1("COMP"), QString::fromLatin1("Computer program")); + s_typeMap->insert(QString::fromLatin1("CONF"), QString::fromLatin1("inproceedings")); // == "conference" ? + s_typeMap->insert(QString::fromLatin1("CTLG"), QString::fromLatin1("Catalog")); + s_typeMap->insert(QString::fromLatin1("DATA"), QString::fromLatin1("Data file")); + s_typeMap->insert(QString::fromLatin1("ELEC"), QString::fromLatin1("Electronic Citation")); + s_typeMap->insert(QString::fromLatin1("GEN"), QString::fromLatin1("Generic")); + s_typeMap->insert(QString::fromLatin1("HEAR"), QString::fromLatin1("Hearing")); + s_typeMap->insert(QString::fromLatin1("ICOMM"), QString::fromLatin1("Internet Communication")); + s_typeMap->insert(QString::fromLatin1("INPR"), QString::fromLatin1("In Press")); + s_typeMap->insert(QString::fromLatin1("JFULL"), QString::fromLatin1("Journal (full)")); // = "periodical" ? + s_typeMap->insert(QString::fromLatin1("JOUR"), QString::fromLatin1("article")); // "Journal" + s_typeMap->insert(QString::fromLatin1("MAP"), QString::fromLatin1("Map")); + s_typeMap->insert(QString::fromLatin1("MGZN"), QString::fromLatin1("article")); // bibtex + s_typeMap->insert(QString::fromLatin1("MPCT"), QString::fromLatin1("Motion picture")); + s_typeMap->insert(QString::fromLatin1("MUSIC"), QString::fromLatin1("Music score")); + s_typeMap->insert(QString::fromLatin1("NEWS"), QString::fromLatin1("Newspaper")); + s_typeMap->insert(QString::fromLatin1("PAMP"), QString::fromLatin1("Pamphlet")); // = "booklet" ? + s_typeMap->insert(QString::fromLatin1("PAT"), QString::fromLatin1("Patent")); + s_typeMap->insert(QString::fromLatin1("PCOMM"), QString::fromLatin1("Personal communication")); + s_typeMap->insert(QString::fromLatin1("RPRT"), QString::fromLatin1("Report")); // = "techreport" ? + s_typeMap->insert(QString::fromLatin1("SER"), QString::fromLatin1("Serial (BookMonograph)")); + s_typeMap->insert(QString::fromLatin1("SLIDE"), QString::fromLatin1("Slide")); + s_typeMap->insert(QString::fromLatin1("SOUND"), QString::fromLatin1("Sound recording")); + s_typeMap->insert(QString::fromLatin1("STAT"), QString::fromLatin1("Statute")); + s_typeMap->insert(QString::fromLatin1("THES"), QString::fromLatin1("phdthesis")); // "mastersthesis" ? + s_typeMap->insert(QString::fromLatin1("UNBILL"), QString::fromLatin1("Unenacted bill/resolution")); + s_typeMap->insert(QString::fromLatin1("UNPB"), QString::fromLatin1("unpublished")); // bibtex + s_typeMap->insert(QString::fromLatin1("VIDEO"), QString::fromLatin1("Video recording")); + } +} + +RISImporter::RISImporter(const KURL::List& urls_) : Tellico::Import::Importer(urls_), m_coll(0), m_cancelled(false) { + initTagMap(); + initTypeMap(); +} + +bool RISImporter::canImport(int type) const { + return type == Data::Collection::Bibtex; +} + +Tellico::Data::CollPtr RISImporter::collection() { + if(m_coll) { + return m_coll; + } + + m_coll = new Data::BibtexCollection(true); + + QDict<Data::Field> risFields; + + // need to know if any extended properties in current collection point to RIS + // if so, add to collection + Data::CollPtr currColl = Data::Document::self()->collection(); + Data::FieldVec vec = currColl->fields(); + for(Data::FieldVec::Iterator it = vec.begin(); it != vec.end(); ++it) { + // continue if property is empty + QString ris = it->property(QString::fromLatin1("ris")); + if(ris.isEmpty()) { + continue; + } + // if current collection has one with the same name, set the property + Data::FieldPtr f = m_coll->fieldByName(it->name()); + if(!f) { + f = new Data::Field(*it); + m_coll->addField(f); + } + f->setProperty(QString::fromLatin1("ris"), ris); + risFields.insert(ris, f); + } + + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(urls().count() * 100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + int count = 0; + KURL::List urls = this->urls(); + for(KURL::List::ConstIterator it = urls.begin(); it != urls.end() && !m_cancelled; ++it, ++count) { + readURL(*it, count, risFields); + } + + if(m_cancelled) { + m_coll = 0; + } + return m_coll; +} + +void RISImporter::readURL(const KURL& url_, int n, const QDict<Data::Field>& risFields_) { + QString str = FileHandler::readTextFile(url_); + if(str.isEmpty()) { + return; + } + + ISBNValidator isbnval(this); + + QTextIStream t(&str); + + const uint length = str.length(); + const uint stepSize = QMAX(s_stepSize, length/100); + const bool showProgress = options() & ImportProgress; + + bool needToAddFinal = false; + + QString sp, ep; + + uint j = 0; + Data::EntryPtr entry = new Data::Entry(m_coll); + // technically, the spec requires a space immediately after the hyphen + // however, at least one website (Springer) outputs RIS with no space after the final "ER -" + // so just strip the white space later + // also be gracious and allow only any amount of space before hyphen + QRegExp rx(QString::fromLatin1("^(\\w\\w)\\s+-(.*)$")); + QString currLine, nextLine; + for(currLine = t.readLine(); !m_cancelled && !currLine.isNull(); currLine = nextLine, j += currLine.length()) { + nextLine = t.readLine(); + rx.search(currLine); + QString tag = rx.cap(1); + QString value = rx.cap(2).stripWhiteSpace(); + if(tag.isEmpty()) { + continue; + } +// myDebug() << tag << ": " << value << endl; + // if the next line is not empty and does not match start regexp, append to value + while(!nextLine.isEmpty() && nextLine.find(rx) == -1) { + value += nextLine.stripWhiteSpace(); + nextLine = t.readLine(); + } + + // every entry ends with "ER" + if(tag == Latin1Literal("ER")) { + m_coll->addEntries(entry); + entry = new Data::Entry(m_coll); + needToAddFinal = false; + continue; + } else if(tag == Latin1Literal("TY") && s_typeMap->contains(value)) { + // for entry-type, switch it to normalized type name + value = (*s_typeMap)[value]; + } else if(tag == Latin1Literal("SN")) { + // test for valid isbn, sometimes the issn gets stuck here + int pos = 0; + if(isbnval.validate(value, pos) != ISBNValidator::Acceptable) { + continue; + } + } else if(tag == Latin1Literal("SP")) { + sp = value; + if(!ep.isEmpty()) { + value = sp + '-' + ep; + tag = QString::fromLatin1("EP"); + sp = QString(); + ep = QString(); + } else { + // nothing else to do + continue; + } + } else if(tag == Latin1Literal("EP")) { + ep = value; + if(!sp.isEmpty()) { + value = sp + '-' + ep; + sp = QString(); + ep = QString(); + } else { + continue; + } + } else if(tag == Latin1Literal("YR") || tag == Latin1Literal("PY")) { // for now, just grab the year + value = value.section('/', 0, 0); + } + + // the lookup scheme is: + // 1. any field has an RIS property that matches the tag name + // 2. default field mapping tag -> field name + Data::FieldPtr f = risFields_.find(tag); + if(!f) { + // special case for BT + // primary title for books, secondary for everything else + if(tag == Latin1Literal("BT")) { + if(entry->field(QString::fromLatin1("entry-type")) == Latin1Literal("book")) { + f = m_coll->fieldByName(QString::fromLatin1("title")); + } else { + f = m_coll->fieldByName(QString::fromLatin1("booktitle")); + } + } else { + f = fieldByTag(tag); + } + } + if(!f) { + continue; + } + needToAddFinal = true; + + // harmless for non-choice fields + // for entry-type, want it in lower case + f->addAllowed(value); + // if the field can have multiple values, append current values to new value + if((f->flags() & Data::Field::AllowMultiple) && !entry->field(f->name()).isEmpty()) { + value.prepend(entry->field(f->name()) + QString::fromLatin1("; ")); + } + entry->setField(f, value); + + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, n*100 + 100*j/length); + kapp->processEvents(); + } + } + + if(needToAddFinal) { + m_coll->addEntries(entry); + } +} + +Tellico::Data::FieldPtr RISImporter::fieldByTag(const QString& tag_) { + Data::FieldPtr f = 0; + const QString& fieldTag = (*s_tagMap)[tag_]; + if(!fieldTag.isEmpty()) { + f = m_coll->fieldByName(fieldTag); + if(f) { + f->setProperty(QString::fromLatin1("ris"), tag_); + return f; + } + } + + // add non-default fields if not already there + if(tag_== Latin1Literal("L1")) { + f = new Data::Field(QString::fromLatin1("pdf"), i18n("PDF"), Data::Field::URL); + f->setProperty(QString::fromLatin1("ris"), QString::fromLatin1("L1")); + f->setCategory(i18n("Miscellaneous")); + } + m_coll->addField(f); + return f; +} + +void RISImporter::slotCancel() { + m_cancelled = true; +} + +#include "risimporter.moc" diff --git a/src/translators/risimporter.h b/src/translators/risimporter.h new file mode 100644 index 0000000..c7d08d2 --- /dev/null +++ b/src/translators/risimporter.h @@ -0,0 +1,71 @@ +/*************************************************************************** + copyright : (C) 2004-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; * + * * + ***************************************************************************/ + +#ifndef RISIMPORTER_H +#define RISIMPORTER_H + +#include "importer.h" +#include "../datavectors.h" + +#include <qstring.h> +#include <qmap.h> + +template<class type> +class QDict; + +namespace Tellico { + namespace Data { + class Field; + } + namespace Import { + +/** + * @author Robby Stephenson + */ +class RISImporter : public Importer { +Q_OBJECT + +public: + /** + */ + RISImporter(const KURL::List& urls); + + /** + * @return A pointer to a @ref Data::Collection, or 0 if none can be created. + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget*, const char*) { return 0; } + virtual bool canImport(int type) const; + +public slots: + void slotCancel(); + +private: + static void initTagMap(); + static void initTypeMap(); + + Data::FieldPtr fieldByTag(const QString& tag); + void readURL(const KURL& url, int n, const QDict<Data::Field>& risFields); + + Data::CollPtr m_coll; + bool m_cancelled; + + static QMap<QString, QString>* s_tagMap; + static QMap<QString, QString>* s_typeMap; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/tellico_xml.cpp b/src/translators/tellico_xml.cpp new file mode 100644 index 0000000..8e7ac61 --- /dev/null +++ b/src/translators/tellico_xml.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellico_xml.h" + +#include <libxml/parserInternals.h> // needed for IS_LETTER +#include <libxml/parser.h> // has to be before valid.h +#include <libxml/valid.h> + +#include <qregexp.h> + +const QString Tellico::XML::nsXSL = QString::fromLatin1("http://www.w3.org/1999/XSL/Transform"); +const QString Tellico::XML::nsBibtexml = QString::fromLatin1("http://bibtexml.sf.net/"); +const QString Tellico::XML::dtdBibtexml = QString::fromLatin1("bibtexml.dtd"); + +/* + * VERSION 2 added namespaces, changed to multiple elements, + * and changed the "keywords" field to "keyword" + * + * VERSION 3 broke out the formatFlag, and changed NoComplete to AllowCompletion + * + * VERSION 4 added a bibtex-field name for Bibtex collections, element name was + * changed to 'entry', field elements changed to 'field', and boolean fields are now "true" + * + * VERSION 5 moved the bibtex-field and any other extended field property to property elements + * inside the field element, and added the image element. + * + * VERSION 6 added id, i18n attributes, and year, month, day elements in date fields with a calendar name + * attribute. + * + * VERSION 7 changed the application name to Tellico, renamed unitTitle to entryTitle, and made the id permanent. + * + * VERSION 8 added loans and saved filters. + * + * VERSION 9 changed music collections to always have three columns by default, with title/artist/length and + * added file catalog collection. + * + * VERSION 10 added the game board collection. + */ +const uint Tellico::XML::syntaxVersion = 10; +const QString Tellico::XML::nsTellico = QString::fromLatin1("http://periapsis.org/tellico/"); + +const QString Tellico::XML::nsBookcase = QString::fromLatin1("http://periapsis.org/bookcase/"); +const QString Tellico::XML::nsDublinCore = QString::fromLatin1("http://purl.org/dc/elements/1.1/"); +const QString Tellico::XML::nsZing = QString::fromLatin1("http://www.loc.gov/zing/srw/"); +const QString Tellico::XML::nsZingDiag = QString::fromLatin1("http://www.loc.gov/zing/srw/diagnostic/"); + +QString Tellico::XML::pubTellico(int version) { + return QString::fromLatin1("-//Robby Stephenson/DTD Tellico V%1.0//EN").arg(version); +} + +QString Tellico::XML::dtdTellico(int version) { + return QString::fromLatin1("http://periapsis.org/tellico/dtd/v%1/tellico.dtd").arg(version); +} + +bool Tellico::XML::validXMLElementName(const QString& name_) { + return xmlValidateNameValue((xmlChar *)name_.utf8().data()); +} + +QString Tellico::XML::elementName(const QString& name_) { + QString name = name_; + // change white space to dashes + name.replace(QRegExp(QString::fromLatin1("\\s+")), QString::fromLatin1("-")); + // first cut, if it passes, we're done + if(XML::validXMLElementName(name)) { + return name; + } + + // next check first characters IS_DIGIT is defined in libxml/vali.d + for(uint i = 0; i < name.length() && (!IS_LETTER(name[i].unicode()) || name[i] == '_'); ++i) { + name = name.mid(1); + } + if(name.isEmpty() || XML::validXMLElementName(name)) { + return name; // empty names are handled later + } + + // now brute-force it, one character at a time + uint i = 0; + while(i < name.length()) { + if(!XML::validXMLElementName(name.left(i+1))) { + name.remove(i, 1); // remember it's zero-indexed + } else { + // character is ok, increment i + ++i; + } + } + return name; +} diff --git a/src/translators/tellico_xml.h b/src/translators/tellico_xml.h new file mode 100644 index 0000000..7c1a3e2 --- /dev/null +++ b/src/translators/tellico_xml.h @@ -0,0 +1,41 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_XML_H +#define TELLICO_XML_H + +#include <qstring.h> + +namespace Tellico { + namespace XML { + extern const QString nsXSL; + extern const QString nsBibtexml; + extern const QString dtdBibtexml; + + extern const uint syntaxVersion; + extern const QString nsTellico; + + QString pubTellico(int version = syntaxVersion); + QString dtdTellico(int version = syntaxVersion); + + extern const QString nsBookcase; + extern const QString nsDublinCore; + extern const QString nsZing; + extern const QString nsZingDiag; + + bool validXMLElementName(const QString& name); + QString elementName(const QString& name); + } +} + +#endif diff --git a/src/translators/tellicoimporter.cpp b/src/translators/tellicoimporter.cpp new file mode 100644 index 0000000..cb3c7a3 --- /dev/null +++ b/src/translators/tellicoimporter.cpp @@ -0,0 +1,1021 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellicoimporter.h" +#include "tellico_xml.h" +#include "../collectionfactory.h" +#include "../collections/bibtexcollection.h" +#include "../entry.h" +#include "../field.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../isbnvalidator.h" +#include "../latin1literal.h" +#include "../tellico_strings.h" +#include "../tellico_kernel.h" +#include "../tellico_utils.h" +#include "../tellico_debug.h" +#include "../progressmanager.h" + +#include <klocale.h> +#include <kmdcodec.h> +#include <kzip.h> +#include <kapplication.h> + +#include <qdom.h> +#include <qbuffer.h> +#include <qfile.h> +#include <qtimer.h> + +using Tellico::Import::TellicoImporter; + +bool TellicoImporter::versionConversion(uint from, uint to) { + // version 10 only added board games to version 9 + return from < to && (from != 9 || to != 10); +} + +TellicoImporter::TellicoImporter(const KURL& url_, bool loadAllImages_) : DataImporter(url_), + m_coll(0), m_loadAllImages(loadAllImages_), m_format(Unknown), m_modified(false), + m_cancelled(false), m_hasImages(false), m_buffer(0), m_zip(0), m_imgDir(0) { +} + +TellicoImporter::TellicoImporter(const QString& text_) : DataImporter(text_), + m_coll(0), m_loadAllImages(true), m_format(Unknown), m_modified(false), + m_cancelled(false), m_hasImages(false), m_buffer(0), m_zip(0), m_imgDir(0) { +} + +TellicoImporter::~TellicoImporter() { + if(m_zip) { + m_zip->close(); + } + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; +} + +Tellico::Data::CollPtr TellicoImporter::collection() { + if(m_coll) { + return m_coll; + } + + QCString s; // read first 5 characters + if(source() == URL) { + if(!fileRef().open()) { + return 0; + } + QIODevice* f = fileRef().file(); + for(uint i = 0; i < 5; ++i) { + s += static_cast<char>(f->getch()); + } + f->reset(); + } else { + if(data().size() < 5) { + m_format = Error; + return 0; + } + s = QCString(data(), 6); + } + + // need to decide if the data is xml text, or a zip file + // if the first 5 characters are <?xml then treat it like text + if(s[0] == '<' && s[1] == '?' && s[2] == 'x' && s[3] == 'm' && s[4] == 'l') { + m_format = XML; + loadXMLData(source() == URL ? fileRef().file()->readAll() : data(), true); + } else { + m_format = Zip; + loadZipData(); + } + return m_coll; +} + +void TellicoImporter::loadXMLData(const QByteArray& data_, bool loadImages_) { + ProgressItem& item = ProgressManager::self()->newProgressItem(this, progressLabel(), true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + QDomDocument dom; + QString errorMsg; + int errorLine, errorColumn; + if(!dom.setContent(data_, true, &errorMsg, &errorLine, &errorColumn)) { + QString str = i18n(errorLoad).arg(url().fileName()) + QChar('\n'); + str += i18n("There is an XML parsing error in line %1, column %2.").arg(errorLine).arg(errorColumn); + str += QString::fromLatin1("\n"); + str += i18n("The error message from Qt is:"); + str += QString::fromLatin1("\n\t") + errorMsg; + myDebug() << str << endl; + setStatusMessage(str); + m_format = Error; + return; + } + + QDomElement root = dom.documentElement(); + + // the syntax version field name changed from "version" to "syntaxVersion" in version 3 + uint syntaxVersion; + if(root.hasAttribute(QString::fromLatin1("syntaxVersion"))) { + syntaxVersion = root.attribute(QString::fromLatin1("syntaxVersion")).toInt(); + } else if (root.hasAttribute(QString::fromLatin1("version"))) { + syntaxVersion = root.attribute(QString::fromLatin1("version")).toInt(); + } else { + if(!url().isEmpty()) { + setStatusMessage(i18n(errorLoad).arg(url().fileName())); + } + m_format = Error; + return; + } +// myDebug() << "TellicoImporter::loadXMLData() - syntaxVersion = " << syntaxVersion << endl; + + if((syntaxVersion > 6 && root.tagName() != Latin1Literal("tellico")) + || (syntaxVersion < 7 && root.tagName() != Latin1Literal("bookcase"))) { + if(!url().isEmpty()) { + setStatusMessage(i18n(errorLoad).arg(url().fileName())); + } + m_format = Error; + return; + } + + if(syntaxVersion > XML::syntaxVersion) { + if(!url().isEmpty()) { + QString str = i18n(errorLoad).arg(url().fileName()) + QChar('\n'); + str += i18n("It is from a future version of Tellico."); + myDebug() << str << endl; + setStatusMessage(str); + } else { + myDebug() << "Unable to load collection, from a future version (" << syntaxVersion << ")" << endl; + } + m_format = Error; + return; + } else if(versionConversion(syntaxVersion, XML::syntaxVersion)) { + // going form version 9 to 10, there's no conversion needed + QString str = i18n("Tellico is converting the file to a more recent document format. " + "Information loss may occur if an older version of Tellico is used " + "to read this file in the future."); + myDebug() << str << endl; +// setStatusMessage(str); + m_modified = true; // mark as modified + } + + m_namespace = syntaxVersion > 6 ? XML::nsTellico : XML::nsBookcase; + + // the collection item should be the first dom element child of the root + QDomElement collelem; + for(QDomNode n = root.firstChild(); !n.isNull(); n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.isElement() && n.localName() == Latin1Literal("collection")) { + collelem = n.toElement(); + break; + } + } + if(collelem.isNull()) { + kdWarning() << "TellicoImporter::loadDomDocument() - No collection item found." << endl; + return; + } + + QString title = collelem.attribute(QString::fromLatin1("title")); + + // be careful not to have element name collision + // for fields, each true field element is a child of a fields element + QDomNodeList fieldelems; + for(QDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + // Latin1Literal is a macro, so can't say Latin1Literal(syntaxVersion > 3 ? "fields" : "attributes") + if((syntaxVersion > 3 && n.localName() == Latin1Literal("fields")) + || (syntaxVersion < 4 && n.localName() == Latin1Literal("attributes"))) { + QDomElement e = n.toElement(); + fieldelems = e.elementsByTagNameNS(m_namespace, (syntaxVersion > 3) ? QString::fromLatin1("field") + : QString::fromLatin1("attribute")); + break; + } + } +// myDebug() << "TellicoImporter::loadXMLData() - " << fieldelems.count() << " field(s)" << endl; + + // the dilemma is when to force the new collection to have all the default attributes + // if there are no attributes or if the first one has the special name of _default + bool addFields = (fieldelems.count() == 0); + if(!addFields) { + QString name = fieldelems.item(0).toElement().attribute(QString::fromLatin1("name")); + addFields = (name == Latin1Literal("_default")); + // removeChild only works for immediate children + // remove _default field + if(addFields) { + fieldelems.item(0).parentNode().removeChild(fieldelems.item(0)); + } + } + + QString entryName; + // in syntax 4, the element name was changed to "entry", always, rather than depending on + // on the entryName of the collection. A type field was added to the collection element + // to specify what type of collection it is. + if(syntaxVersion > 3) { + entryName = QString::fromLatin1("entry"); + QString typeStr = collelem.attribute(QString::fromLatin1("type")); + Data::Collection::Type type = static_cast<Data::Collection::Type>(typeStr.toInt()); + m_coll = CollectionFactory::collection(type, addFields); + } else { + entryName = collelem.attribute(QString::fromLatin1("unit")); + m_coll = CollectionFactory::collection(entryName, addFields); + } + + if(!title.isEmpty()) { + m_coll->setTitle(title); + } + + for(uint j = 0; j < fieldelems.count(); ++j) { + readField(syntaxVersion, fieldelems.item(j).toElement()); + } + + if(m_coll->type() == Data::Collection::Bibtex) { + Data::BibtexCollection* c = static_cast<Data::BibtexCollection*>(m_coll.data()); + QDomNodeList macroelems; + for(QDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.localName() == Latin1Literal("macros")) { + macroelems = n.toElement().elementsByTagNameNS(m_namespace, QString::fromLatin1("macro")); + break; + } + } +// myDebug() << "TellicoImporter::loadXMLData() - found " << macroelems.count() << " macros" << endl; + for(uint j = 0; c && j < macroelems.count(); ++j) { + QDomElement elem = macroelems.item(j).toElement(); + c->addMacro(elem.attribute(QString::fromLatin1("name")), elem.text()); + } + + for(QDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.localName() == Latin1Literal("bibtex-preamble")) { + c->setPreamble(n.toElement().text()); + break; + } + } + } + + if(m_cancelled) { + m_coll = 0; + return; + } + +// as a special case, for old book collections with a bibtex-id field, convert to Bibtex + if(syntaxVersion < 4 && m_coll->type() == Data::Collection::Book + && m_coll->hasField(QString::fromLatin1("bibtex-id"))) { + m_coll = Data::BibtexCollection::convertBookCollection(m_coll); + } + + const uint count = collelem.childNodes().count(); + const uint stepSize = QMAX(s_stepSize, count/100); + const bool showProgress = options() & ImportProgress; + + item.setTotalSteps(count); + + // have to read images before entries so we can figure out if + // linkOnly() is true + // m_loadAllImages only pertains to zip files + QDomNodeList imgelems; + for(QDomNode n = collelem.firstChild(); !n.isNull(); n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.localName() == Latin1Literal("images")) { + imgelems = n.toElement().elementsByTagNameNS(m_namespace, QString::fromLatin1("image")); + break; + } + } + for(uint j = 0; j < imgelems.count(); ++j) { + readImage(imgelems.item(j).toElement(), loadImages_); + } + + if(m_cancelled) { + m_coll = 0; + return; + } + + uint j = 0; + for(QDomNode n = collelem.firstChild(); !n.isNull() && !m_cancelled; n = n.nextSibling(), ++j) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.localName() == entryName) { + readEntry(syntaxVersion, n.toElement()); + + // not exactly right, but close enough + if(showProgress && j%stepSize == 0) { + ProgressManager::self()->setProgress(this, j); + kapp->processEvents(); + } + } else { +// myDebug() << "...skipping " << n.localName() << " (" << n.namespaceURI() << ")" << endl; + } + } // end entry loop + + if(m_cancelled) { + m_coll = 0; + return; + } + + // filters and borrowers are at document root level, not collection + for(QDomNode n = root.firstChild(); !n.isNull() && !m_cancelled; n = n.nextSibling()) { + if(n.namespaceURI() != m_namespace) { + continue; + } + if(n.localName() == Latin1Literal("borrowers")) { + QDomNodeList borrowerElems = n.toElement().elementsByTagNameNS(m_namespace, QString::fromLatin1("borrower")); + for(uint j = 0; j < borrowerElems.count(); ++j) { + readBorrower(borrowerElems.item(j).toElement()); + } + } else if(n.localName() == Latin1Literal("filters")) { + QDomNodeList filterElems = n.toElement().elementsByTagNameNS(m_namespace, QString::fromLatin1("filter")); + for(uint j = 0; j < filterElems.count(); ++j) { + readFilter(filterElems.item(j).toElement()); + } + } + } + + // special for user, if using an older document format, add some nice new filters + if(syntaxVersion < 8) { + addDefaultFilters(); + } + + if(m_cancelled) { + m_coll = 0; + } +} + +void TellicoImporter::readField(uint syntaxVersion_, const QDomElement& elem_) { + // special case: if the i18n attribute equals true, then translate the title, description, and category + bool isI18n = elem_.attribute(QString::fromLatin1("i18n")) == Latin1Literal("true"); + + QString name = elem_.attribute(QString::fromLatin1("name"), QString::fromLatin1("unknown")); + QString title = elem_.attribute(QString::fromLatin1("title"), i18n("Unknown")); + if(isI18n) { + title = i18n(title.utf8()); + } + + QString typeStr = elem_.attribute(QString::fromLatin1("type"), QString::number(Data::Field::Line)); + Data::Field::Type type = static_cast<Data::Field::Type>(typeStr.toInt()); + + Data::FieldPtr field; + if(type == Data::Field::Choice) { + QStringList allowed = QStringList::split(QString::fromLatin1(";"), + elem_.attribute(QString::fromLatin1("allowed"))); + if(isI18n) { + for(QStringList::Iterator it = allowed.begin(); it != allowed.end(); ++it) { + (*it) = i18n((*it).utf8()); + } + } + field = new Data::Field(name, title, allowed); + } else { + field = new Data::Field(name, title, type); + } + + if(elem_.hasAttribute(QString::fromLatin1("category"))) { + // at one point, the categories had keyboard accels + QString cat = elem_.attribute(QString::fromLatin1("category")); + if(syntaxVersion_ < 9 && cat.find('&') > -1) { + cat.remove('&'); + } + if(isI18n) { + cat = i18n(cat.utf8()); + } + field->setCategory(cat); + } + + if(elem_.hasAttribute(QString::fromLatin1("flags"))) { + int flags = elem_.attribute(QString::fromLatin1("flags")).toInt(); + // I also changed the enum values for syntax 3, but the only custom field + // would have been bibtex-id + if(syntaxVersion_ < 3 && field->name() == Latin1Literal("bibtex-id")) { + flags = 0; + } + + // in syntax version 4, added a flag to disallow deleting attributes + // if it's a version before that and is the title, then add the flag + if(syntaxVersion_ < 4 && field->name() == Latin1Literal("title")) { + flags |= Data::Field::NoDelete; + } + field->setFlags(flags); + } + + QString formatStr = elem_.attribute(QString::fromLatin1("format"), QString::number(Data::Field::FormatNone)); + Data::Field::FormatFlag format = static_cast<Data::Field::FormatFlag>(formatStr.toInt()); + field->setFormatFlag(format); + + if(elem_.hasAttribute(QString::fromLatin1("description"))) { + QString desc = elem_.attribute(QString::fromLatin1("description")); + if(isI18n) { + desc = i18n(desc.utf8()); + } + field->setDescription(desc); + } + + if(syntaxVersion_ >= 5) { + QDomNodeList props = elem_.elementsByTagNameNS(m_namespace, QString::fromLatin1("prop")); + for(uint i = 0; i < props.count(); ++i) { + QDomElement e = props.item(i).toElement(); + field->setProperty(e.attribute(QString::fromLatin1("name")), e.text()); + } + // all track fields in music collections prior to version 9 get converted to three columns + if(syntaxVersion_ < 9) { + if(m_coll->type() == Data::Collection::Album && field->name() == Latin1Literal("track")) { + field->setProperty(QString::fromLatin1("columns"), QChar('3')); + field->setProperty(QString::fromLatin1("column1"), i18n("Title")); + field->setProperty(QString::fromLatin1("column2"), i18n("Artist")); + field->setProperty(QString::fromLatin1("column3"), i18n("Length")); + } else if(m_coll->type() == Data::Collection::Video && field->name() == Latin1Literal("cast")) { + field->setProperty(QString::fromLatin1("column1"), i18n("Actor/Actress")); + field->setProperty(QString::fromLatin1("column2"), i18n("Role")); + } + } + } else if(elem_.hasAttribute(QString::fromLatin1("bibtex-field"))) { + field->setProperty(QString::fromLatin1("bibtex"), elem_.attribute(QString::fromLatin1("bibtex-field"))); + } + + // Table2 is deprecated + if(field->type() == Data::Field::Table2) { + field->setType(Data::Field::Table); + field->setProperty(QString::fromLatin1("columns"), QChar('2')); + } + // for syntax 8, rating fields got their own type + if(syntaxVersion_ < 8) { + Data::Field::convertOldRating(field); // does all its own checking + } + m_coll->addField(field); +// myDebug() << QString(" Added field: %1, %2").arg(field->name()).arg(field->title()) << endl; +} + +void TellicoImporter::readEntry(uint syntaxVersion_, const QDomElement& entryElem_) { + const int id = entryElem_.attribute(QString::fromLatin1("id")).toInt(); + Data::EntryPtr entry; + if(id > 0) { + entry = new Data::Entry(m_coll, id); + } else { + entry = new Data::Entry(m_coll); + } + + bool oldMusic = (syntaxVersion_ < 9 && m_coll->type() == Data::Collection::Album); + + // iterate over all field value children + for(QDomNode node = entryElem_.firstChild(); !node.isNull(); node = node.nextSibling()) { + QDomElement elem = node.toElement(); + if(elem.isNull()) { + continue; + } + + bool isI18n = elem.attribute(QString::fromLatin1("i18n")) == Latin1Literal("true"); + + // Entry::setField checks to see if an field of 'name' is allowed + // in version 3 and prior, checkbox attributes had no text(), set it to "true" now + if(syntaxVersion_ < 4 && elem.text().isEmpty()) { + // "true" means checked + entry->setField(elem.localName(), QString::fromLatin1("true")); + continue; + } + + QString name = elem.localName(); + Data::FieldPtr f = m_coll->fieldByName(name); + + // if the first child of the node is a text node, just set the attribute text + // otherwise, recurse over the node's children + // this is the case for <authors><author>..</author></authors> + // but if there's nothing but white space, then it's a BaseNode for some reason +// if(node.firstChild().nodeType() == QDomNode::TextNode) { + if(f) { + // if it's a derived value, no field value is added + if(f->type() == Data::Field::Dependent) { + continue; + } + + // special case for Date fields + if(f->type() == Data::Field::Date) { + if(elem.hasChildNodes()) { + QString value; + QDomNode yNode = elem.elementsByTagNameNS(m_namespace, QString::fromLatin1("year")).item(0); + if(!yNode.isNull()) { + value += yNode.toElement().text(); + } + value += '-'; + QDomNode mNode = elem.elementsByTagNameNS(m_namespace, QString::fromLatin1("month")).item(0); + if(!mNode.isNull()) { + value += mNode.toElement().text(); + } + value += '-'; + QDomNode dNode = elem.elementsByTagNameNS(m_namespace, QString::fromLatin1("day")).item(0); + if(!dNode.isNull()) { + value += dNode.toElement().text(); + } + entry->setField(name, value); + } else { + // if no child nodes, the code will later assume the value to be the year + entry->setField(name, elem.text()); + } + // go to next value in loop + continue; + } + + // this may be a performance hit to be stripping white space all the time + // unfortunately, text() will include a carriage-return in cases like + // <value> + // text + // </value + // so we arbitrarily decide that only paragraphs get to have CRs? + QString value = elem.text(); + if(f->type() != Data::Field::Para) { + value = value.stripWhiteSpace(); + } + + if(value.isEmpty()) { + continue; + } + + if(f->type() == Data::Field::Image) { + // image info should have already been loaded + const Data::ImageInfo& info = ImageFactory::imageInfo(value); + // possible that value needs to be cleaned first in which case info is null + if(info.isNull() || !info.linkOnly) { + // for local files only, allow paths here + KURL u = KURL::fromPathOrURL(value); + if(u.isValid() && u.isLocalFile()) { + QString result = ImageFactory::addImage(u, false /* quiet */); + if(!result.isEmpty()) { + value = result; + } + } + value = Data::Image::idClean(value); + } + } + + // in version 8, old rating fields get changed + if(syntaxVersion_ < 8 && f->type() == Data::Field::Rating) { + bool ok; + uint i = Tellico::toUInt(value, &ok); + if(ok) { + value = QString::number(i); + } + } else if(syntaxVersion_ < 2 && name == Latin1Literal("keywords")) { + // in version 2, "keywords" changed to "keyword" + name = QString::fromLatin1("keyword"); + } + // special case: if the i18n attribute equals true, then translate the title, description, and category + if(isI18n) { + entry->setField(name, i18n(value.utf8())); + } else { + // special case for isbn fields, go ahead and validate + if(name == Latin1Literal("isbn")) { + const ISBNValidator val(0); + if(elem.attribute(QString::fromLatin1("validate")) != Latin1Literal("no")) { + val.fixup(value); + } + } + entry->setField(name, value); + } + } else { // if no field by the tag name, then it has children, iterate through them + // the field name has the final 's', so remove it + name.truncate(name.length() - 1); + f = m_coll->fieldByName(name); + + // if it's a derived value, no field value is added + if(!f || f->type() == Data::Field::Dependent) { + continue; + } + + const bool oldTracks = (oldMusic && name == Latin1Literal("track")); + + QStringList values; + // concatenate values + for(QDomNode childNode = node.firstChild(); !childNode.isNull(); childNode = childNode.nextSibling()) { + QString value; + // don't worry about i18n here, Tables are never translated + QDomNodeList cols = childNode.toElement().elementsByTagNameNS(m_namespace, QString::fromLatin1("column")); + if(cols.count() > 0) { + for(uint i = 0; i < cols.count(); ++i) { + // special case for old tracks + if(oldTracks && i == 1) { + // if the second column holds the track length, bump it to next column + QRegExp rx(QString::fromLatin1("\\d+:\\d\\d")); + if(rx.exactMatch(cols.item(i).toElement().text())) { + value += entry->field(QString::fromLatin1("artist")); + value += QString::fromLatin1("::"); + } + } + value += cols.item(i).toElement().text().stripWhiteSpace(); + if(i < cols.count()-1) { + value += QString::fromLatin1("::"); + } else if(oldTracks && cols.count() == 1) { + value += QString::fromLatin1("::"); + value += entry->field(QString::fromLatin1("artist")); + } + } + values += value; + } else { + // really loose here, we don't even check that the element name + // is what we think it is + QString s = childNode.toElement().text().stripWhiteSpace(); + if(isI18n && !s.isEmpty()) { + value += i18n(s.utf8()); + } else { + value += s; + } + if(oldTracks) { + value += QString::fromLatin1("::"); + value += entry->field(QString::fromLatin1("artist")); + } + if(values.findIndex(value) == -1) { + values += value; + } + } + } + entry->setField(name, values.join(QString::fromLatin1("; "))); + } + } // end field value loop + + m_coll->addEntries(entry); +} + +void TellicoImporter::readImage(const QDomElement& elem_, bool loadImage_) { + QString format = elem_.attribute(QString::fromLatin1("format")); + const bool link = elem_.attribute(QString::fromLatin1("link")) == Latin1Literal("true"); + QString id = shareString(link ? elem_.attribute(QString::fromLatin1("id")) + : Data::Image::idClean(elem_.attribute(QString::fromLatin1("id")))); + + bool readInfo = true; + if(loadImage_) { + QByteArray ba; + KCodecs::base64Decode(QCString(elem_.text().latin1()), ba); + if(!ba.isEmpty()) { + QString result = ImageFactory::addImage(ba, format, id); + if(result.isEmpty()) { + myDebug() << "TellicoImporter::readImage(XML) - null image for " << id << endl; + } + m_hasImages = true; + readInfo = false; + } + } + if(readInfo) { + // a width or height of 0 is ok here + int width = elem_.attribute(QString::fromLatin1("width")).toInt(); + int height = elem_.attribute(QString::fromLatin1("height")).toInt(); + Data::ImageInfo info(id, format.latin1(), width, height, link); + ImageFactory::cacheImageInfo(info); + } +} + +void TellicoImporter::readFilter(const QDomElement& elem_) { + FilterPtr f = new Filter(Filter::MatchAny); + f->setName(elem_.attribute(QString::fromLatin1("name"))); + + QString match = elem_.attribute(QString::fromLatin1("match")); + if(match == Latin1Literal("all")) { + f->setMatch(Filter::MatchAll); + } + + QDomNodeList rules = elem_.elementsByTagNameNS(m_namespace, QString::fromLatin1("rule")); + for(uint i = 0; i < rules.count(); ++i) { + QDomElement e = rules.item(i).toElement(); + if(e.isNull()) { + continue; + } + + QString field = e.attribute(QString::fromLatin1("field")); + // empty field means match any of them + QString pattern = e.attribute(QString::fromLatin1("pattern")); + // empty pattern is bad + if(pattern.isEmpty()) { + kdWarning() << "TellicoImporter::readFilter() - empty rule!" << endl; + continue; + } + QString function = e.attribute(QString::fromLatin1("function")).lower(); + FilterRule::Function func; + if(function == Latin1Literal("contains")) { + func = FilterRule::FuncContains; + } else if(function == Latin1Literal("notcontains")) { + func = FilterRule::FuncNotContains; + } else if(function == Latin1Literal("equals")) { + func = FilterRule::FuncEquals; + } else if(function == Latin1Literal("notequals")) { + func = FilterRule::FuncNotEquals; + } else if(function == Latin1Literal("regexp")) { + func = FilterRule::FuncRegExp; + } else if(function == Latin1Literal("notregexp")) { + func = FilterRule::FuncNotRegExp; + } else { + kdWarning() << "TellicoImporter::readFilter() - invalid rule function: " << function << endl; + continue; + } + f->append(new FilterRule(field, pattern, func)); + } + + if(!f->isEmpty()) { + m_coll->addFilter(f); + } +} + +void TellicoImporter::readBorrower(const QDomElement& elem_) { + QString name = elem_.attribute(QString::fromLatin1("name")); + QString uid = elem_.attribute(QString::fromLatin1("uid")); + Data::BorrowerPtr b = new Data::Borrower(name, uid); + + QDomNodeList loans = elem_.elementsByTagNameNS(m_namespace, QString::fromLatin1("loan")); + for(uint i = 0; i < loans.count(); ++i) { + QDomElement e = loans.item(i).toElement(); + if(e.isNull()) { + continue; + } + long id = e.attribute(QString::fromLatin1("entryRef")).toLong(); + Data::EntryPtr entry = m_coll->entryById(id); + if(!entry) { + myDebug() << "TellicoImporter::readBorrower() - no entry with id = " << id << endl; + continue; + } + QString uid = e.attribute(QString::fromLatin1("uid")); + QDate loanDate, dueDate; + QString s = e.attribute(QString::fromLatin1("loanDate")); + if(!s.isEmpty()) { + loanDate = QDate::fromString(s, Qt::ISODate); + } + s = e.attribute(QString::fromLatin1("dueDate")); + if(!s.isEmpty()) { + dueDate = QDate::fromString(s, Qt::ISODate); + } + Data::LoanPtr loan = new Data::Loan(entry, loanDate, dueDate, e.text()); + loan->setUID(uid); + b->addLoan(loan); + s = e.attribute(QString::fromLatin1("calendar")); + loan->setInCalendar(s == Latin1Literal("true")); + } + if(!b->isEmpty()) { + m_coll->addBorrower(b); + } +} + +void TellicoImporter::loadZipData() { + delete m_buffer; + delete m_zip; + if(source() == URL) { + m_buffer = 0; + m_zip = new KZip(fileRef().fileName()); + } else { + m_buffer = new QBuffer(data()); + m_zip = new KZip(m_buffer); + } + if(!m_zip->open(IO_ReadOnly)) { + setStatusMessage(i18n(errorLoad).arg(url().fileName())); + m_format = Error; + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + + const KArchiveDirectory* dir = m_zip->directory(); + if(!dir) { + QString str = i18n(errorLoad).arg(url().fileName()) + QChar('\n'); + str += i18n("The file is empty."); + setStatusMessage(str); + m_format = Error; + m_zip->close(); + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + + // main file was changed from bookcase.xml to tellico.xml as of version 0.13 + const KArchiveEntry* entry = dir->entry(QString::fromLatin1("tellico.xml")); + if(!entry) { + entry = dir->entry(QString::fromLatin1("bookcase.xml")); + } + if(!entry || !entry->isFile()) { + QString str = i18n(errorLoad).arg(url().fileName()) + QChar('\n'); + str += i18n("The file contains no collection data."); + setStatusMessage(str); + m_format = Error; + m_zip->close(); + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + + const QByteArray xmlData = static_cast<const KArchiveFile*>(entry)->data(); + loadXMLData(xmlData, false); + if(!m_coll) { + m_format = Error; + m_zip->close(); + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + + if(m_cancelled) { + m_zip->close(); + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + + const KArchiveEntry* imgDirEntry = dir->entry(QString::fromLatin1("images")); + if(!imgDirEntry || !imgDirEntry->isDirectory()) { + m_zip->close(); + delete m_zip; + m_zip = 0; + delete m_buffer; + m_buffer = 0; + return; + } + m_imgDir = static_cast<const KArchiveDirectory*>(imgDirEntry); + m_images.clear(); + m_images.add(m_imgDir->entries()); + m_hasImages = !m_images.isEmpty(); + + // if all the images are not to be loaded, then we're done + if(!m_loadAllImages) { +// myLog() << "TellicoImporter::loadZipData() - delayed loading for " << m_images.count() << " images" << endl; + return; + } + + const QStringList images = static_cast<const KArchiveDirectory*>(imgDirEntry)->entries(); + const uint stepSize = QMAX(s_stepSize, images.count()/100); + + uint j = 0; + for(QStringList::ConstIterator it = images.begin(); !m_cancelled && it != images.end(); ++it, ++j) { + const KArchiveEntry* file = m_imgDir->entry(*it); + if(file && file->isFile()) { + ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(), + (*it).section('.', -1).upper(), (*it)); + m_images.remove(*it); + } + if(j%stepSize == 0) { + kapp->processEvents(); + } + } + + if(m_images.isEmpty()) { + // give it some time + QTimer::singleShot(3000, this, SLOT(deleteLater())); + } +} + +bool TellicoImporter::loadImage(const QString& id_) { +// myLog() << "TellicoImporter::loadImage() - id = " << id_ << endl; + if(m_format != Zip || !m_imgDir) { + return false; + } + const KArchiveEntry* file = m_imgDir->entry(id_); + if(!file || !file->isFile()) { + return false; + } + QString newID = ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(), + id_.section('.', -1).upper(), id_); + m_images.remove(id_); + if(m_images.isEmpty()) { + // give it some time + QTimer::singleShot(3000, this, SLOT(deleteLater())); + } + return !newID.isEmpty(); +} + +// static +bool TellicoImporter::loadAllImages(const KURL& url_) { + // only local files are allowed + if(url_.isEmpty() || !url_.isValid() || !url_.isLocalFile()) { +// myDebug() << "TellicoImporter::loadAllImages() - returning" << endl; + return false; + } + + // keep track of url for error reporting + static KURL u; + + KZip zip(url_.path()); + if(!zip.open(IO_ReadOnly)) { + if(u != url_) { + Kernel::self()->sorry(i18n(errorImageLoad).arg(url_.fileName())); + } + u = url_; + return false; + } + + const KArchiveDirectory* dir = zip.directory(); + if(!dir) { + if(u != url_) { + Kernel::self()->sorry(i18n(errorImageLoad).arg(url_.fileName())); + } + u = url_; + zip.close(); + return false; + } + + const KArchiveEntry* imgDirEntry = dir->entry(QString::fromLatin1("images")); + if(!imgDirEntry || !imgDirEntry->isDirectory()) { + zip.close(); + return false; + } + const QStringList images = static_cast<const KArchiveDirectory*>(imgDirEntry)->entries(); + for(QStringList::ConstIterator it = images.begin(); it != images.end(); ++it) { + const KArchiveEntry* file = static_cast<const KArchiveDirectory*>(imgDirEntry)->entry(*it); + if(file && file->isFile()) { + ImageFactory::addImage(static_cast<const KArchiveFile*>(file)->data(), + (*it).section('.', -1).upper(), (*it)); + } + } + zip.close(); + return true; +} + +void TellicoImporter::addDefaultFilters() { + switch(m_coll->type()) { + case Data::Collection::Book: + if(m_coll->hasField(QString::fromLatin1("read"))) { + FilterPtr f = new Filter(Filter::MatchAny); + f->setName(i18n("Unread Books")); + f->append(new FilterRule(QString::fromLatin1("read"), QString::fromLatin1("true"), FilterRule::FuncNotContains)); + m_coll->addFilter(f); + m_modified = true; + } + break; + + case Data::Collection::Video: + if(m_coll->hasField(QString::fromLatin1("year"))) { + FilterPtr f = new Filter(Filter::MatchAny); + f->setName(i18n("Old Movies")); + // old movies from before 1960 + f->append(new FilterRule(QString::fromLatin1("year"), QString::fromLatin1("19[012345]\\d"), FilterRule::FuncRegExp)); + m_coll->addFilter(f); + m_modified = true; + } + if(m_coll->hasField(QString::fromLatin1("widescreen"))) { + FilterPtr f = new Filter(Filter::MatchAny); + f->setName(i18n("Widescreen")); + f->append(new FilterRule(QString::fromLatin1("widescreen"), QString::fromLatin1("true"), FilterRule::FuncContains)); + m_coll->addFilter(f); + m_modified = true; + } + break; + + case Data::Collection::Album: + if(m_coll->hasField(QString::fromLatin1("year"))) { + FilterPtr f = new Filter(Filter::MatchAny); + f->setName(i18n("80's Music")); + f->append(new FilterRule(QString::fromLatin1("year"), QString::fromLatin1("198\\d"),FilterRule::FuncRegExp)); + m_coll->addFilter(f); + m_modified = true; + } + break; + + default: + break; + } + if(m_coll->hasField(QString::fromLatin1("rating"))) { + FilterPtr filter = new Filter(Filter::MatchAny); + filter->setName(i18n("Favorites")); + // check all the numbers, and use top 20% or so + Data::FieldPtr field = m_coll->fieldByName(QString::fromLatin1("rating")); + bool ok; + uint min = Tellico::toUInt(field->property(QString::fromLatin1("minimum")), &ok); + if(!ok) { + min = 1; + } + uint max = Tellico::toUInt(field->property(QString::fromLatin1("maximum")), &ok); + if(!ok) { + min = 5; + } + for(uint i = QMAX(min, static_cast<uint>(0.8*(max-min+1))); i <= max; ++i) { + filter->append(new FilterRule(QString::fromLatin1("rating"), QString::number(i), FilterRule::FuncContains)); + } + if(!filter->isEmpty()) { + m_coll->addFilter(filter); + m_modified = true; + } + } +} + +void TellicoImporter::slotCancel() { + m_cancelled = true; + m_format = Cancel; +} + +#include "tellicoimporter.moc" diff --git a/src/translators/tellicoimporter.h b/src/translators/tellicoimporter.h new file mode 100644 index 0000000..d4c6e13 --- /dev/null +++ b/src/translators/tellicoimporter.h @@ -0,0 +1,100 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_IMPORTER_H +#define TELLICO_IMPORTER_H + +class QBuffer; +class KZip; +class KArchiveDirectory; + +#include "dataimporter.h" +#include "../datavectors.h" +#include "../stringset.h" + +class QDomElement; + +namespace Tellico { + namespace Import { + +/** + * Reading the @ref Tellico data files is done by the TellicoImporter. + * + * @author Robby Stephenson + */ +class TellicoImporter : public DataImporter { +Q_OBJECT + +public: + enum Format { Unknown, Error, XML, Zip, Cancel }; + + /** + * @param url The tellico data file. + */ + TellicoImporter(const KURL& url, bool loadAllImages=true); + /** + * Constructor used to convert arbitrary text to a @ref Collection + * + * @param text The text + */ + TellicoImporter(const QString& text); + virtual ~TellicoImporter(); + + /** + * sometimes, a new document format might add data + */ + bool modifiedOriginal() const { return m_modified; } + + /** + */ + virtual Data::CollPtr collection(); + Format format() const { return m_format; } + + bool hasImages() const { return m_hasImages; } + bool loadImage(const QString& id_); + + static bool loadAllImages(const KURL& url); + +public slots: + void slotCancel(); + +private: + static bool versionConversion(uint from, uint to); + + void loadXMLData(const QByteArray& data, bool loadImages); + void loadZipData(); + + void readField(uint syntaxVersion, const QDomElement& elem); + void readEntry(uint syntaxVersion, const QDomElement& elem); + void readImage(const QDomElement& elem, bool loadImage); + void readFilter(const QDomElement& elem); + void readBorrower(const QDomElement& elem); + void addDefaultFilters(); + + Data::CollPtr m_coll; + bool m_loadAllImages; + QString m_namespace; + Format m_format; + bool m_modified : 1; + bool m_cancelled : 1; + bool m_hasImages : 1; + StringSet m_images; + + QBuffer* m_buffer; + KZip* m_zip; + const KArchiveDirectory* m_imgDir; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/tellicoxmlexporter.cpp b/src/translators/tellicoxmlexporter.cpp new file mode 100644 index 0000000..6335ed1 --- /dev/null +++ b/src/translators/tellicoxmlexporter.cpp @@ -0,0 +1,505 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellicoxmlexporter.h" +#include "../collections/bibtexcollection.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../controller.h" // needed for getting groupView pointer +#include "../entryitem.h" +#include "../latin1literal.h" +#include "../filehandler.h" +#include "../groupiterator.h" +#include "../tellico_utils.h" +#include "../tellico_kernel.h" +#include "../tellico_debug.h" +#include "tellico_xml.h" +#include "../document.h" // needed for sorting groups +#include "../translators/bibtexhandler.h" // needed for cleaning text + +#include <klocale.h> +#include <kconfig.h> +#include <kmdcodec.h> +#include <kglobal.h> +#include <kcalendarsystem.h> + +#include <qlayout.h> +#include <qgroupbox.h> +#include <qcheckbox.h> +#include <qwhatsthis.h> +#include <qdom.h> +#include <qtextcodec.h> + +using Tellico::Export::TellicoXMLExporter; + +TellicoXMLExporter::TellicoXMLExporter() : Exporter(), + m_includeImages(false), m_includeGroups(false), m_widget(0) { + setOptions(options() | Export::ExportImages | Export::ExportImageSize); // not included by default +} + +TellicoXMLExporter::TellicoXMLExporter(Data::CollPtr coll) : Exporter(coll), + m_includeImages(false), m_includeGroups(false), m_widget(0) { + setOptions(options() | Export::ExportImages | Export::ExportImageSize); // not included by default +} + +QString TellicoXMLExporter::formatString() const { + return i18n("XML"); +} + +QString TellicoXMLExporter::fileFilter() const { + return i18n("*.xml|XML Files (*.xml)") + QChar('\n') + i18n("*|All Files"); +} + +bool TellicoXMLExporter::exec() { + QDomDocument doc = exportXML(); + if(doc.isNull()) { + return false; + } + return FileHandler::writeTextURL(url(), doc.toString(), + options() & ExportUTF8, + options() & Export::ExportForce); +} + +QDomDocument TellicoXMLExporter::exportXML() const { + // don't be hard on people with older versions. The only difference with DTD 10 was adding + // a board game collection, so use 9 still unless it's a board game + int exportVersion = (XML::syntaxVersion == 10 && collection()->type() != Data::Collection::BoardGame) + ? 9 + : XML::syntaxVersion; + + QDomImplementation impl; + QDomDocumentType doctype = impl.createDocumentType(QString::fromLatin1("tellico"), + XML::pubTellico(exportVersion), + XML::dtdTellico(exportVersion)); + //default namespace + const QString& ns = XML::nsTellico; + + QDomDocument dom = impl.createDocument(ns, QString::fromLatin1("tellico"), doctype); + + // root tellico element + QDomElement root = dom.documentElement(); + + QString encodeStr = QString::fromLatin1("version=\"1.0\" encoding=\""); + if(options() & Export::ExportUTF8) { + encodeStr += QString::fromLatin1("UTF-8"); + } else { + encodeStr += QString::fromLatin1(QTextCodec::codecForLocale()->mimeName()); + } + encodeStr += QChar('"'); + + // createDocument creates a root node, insert the processing instruction before it + dom.insertBefore(dom.createProcessingInstruction(QString::fromLatin1("xml"), encodeStr), root); + + root.setAttribute(QString::fromLatin1("syntaxVersion"), exportVersion); + + exportCollectionXML(dom, root, options() & Export::ExportFormatted); + + // clear image list + m_images.clear(); + + return dom; +} + +QString TellicoXMLExporter::exportXMLString() const { + return exportXML().toString(); +} + +void TellicoXMLExporter::exportCollectionXML(QDomDocument& dom_, QDomElement& parent_, bool format_) const { + Data::CollPtr coll = collection(); + if(!coll) { + kdWarning() << "TellicoXMLExporter::exportCollectionXML() - no collection pointer!" << endl; + return; + } + + QDomElement collElem = dom_.createElement(QString::fromLatin1("collection")); + collElem.setAttribute(QString::fromLatin1("type"), coll->type()); + collElem.setAttribute(QString::fromLatin1("title"), coll->title()); + + QDomElement fieldsElem = dom_.createElement(QString::fromLatin1("fields")); + collElem.appendChild(fieldsElem); + + Data::FieldVec fields = coll->fields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + exportFieldXML(dom_, fieldsElem, fIt); + } + + if(coll->type() == Data::Collection::Bibtex) { + const Data::BibtexCollection* c = static_cast<const Data::BibtexCollection*>(coll.data()); + if(!c->preamble().isEmpty()) { + QDomElement preElem = dom_.createElement(QString::fromLatin1("bibtex-preamble")); + preElem.appendChild(dom_.createTextNode(c->preamble())); + collElem.appendChild(preElem); + } + + QDomElement macrosElem = dom_.createElement(QString::fromLatin1("macros")); + for(StringMap::ConstIterator macroIt = c->macroList().constBegin(); macroIt != c->macroList().constEnd(); ++macroIt) { + if(!macroIt.data().isEmpty()) { + QDomElement macroElem = dom_.createElement(QString::fromLatin1("macro")); + macroElem.setAttribute(QString::fromLatin1("name"), macroIt.key()); + macroElem.appendChild(dom_.createTextNode(macroIt.data())); + macrosElem.appendChild(macroElem); + } + } + if(macrosElem.childNodes().count() > 0) { + collElem.appendChild(macrosElem); + } + } + + Data::EntryVec evec = entries(); + for(Data::EntryVec::Iterator entry = evec.begin(); entry != evec.end(); ++entry) { + exportEntryXML(dom_, collElem, entry, format_); + } + + if(!m_images.isEmpty() && (options() & Export::ExportImages)) { + QDomElement imgsElem = dom_.createElement(QString::fromLatin1("images")); + collElem.appendChild(imgsElem); + const QStringList imageIds = m_images.toList(); + for(QStringList::ConstIterator it = imageIds.begin(); it != imageIds.end(); ++it) { + exportImageXML(dom_, imgsElem, *it); + } + } + + if(m_includeGroups) { + exportGroupXML(dom_, collElem); + } + + parent_.appendChild(collElem); + + // the borrowers and filters are in the tellico object, not the collection + if(options() & Export::ExportComplete) { + QDomElement bElem = dom_.createElement(QString::fromLatin1("borrowers")); + Data::BorrowerVec borrowers = coll->borrowers(); + for(Data::BorrowerVec::Iterator bIt = borrowers.begin(); bIt != borrowers.end(); ++bIt) { + exportBorrowerXML(dom_, bElem, bIt); + } + if(bElem.hasChildNodes()) { + parent_.appendChild(bElem); + } + + QDomElement fElem = dom_.createElement(QString::fromLatin1("filters")); + FilterVec filters = coll->filters(); + for(FilterVec::Iterator fIt = filters.begin(); fIt != filters.end(); ++fIt) { + exportFilterXML(dom_, fElem, fIt); + } + if(fElem.hasChildNodes()) { + parent_.appendChild(fElem); + } + } +} + +void TellicoXMLExporter::exportFieldXML(QDomDocument& dom_, QDomElement& parent_, Data::FieldPtr field_) const { + QDomElement elem = dom_.createElement(QString::fromLatin1("field")); + + elem.setAttribute(QString::fromLatin1("name"), field_->name()); + elem.setAttribute(QString::fromLatin1("title"), field_->title()); + elem.setAttribute(QString::fromLatin1("category"), field_->category()); + elem.setAttribute(QString::fromLatin1("type"), field_->type()); + elem.setAttribute(QString::fromLatin1("flags"), field_->flags()); + elem.setAttribute(QString::fromLatin1("format"), field_->formatFlag()); + + if(field_->type() == Data::Field::Choice) { + elem.setAttribute(QString::fromLatin1("allowed"), field_->allowed().join(QString::fromLatin1(";"))); + } + + // only save description if it's not equal to title, which is the default + // title is never empty, so this indirectly checks for empty descriptions + if(field_->description() != field_->title()) { + elem.setAttribute(QString::fromLatin1("description"), field_->description()); + } + + for(StringMap::ConstIterator it = field_->propertyList().begin(); it != field_->propertyList().end(); ++it) { + if(it.data().isEmpty()) { + continue; + } + QDomElement e = dom_.createElement(QString::fromLatin1("prop")); + e.setAttribute(QString::fromLatin1("name"), it.key()); + e.appendChild(dom_.createTextNode(it.data())); + elem.appendChild(e); + } + + parent_.appendChild(elem); +} + +void TellicoXMLExporter::exportEntryXML(QDomDocument& dom_, QDomElement& parent_, Data::EntryPtr entry_, bool format_) const { + QDomElement entryElem = dom_.createElement(QString::fromLatin1("entry")); + entryElem.setAttribute(QString::fromLatin1("id"), entry_->id()); + + // iterate through every field for the entry + Data::FieldVec fields = entry_->collection()->fields(); + for(Data::FieldVec::Iterator fIt = fields.begin(); fIt != fields.end(); ++fIt) { + QString fieldName = fIt->name(); + + // Date fields are special, don't format in export + QString fieldValue = (format_ && fIt->type() != Data::Field::Date) ? entry_->formattedField(fieldName) + : entry_->field(fieldName); + if(options() & ExportClean) { + BibtexHandler::cleanText(fieldValue); + } + + // if empty, then no field element is added and just continue + if(fieldValue.isEmpty()) { + continue; + } + + // optionally, verify images exist + if(fIt->type() == Data::Field::Image && (options() & Export::ExportVerifyImages)) { + if(!ImageFactory::validImage(fieldValue)) { + myDebug() << "TellicoXMLExporter::exportEntryXML() - entry: " << entry_->title() << endl; + myDebug() << "TellicoXMLExporter::exportEntryXML() - skipping image: " << fieldValue << endl; + continue; + } + } + + // if multiple versions are allowed, split them into separate elements + if(fIt->flags() & Data::Field::AllowMultiple) { + // parent element if field contains multiple values, child of entryElem + // who cares about grammar, just add an 's' to the name + QDomElement parElem = dom_.createElement(fieldName + 's'); + entryElem.appendChild(parElem); + + // the space after the semi-colon is enforced when the field is set for the entry + QStringList fields = QStringList::split(QString::fromLatin1("; "), fieldValue, true); + for(QStringList::ConstIterator it = fields.begin(); it != fields.end(); ++it) { + // element for field value, child of either entryElem or ParentElem + QDomElement fieldElem = dom_.createElement(fieldName); + // special case for multi-column tables + int ncols = 0; + if(fIt->type() == Data::Field::Table) { + bool ok; + ncols = Tellico::toUInt(fIt->property(QString::fromLatin1("columns")), &ok); + if(!ok) { + ncols = 1; + } + } + if(ncols > 1) { + for(int col = 0; col < ncols; ++col) { + QDomElement elem; + elem = dom_.createElement(QString::fromLatin1("column")); + elem.appendChild(dom_.createTextNode((*it).section(QString::fromLatin1("::"), col, col))); + fieldElem.appendChild(elem); + } + } else { + fieldElem.appendChild(dom_.createTextNode(*it)); + } + parElem.appendChild(fieldElem); + } + } else { + QDomElement fieldElem = dom_.createElement(fieldName); + entryElem.appendChild(fieldElem); + // Date fields get special treatment + if(fIt->type() == Data::Field::Date) { + fieldElem.setAttribute(QString::fromLatin1("calendar"), KGlobal::locale()->calendar()->calendarName()); + QStringList s = QStringList::split('-', fieldValue, true); + if(s.count() > 0 && !s[0].isEmpty()) { + QDomElement e = dom_.createElement(QString::fromLatin1("year")); + fieldElem.appendChild(e); + e.appendChild(dom_.createTextNode(s[0])); + } + if(s.count() > 1 && !s[1].isEmpty()) { + QDomElement e = dom_.createElement(QString::fromLatin1("month")); + fieldElem.appendChild(e); + e.appendChild(dom_.createTextNode(s[1])); + } + if(s.count() > 2 && !s[2].isEmpty()) { + QDomElement e = dom_.createElement(QString::fromLatin1("day")); + fieldElem.appendChild(e); + e.appendChild(dom_.createTextNode(s[2])); + } + } else if(fIt->type() == Data::Field::URL && + fIt->property(QString::fromLatin1("relative")) == Latin1Literal("true") && + !url().isEmpty()) { + // if a relative URL and url() is not empty, change the value! + KURL old_url(Kernel::self()->URL(), fieldValue); + fieldElem.appendChild(dom_.createTextNode(KURL::relativeURL(url(), old_url))); + } else { + fieldElem.appendChild(dom_.createTextNode(fieldValue)); + } + } + + if(fIt->type() == Data::Field::Image) { + // possible to have more than one entry with the same image + // only want to include it in the output xml once + m_images.add(fieldValue); + } + } // end field loop + + parent_.appendChild(entryElem); +} + +void TellicoXMLExporter::exportImageXML(QDomDocument& dom_, QDomElement& parent_, const QString& id_) const { + if(id_.isEmpty()) { + myDebug() << "TellicoXMLExporter::exportImageXML() - empty image!" << endl; + return; + } +// myLog() << "TellicoXMLExporter::exportImageXML() - id = " << id_ << endl; + + QDomElement imgElem = dom_.createElement(QString::fromLatin1("image")); + if(m_includeImages) { + const Data::Image& img = ImageFactory::imageById(id_); + if(img.isNull()) { + myDebug() << "TellicoXMLExporter::exportImageXML() - null image - " << id_ << endl; + return; + } + imgElem.setAttribute(QString::fromLatin1("format"), img.format()); + imgElem.setAttribute(QString::fromLatin1("id"), img.id()); + imgElem.setAttribute(QString::fromLatin1("width"), img.width()); + imgElem.setAttribute(QString::fromLatin1("height"), img.height()); + if(img.linkOnly()) { + imgElem.setAttribute(QString::fromLatin1("link"), QString::fromLatin1("true")); + } + QCString imgText = KCodecs::base64Encode(img.byteArray()); + imgElem.appendChild(dom_.createTextNode(QString::fromLatin1(imgText))); + } else { + const Data::ImageInfo& info = ImageFactory::imageInfo(id_); + if(info.isNull()) { + return; + } + imgElem.setAttribute(QString::fromLatin1("format"), info.format); + imgElem.setAttribute(QString::fromLatin1("id"), info.id); + // only load the images to read the size if necessary + const bool loadImageIfNecessary = options() & Export::ExportImageSize; + imgElem.setAttribute(QString::fromLatin1("width"), info.width(loadImageIfNecessary)); + imgElem.setAttribute(QString::fromLatin1("height"), info.height(loadImageIfNecessary)); + if(info.linkOnly) { + imgElem.setAttribute(QString::fromLatin1("link"), QString::fromLatin1("true")); + } + } + parent_.appendChild(imgElem); +} + +void TellicoXMLExporter::exportGroupXML(QDomDocument& dom_, QDomElement& parent_) const { + Data::EntryVec vec = entries(); // need a copy for ::contains(); + bool exportAll = collection()->entries().count() == vec.count(); + // iterate over each group, which are the first children + for(GroupIterator gIt = Controller::self()->groupIterator(); gIt.group(); ++gIt) { + if(gIt.group()->isEmpty()) { + continue; + } + QDomElement groupElem = dom_.createElement(QString::fromLatin1("group")); + groupElem.setAttribute(QString::fromLatin1("title"), gIt.group()->groupName()); + // now iterate over all entry items in the group + Data::EntryVec sorted = Data::Document::self()->sortEntries(*gIt.group()); + for(Data::EntryVec::Iterator eIt = sorted.begin(); eIt != sorted.end(); ++eIt) { + if(!exportAll && !vec.contains(eIt)) { + continue; + } + QDomElement entryRefElem = dom_.createElement(QString::fromLatin1("entryRef")); + entryRefElem.setAttribute(QString::fromLatin1("id"), eIt->id()); + groupElem.appendChild(entryRefElem); + } + if(groupElem.hasChildNodes()) { + parent_.appendChild(groupElem); + } + } +} + +void TellicoXMLExporter::exportFilterXML(QDomDocument& dom_, QDomElement& parent_, FilterPtr filter_) const { + QDomElement filterElem = dom_.createElement(QString::fromLatin1("filter")); + filterElem.setAttribute(QString::fromLatin1("name"), filter_->name()); + + QString match = (filter_->op() == Filter::MatchAll) ? QString::fromLatin1("all") : QString::fromLatin1("any"); + filterElem.setAttribute(QString::fromLatin1("match"), match); + + for(QPtrListIterator<FilterRule> it(*filter_); it.current(); ++it) { + QDomElement ruleElem = dom_.createElement(QString::fromLatin1("rule")); + ruleElem.setAttribute(QString::fromLatin1("field"), it.current()->fieldName()); + ruleElem.setAttribute(QString::fromLatin1("pattern"), it.current()->pattern()); + switch(it.current()->function()) { + case FilterRule::FuncContains: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("contains")); + break; + case FilterRule::FuncNotContains: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("notcontains")); + break; + case FilterRule::FuncEquals: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("equals")); + break; + case FilterRule::FuncNotEquals: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("notequals")); + break; + case FilterRule::FuncRegExp: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("regexp")); + break; + case FilterRule::FuncNotRegExp: + ruleElem.setAttribute(QString::fromLatin1("function"), QString::fromLatin1("notregexp")); + break; + default: + kdWarning() << "TellicoXMLExporter::exportFilterXML() - no matching rule function!" << endl; + } + filterElem.appendChild(ruleElem); + } + + parent_.appendChild(filterElem); +} + +void TellicoXMLExporter::exportBorrowerXML(QDomDocument& dom_, QDomElement& parent_, + Data::BorrowerPtr borrower_) const { + if(borrower_->isEmpty()) { + return; + } + + QDomElement bElem = dom_.createElement(QString::fromLatin1("borrower")); + parent_.appendChild(bElem); + + bElem.setAttribute(QString::fromLatin1("name"), borrower_->name()); + bElem.setAttribute(QString::fromLatin1("uid"), borrower_->uid()); + + const Data::LoanVec& loans = borrower_->loans(); + for(Data::LoanVec::ConstIterator it = loans.constBegin(); it != loans.constEnd(); ++it) { + QDomElement lElem = dom_.createElement(QString::fromLatin1("loan")); + bElem.appendChild(lElem); + + lElem.setAttribute(QString::fromLatin1("uid"), it->uid()); + lElem.setAttribute(QString::fromLatin1("entryRef"), it->entry()->id()); + lElem.setAttribute(QString::fromLatin1("loanDate"), it->loanDate().toString(Qt::ISODate)); + lElem.setAttribute(QString::fromLatin1("dueDate"), it->dueDate().toString(Qt::ISODate)); + if(it->inCalendar()) { + lElem.setAttribute(QString::fromLatin1("calendar"), QString::fromLatin1("true")); + } + + lElem.appendChild(dom_.createTextNode(it->note())); + } +} + +QWidget* TellicoXMLExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Horizontal, i18n("Tellico XML Options"), m_widget); + l->addWidget(box); + + m_checkIncludeImages = new QCheckBox(i18n("Include images in XML document"), box); + m_checkIncludeImages->setChecked(m_includeImages); + QWhatsThis::add(m_checkIncludeImages, i18n("If checked, the images in the document will be included " + "in the XML stream as base64 encoded elements.")); + + return m_widget; +} + +void TellicoXMLExporter::readOptions(KConfig* config_) { + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + m_includeImages = group.readBoolEntry("Include Images", m_includeImages); +} + +void TellicoXMLExporter::saveOptions(KConfig* config_) { + m_includeImages = m_checkIncludeImages->isChecked(); + + KConfigGroup group(config_, QString::fromLatin1("ExportOptions - %1").arg(formatString())); + group.writeEntry("Include Images", m_includeImages); +} + +#include "tellicoxmlexporter.moc" diff --git a/src/translators/tellicoxmlexporter.h b/src/translators/tellicoxmlexporter.h new file mode 100644 index 0000000..705c2dc --- /dev/null +++ b/src/translators/tellicoxmlexporter.h @@ -0,0 +1,80 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOXMLEXPORTER_H +#define TELLICOXMLEXPORTER_H + +namespace Tellico { + class Filter; +} + +class QDomDocument; +class QDomElement; +class QCheckBox; + +#include "exporter.h" +#include "../stringset.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class TellicoXMLExporter : public Exporter { +Q_OBJECT + +public: + TellicoXMLExporter(); + TellicoXMLExporter(Data::CollPtr coll); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + QDomDocument exportXML() const; + QString exportXMLString() const; + + void setIncludeImages(bool b) { m_includeImages = b; } + void setIncludeGroups(bool b) { m_includeGroups = b; } + + virtual QWidget* widget(QWidget*, const char*); + virtual void readOptions(KConfig* cfg); + virtual void saveOptions(KConfig* cfg); + + /** + * An integer indicating format version. + */ + static const unsigned syntaxVersion; + +private: + void exportCollectionXML(QDomDocument& doc, QDomElement& parent, bool format) const; + void exportFieldXML(QDomDocument& doc, QDomElement& parent, Data::FieldPtr field) const; + void exportEntryXML(QDomDocument& doc, QDomElement& parent, Data::EntryPtr entry, bool format) const; + void exportImageXML(QDomDocument& doc, QDomElement& parent, const QString& imageID) const; + void exportGroupXML(QDomDocument& doc, QDomElement& parent) const; + void exportFilterXML(QDomDocument& doc, QDomElement& parent, FilterPtr filter) const; + void exportBorrowerXML(QDomDocument& doc, QDomElement& parent, Data::BorrowerPtr borrower) const; + + // keep track of which images were written, since some entries could have same image + mutable StringSet m_images; + bool m_includeImages : 1; + bool m_includeGroups : 1; + + QWidget* m_widget; + QCheckBox* m_checkIncludeImages; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/tellicozipexporter.cpp b/src/translators/tellicozipexporter.cpp new file mode 100644 index 0000000..42e0e70 --- /dev/null +++ b/src/translators/tellicozipexporter.cpp @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2003-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 "tellicozipexporter.h" +#include "tellicoxmlexporter.h" +#include "../collection.h" +#include "../imagefactory.h" +#include "../image.h" +#include "../filehandler.h" +#include "../stringset.h" +#include "../tellico_debug.h" +#include "../progressmanager.h" + +#include <klocale.h> +#include <kconfig.h> +#include <kzip.h> +#include <kapplication.h> + +#include <qdom.h> +#include <qbuffer.h> + +using Tellico::Export::TellicoZipExporter; + +QString TellicoZipExporter::formatString() const { + return i18n("Tellico Zip File"); +} + +QString TellicoZipExporter::fileFilter() const { + return i18n("*.tc *.bc|Tellico Files (*.tc)") + QChar('\n') + i18n("*|All Files"); +} + +bool TellicoZipExporter::exec() { + m_cancelled = false; + Data::CollPtr coll = collection(); + if(!coll) { + return false; + } + + // TODO: maybe need label? + ProgressItem& item = ProgressManager::self()->newProgressItem(this, QString::null, true); + item.setTotalSteps(100); + connect(&item, SIGNAL(signalCancelled(ProgressItem*)), SLOT(slotCancel())); + ProgressItem::Done done(this); + + TellicoXMLExporter exp; + exp.setEntries(entries()); + exp.setURL(url()); // needed in case of relative URL values + long opt = options(); + opt |= Export::ExportUTF8; // always export to UTF-8 + opt |= Export::ExportImages; // always list the images in the xml + opt &= ~Export::ExportProgress; // don't show progress for xml export + exp.setOptions(opt); + exp.setIncludeImages(false); // do not include the images themselves in XML + QCString xml = exp.exportXML().toCString(); // encoded in utf-8 + ProgressManager::self()->setProgress(this, 5); + + QByteArray data; + QBuffer buf(data); + + if(m_cancelled) { + return true; // intentionally cancelled + } + + KZip zip(&buf); + zip.open(IO_WriteOnly); + zip.writeFile(QString::fromLatin1("tellico.xml"), QString::null, QString::null, xml.length(), xml); + + if(m_includeImages) { + ProgressManager::self()->setProgress(this, 10); + // gonna be lazy and just increment progress every 3 images + // it might be less, might be more + uint j = 0; + const QString imagesDir = QString::fromLatin1("images/"); + StringSet imageSet; + Data::FieldVec imageFields = coll->imageFields(); + // already took 10%, only 90% left + const uint stepSize = QMAX(1, (coll->entryCount() * imageFields.count()) / 90); + for(Data::EntryVec::ConstIterator it = entries().begin(); it != entries().end() && !m_cancelled; ++it) { + for(Data::FieldVec::Iterator fIt = imageFields.begin(); fIt != imageFields.end(); ++fIt, ++j) { + const QString id = it->field(fIt); + if(id.isEmpty() || imageSet.has(id)) { + continue; + } + const Data::ImageInfo& info = ImageFactory::imageInfo(id); + if(info.linkOnly) { + myLog() << "TellicoZipExporter::exec() - not copying linked image: " << id << endl; + continue; + } + const Data::Image& img = ImageFactory::imageById(id); + // if no image, continue + if(img.isNull()) { + kdWarning() << "TellicoZipExporter::exec() - no image found for " << fIt->title() << " field" << endl; + kdWarning() << "...for the entry titled " << it->title() << endl; + continue; + } + QByteArray ba = img.byteArray(); +// myDebug() << "TellicoZipExporter::data() - adding image id = " << it->field(fIt) << endl; + zip.writeFile(imagesDir + id, QString::null, QString::null, ba.size(), ba); + imageSet.add(id); + if(j%stepSize == 0) { + ProgressManager::self()->setProgress(this, QMIN(10+j/stepSize, 99)); + kapp->processEvents(); + } + } + } + } else { + ProgressManager::self()->setProgress(this, 80); + } + + zip.close(); + if(m_cancelled) { + return true; + } + + bool success = FileHandler::writeDataURL(url(), data, options() & Export::ExportForce); + return success; +} + +void TellicoZipExporter::slotCancel() { + m_cancelled = true; +} + +#include "tellicozipexporter.moc" diff --git a/src/translators/tellicozipexporter.h b/src/translators/tellicozipexporter.h new file mode 100644 index 0000000..da167d5 --- /dev/null +++ b/src/translators/tellicozipexporter.h @@ -0,0 +1,50 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TELLICOZIPEXPORTER_H +#define TELLICOZIPEXPORTER_H + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class TellicoZipExporter : public Exporter { +Q_OBJECT + +public: + TellicoZipExporter() : Exporter(), m_includeImages(true), m_cancelled(false) {} + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + // no options + virtual QWidget* widget(QWidget*, const char*) { return 0; } + + void setIncludeImages(bool b) { m_includeImages = b; } + +public slots: + void slotCancel(); + +private: + bool m_includeImages : 1; + bool m_cancelled : 1; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/textimporter.cpp b/src/translators/textimporter.cpp new file mode 100644 index 0000000..3130a0f --- /dev/null +++ b/src/translators/textimporter.cpp @@ -0,0 +1,29 @@ +/*************************************************************************** + copyright : (C) 2003-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 "textimporter.h" +#include "../filehandler.h" + +using Tellico::Import::TextImporter; + +TextImporter::TextImporter(const KURL& url_, bool useUTF8_) + : Import::Importer(url_) { + if(url_.isValid()) { + setText(FileHandler::readTextFile(url_, false, useUTF8_)); + } +} + +TextImporter::TextImporter(const QString& text_) : Import::Importer(text_) { +} + +#include "textimporter.moc" diff --git a/src/translators/textimporter.h b/src/translators/textimporter.h new file mode 100644 index 0000000..c4500e5 --- /dev/null +++ b/src/translators/textimporter.h @@ -0,0 +1,42 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TEXTIMPORTER_H +#define TEXTIMPORTER_H + +#include "importer.h" + +namespace Tellico { + namespace Import { + +/** + * The TextImporter class is meant as an abstract class for any importer which reads text files. + * + * @author Robby Stephenson + */ +class TextImporter : public Importer { +Q_OBJECT + +public: + /** + * In the constructor, the contents of the file are read. + * + * @param url The file to be imported + */ + TextImporter(const KURL& url, bool useUTF8_=false); + TextImporter(const QString& text); +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/translators.h b/src/translators/translators.h new file mode 100644 index 0000000..c6c3bc3 --- /dev/null +++ b/src/translators/translators.h @@ -0,0 +1,77 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef TRANSLATORS_H +#define TRANSLATORS_H + +namespace Tellico { + namespace Import { + enum Format { + TellicoXML = 0, + Bibtex, + Bibtexml, + CSV, + XSLT, + AudioFile, + MODS, + Alexandria, + FreeDB, + RIS, + GCfilms, + FileListing, + GRS1, + AMC, + Griffith, + PDF, + Referencer, + Delicious + }; + + enum Action { + Replace, + Append, + Merge + }; + + enum Target { + None, + File, + Dir + }; + } + + namespace Export { + enum Format { + TellicoXML = 0, + TellicoZip, + Bibtex, + Bibtexml, + HTML, + CSV, + XSLT, + Text, + PilotDB, + Alexandria, + ONIX, + GCfilms + }; + + enum Target { + None, + File, + Dir + }; + } +} + +#endif diff --git a/src/translators/xmlimporter.cpp b/src/translators/xmlimporter.cpp new file mode 100644 index 0000000..ce345c4 --- /dev/null +++ b/src/translators/xmlimporter.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + copyright : (C) 2003-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 "xmlimporter.h" +#include "../filehandler.h" +#include "../collection.h" + +#include <klocale.h> + +using Tellico::Import::XMLImporter; + +XMLImporter::XMLImporter(const KURL& url_) : Import::Importer(url_) { + if(!url_.isEmpty() && url_.isValid()) { + m_dom = FileHandler::readXMLFile(url_, true); + } +} + +XMLImporter::XMLImporter(const QString& text_) : Import::Importer(text_) { + if(text_.isEmpty()) { + return; + } + setText(text_); +} + +XMLImporter::XMLImporter(const QByteArray& data_) : Import::Importer(KURL()) { + if(data_.isEmpty()) { + return; + } + + QString errorMsg; + int errorLine, errorColumn; + if(!m_dom.setContent(data_, true, &errorMsg, &errorLine, &errorColumn)) { + QString str = i18n("There is an XML parsing error in line %1, column %2.").arg(errorLine).arg(errorColumn); + str += QString::fromLatin1("\n"); + str += i18n("The error message from Qt is:"); + str += QString::fromLatin1("\n\t") + errorMsg; + setStatusMessage(str); + return; + } +} + +XMLImporter::XMLImporter(const QDomDocument& dom_) : Import::Importer(KURL()), m_dom(dom_) { +} + +void XMLImporter::setText(const QString& text_) { + Importer::setText(text_); + QString errorMsg; + int errorLine, errorColumn; + if(!m_dom.setContent(text_, true, &errorMsg, &errorLine, &errorColumn)) { + QString str = i18n("There is an XML parsing error in line %1, column %2.").arg(errorLine).arg(errorColumn); + str += QString::fromLatin1("\n"); + str += i18n("The error message from Qt is:"); + str += QString::fromLatin1("\n\t") + errorMsg; + setStatusMessage(str); + } +} + +Tellico::Data::CollPtr XMLImporter::collection() { + return 0; +} + +#include "xmlimporter.moc" diff --git a/src/translators/xmlimporter.h b/src/translators/xmlimporter.h new file mode 100644 index 0000000..743a1c1 --- /dev/null +++ b/src/translators/xmlimporter.h @@ -0,0 +1,74 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef XMLIMPORTER_H +#define XMLIMPORTER_H + +#include "importer.h" + +#include <qdom.h> + +namespace Tellico { + namespace Import { + +/** + * The XMLImporter class is meant as an abstract class for any importer which reads xml files. + * + * @author Robby Stephenson + */ +class XMLImporter : public Importer { +Q_OBJECT + +public: + /** + * In the constructor, the contents of the file are read. + * + * @param url The file to be imported + */ + XMLImporter(const KURL& url); + /** + * Imports xml text. + * + * @param text The text + */ + XMLImporter(const QString& text); + /** + * Imports xml text from a byte array. + * + * @param data The Data + */ + XMLImporter(const QByteArray& data); + XMLImporter(const QDomDocument& dom); + + virtual void setText(const QString& text); + + /** + * This class gets used as a utility XML loader. This should never get called, + * but cannot be abstract. + */ + virtual Data::CollPtr collection(); + + /** + * Returns the contents of the imported file. + * + * @return The file contents + */ + const QDomDocument& domDocument() const { return m_dom; } + +private: + QDomDocument m_dom; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/xsltexporter.cpp b/src/translators/xsltexporter.cpp new file mode 100644 index 0000000..54ca8aa --- /dev/null +++ b/src/translators/xsltexporter.cpp @@ -0,0 +1,80 @@ +/*************************************************************************** + copyright : (C) 2003-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 "xsltexporter.h" +#include "xslthandler.h" +#include "tellicoxmlexporter.h" +#include "../filehandler.h" + +#include <klocale.h> +#include <kurlrequester.h> + +#include <qlabel.h> +#include <qgroupbox.h> +#include <qlayout.h> +#include <qhbox.h> +#include <qdom.h> +#include <qwhatsthis.h> + +using Tellico::Export::XSLTExporter; + +XSLTExporter::XSLTExporter() : Export::Exporter(), + m_widget(0), + m_URLRequester(0) { +} + +QString XSLTExporter::formatString() const { + return i18n("XSLT"); +} + +QString XSLTExporter::fileFilter() const { + return i18n("*|All Files"); +} + + +bool XSLTExporter::exec() { + KURL u = m_URLRequester->url(); + if(u.isEmpty() || !u.isValid()) { + return QString::null; + } + // XSLTHandler handler(FileHandler::readXMLFile(url)); + XSLTHandler handler(u); + + TellicoXMLExporter exporter; + exporter.setEntries(entries()); + exporter.setOptions(options()); + QDomDocument dom = exporter.exportXML(); + return FileHandler::writeTextURL(url(), handler.applyStylesheet(dom.toString()), + options() & ExportUTF8, options() & Export::ExportForce); +} + +QWidget* XSLTExporter::widget(QWidget* parent_, const char* name_/*=0*/) { + if(m_widget && m_widget->parent() == parent_) { + return m_widget; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* group = new QGroupBox(1, Qt::Horizontal, i18n("XSLT Options"), m_widget); + l->addWidget(group); + + QHBox* box = new QHBox(group); + box->setSpacing(4); + (void) new QLabel(i18n("XSLT file:"), box); + m_URLRequester = new KURLRequester(box); + QWhatsThis::add(m_URLRequester, i18n("Choose the XSLT file used to transform the Tellico XML data.")); + + l->addStretch(1); + return m_widget; +} diff --git a/src/translators/xsltexporter.h b/src/translators/xsltexporter.h new file mode 100644 index 0000000..ae353d2 --- /dev/null +++ b/src/translators/xsltexporter.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef XSLTEXPORTER_H +#define XSLTEXPORTER_H + +class KURLRequester; + +#include "exporter.h" + +namespace Tellico { + namespace Export { + +/** + * @author Robby Stephenson + */ +class XSLTExporter : public Exporter { +public: + XSLTExporter(); + + virtual bool exec(); + virtual QString formatString() const; + virtual QString fileFilter() const; + + virtual QWidget* widget(QWidget* parent, const char* name=0); + +private: + QWidget* m_widget; + KURLRequester* m_URLRequester; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/translators/xslthandler.cpp b/src/translators/xslthandler.cpp new file mode 100644 index 0000000..e25eef5 --- /dev/null +++ b/src/translators/xslthandler.cpp @@ -0,0 +1,267 @@ +/*************************************************************************** + copyright : (C) 2003-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 "xslthandler.h" +#include "../latin1literal.h" +#include "../tellico_debug.h" +#include "../tellico_utils.h" + +#include <qdom.h> +#include <qtextcodec.h> + +#include <kurl.h> + +extern "C" { +#include <libxslt/xslt.h> +#include <libxslt/transform.h> +#include <libxslt/xsltutils.h> +#include <libxslt/extensions.h> + +#include <libexslt/exslt.h> +} + +// I don't want any network I/O at all +static const int xml_options = XML_PARSE_NOENT | XML_PARSE_NONET | XML_PARSE_NOCDATA; +static const int xslt_options = xml_options; + +/* some functions to pass to the XSLT libs */ +static int writeToQString(void* context, const char* buffer, int len) { + QString* t = static_cast<QString*>(context); + *t += QString::fromUtf8(buffer, len); + return len; +} + +static void closeQString(void* context) { + QString* t = static_cast<QString*>(context); + *t += QString::fromLatin1("\n"); +} + +using Tellico::XSLTHandler; + +XSLTHandler::XMLOutputBuffer::XMLOutputBuffer() : m_res(QString::null) { + m_buf = xmlOutputBufferCreateIO((xmlOutputWriteCallback)writeToQString, + (xmlOutputCloseCallback)closeQString, + &m_res, 0); + if(m_buf) { + m_buf->written = 0; + } else { + myDebug() << "XMLOutputBuffer::XMLOutputBuffer() - error writing output buffer!" << endl; + } +} + +XSLTHandler::XMLOutputBuffer::~XMLOutputBuffer() { + if(m_buf) { + xmlOutputBufferClose(m_buf); //also flushes + m_buf = 0; + } +} + +int XSLTHandler::s_initCount = 0; + +XSLTHandler::XSLTHandler(const QCString& xsltFile_) : + m_stylesheet(0), + m_docIn(0), + m_docOut(0) { + init(); + QString file = KURL::encode_string(QString::fromLocal8Bit(xsltFile_)); + if(!file.isEmpty()) { + xmlDocPtr xsltDoc = xmlReadFile(file.utf8(), NULL, xslt_options); + m_stylesheet = xsltParseStylesheetDoc(xsltDoc); + if(!m_stylesheet) { + myDebug() << "XSLTHandler::applyStylesheet() - null stylesheet pointer for " << xsltFile_ << endl; + } + } +} + +XSLTHandler::XSLTHandler(const KURL& xsltURL_) : + m_stylesheet(0), + m_docIn(0), + m_docOut(0) { + init(); + if(xsltURL_.isValid() && xsltURL_.isLocalFile()) { + xmlDocPtr xsltDoc = xmlReadFile(xsltURL_.encodedPathAndQuery().utf8(), NULL, xslt_options); + m_stylesheet = xsltParseStylesheetDoc(xsltDoc); + if(!m_stylesheet) { + myDebug() << "XSLTHandler::applyStylesheet() - null stylesheet pointer for " << xsltURL_.path() << endl; + } + } +} + +XSLTHandler::XSLTHandler(const QDomDocument& xsltDoc_, const QCString& xsltFile_, bool translate_) : + m_stylesheet(0), + m_docIn(0), + m_docOut(0) { + init(); + QString file = KURL::encode_string(QString::fromLocal8Bit(xsltFile_)); + if(!xsltDoc_.isNull() && !file.isEmpty()) { + setXSLTDoc(xsltDoc_, file.utf8(), translate_); + } +} + +XSLTHandler::~XSLTHandler() { + if(m_stylesheet) { + xsltFreeStylesheet(m_stylesheet); + } + + if(m_docIn) { + xmlFreeDoc(m_docIn); + } + + if(m_docOut) { + xmlFreeDoc(m_docOut); + } + + --s_initCount; + if(s_initCount == 0) { + xsltUnregisterExtModule(EXSLT_STRINGS_NAMESPACE); + xsltUnregisterExtModule(EXSLT_DYNAMIC_NAMESPACE); + xsltCleanupGlobals(); + xmlCleanupParser(); + } +} + +void XSLTHandler::init() { + if(s_initCount == 0) { + xmlSubstituteEntitiesDefault(1); + xmlLoadExtDtdDefaultValue = 0; + + // register all exslt extensions + exsltRegisterAll(); + } + ++s_initCount; + + m_params.clear(); +} + +void XSLTHandler::setXSLTDoc(const QDomDocument& dom_, const QCString& xsltFile_, bool translate_) { + bool utf8 = true; // XML defaults to utf-8 + + // need to find out if utf-8 or not + const QDomNodeList childs = dom_.childNodes(); + for(uint j = 0; j < childs.count(); ++j) { + if(childs.item(j).isProcessingInstruction()) { + QDomProcessingInstruction pi = childs.item(j).toProcessingInstruction(); + if(pi.data().lower().contains(QString::fromLatin1("encoding"))) { + if(!pi.data().lower().contains(QString::fromLatin1("utf-8"))) { + utf8 = false; +// } else { +// myDebug() << "XSLTHandler::setXSLTDoc() - PI = " << pi.data() << endl; + } + break; + } + } + } + + QString s; + if(translate_) { + s = Tellico::i18nReplace(dom_.toString(0 /* indent */)); + } else { + s = dom_.toString(); + } + + xmlDocPtr xsltDoc; + if(utf8) { + xsltDoc = xmlReadDoc(reinterpret_cast<xmlChar*>(s.utf8().data()), xsltFile_.data(), NULL, xslt_options); + } else { + xsltDoc = xmlReadDoc(reinterpret_cast<xmlChar*>(s.local8Bit().data()), xsltFile_.data(), NULL, xslt_options); + } + + if(m_stylesheet) { + xsltFreeStylesheet(m_stylesheet); + } + m_stylesheet = xsltParseStylesheetDoc(xsltDoc); + if(!m_stylesheet) { + myDebug() << "XSLTHandler::applyStylesheet() - null stylesheet pointer for " << xsltFile_ << endl; + } +// xmlFreeDoc(xsltDoc); // this causes a crash for some reason +} + +void XSLTHandler::addStringParam(const QCString& name_, const QCString& value_) { + QCString value = value_; + value.replace('\'', "'"); + addParam(name_, QCString("'") + value + QCString("'")); +} + +void XSLTHandler::addParam(const QCString& name_, const QCString& value_) { + m_params.insert(name_, value_); +// myDebug() << "XSLTHandler::addParam() - " << name_ << ":" << value_ << endl; +} + +void XSLTHandler::removeParam(const QCString& name_) { + m_params.remove(name_); +} + +const QCString& XSLTHandler::param(const QCString& name_) { + return m_params[name_]; +} + +QString XSLTHandler::applyStylesheet(const QString& text_) { + if(!m_stylesheet) { + myDebug() << "XSLTHandler::applyStylesheet() - null stylesheet pointer!" << endl; + return QString::null; + } + + m_docIn = xmlReadDoc(reinterpret_cast<xmlChar*>(text_.utf8().data()), NULL, NULL, xml_options); + + return process(); +} + +QString XSLTHandler::process() { + if(!m_docIn) { + myDebug() << "XSLTHandler::process() - error parsing input string!" << endl; + return QString::null; + } + + QMemArray<const char*> params(2*m_params.count() + 1); + params[0] = NULL; + QMap<QCString, QCString>::ConstIterator it = m_params.constBegin(); + QMap<QCString, QCString>::ConstIterator end = m_params.constEnd(); + for(uint i = 0; it != end; ++it) { + params[i ] = qstrdup(it.key()); + params[i+1] = qstrdup(it.data()); + params[i+2] = NULL; + i += 2; + } + // returns NULL on error + m_docOut = xsltApplyStylesheet(m_stylesheet, m_docIn, params.data()); + for(uint i = 0; i < 2*m_params.count(); ++i) { + delete[] params[i]; + } + if(!m_docOut) { + myDebug() << "XSLTHandler::applyStylesheet() - error applying stylesheet!" << endl; + return QString::null; + } + + XMLOutputBuffer output; + if(output.isValid()) { + int num_bytes = xsltSaveResultTo(output.buffer(), m_docOut, m_stylesheet); + if(num_bytes == -1) { + myDebug() << "XSLTHandler::applyStylesheet() - error saving output buffer!" << endl; + } + } + return output.result(); +} + +//static +QDomDocument& XSLTHandler::setLocaleEncoding(QDomDocument& dom_) { + const QDomNodeList childs = dom_.documentElement().childNodes(); + for(unsigned j = 0; j < childs.count(); ++j) { + if(childs.item(j).isElement() && childs.item(j).nodeName() == Latin1Literal("xsl:output")) { + QDomElement e = childs.item(j).toElement(); + const QString encoding = QString::fromLatin1(QTextCodec::codecForLocale()->name()); + e.setAttribute(QString::fromLatin1("encoding"), encoding); + break; + } + } + return dom_; +} diff --git a/src/translators/xslthandler.h b/src/translators/xslthandler.h new file mode 100644 index 0000000..f51b47c --- /dev/null +++ b/src/translators/xslthandler.h @@ -0,0 +1,112 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef XSLTHANDLER_H +#define XSLTHANDLER_H + +#include <qmap.h> + +extern "C" { +// for xmlDocPtr +#include <libxml/tree.h> +// for xsltStyleSheetPtr +#include <libxslt/xsltInternals.h> +} + +class KURL; +class QDomDocument; + +namespace Tellico { + +/** + * The XSLTHandler contains all the code which uses XSLT processing to generate HTML or to + * translate to other formats. + * + * @author Robby Stephenson + */ +class XSLTHandler { + +public: + class XMLOutputBuffer { + public: + XMLOutputBuffer(); + ~XMLOutputBuffer(); + bool isValid() const { return (m_buf != 0); } + xmlOutputBuffer* buffer() const { return m_buf; } + QString result() const { return m_res; } + private: + xmlOutputBuffer* m_buf; + QString m_res; + }; + + /** + * @param xsltFile The XSLT file + */ + XSLTHandler(const QCString& xsltFile); + /** + * @param xsltURL The XSLT URL + */ + XSLTHandler(const KURL& xsltURL); + /** + * @param xsltDoc The XSLT DOM document + * @param xsltFile The XSLT file, should be a url? + */ + XSLTHandler(const QDomDocument& xsltDoc, const QCString& xsltFile, bool translate=false); + /** + */ + ~XSLTHandler(); + + bool isValid() const { return (m_stylesheet != NULL); } + /** + * Set the XSLT text + * + * @param dom The XSLT DOM document + * @param xsltFile The XSLT file, should be a url? + */ + void setXSLTDoc(const QDomDocument& dom, const QCString& xsltFile, bool translate=false); + /** + * Adds a param + */ + void addParam(const QCString& name, const QCString& value); + /** + * Adds a string param + */ + void addStringParam(const QCString& name, const QCString& value); + void removeParam(const QCString& name); + const QCString& param(const QCString& name); + /** + * Processes text through the XSLT transformation. + * + * @param text The text to be transformed + * @param encodedUTF8 Whether the text is encoded in utf-8 or not + * @return The transformed text + */ + QString applyStylesheet(const QString& text); + + static QDomDocument& setLocaleEncoding(QDomDocument& dom); + +private: + void init(); + QString process(); + + xsltStylesheetPtr m_stylesheet; + xmlDocPtr m_docIn; + xmlDocPtr m_docOut; + + QMap<QCString, QCString> m_params; + + static int s_initCount; +}; + +} // end namespace +#endif diff --git a/src/translators/xsltimporter.cpp b/src/translators/xsltimporter.cpp new file mode 100644 index 0000000..67f1fd2 --- /dev/null +++ b/src/translators/xsltimporter.cpp @@ -0,0 +1,112 @@ +/*************************************************************************** + copyright : (C) 2003-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 "xsltimporter.h" +#include "xslthandler.h" +#include "tellicoimporter.h" +#include "../filehandler.h" +#include "../collection.h" + +#include <klocale.h> +#include <kurlrequester.h> + +#include <qhbox.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qgroupbox.h> + +#include <memory> + +using Tellico::Import::XSLTImporter; + +namespace { + +static bool isUTF8(const KURL& url_) { + // read first line to check encoding + std::auto_ptr<Tellico::FileHandler::FileRef> ref(Tellico::FileHandler::fileRef(url_)); + if(!ref->isValid()) { + return false; + } + + ref->open(); + QTextStream stream(ref->file()); + QString line = stream.readLine().lower(); + return line.find(QString::fromLatin1("utf-8")) > 0; +} + +} + +// always use utf8 for xslt +XSLTImporter::XSLTImporter(const KURL& url_) : Tellico::Import::TextImporter(url_, isUTF8(url_)), + m_coll(0), + m_widget(0), + m_URLRequester(0) { +} + +Tellico::Data::CollPtr XSLTImporter::collection() { + if(m_coll) { + return m_coll; + } + + if(m_xsltURL.isEmpty()) { + // if there's also no widget, then something went wrong + if(!m_widget) { + setStatusMessage(i18n("A valid XSLT file is needed to import the file.")); + return 0; + } + m_xsltURL = m_URLRequester->url(); + } + if(m_xsltURL.isEmpty() || !m_xsltURL.isValid()) { + setStatusMessage(i18n("A valid XSLT file is needed to import the file.")); + return 0; + } + + XSLTHandler handler(m_xsltURL); + if(!handler.isValid()) { + setStatusMessage(i18n("Tellico encountered an error in XSLT processing.")); + return 0; + } +// kdDebug() << text() << endl; + QString str = handler.applyStylesheet(text()); +// kdDebug() << str << endl; + + Import::TellicoImporter imp(str); + m_coll = imp.collection(); + setStatusMessage(imp.statusMessage()); + return m_coll; +} + +QWidget* XSLTImporter::widget(QWidget* parent_, const char* name_) { + // if the url has already been set, then there's no widget + if(!m_xsltURL.isEmpty()) { + return 0; + } + + m_widget = new QWidget(parent_, name_); + QVBoxLayout* l = new QVBoxLayout(m_widget); + + QGroupBox* box = new QGroupBox(1, Qt::Vertical, i18n("XSLT Options"), m_widget); + l->addWidget(box); + + (void) new QLabel(i18n("XSLT file:"), box); + m_URLRequester = new KURLRequester(box); + + QString filter = i18n("*.xsl|XSL Files (*.xsl)") + QChar('\n'); + filter += i18n("*|All Files"); + m_URLRequester->setFilter(filter); + + l->addStretch(1); + return m_widget; +} + +#include "xsltimporter.moc" diff --git a/src/translators/xsltimporter.h b/src/translators/xsltimporter.h new file mode 100644 index 0000000..578b552 --- /dev/null +++ b/src/translators/xsltimporter.h @@ -0,0 +1,56 @@ +/*************************************************************************** + copyright : (C) 2003-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; * + * * + ***************************************************************************/ + +#ifndef XSLTIMPORTER_H +#define XSLTIMPORTER_H + +class KURLRequester; + +#include "textimporter.h" +#include "../datavectors.h" + +namespace Tellico { + namespace Import { + +/** + * The XSLTImporter class takes care of transforming XML data using an XSL stylesheet. + * + * @author Robby Stephenson + */ +class XSLTImporter : public TextImporter { +Q_OBJECT + +public: + /** + */ + XSLTImporter(const KURL& url); + + /** + */ + virtual Data::CollPtr collection(); + /** + */ + virtual QWidget* widget(QWidget* parent, const char* name=0); + void setXSLTURL(const KURL& url) { m_xsltURL = url; } + +private: + Data::CollPtr m_coll; + + QWidget* m_widget; + KURLRequester* m_URLRequester; + KURL m_xsltURL; +}; + + } // end namespace +} // end namespace +#endif diff --git a/src/upcvalidator.cpp b/src/upcvalidator.cpp new file mode 100644 index 0000000..2d5b7a2 --- /dev/null +++ b/src/upcvalidator.cpp @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2005-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 "upcvalidator.h" +#include "isbnvalidator.h" + +#include <kmdcodec.h> + +using Tellico::UPCValidator; + +UPCValidator::UPCValidator(QObject* parent_, const char* name_/*=0*/) + : QValidator(parent_, name_), m_checkISBN(false) { +} + +QValidator::State UPCValidator::validate(QString& input_, int& pos_) const { + // check if it's a cuecat first + State catState = decodeCat(input_); + if(catState == Acceptable) { + pos_ = input_.length(); + return catState; + } + + // no spaces allowed + if(input_.contains(' ')) { + return QValidator::Invalid; + } + + // no real checking, just if starts with 978, use isbnvalidator + const uint len = input_.length(); + if(len < 10) { + m_isbn = false; + } + + if(!m_checkISBN || (!m_isbn && len < 13)) { + return QValidator::Intermediate; + } + + // once it gets converted to an ISBN, remember that, and use it for later + if(input_.startsWith(QString::fromLatin1("978")) || input_.startsWith(QString::fromLatin1("979"))) { + ISBNValidator val(0); + QValidator::State s = val.validate(input_, pos_); + if(s == QValidator::Acceptable) { + m_isbn = true; + // bad hack + UPCValidator* that = const_cast<UPCValidator*>(this); + that->signalISBN(); + } + return s; + } + + return QValidator::Intermediate; +} + +void UPCValidator::fixup(QString& input_) const { + if(input_.isEmpty()) { + return; + } + input_ = input_.stripWhiteSpace(); + + int pos = input_.find(' '); + if(pos > -1) { + input_ = input_.left(pos); + } + + if(!m_checkISBN) { + return; + } + + const uint len = input_.length(); + if(len > 12 && (input_.startsWith(QString::fromLatin1("978")) || input_.startsWith(QString::fromLatin1("979")))) { + QString s = input_; + ISBNValidator val(0); + int p = 0; + int state = val.validate(s, p); + if(state == QValidator::Acceptable) { + // bad hack + UPCValidator* that = const_cast<UPCValidator*>(this); + that->signalISBN(); + input_ = s; + } + } +} + +QValidator::State UPCValidator::decodeCat(QString& input_) const { + if(input_.length() < 3) { + return Intermediate; + } + if(!input_.startsWith(QString::fromLatin1(".C3"))) { // all cuecat codes start with .C3 + return Invalid; + } + const int periods = input_.contains('.'); + if(periods < 4) { + return Intermediate; // not enough yet + } else if(periods > 4) { + return Invalid; + } + + // ok, let's have a go, take the third token + QString code = QStringList::split('.', input_)[2]; + while(code.length() % 4 > 0) { + code += '='; + } + + for(uint i = 0; i < code.length(); ++i) { + if(code[i] >= 'A' && code[i] <= 'Z') { + code.replace(i, 1, code[i].lower()); + } else if(code[i] >= 'a' && code[i] <= 'z') { + code.replace(i, 1, code[i].upper()); + } + } + + code = QString::fromLatin1(KCodecs::base64Decode(code.latin1())); + + for(uint i = 0; i < code.length(); ++i) { + char c = code[i].latin1() ^ 'C'; + code.replace(i, 1, c); + } + + input_ = code; + return Acceptable; +} + +#include "upcvalidator.moc" diff --git a/src/upcvalidator.h b/src/upcvalidator.h new file mode 100644 index 0000000..73afa09 --- /dev/null +++ b/src/upcvalidator.h @@ -0,0 +1,46 @@ +/*************************************************************************** + copyright : (C) 2005-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; * + * * + ***************************************************************************/ + +#ifndef UPCVALIDATOR_H +#define UPCVALIDATOR_H + +#include <qvalidator.h> + +namespace Tellico { + +/** + * @author Robby Stephenson + */ +class UPCValidator : public QValidator { +Q_OBJECT + +public: + UPCValidator(QObject* parent, const char* name=0); + + virtual QValidator::State validate(QString& input, int& pos) const; + virtual void fixup(QString& input) const; + + void setCheckISBN(bool b) { m_checkISBN = b; } + +signals: + void signalISBN(); + +private: + State decodeCat(QString& str) const; + + bool m_checkISBN : 1; + mutable bool m_isbn : 1; +}; + +} // end namespace +#endif diff --git a/src/viewstack.cpp b/src/viewstack.cpp new file mode 100644 index 0000000..69d6ff8 --- /dev/null +++ b/src/viewstack.cpp @@ -0,0 +1,55 @@ +/*************************************************************************** + copyright : (C) 2002-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 "viewstack.h" +#include "entryview.h" +#include "entryiconview.h" +#include "tellico_debug.h" +#include "imagefactory.h" + +#include <khtmlview.h> +#include <klocale.h> + +#include <qwhatsthis.h> + +using Tellico::ViewStack; + +ViewStack::ViewStack(QWidget* parent_, const char* name_/*=0*/) : QWidgetStack(parent_, name_), + m_entryView(new EntryView(this)), m_iconView(new EntryIconView(this)) { + QWhatsThis::add(m_entryView->view(), i18n("<qt>The <i>Entry View</i> shows a formatted view of the entry's " + "contents.</qt>")); + QWhatsThis::add(m_iconView, i18n("<qt>The <i>Icon View</i> shows each entry in the collection or group using " + "an icon, which may be an image in the entry.</qt>")); +} + +void ViewStack::clear() { + m_entryView->clear(); + m_iconView->clear(); +} + +void ViewStack::refresh() { + m_entryView->slotRefresh(); + m_iconView->refresh(); +} + +void ViewStack::showEntry(Data::EntryPtr entry_) { + m_entryView->showEntry(entry_); + raiseWidget(m_entryView->view()); +} + +void ViewStack::showEntries(const Data::EntryVec& entries_) { + m_iconView->showEntries(entries_); + raiseWidget(m_iconView); +} + +#include "viewstack.moc" diff --git a/src/viewstack.h b/src/viewstack.h new file mode 100644 index 0000000..73fa814 --- /dev/null +++ b/src/viewstack.h @@ -0,0 +1,48 @@ +/*************************************************************************** + copyright : (C) 2002-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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_VIEWSTACK_H +#define TELLICO_VIEWSTACK_H + +#include "datavectors.h" + +#include <qwidgetstack.h> + +namespace Tellico { + class EntryView; + class EntryIconView; + +/** + * @author Robby Stephenson + */ +class ViewStack : public QWidgetStack { +Q_OBJECT + +public: + ViewStack(QWidget* parent, const char* name = 0); + + EntryView* entryView() const { return m_entryView; } + EntryIconView* iconView() const { return m_iconView; } + + void clear(); + void refresh(); + void showEntry(Data::EntryPtr entry); + void showEntries(const Data::EntryVec& entries); + +private: + EntryView* m_entryView; + EntryIconView* m_iconView; +}; + +} // end namespace +#endif diff --git a/src/xmphandler.cpp b/src/xmphandler.cpp new file mode 100644 index 0000000..de5d5d6 --- /dev/null +++ b/src/xmphandler.cpp @@ -0,0 +1,90 @@ +/*************************************************************************** + copyright : (C) 2007 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 "xmphandler.h" +#include "tellico_debug.h" +#include <config.h> + +#include <qfile.h> +#include <qtextstream.h> +#ifdef HAVE_EXEMPI +#include <exempi/xmp.h> +#endif + +using Tellico::XMPHandler; + +int XMPHandler::s_initCount = 0; + +bool XMPHandler::isXMPEnabled() { +#ifdef HAVE_EXEMPI + return true; +#else + return false; +#endif +} + +XMPHandler::XMPHandler() { + init(); +} + +XMPHandler::~XMPHandler() { +#ifdef HAVE_EXEMPI + --s_initCount; + if(s_initCount == 0) { + xmp_terminate(); + } +#endif +} + +void XMPHandler::init() { +#ifdef HAVE_EXEMPI + if(s_initCount == 0) { + xmp_init(); + } + ++s_initCount; +#endif +} + +QString XMPHandler::extractXMP(const QString& file) { + QString result; +#ifdef HAVE_EXEMPI + XmpFilePtr xmpfile = xmp_files_open_new(QFile::encodeName(file), XMP_OPEN_READ); + if(!xmpfile) { + myDebug() << "XMPHandler::parse() - unable to open " << file << endl; + return result; + } + XmpPtr xmp = xmp_files_get_new_xmp(xmpfile); + if(xmp) { + XmpStringPtr buffer = xmp_string_new(); + xmp_serialize(xmp, buffer, 0, 0); + result = QString::fromUtf8(xmp_string_cstr(buffer)); + xmp_string_free(buffer); +// myDebug() << result << endl; +#if 0 + kdWarning() << "XMPHandler::parse() - turn me off!" << endl; + QFile f1(QString::fromLatin1("/tmp/xmp.xml")); + if(f1.open(IO_WriteOnly)) { + QTextStream t(&f1); + t << result; + } + f1.close(); +#endif + xmp_free(xmp); + xmp_files_close(xmpfile, XMP_CLOSE_NOOPTION); + xmp_files_free(xmpfile); + } else { + myDebug() << "XMPHandler::parse() - unable to parse " << file << endl; + } +#endif + return result; +} diff --git a/src/xmphandler.h b/src/xmphandler.h new file mode 100644 index 0000000..07c0631 --- /dev/null +++ b/src/xmphandler.h @@ -0,0 +1,37 @@ +/*************************************************************************** + copyright : (C) 2007 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; * + * * + ***************************************************************************/ + +#ifndef TELLICO_XMPHANDLER_H +#define TELLICO_XMPHANDLER_H + +class QString; + +namespace Tellico { + +class XMPHandler { +public: + XMPHandler(); + ~XMPHandler(); + + QString extractXMP(const QString& file); + + static bool isXMPEnabled(); + +private: + void init(); + + static int s_initCount; +}; + +} +#endif |